Merge pull request #1263 from phw/PICARD-1586-replaygain-tags

PICARD-1586: ReplayGain 2.0 tags
This commit is contained in:
Philipp Wolfer
2019-09-06 17:05:12 +02:00
committed by GitHub
14 changed files with 394 additions and 22 deletions

View File

@@ -55,6 +55,8 @@ UNSUPPORTED_TAGS = [
'podcasturl',
'show',
'showsort',
'r128_album_gain',
'r128_track_gain',
]
@@ -97,6 +99,13 @@ class APEv2File(File):
"musicbrainz_trackid": "musicbrainz_recordingid",
"musicbrainz_releasetrackid": "musicbrainz_trackid",
"Original Artist": "originalartist",
"REPLAYGAIN_ALBUM_GAIN": "replaygain_album_gain",
"REPLAYGAIN_ALBUM_PEAK": "replaygain_album_peak",
"REPLAYGAIN_ALBUM_RANGE": "replaygain_album_range",
"REPLAYGAIN_TRACK_GAIN": "replaygain_track_gain",
"REPLAYGAIN_TRACK_PEAK": "replaygain_track_peak",
"REPLAYGAIN_TRACK_RANGE": "replaygain_track_range",
"REPLAYGAIN_REFERENCE_LOUDNESS": "replaygain_reference_loudness",
}
__rtranslate = dict([(v, k) for k, v in __translate.items()])

View File

