Showing with 173 additions and 54 deletions.
  1. +3 −0 MANIFEST.in
  2. +17 −0 NEWS.md
  3. +5 −0 org.musicbrainz.Picard.desktop
  4. +1 −1 picard/__init__.py
  5. +49 −28 picard/mbjson.py
  6. +10 −5 picard/util/__init__.py
  7. +24 −12 picard/util/pipe.py
  8. +3 −3 po/fi.po
  9. +8 −3 scripts/package/win-common.ps1
  10. +40 −0 test/test_mbjson.py
  11. +13 −2 test/test_utils.py
3 changes: 3 additions & 0 deletions MANIFEST.in
Expand Up @@ -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__ *
Expand Down
17 changes: 17 additions & 0 deletions 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
Expand Down
5 changes: 5 additions & 0 deletions org.musicbrainz.Picard.desktop
Expand Up @@ -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
2 changes: 1 addition & 1 deletion picard/__init__.py
Expand Up @@ -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, 'final', 0)


# optional build version
Expand Down
77 changes: 49 additions & 28 deletions picard/mbjson.py
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -246,11 +266,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 = []
Expand All @@ -259,9 +279,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
Expand Down
15 changes: 10 additions & 5 deletions picard/util/__init__.py
Expand Up @@ -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)


Expand Down
36 changes: 24 additions & 12 deletions picard/util/pipe.py
Expand Up @@ -413,19 +413,31 @@ 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(
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:
Expand All @@ -440,7 +452,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
Expand All @@ -453,7 +464,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")
Expand Down
6 changes: 3 additions & 3 deletions 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ä <jormangeud@gmail.com>, 2012-2015,2017-2023
# Jaakko Perttilä <jormangeud@gmail.com>, 2018
Expand Down Expand Up @@ -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 ""
Expand Down
11 changes: 8 additions & 3 deletions scripts/package/win-common.ps1
Expand Up @@ -37,7 +37,12 @@ Function FinalizePackage {
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)
# 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
}
40 changes: 40 additions & 0 deletions test/test_mbjson.py
Expand Up @@ -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):

Expand Down
15 changes: 13 additions & 2 deletions test/test_utils.py
Expand Up @@ -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")


Expand Down