From 6c89f1f854196cdbf779fe7d042a5ce0a8fefc2a Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Wed, 16 Aug 2023 11:38:21 +0200 Subject: [PATCH 01/12] Include resources/images and image compile tools in source archives This gives users of the source the ability to manipulate those files instead of just receiving the compiled resources.py (which essentially is just an unmodifieable binary dump). --- MANIFEST.in | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MANIFEST.in b/MANIFEST.in index 7e494766a9..0659649abe 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,6 +8,9 @@ include *.in include *.md include *.txt +recursive-include resources *.py +recursive-include resources/images * + recursive-include test *.py recursive-include test/data * recursive-exclude test/data/testplugins/module/dummyplugin/__pycache__ * From ecae5474527b7921ec0bf3da0a2ce55ab35ac73a Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 18 Aug 2023 08:35:59 +0200 Subject: [PATCH 02/12] PICARD-2724: Fix potential crash in artist alias translation --- picard/mbjson.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/picard/mbjson.py b/picard/mbjson.py index e6ddcae3cc..cb8a963899 100644 --- a/picard/mbjson.py +++ b/picard/mbjson.py @@ -246,11 +246,11 @@ def check_higher_score(locale_dict, locale, score): full_locales = {} root_locales = {} for alias in aliases: - if not alias['primary']: + if not alias.get('primary'): continue - if 'locale' not in alias: + full_locale = alias.get('locale') + if not full_locale: continue - full_locale = alias['locale'] root_locale = full_locale.split('_')[0] full_parts = [] root_parts = [] @@ -259,9 +259,10 @@ def check_higher_score(locale_dict, locale, score): if '_' in full_locale: score = 0.4 root_parts.append((score, 5)) - if alias['type-id'] == ALIAS_TYPE_ARTIST_NAME_ID: + type_id = alias.get('type-id') + if type_id == ALIAS_TYPE_ARTIST_NAME_ID: score = 0.8 - elif alias['type-id'] == ALIAS_TYPE_LEGAL_NAME_ID: + elif type_id == ALIAS_TYPE_LEGAL_NAME_ID: score = 0.5 else: # as 2014/09/19, only Artist or Legal names should have the From 595c9e963b5f7f0f150703c1cdcb81a97680b830 Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Mon, 21 Aug 2023 13:58:11 +0200 Subject: [PATCH 03/12] PICARD-2700: fix accumulating series metadata on refresh --- picard/mbjson.py | 66 +++++++++++++++++++++++++++++---------------- test/test_mbjson.py | 40 +++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 23 deletions(-) diff --git a/picard/mbjson.py b/picard/mbjson.py index cb8a963899..be8380b9da 100644 --- a/picard/mbjson.py +++ b/picard/mbjson.py @@ -27,7 +27,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -from collections import namedtuple +from types import SimpleNamespace from picard import log from picard.config import get_config @@ -205,38 +205,58 @@ def _relations_to_metadata_target_type_series(relation, m, context): entity = context.entity series = relation['series'] var_prefix = f'~{entity}_' if entity else '~' - m.add(f'{var_prefix}series', series['name']) - m.add(f'{var_prefix}seriesid', series['id']) - m.add(f'{var_prefix}seriescomment', series['disambiguation']) - m.add(f'{var_prefix}seriesnumber', relation['attribute-values'].get('number', '')) + name = f'{var_prefix}series' + mbid = f'{var_prefix}seriesid' + comment = f'{var_prefix}seriescomment' + number = f'{var_prefix}seriesnumber' + if not context.metadata_was_cleared['series']: + # Clear related metadata first to prevent accumulation + # of identical value, see PICARD-2700 issue + m.unset(name) + m.unset(mbid) + m.unset(comment) + m.unset(number) + # That's to ensure it is done only once + context.metadata_was_cleared['series'] = True + m.add(name, series['name']) + m.add(mbid, series['id']) + m.add(comment, series['disambiguation']) + m.add(number, relation['attribute-values'].get('number', '')) + + +class RelFunc(SimpleNamespace): + clear_metadata_first = False + func = None _RELATIONS_TO_METADATA_TARGET_TYPE_FUNC = { - 'artist': _relations_to_metadata_target_type_artist, - 'series': _relations_to_metadata_target_type_series, - 'url': _relations_to_metadata_target_type_url, - 'work': _relations_to_metadata_target_type_work, + 'artist': RelFunc(func=_relations_to_metadata_target_type_artist), + 'series': RelFunc( + func=_relations_to_metadata_target_type_series, + clear_metadata_first=True + ), + 'url': RelFunc(func=_relations_to_metadata_target_type_url), + 'work': RelFunc(func=_relations_to_metadata_target_type_work), } -TargetTypeFuncContext = namedtuple( - 'TargetTypeFuncContext', - 'config entity instrumental use_credited_as use_instrument_credits' -) - - def _relations_to_metadata(relations, m, instrumental=False, config=None, entity=None): config = config or get_config() - context = TargetTypeFuncContext( - config, - entity, - instrumental, - not config.setting['standardize_artists'], - not config.setting['standardize_instruments'], + context = SimpleNamespace( + config=config, + entity=entity, + instrumental=instrumental, + use_credited_as=not config.setting['standardize_artists'], + use_instrument_credits=not config.setting['standardize_instruments'], + metadata_was_cleared=dict(), ) for relation in relations: - if relation['target-type'] in _RELATIONS_TO_METADATA_TARGET_TYPE_FUNC: - _RELATIONS_TO_METADATA_TARGET_TYPE_FUNC[relation['target-type']](relation, m, context) + target = relation['target-type'] + if target in _RELATIONS_TO_METADATA_TARGET_TYPE_FUNC: + relfunc = _RELATIONS_TO_METADATA_TARGET_TYPE_FUNC[target] + if target not in context.metadata_was_cleared: + context.metadata_was_cleared[target] = not relfunc.clear_metadata_first + relfunc.func(relation, m, context) def _locales_from_aliases(aliases): diff --git a/test/test_mbjson.py b/test/test_mbjson.py index eefc641ee9..f466fc5450 100644 --- a/test/test_mbjson.py +++ b/test/test_mbjson.py @@ -206,6 +206,46 @@ def test_release_group_rels(self): ]) self.assertEqual(m.getall('~releasegroup_seriesnumber'), ['15', '291']) + def test_release_group_rels_double(self): + m = Metadata() + release_group_to_metadata(self.json_doc['release-group'], m) + + # load it twice and check for duplicates + release_group_to_metadata(self.json_doc['release-group'], m) + self.assertEqual(m.getall('~releasegroup_series'), [ + "Absolute Radio's The 100 Collection", + '1001 Albums You Must Hear Before You Die' + ]) + self.assertEqual(m.getall('~releasegroup_seriesid'), [ + '4bf41050-6fa9-41a6-8398-15bdab4b0352', + '4bc2a338-e1d8-4546-8a61-640da8aaf888' + ]) + self.assertEqual(m.getall('~releasegroup_seriescomment'), [ + '2005 edition' + ]) + self.assertEqual(m.getall('~releasegroup_seriesnumber'), ['15', '291']) + + def test_release_group_rels_removed(self): + m = Metadata() + release_group_to_metadata(self.json_doc['release-group'], m) + + # remove one of the series from original metadata + for i, rel in enumerate(self.json_doc['release-group']['relations']): + if not rel['type'] == 'part of': + continue + if rel['series']['name'] == '1001 Albums You Must Hear Before You Die': + del self.json_doc['release-group']['relations'][i] + break + release_group_to_metadata(self.json_doc['release-group'], m) + self.assertEqual(m.getall('~releasegroup_series'), [ + "Absolute Radio's The 100 Collection", + ]) + self.assertEqual(m.getall('~releasegroup_seriesid'), [ + '4bf41050-6fa9-41a6-8398-15bdab4b0352', + ]) + self.assertEqual(m.getall('~releasegroup_seriescomment'), []) + self.assertEqual(m.getall('~releasegroup_seriesnumber'), ['15']) + class NullReleaseTest(MBJSONTest): From 7d668880c8c47c9e32f1797ec731b9cd15cecb05 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 31 Aug 2023 08:30:14 +0200 Subject: [PATCH 04/12] PICARD-2736: Fix Windows system wide libssl conflicting with bundled libssl Removed an old workaround that no longer applies for current PyQt5 and/or PyInstaller. The OpenSSL DLLs should be kept in the main install folder in order to be prioritized over system libs. --- scripts/package/win-common.ps1 | 4 ---- 1 file changed, 4 deletions(-) diff --git a/scripts/package/win-common.ps1 b/scripts/package/win-common.ps1 index 570b6d2ec6..d6fb096d05 100644 --- a/scripts/package/win-common.ps1 +++ b/scripts/package/win-common.ps1 @@ -36,8 +36,4 @@ Function FinalizePackage { CodeSignBinary (Join-Path $Path picard.exe) CodeSignBinary (Join-Path $Path fpcalc.exe) CodeSignBinary (Join-Path $Path discid.dll) - - # Delete unused files - Remove-Item -Path (Join-Path $Path libcrypto-1_1.dll) - Remove-Item -Path (Join-Path $Path libssl-1_1.dll) } From d71460d511b81b1daa334decca2444ecc290562b Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Wed, 30 Aug 2023 18:37:07 +0200 Subject: [PATCH 05/12] PICARD-2722: Fix pipe recreation race condition on exit Only re-create pipe on Windows if it was not stopped. Otherwise re-creating the pipe can cause the pipe server thread to keep running. --- picard/util/pipe.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/picard/util/pipe.py b/picard/util/pipe.py index fb3d1272d1..45db5349a5 100644 --- a/picard/util/pipe.py +++ b/picard/util/pipe.py @@ -413,8 +413,12 @@ def __create_pipe(self): def __close_pipe(self): if self.__pipe: - win32file.CloseHandle(self.__pipe) + handle = self.__pipe self.__pipe = None + try: + win32file.CloseHandle(handle) + except WinApiError: + log.error('Error closing pipe', exc_info=True) def _sender(self, message: str) -> bool: pipe = win32file.CreateFile( @@ -440,7 +444,6 @@ def _reader(self) -> str: try: win32pipe.ConnectNamedPipe(self.__pipe, None) (exit_code, message) = win32file.ReadFile(self.__pipe, self.__BUFFER_SIZE) - except WinApiError as err: if err.winerror == self.__FILE_NOT_FOUND_ERROR_CODE: # we just keep reopening the pipe, nothing wrong is happening @@ -453,7 +456,8 @@ def _reader(self) -> str: finally: # Pipe was closed when client disconnected, recreate self.__close_pipe() - self.__create_pipe() + if self.pipe_running: + self.__create_pipe() if message is not None: message = message.decode("utf-8") From e14c9a0100be66acf89eb7477e0b9bdb5c552cb2 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Wed, 30 Aug 2023 18:51:33 +0200 Subject: [PATCH 06/12] Avoid pipe error output on startup When starting the initial instance the pipe not being available to write to is an expected condition. Handle this gracefully. --- picard/util/pipe.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/picard/util/pipe.py b/picard/util/pipe.py index 45db5349a5..455571ca55 100644 --- a/picard/util/pipe.py +++ b/picard/util/pipe.py @@ -421,15 +421,23 @@ def __close_pipe(self): log.error('Error closing pipe', exc_info=True) def _sender(self, message: str) -> bool: - pipe = win32file.CreateFile( - self.path, - win32file.GENERIC_READ | win32file.GENERIC_WRITE, - self.__SHARE_MODE, - None, - win32file.OPEN_EXISTING, - self.__FLAGS_AND_ATTRIBUTES, - None - ) + try: + pipe = win32file.CreateFile( + self.path, + win32file.GENERIC_READ | win32file.GENERIC_WRITE, + self.__SHARE_MODE, + None, + win32file.OPEN_EXISTING, + self.__FLAGS_AND_ATTRIBUTES, + None + ) + except WinApiError as err: + # File did not exist, no existing pipe to write to + if err.winerror == self.__FILE_NOT_FOUND_ERROR_CODE: + return False + else: + raise + try: win32file.WriteFile(pipe, str.encode(message)) finally: From 1743e944109f7ac7bdbe4fac84ea334a3c4c5a38 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 8 Sep 2023 08:07:05 +0200 Subject: [PATCH 07/12] PICARD-2724: Move all Qt5 DLLs into main directory on Windows Avoids import errors if a system wide, incompatible Qt5 installation is present on the system. --- scripts/package/win-common.ps1 | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/scripts/package/win-common.ps1 b/scripts/package/win-common.ps1 index d6fb096d05..1e0607d4ab 100644 --- a/scripts/package/win-common.ps1 +++ b/scripts/package/win-common.ps1 @@ -36,4 +36,13 @@ Function FinalizePackage { CodeSignBinary (Join-Path $Path picard.exe) CodeSignBinary (Join-Path $Path fpcalc.exe) CodeSignBinary (Join-Path $Path discid.dll) + + # Move all Qt5 DLLs into the main folder to avoid conflicts with system wide + # versions of those dependencies. Since some version PyInstaller tries to + # maintain the file hierarchy of imported modules, but this easily breaks + # DLL loading on Windows. + # Workaround for https://tickets.metabrainz.org/browse/PICARD-2736 + $Qt5BinDir = (Join-Path $Path PyQt5 Qt5 bin) + Move-Item (Join-Path $Qt5BinDir *.dll) $Path -Force + Remove-Item $Qt5BinDir } From 352ba1b4cbfb933310b6660e629f0ab937caae68 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 10 Sep 2023 15:53:02 +0200 Subject: [PATCH 08/12] PICARD-2733: Fixed crash in save warning dialog with Finnish locale --- po/fi.po | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/po/fi.po b/po/fi.po index cd45905719..8e51a2cbca 100644 --- a/po/fi.po +++ b/po/fi.po @@ -1,7 +1,7 @@ # Translations template for picard. # Copyright (C) 2023 ORGANIZATION # This file is distributed under the same license as the picard project. -# +# # Translators: # Jaakko Perttilä , 2012-2015,2017-2023 # Jaakko Perttilä , 2018 @@ -7518,8 +7518,8 @@ msgstr[1] "siirtää tiedostoja toiseen sijaintiin" #: picard/ui/savewarningdialog.py:54 msgid "You are about to save {file_count:,d} file and this will:" msgid_plural "You are about to save {file_count:,d} files and this will:" -msgstr[0] "Olet aikeissa muuttaa {file_count;d} tiedoston ja täten:" -msgstr[1] "Olet aikeissa muuttaa {file_count;d} tiedostoa ja täten:" +msgstr[0] "Olet aikeissa muuttaa {file_count:,d} tiedoston ja täten:" +msgstr[1] "Olet aikeissa muuttaa {file_count:,d} tiedostoa ja täten:" #: picard/ui/savewarningdialog.py:58 msgid "" From c8a34046ec7aba553211c6c8547f51dc226cfcc7 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 17 Aug 2023 08:40:49 +0200 Subject: [PATCH 09/12] PICARD-2712: Fix sanitize date removing 0 values Sanitize date cleared zero values in between, such turning "YYYY-00-DD" into "YYYY-DD", which would be a completely different date. Fix this by keeping empty values if they are not the last element. --- picard/util/__init__.py | 15 ++++++++++----- test/test_utils.py | 15 +++++++++++++-- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/picard/util/__init__.py b/picard/util/__init__.py index 21d11f77ca..b5a6290e7c 100644 --- a/picard/util/__init__.py +++ b/picard/util/__init__.py @@ -312,18 +312,23 @@ def format_time(ms, display_zero=False): def sanitize_date(datestr): """Sanitize date format. - e.g.: "YYYY-00-00" -> "YYYY" - "YYYY- - " -> "YYYY" + e.g.: "1980-00-00" -> "1980" + "1980- - " -> "1980" + "1980-00-23" -> "1980-00-23" ... """ date = [] - for num in datestr.split("-"): + for num in reversed(datestr.split("-")): try: num = int(num.strip()) except ValueError: - break - if num: + if num == '': + num = 0 + else: + break + if num or (num == 0 and date): date.append(num) + date.reverse() return ("", "%04d", "%04d-%02d", "%04d-%02d-%02d")[len(date)] % tuple(date) diff --git a/test/test_utils.py b/test/test_utils.py index 0077e878a2..95c31ec40c 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -188,15 +188,26 @@ def test_mapping(self): class SanitizeDateTest(PicardTestCase): def test_correct(self): + self.assertEqual(util.sanitize_date(""), "") + self.assertEqual(util.sanitize_date("0"), "") + self.assertEqual(util.sanitize_date("0000"), "") + self.assertEqual(util.sanitize_date("2006"), "2006") self.assertEqual(util.sanitize_date("2006--"), "2006") - self.assertEqual(util.sanitize_date("2006--02"), "2006") + self.assertEqual(util.sanitize_date("2006-00-02"), "2006-00-02") self.assertEqual(util.sanitize_date("2006 "), "2006") self.assertEqual(util.sanitize_date("2006 02"), "") self.assertEqual(util.sanitize_date("2006.02"), "") self.assertEqual(util.sanitize_date("2006-02"), "2006-02") + self.assertEqual(util.sanitize_date("2006-02-00"), "2006-02") + self.assertEqual(util.sanitize_date("2006-00-00"), "2006") + self.assertEqual(util.sanitize_date("2006-02-23"), "2006-02-23") + self.assertEqual(util.sanitize_date("2006-00-23"), "2006-00-23") + self.assertEqual(util.sanitize_date("0000-00-23"), "0000-00-23") + self.assertEqual(util.sanitize_date("0000-02"), "0000-02") + self.assertEqual(util.sanitize_date("--23"), "0000-00-23") def test_incorrect(self): - self.assertNotEqual(util.sanitize_date("2006--02"), "2006-02") + self.assertNotEqual(util.sanitize_date("2006--02"), "2006") self.assertNotEqual(util.sanitize_date("2006.03.02"), "2006-03-02") From d5ec0152fb1ba013da9fcc677d208b5dcd251db2 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Wed, 16 Aug 2023 14:48:26 +0200 Subject: [PATCH 10/12] PICARD-2720: Add .desktop file action to explicitly open new window --- org.musicbrainz.Picard.desktop | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/org.musicbrainz.Picard.desktop b/org.musicbrainz.Picard.desktop index 96d4b5872f..a35b70faca 100644 --- a/org.musicbrainz.Picard.desktop +++ b/org.musicbrainz.Picard.desktop @@ -9,3 +9,8 @@ StartupWMClass=Picard Icon=org.musicbrainz.Picard Categories=AudioVideo;Audio;AudioVideoEditing; MimeType=application/ogg;application/x-flac;audio/aac;audio/ac3;audio/aiff;audio/ape;audio/dsf;audio/flac;audio/midi;audio/mp4;audio/mpeg;audio/mpeg4;audio/mpg;audio/ogg;audio/vorbis;audio/x-aac;audio/x-aiff;audio/x-ape;audio/x-flac;audio/x-flac+ogg;audio/x-m4a;audio/x-midi;audio/x-mp3;audio/x-mpc;audio/x-mpeg;audio/x-ms-wma;audio/x-ms-wmv;audio/x-musepack;audio/x-oggflac;audio/x-speex;audio/x-speex+ogg;audio/x-tak;audio/x-tta;audio/x-vorbis;audio/x-vorbis+ogg;audio/x-wav;audio/x-wavpack;audio/x-wma;video/x-ms-asf;video/x-theora;video/x-wmv; +Actions=new-window; + +[Desktop Action new-window] +Name=New Window +Exec=picard --stand-alone-instance %F From 97a8230479a42aba2e714994d002b0f507b6629f Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 11 Sep 2023 13:01:36 +0200 Subject: [PATCH 11/12] Bump version 2.9.2dev1 --- picard/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picard/__init__.py b/picard/__init__.py index 5535fda3db..07ab90ec5e 100644 --- a/picard/__init__.py +++ b/picard/__init__.py @@ -40,7 +40,7 @@ PICARD_DISPLAY_NAME = "MusicBrainz Picard" PICARD_APP_ID = "org.musicbrainz.Picard" PICARD_DESKTOP_NAME = PICARD_APP_ID + ".desktop" -PICARD_VERSION = Version(2, 9, 1, 'final', 0) +PICARD_VERSION = Version(2, 9, 2, 'dev', 1) # optional build version From 1cbb7a8522f945e602744c6e6aa935ec778b3cce Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 12 Sep 2023 07:06:01 +0200 Subject: [PATCH 12/12] Release 2.9.2 --- NEWS.md | 17 +++++++++++++++++ picard/__init__.py | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 36b9ce147c..54d5378cab 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,20 @@ +# Version 2.9.2 - 2023-09-12 + +## Bugfixes +- [PICARD-2700](https://tickets.metabrainz.org/browse/PICARD-2700) - Content of series variables gets duplicated on each refresh +- [PICARD-2712](https://tickets.metabrainz.org/browse/PICARD-2712) - "00" is always stripped from DATE tag on save +- [PICARD-2722](https://tickets.metabrainz.org/browse/PICARD-2722) - Windows version can crash on exit and prevent restart of Picard +- [PICARD-2724](https://tickets.metabrainz.org/browse/PICARD-2724) - Crash in track search dialog if artist name translation is enabled +- [PICARD-2733](https://tickets.metabrainz.org/browse/PICARD-2733) - Crash when saving files with UI language set to Finnish +- [PICARD-2736](https://tickets.metabrainz.org/browse/PICARD-2736) - Windows: SSL errors if conflicting libssl is installed system wide + +## Tasks +- [PICARD-2752](https://tickets.metabrainz.org/browse/PICARD-2752) - Include resource/images in source archive + +## Improvements +- [PICARD-2720](https://tickets.metabrainz.org/browse/PICARD-2720) - Linux: Allow opening new instance via XDG desktop entry application action + + # Version 2.9.1 - 2023-08-16 ## Bugfixes diff --git a/picard/__init__.py b/picard/__init__.py index 07ab90ec5e..b0d8a6e6a8 100644 --- a/picard/__init__.py +++ b/picard/__init__.py @@ -40,7 +40,7 @@ PICARD_DISPLAY_NAME = "MusicBrainz Picard" PICARD_APP_ID = "org.musicbrainz.Picard" PICARD_DESKTOP_NAME = PICARD_APP_ID + ".desktop" -PICARD_VERSION = Version(2, 9, 2, 'dev', 1) +PICARD_VERSION = Version(2, 9, 2, 'final', 0) # optional build version