From 4d85c2e31fc0fe9dfd71f9e2989bf4a699bfa7be Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Wed, 4 Sep 2019 18:51:03 +0200 Subject: [PATCH] PICARD-1589: Support language for ID3 comments. Adds syntax "comment:{language}:{description}" in addition to existing "comment:{description}" for comment tag names. --- picard/formats/id3.py | 16 +++++++++++----- picard/util/tags.py | 21 ++++++++++++++++++++- test/formats/common.py | 1 + test/formats/test_id3.py | 4 ++++ test/test_util_tags.py | 18 ++++++++++++++++++ 5 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 test/test_util_tags.py diff --git a/picard/formats/id3.py b/picard/formats/id3.py index 604fb05c0..6134e6d83 100644 --- a/picard/formats/id3.py +++ b/picard/formats/id3.py @@ -46,6 +46,7 @@ from picard.util import ( encode_filename, sanitize_date, ) +from picard.util.tags import parse_comment_tag id3.GRP1 = compatid3.GRP1 @@ -254,7 +255,11 @@ class ID3File(File): elif frameid == 'COMM': for text in frame.text: if text: - metadata.add('%s:%s' % (name, frame.desc), text) + if frame.lang == 'eng': + name = '%s:%s' % (name, frame.desc) + else: + name = '%s:%s:%s' % (name, frame.lang, frame.desc) + metadata.add(name, text) else: metadata.add(name, frame) elif frameid == 'TIT1': @@ -411,12 +416,12 @@ class ID3File(File): else: tmcl.people.append([role, value]) elif name.startswith('comment:'): - desc = name.split(':', 1)[1] + (lang, desc) = parse_comment_tag(name) if desc.lower()[:4] == 'itun': tags.delall('COMM:' + desc) tags.add(id3.COMM(encoding=0, desc=desc, lang='eng', text=[v + '\x00' for v in values])) else: - tags.add(id3.COMM(encoding=encoding, desc=desc, lang='eng', text=values)) + tags.add(id3.COMM(encoding=encoding, desc=desc, lang=lang, text=values)) elif name.startswith('lyrics:') or name == 'lyrics': if ':' in name: desc = name.split(':', 1)[1] @@ -528,10 +533,11 @@ class ID3File(File): if people[0] == role: frame.people.remove(people) elif name.startswith('comment:'): - desc = name.split(':', 1)[1] + (lang, desc) = parse_comment_tag(name) if desc.lower()[:4] != 'itun': for key, frame in list(tags.items()): - if frame.FrameID == 'COMM' and frame.desc == desc: + if (frame.FrameID == 'COMM' and frame.desc == desc + and frame.lang == lang): del tags[key] elif name.startswith('lyrics:') or name == 'lyrics': if ':' in name: diff --git a/picard/util/tags.py b/picard/util/tags.py index 201ac65d1..10d7ffde6 100644 --- a/picard/util/tags.py +++ b/picard/util/tags.py @@ -17,6 +17,9 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +import re + + TAG_NAMES = { 'acoustid_fingerprint': N_('AcoustID Fingerprint'), 'acoustid_id': N_('AcoustID'), @@ -32,7 +35,7 @@ TAG_NAMES = { 'barcode': N_('Barcode'), 'bpm': N_('BPM'), 'catalognumber': N_('Catalog Number'), - 'comment:': N_('Comment'), + 'comment': N_('Comment'), 'compilation': N_('Compilation (iTunes)'), 'composer': N_('Composer'), 'composersort': N_('Composer Sort Order'), @@ -127,3 +130,19 @@ def display_tag_name(name): if desc: return '%s [%s]' % (_(TAG_NAMES.get(name, name)), desc) return _(TAG_NAMES.get(name, name)) + + +RE_COMMENT_LANG = re.compile('^([a-zA-Z]{3}):') +def parse_comment_tag(name): # noqa: E302 + """ + Parses a tag name like "comment:XXX:desc", where XXX is the language. + If language is not set ("comment:desc") "eng" is assumed as default. + Returns a (lang, desc) tuple. + """ + desc = name.split(':', 1)[1] + lang = 'eng' + match = RE_COMMENT_LANG.match(desc) + if match: + lang = match.group(1) + desc = desc[4:] + return (lang, desc) diff --git a/test/formats/common.py b/test/formats/common.py index 25d7ecf66..114420341 100644 --- a/test/formats/common.py +++ b/test/formats/common.py @@ -87,6 +87,7 @@ TAGS = { 'catalognumber': 'Foo', 'comment:': 'Foo', 'comment:foo': 'Foo', + 'comment:deu:foo': 'Foo', 'compilation': '1', 'composer': 'Foo', 'composersort': 'Foo', diff --git a/test/formats/test_id3.py b/test/formats/test_id3.py index a058cd557..07f0fcd39 100644 --- a/test/formats/test_id3.py +++ b/test/formats/test_id3.py @@ -101,14 +101,18 @@ class CommonId3Tests: def test_comment_delete(self): metadata = Metadata(self.tags) metadata['comment:bar'] = 'Foo' + metadata['comment:XXX:withlang'] = 'Foo' original_metadata = save_and_load_metadata(self.filename, metadata) del metadata['comment:bar'] + del metadata['comment:XXX:withlang'] new_metadata = save_and_load_metadata(self.filename, metadata) self.assertIn('comment:foo', original_metadata) self.assertIn('comment:bar', original_metadata) + self.assertIn('comment:XXX:withlang', original_metadata) self.assertIn('comment:foo', new_metadata) self.assertNotIn('comment:bar', new_metadata) + self.assertNotIn('comment:XXX:withlang', new_metadata) @skipUnlessTestfile def test_id3v23_simple_tags(self): diff --git a/test/test_util_tags.py b/test/test_util_tags.py new file mode 100644 index 000000000..f6dc33604 --- /dev/null +++ b/test/test_util_tags.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from test.picardtestcase import PicardTestCase + +from picard.util.tags import ( + display_tag_name, + parse_comment_tag, +) + + +class UtilTagsTest(PicardTestCase): + def test_display_tag_name(self): + self.assertEqual('Artist', display_tag_name('artist')) + self.assertEqual('Lyrics', display_tag_name('lyrics:')) + self.assertEqual('Comment [Foo]', display_tag_name('comment:Foo')) + + def test_parse_comment_tag(self): + self.assertEqual(('XXX', 'foo'), parse_comment_tag('comment:XXX:foo')) + self.assertEqual(('eng', 'foo'), parse_comment_tag('comment:foo'))