@@ -37,6 +37,7 @@ from picard.formats.id3 import (
image_type_as_id3_num,
types_from_id3,
)
from picard.formats.mutagenext import delall_ci
from picard.metadata import Metadata
from picard.util import encode_filename
@@ -161,8 +162,25 @@ class ASFFile(File):
}
__RTRANS = dict([(b, a) for a, b in __TRANS.items()])
# Tags to load case insensitive
__TRANS_CI = {
'replaygain_album_gain': 'REPLAYGAIN_ALBUM_GAIN',
'replaygain_album_peak': 'REPLAYGAIN_ALBUM_PEAK',
'replaygain_album_range': 'REPLAYGAIN_ALBUM_RANGE',
'replaygain_track_gain': 'REPLAYGAIN_TRACK_GAIN',
'replaygain_track_peak': 'REPLAYGAIN_TRACK_PEAK',
'replaygain_track_range': 'REPLAYGAIN_TRACK_RANGE',
'replaygain_reference_loudness': 'REPLAYGAIN_REFERENCE_LOUDNESS',
}
__RTRANS_CI = dict([(b.lower(), a) for a, b in __TRANS_CI.items()])
def __init__(self, filename):
super().__init__(filename)
self.__casemap = {}
def _load(self, filename):
log.debug("Loading file %r", filename)
self.__casemap = {}
file = ASF(encode_filename(filename))
metadata = Metadata()
for name, values in file.tags.items():
@@ -184,8 +202,6 @@ class ASFFile(File):
else:
metadata.images.append(coverartimage)
continue
elif name not in self.__RTRANS:
continue
elif name == 'WM/SharedUserRating':
# Rating in WMA ranges from 0 to 99, normalize this to the range 0 to 5
@@ -195,7 +211,15 @@ class ASFFile(File):
if len(disc) > 1:
metadata["totaldiscs"] = disc[1]
values[0] = disc[0]
name = self.__RTRANS[name]
name_lower = name.lower()
if name in self.__RTRANS:
name = self.__RTRANS[name]
elif name_lower in self.__RTRANS_CI:
orig_name = name
name = self.__RTRANS_CI[name_lower]
self.__casemap[name] = orig_name
else:
continue
values = [str(value) for value in values if value]
if values:
metadata[name] = values
@@ -224,9 +248,16 @@ class ASFFile(File):
values = [int(values[0]) * 99 // (config.setting['rating_steps'] - 1)]
elif name == 'discnumber' and 'totaldiscs' in metadata:
values = ['%s/%s' % (metadata['discnumber'], metadata['totaldiscs'])]
if name not in self.__TRANS:
if name in self.__TRANS:
name = self.__TRANS[name]
elif name in self.__TRANS_CI:
if name in self.__casemap:
name = self.__casemap[name]
else:
name = self.__TRANS_CI[name]
delall_ci(tags, name)
else:
continue
name = self.__TRANS[name]
tags[name] = values
self._remove_deleted_tags(metadata, tags)
@@ -243,6 +274,7 @@ class ASFFile(File):
@classmethod
def supports_tag(cls, name):
return (name in cls.__TRANS
or name in cls.__TRANS_CI
or name in ('~rating', '~length', 'totaldiscs')
or name.startswith('lyrics'))

View File

@@ -37,7 +37,10 @@ from picard.coverart.image import (
TagCoverArtImage,
)
from picard.file import File
from picard.formats.mutagenext import compatid3
from picard.formats.mutagenext import (
compatid3,
delall_ci,
)
from picard.metadata import Metadata
from picard.util import (
encode_filename,
@@ -177,6 +180,18 @@ class ID3File(File):
__rtranslate_freetext = dict([(v, k) for k, v in __translate_freetext.items()])
__translate_freetext['writer'] = 'writer' # For backward compatibility of case
# Freetext fields that are loaded case-insensitive
__rtranslate_freetext_ci = {
'replaygain_album_gain': 'REPLAYGAIN_ALBUM_GAIN',
'replaygain_album_peak': 'REPLAYGAIN_ALBUM_PEAK',
'replaygain_album_range': 'REPLAYGAIN_ALBUM_RANGE',
'replaygain_track_gain': 'REPLAYGAIN_TRACK_GAIN',
'replaygain_track_peak': 'REPLAYGAIN_TRACK_PEAK',
'replaygain_track_range': 'REPLAYGAIN_TRACK_RANGE',
'replaygain_reference_loudness': 'REPLAYGAIN_REFERENCE_LOUDNESS',
}
__translate_freetext_ci = dict([(b.lower(), a) for a, b in __rtranslate_freetext_ci.items()])
# Obsolete tag names which will still be loaded, but will get renamed on saving
__rename_freetext = {
'Artists': 'ARTISTS',
@@ -202,6 +217,10 @@ class ID3File(File):
'MVIN': re.compile(r'^(?P<movementnumber>\d+)(?:/(?P<movementtotal>\d+))?$')
}
def __init__(self, filename):
super().__init__(filename)
self.__casemap = {}
def build_TXXX(self, encoding, desc, values):
"""Construct and return a TXXX frame."""
# This is here so that plugins can customize the behavior of TXXX
@@ -214,6 +233,7 @@ class ID3File(File):
def _load(self, filename):
log.debug("Loading file %r", filename)
self.__casemap = {}
file = self._get_file(encode_filename(filename))
tags = file.tags or {}
# upgrade custom 2.3 frames to 2.4
@@ -256,9 +276,14 @@ class ID3File(File):
metadata.add('performer:%s' % role, name)
elif frameid == 'TXXX':
name = frame.desc
name_lower = name.lower()
if name in self.__rename_freetext:
name = self.__rename_freetext[name]
if name in self.__translate_freetext:
if name_lower in self.__translate_freetext_ci:
orig_name = name
name = self.__translate_freetext_ci[name_lower]
self.__casemap[name] = orig_name
elif name in self.__translate_freetext:
name = self.__translate_freetext[name]
elif ((name in self.__rtranslate)
!= (name in self.__rtranslate_freetext)):
@@ -370,7 +395,6 @@ class ID3File(File):
tmcl = mutagen.id3.TMCL(encoding=encoding, people=[])
tipl = mutagen.id3.TIPL(encoding=encoding, people=[])
for name, values in metadata.rawitems():
values = [id3text(v, encoding) for v in values]
name = id3text(name, encoding)
@@ -451,6 +475,13 @@ class ID3File(File):
tags.delall('XSOP')
elif frameid == 'TSO2':
tags.delall('TXXX:ALBUMARTISTSORT')
elif name in self.__rtranslate_freetext_ci:
if name in self.__casemap:
description = self.__casemap[name]
else:
description = self.__rtranslate_freetext_ci[name]
delall_ci(tags, 'TXXX:' + description)
tags.add(self.build_TXXX(encoding, description, values))
elif name in self.__rtranslate_freetext:
description = self.__rtranslate_freetext[name]
if description in self.__rrename_freetext:
@@ -542,7 +573,7 @@ class ID3File(File):
@classmethod
def supports_tag(cls, name):
unsupported_tags = {}
unsupported_tags = ['r128_album_gain', 'r128_track_gain']
return ((name and not name.startswith("~") and name not in unsupported_tags)
or name in ("~rating", "~length")
or name.startswith("~id3"))

View File

@@ -31,6 +31,7 @@ from picard.coverart.image import (
TagCoverArtImage,
)
from picard.file import File
from picard.formats.mutagenext import delall_ci
from picard.metadata import Metadata
from picard.util import encode_filename
@@ -124,15 +125,33 @@ class MP4File(File):
}
__r_freeform_tags = dict([(v, k) for k, v in __freeform_tags.items()])
# Tags to load case insensitive
__r_freeform_tags_ci = {
"replaygain_album_gain": "----:com.apple.iTunes:REPLAYGAIN_ALBUM_GAIN",
"replaygain_album_peak": "----:com.apple.iTunes:REPLAYGAIN_ALBUM_PEAK",
"replaygain_album_range": "----:com.apple.iTunes:REPLAYGAIN_ALBUM_RANGE",
"replaygain_track_gain": "----:com.apple.iTunes:REPLAYGAIN_TRACK_GAIN",
"replaygain_track_peak": "----:com.apple.iTunes:REPLAYGAIN_TRACK_PEAK",
"replaygain_track_range": "----:com.apple.iTunes:REPLAYGAIN_TRACK_RANGE",
"replaygain_reference_loudness": "----:com.apple.iTunes:REPLAYGAIN_REFERENCE_LOUDNESS",
}
__freeform_tags_ci = dict([(b.lower(), a) for a, b in __r_freeform_tags_ci.items()])
__other_supported_tags = ("discnumber", "tracknumber",
"totaldiscs", "totaltracks")
def __init__(self, filename):
super().__init__(filename)
self.__casemap = {}
def _load(self, filename):
log.debug("Loading file %r", filename)
self.__casemap = {}
file = MP4(encode_filename(filename))
tags = file.tags or {}
metadata = Metadata()
for name, values in tags.items():
name_lower = name.lower()
if name in self.__text_tags:
for value in values:
metadata.add(self.__text_tags[name], value)
@@ -145,6 +164,12 @@ class MP4File(File):
for value in values:
value = value.decode("utf-8", "replace").strip("\x00")
metadata.add(self.__freeform_tags[name], value)
elif name_lower in self.__freeform_tags_ci:
for value in values:
value = value.decode("utf-8", "replace").strip("\x00")
tag_name = self.__freeform_tags_ci[name_lower]
metadata.add(tag_name, value)
self.__casemap[tag_name] = name
elif name == "----:com.apple.iTunes:fingerprint":
for value in values:
value = value.decode("utf-8", "replace").strip("\x00")
@@ -201,6 +226,14 @@ class MP4File(File):
elif name in self.__r_freeform_tags:
values = [v.encode("utf-8") for v in values]
tags[self.__r_freeform_tags[name]] = values
elif name in self.__r_freeform_tags_ci:
values = [v.encode("utf-8") for v in values]
delall_ci(tags, self.__r_freeform_tags_ci[name])
if name in self.__casemap:
name = self.__casemap[name]
else:
name = self.__r_freeform_tags_ci[name]
tags[name] = values
elif name == "musicip_fingerprint":
tags["----:com.apple.iTunes:fingerprint"] = [b"MusicMagic Fingerprint%s" % v.encode('ascii') for v in values]
@@ -244,6 +277,7 @@ class MP4File(File):
return (name in cls.__r_text_tags
or name in cls.__r_bool_tags
or name in cls.__r_freeform_tags
or name in cls.__r_freeform_tags_ci
or name in cls.__r_int_tags
or name in cls.__other_supported_tags
or name.startswith('lyrics:')
@@ -260,6 +294,8 @@ class MP4File(File):
return self.__r_int_tags[name]
elif name in self.__r_freeform_tags:
return self.__r_freeform_tags[name]
elif name in self.__r_freeform_tags_ci:
return self.__r_freeform_tags_ci[name]
elif name == "musicip_fingerprint":
return "----:com.apple.iTunes:fingerprint"
elif name in ("tracknumber", "totaltracks"):

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
#
# Picard, the next-generation MusicBrainz tagger
# Copyright (C) 2019 Philipp Wolfer
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
def delall_ci(tags, key):
"""Delete all tags with given key, case-insensitive"""
key = key.lower()
for k in list(tags.keys()):
if k.lower() == key:
del tags[k]

View File

@@ -304,7 +304,7 @@ class VCommentFile(File):
@classmethod
def supports_tag(cls, name):
unsupported_tags = {}
unsupported_tags = ['r128_album_gain', 'r128_track_gain']
return (bool(name) and name not in unsupported_tags
and is_valid_key(name))
@@ -360,6 +360,14 @@ class OggOpusFile(VCommentFile):
NAME = "Ogg Opus"
_File = mutagen.oggopus.OggOpus
@classmethod
def supports_tag(cls, name):
if name.startswith('replaygain_'):
return False
elif name.startswith('r128_'):
return True
return VCommentFile.supports_tag(name)
def OggAudioFile(filename):
"""Generic Ogg audio file."""

View File

@@ -96,6 +96,15 @@ TAG_NAMES = {
'musicbrainz_originalartistid': N_('MusicBrainz Original Artist Id'),
'originalalbum': N_('Original Album'),
'musicbrainz_originalalbumid': N_('MusicBrainz Original Release Id'),
'replaygain_album_gain': N_('ReplayGain Album Gain'),
'replaygain_album_peak': N_('ReplayGain Album Peak'),
'replaygain_album_range': N_('ReplayGain Album Range'),
'replaygain_track_gain': N_('ReplayGain Track Gain'),
'replaygain_track_peak': N_('ReplayGain Track Peak'),
'replaygain_track_range': N_('ReplayGain Track Range'),
'replaygain_reference_loudness': N_('ReplayGain Reference Loudness'),
'r128_album_gain': N_('R128 Album Gain'),
'r128_track_gain': N_('R128 Track Gain'),
}
PRESERVED_TAGS = [

View File

@@ -11,6 +11,7 @@ from test.picardtestcase import PicardTestCase
from picard import config
import picard.formats
from picard.formats import ext_to_format
from picard.formats.mutagenext.tak import TAK
from picard.metadata import Metadata
@@ -34,6 +35,8 @@ settings = {
def save_metadata(filename, metadata):
f = picard.formats.open_(filename)
loaded_metadata = f._load(filename)
f._copy_loaded_metadata(loaded_metadata)
f._save(filename, metadata)
@@ -52,7 +55,17 @@ def save_and_load_metadata(filename, metadata):
def load_raw(filename):
return mutagen.File(filename)
f = mutagen.File(filename)
if f is None:
f = mutagen.File(filename, [TAK])
return f
def save_raw(filename, tags):
f = load_raw(filename)
for k, v in tags.items():
f[k] = v
f.save()
TAGS = {
@@ -130,6 +143,16 @@ TAGS = {
'work': 'Foo'
}
REPLAYGAIN_TAGS = {
'replaygain_album_gain': '-6.48 dB',
'replaygain_album_peak': '0.978475',
'replaygain_album_range': '7.84 dB',
'replaygain_track_gain': '-6.16 dB',
'replaygain_track_peak': '0.976991',
'replaygain_track_range': '8.22 dB',
'replaygain_reference_loudness': '-18.00 LUFS',
}
def skipUnlessTestfile(func):
def _decorator(self, *args, **kwargs):
@@ -190,6 +213,8 @@ class CommonTests:
def setUp(self):
super().setUp()
self.tags = TAGS.copy()
self.replaygain_tags = REPLAYGAIN_TAGS.copy()
self.unsupported_tags = {}
self.setup_tags()
def setup_tags(self):
@@ -208,9 +233,26 @@ class CommonTests:
@skipUnlessTestfile
def test_simple_tags(self):
metadata = Metadata(self.tags)
loaded_metadata = save_and_load_metadata(self.filename, metadata)
for (key, value) in self.tags.items():
self._test_supported_tags(self.tags)
@skipUnlessTestfile
def test_replaygain_tags(self):
self._test_supported_tags(self.replaygain_tags)
@skipUnlessTestfile
def test_replaygain_tags_case_insensitive(self):
tags = {
'replaygain_album_gain': '-6.48 dB',
'Replaygain_Album_Peak': '0.978475',
'replaygain_album_range': '7.84 dB',
'replaygain_track_gain': '-6.16 dB',
'replaygain_track_peak': '0.976991',
'replaygain_track_range': '8.22 dB',
'replaygain_reference_loudness': '-18.00 LUFS',
}
save_raw(self.filename, tags)
loaded_metadata = load_metadata(self.filename)
for (key, value) in self.replaygain_tags.items():
self.assertEqual(loaded_metadata[key], value, '%s: %r != %r' % (key, loaded_metadata[key], value))
@skipUnlessTestfile
@@ -225,10 +267,7 @@ class CommonTests:
@skipUnlessTestfile
def test_unsupported_tags(self):
metadata = Metadata(self.unsupported_tags)
loaded_metadata = save_and_load_metadata(self.filename, metadata)
for tag in self.unsupported_tags:
self.assertTrue(tag not in loaded_metadata, '%s: %r != None' % (tag, loaded_metadata[tag]))
self._test_unsupported_tags(self.unsupported_tags)
@skipUnlessTestfile
def test_preserve_unchanged_tags(self):
@@ -328,3 +367,15 @@ class CommonTests:
self.assertEqual(f._fixed_splitext(f.EXTENSIONS[0]), ('', f.EXTENSIONS[0]))
self.assertEqual(f._fixed_splitext('.test'), os.path.splitext('.test'))
self.assertNotEqual(f._fixed_splitext(f.EXTENSIONS[0]), os.path.splitext(f.EXTENSIONS[0]))
def _test_supported_tags(self, tags):
metadata = Metadata(tags)
loaded_metadata = save_and_load_metadata(self.filename, metadata)
for (key, value) in tags.items():
self.assertEqual(loaded_metadata[key], value, '%s: %r != %r' % (key, loaded_metadata[key], value))
def _test_unsupported_tags(self, tags):
metadata = Metadata(tags)
loaded_metadata = save_and_load_metadata(self.filename, metadata)
for tag in tags:
self.assertTrue(tag not in loaded_metadata, '%s: %r != None' % (tag, loaded_metadata[tag]))

View File

@@ -36,6 +36,11 @@ SUPPORTED_TAGS = list(set(TAGS.keys()) - set(apev2.UNSUPPORTED_TAGS))
class CommonApeTests:
class ApeTestCase(CommonTests.TagFormatsTestCase):
def setup_tags(self):
super().setup_tags()
self.unsupported_tags['r128_album_gain'] = '-2857'
self.unsupported_tags['r128_track_gain'] = '-2857'
def test_supports_tags(self):
supports_tag = self.format.supports_tag
for key in VALID_KEYS + SUPPORTED_TAGS:

View File

@@ -3,13 +3,58 @@ from test.picardtestcase import (
create_fake_png,
)
from picard.formats import asf
from picard.formats import (
asf,
ext_to_format,
)
from .common import CommonTests
from .common import (
CommonTests,
load_metadata,
load_raw,
save_metadata,
save_raw,
skipUnlessTestfile,
)
from .coverart import CommonCoverArtTests
class ASFTest(CommonTests.TagFormatsTestCase):
# prevent unittest to run tests in those classes
class CommonAsfTests:
class AsfTestCase(CommonTests.TagFormatsTestCase):
def test_supports_tag(self):
fmt = ext_to_format(self.testfile_ext[1:])
self.assertTrue(fmt.supports_tag('copyright'))
self.assertTrue(fmt.supports_tag('compilation'))
self.assertTrue(fmt.supports_tag('bpm'))
self.assertTrue(fmt.supports_tag('djmixer'))
self.assertTrue(fmt.supports_tag('discnumber'))
self.assertTrue(fmt.supports_tag('lyrics:lead'))
self.assertTrue(fmt.supports_tag('~length'))
for tag in self.replaygain_tags.keys():
self.assertTrue(fmt.supports_tag(tag))
@skipUnlessTestfile
def test_ci_tags_preserve_case(self):
# Ensure values are not duplicated on repeated save and are saved
# case preserving.
tags = {
'Replaygain_Album_Peak': '-6.48 dB'
}
save_raw(self.filename, tags)
loaded_metadata = load_metadata(self.filename)
loaded_metadata['replaygain_album_peak'] = '1.0'
save_metadata(self.filename, loaded_metadata)
raw_metadata = load_raw(self.filename)
self.assertIn('Replaygain_Album_Peak', raw_metadata)
self.assertEqual(raw_metadata['Replaygain_Album_Peak'][0], loaded_metadata['replaygain_album_peak'])
self.assertEqual(1, len(raw_metadata['Replaygain_Album_Peak']))
self.assertNotIn('REPLAYGAIN_ALBUM_PEAK', raw_metadata)
class ASFTest(CommonAsfTests.AsfTestCase):
testfile = 'test.asf'
supports_ratings = True
expected_info = {
@@ -20,7 +65,7 @@ class ASFTest(CommonTests.TagFormatsTestCase):
}
class WMATest(CommonTests.TagFormatsTestCase):
class WMATest(CommonAsfTests.AsfTestCase):
testfile = 'test.wma'
supports_ratings = True
expected_info = {

View File

@@ -1,5 +1,7 @@
import os.path
import mutagen
from test.picardtestcase import PicardTestCase
from picard import config
@@ -12,6 +14,7 @@ from .common import (
load_raw,
save_and_load_metadata,
save_metadata,
save_raw,
skipUnlessTestfile,
)
from .coverart import CommonCoverArtTests
@@ -28,6 +31,8 @@ class CommonId3Tests:
self.set_tags({
'originaldate': '1980'
})
self.unsupported_tags['r128_album_gain'] = '-2857'
self.unsupported_tags['r128_track_gain'] = '-2857'
@skipUnlessTestfile
def test_id3_freeform_delete(self):
@@ -191,6 +196,39 @@ class CommonId3Tests:
config.setting['write_id3v23'] = True
self.test_preserve_unchanged_tags()
@skipUnlessTestfile
def test_replaygain_tags_case_insensitive(self):
tags = mutagen.id3.ID3Tags()
tags.add(mutagen.id3.TXXX(desc='replaygain_album_gain', text='-6.48 dB'))
tags.add(mutagen.id3.TXXX(desc='Replaygain_Album_Peak', text='0.978475'))
tags.add(mutagen.id3.TXXX(desc='replaygain_album_range', text='7.84 dB'))
tags.add(mutagen.id3.TXXX(desc='replaygain_track_gain', text='-6.16 dB'))
tags.add(mutagen.id3.TXXX(desc='REPLAYGAIN_track_peak', text='0.976991'))
tags.add(mutagen.id3.TXXX(desc='REPLAYGAIN_TRACK_RANGE', text='8.22 dB'))
tags.add(mutagen.id3.TXXX(desc='replaygain_reference_loudness', text='-18.00 LUFS'))
save_raw(self.filename, tags)
loaded_metadata = load_metadata(self.filename)
for (key, value) in self.replaygain_tags.items():
self.assertEqual(loaded_metadata[key], value, '%s: %r != %r' % (key, loaded_metadata[key], value))
@skipUnlessTestfile
def test_ci_tags_preserve_case(self):
# Ensure values are not duplicated on repeated save and are saved
# case preserving.
tags = mutagen.id3.ID3Tags()
tags.add(mutagen.id3.TXXX(desc='Replaygain_Album_Peak', text='0.978475'))
save_raw(self.filename, tags)
loaded_metadata = load_metadata(self.filename)
loaded_metadata['replaygain_album_peak'] = '1.0'
save_metadata(self.filename, loaded_metadata)
raw_metadata = load_raw(self.filename)
self.assertIn('TXXX:Replaygain_Album_Peak', raw_metadata)
self.assertEqual(
raw_metadata['TXXX:Replaygain_Album_Peak'].text[0],
loaded_metadata['replaygain_album_peak'])
self.assertEqual(1, len(raw_metadata['TXXX:Replaygain_Album_Peak'].text))
self.assertNotIn('TXXX:REPLAYGAIN_ALBUM_PEAK', raw_metadata)
class MP3Test(CommonId3Tests.Id3TestCase):
testfile = 'test.mp3'

View File

@@ -1,8 +1,14 @@
import mutagen
from picard.formats import ext_to_format
from .common import (
CommonTests,
load_metadata,
load_raw,
save_metadata,
save_raw,
skipUnlessTestfile,
)
from .coverart import CommonCoverArtTests
@@ -27,11 +33,46 @@ class MP4Test(CommonTests.TagFormatsTestCase):
self.assertTrue(fmt.supports_tag('discnumber'))
self.assertTrue(fmt.supports_tag('lyrics:lead'))
self.assertTrue(fmt.supports_tag('~length'))
for tag in self.replaygain_tags.keys():
self.assertTrue(fmt.supports_tag(tag))
def test_format(self):
metadata = load_metadata(self.filename)
self.assertIn('AAC LC', metadata['~format'])
@skipUnlessTestfile
def test_replaygain_tags_case_insensitive(self):
tags = mutagen.mp4.MP4Tags()
tags['----:com.apple.iTunes:replaygain_album_gain'] = [b'-6.48 dB']
tags['----:com.apple.iTunes:Replaygain_Album_Peak'] = [b'0.978475']
tags['----:com.apple.iTunes:replaygain_album_range'] = [b'7.84 dB']
tags['----:com.apple.iTunes:replaygain_track_gain'] = [b'-6.16 dB']
tags['----:com.apple.iTunes:REPLAYGAIN_track_peak'] = [b'0.976991']
tags['----:com.apple.iTunes:REPLAYGAIN_TRACK_RANGE'] = [b'8.22 dB']
tags['----:com.apple.iTunes:replaygain_reference_loudness'] = [b'-18.00 LUFS']
save_raw(self.filename, tags)
loaded_metadata = load_metadata(self.filename)
for (key, value) in self.replaygain_tags.items():
self.assertEqual(loaded_metadata[key], value, '%s: %r != %r' % (key, loaded_metadata[key], value))
@skipUnlessTestfile
def test_ci_tags_preserve_case(self):
# Ensure values are not duplicated on repeated save and are saved
# case preserving.
tags = mutagen.mp4.MP4Tags()
tags['----:com.apple.iTunes:Replaygain_Album_Peak'] = [b'-6.48 dB']
save_raw(self.filename, tags)
loaded_metadata = load_metadata(self.filename)
loaded_metadata['replaygain_album_peak'] = '1.0'
save_metadata(self.filename, loaded_metadata)
raw_metadata = load_raw(self.filename)
self.assertIn('----:com.apple.iTunes:Replaygain_Album_Peak', raw_metadata)
self.assertEqual(
raw_metadata['----:com.apple.iTunes:Replaygain_Album_Peak'][0].decode('utf-8'),
loaded_metadata['replaygain_album_peak'])
self.assertEqual(1, len(raw_metadata['----:com.apple.iTunes:Replaygain_Album_Peak']))
self.assertNotIn('----:com.apple.iTunes:REPLAYGAIN_ALBUM_PEAK', raw_metadata)
class Mp4CoverArtTest(CommonCoverArtTests.CoverArtTestCase):
testfile = 'test.m4a'

View File

@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
from test.picardtestcase import PicardTestCase
from picard.formats import mutagenext
class MutagenExtTest(PicardTestCase):
def test_delall_ci(self):
tags = {
'TAGNAME:ABC': 'a',
'tagname:abc': 'a',
'TagName:Abc': 'a',
'OtherTag': 'a'
}
mutagenext.delall_ci(tags, 'tagname:Abc')
self.assertEqual({'OtherTag': 'a'}, tags)

View File

@@ -11,6 +11,7 @@ from picard.coverart.image import CoverArtImage
from picard.formats import vorbis
from .common import (
REPLAYGAIN_TAGS,
TAGS,
CommonTests,
load_metadata,
@@ -56,6 +57,15 @@ class CommonVorbisTests:
for key in INVALID_KEYS + ['']:
self.assertFalse(supports_tag(key), '%r should be unsupported' % key)
@skipUnlessTestfile
def test_r128_replaygain_tags(self):
# Vorbis files other then Opus must not support the r128_* tags
tags = {
'r128_album_gain': '-2857',
'r128_track_gain': '-2857',
}
self._test_unsupported_tags(tags)
class FLACTest(CommonVorbisTests.VorbisTestCase):
testfile = 'test.flac'
@@ -103,6 +113,20 @@ class OggOpusTest(CommonVorbisTests.VorbisTestCase):
'~channels': '2',
}
@skipUnlessTestfile
def test_replaygain_tags(self):
# The normal replaygain tags are not supported by Opus
tags = REPLAYGAIN_TAGS.copy()
self._test_unsupported_tags(tags)
@skipUnlessTestfile
def test_r128_replaygain_tags(self):
tags = {
'r128_album_gain': '-2857',
'r128_track_gain': '-2857',
}
self._test_supported_tags(tags)
class VorbisUtilTest(PicardTestCase):
def test_sanitize_key(self):