diff --git a/docs/conf.py b/docs/conf.py index 4ab1e0c..62578b1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -17,8 +17,8 @@ copyright = '2016, the beets project' author = 'the beets project' -version = '0.5' -release = '0.5.0' +version = '0.9' +release = '0.9.0' pygments_style = 'sphinx' htmlhelp_basename = 'mediafiledoc' diff --git a/docs/index.rst b/docs/index.rst index 15461b0..b8226f2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -30,7 +30,7 @@ The metadata schema is generally based on MusicBrainz' schema with similar namin * ``lyrics``, ``copyright``, ``url`` * calculated metadata like ``bpm`` (beats per minute) and ``r128_track_gain`` (ReplayGain), * embedded images (e.g. album art), -* file metadata like ``bitrate`` and ``length``. +* file metadata like ``samplerate``, ``bitdepth``, ``channels``, ``bitrate``, ``bitrate_mode``, ``encoder_info``, ``encoder_settings`` and ``length``. Compatibility ------------- @@ -135,6 +135,12 @@ To copy tags from one MediaFile to another: Changelog --------- +v0.9.0 +'''''' + +- Add the properties ``bitrate_mode``, ``encoder_info`` and + ``encoder_settings``. + v0.8.1 '''''' diff --git a/mediafile.py b/mediafile.py index 4d3f936..ca3747d 100644 --- a/mediafile.py +++ b/mediafile.py @@ -37,6 +37,7 @@ import mutagen import mutagen.id3 +import mutagen.mp3 import mutagen.mp4 import mutagen.flac import mutagen.asf @@ -58,7 +59,7 @@ import traceback -__version__ = '0.8.1' +__version__ = '0.9.0' __all__ = ['UnreadableFileError', 'FileTypeError', 'MediaFile'] log = logging.getLogger(__name__) @@ -1721,7 +1722,8 @@ def readable_fields(cls): for property in cls.fields(): yield property for property in ('length', 'samplerate', 'bitdepth', 'bitrate', - 'channels', 'format'): + 'bitrate_mode', 'channels', 'encoder_info', + 'encoder_settings', 'format'): yield property @classmethod @@ -2326,6 +2328,42 @@ def bitrate(self): return 0 return int(self.filesize * 8 / self.length) + @property + def bitrate_mode(self): + """The mode of the bitrate used in the audio coding + (a string, eg. "CBR", "VBR" or "ABR"). + Only available for the MP3 file format (empty where unavailable). + """ + if hasattr(self.mgfile.info, 'bitrate_mode'): + return { + mutagen.mp3.BitrateMode.CBR: 'CBR', + mutagen.mp3.BitrateMode.VBR: 'VBR', + mutagen.mp3.BitrateMode.ABR: 'ABR', + }.get(self.mgfile.info.bitrate_mode, '') + else: + return '' + + @property + def encoder_info(self): + """The name and/or version of the encoder used + (a string, eg. "LAME 3.97.0"). + Only available for some formats (empty where unavailable). + """ + if hasattr(self.mgfile.info, 'encoder_info'): + return self.mgfile.info.encoder_info + else: + return '' + + @property + def encoder_settings(self): + """A guess of the settings used for the encoder (a string, eg. "-V2"). + Only available for the MP3 file format (empty where unavailable). + """ + if hasattr(self.mgfile.info, 'encoder_settings'): + return self.mgfile.info.encoder_settings + else: + return '' + @property def format(self): """A string describing the file format/codec.""" diff --git a/test/rsrc/cbr.mp3 b/test/rsrc/cbr.mp3 new file mode 100644 index 0000000..5edf46a Binary files /dev/null and b/test/rsrc/cbr.mp3 differ diff --git a/test/test_mediafile.py b/test/test_mediafile.py index f6aba58..acaa6c3 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -761,6 +761,9 @@ class MP3Test(ReadWriteTestBase, PartialTestMixin, audio_properties = { 'length': 1.0, 'bitrate': 80000, + 'bitrate_mode': '', + 'encoder_info': '', + 'encoder_settings': '', 'format': 'MP3', 'samplerate': 44100, 'bitdepth': 0, @@ -771,6 +774,18 @@ def test_unknown_apic_type(self): mediafile = self._mediafile_fixture('image_unknown_type') self.assertEqual(mediafile.images[0].type, ImageType.other) + def test_bitrate_mode(self): + mediafile = self._mediafile_fixture('cbr') + self.assertEqual(mediafile.bitrate_mode, 'CBR') + + def test_encoder_info(self): + mediafile = self._mediafile_fixture('cbr') + self.assertEqual(mediafile.encoder_info, 'LAME 3.100.0+') + + def test_encoder_settings(self): + mediafile = self._mediafile_fixture('cbr') + self.assertEqual(mediafile.encoder_settings, '-b 80') + class MP4Test(ReadWriteTestBase, PartialTestMixin, ImageStructureTestMixin, unittest.TestCase): @@ -778,6 +793,9 @@ class MP4Test(ReadWriteTestBase, PartialTestMixin, audio_properties = { 'length': 1.0, 'bitrate': 64000, + 'bitrate_mode': '', + 'encoder_info': '', + 'encoder_settings': '', 'format': 'AAC', 'samplerate': 44100, 'bitdepth': 16, @@ -799,6 +817,9 @@ class AlacTest(ReadWriteTestBase, unittest.TestCase): audio_properties = { 'length': 1.0, 'bitrate': 21830, + 'bitrate_mode': '', + 'encoder_info': '', + 'encoder_settings': '', # 'format': 'ALAC', 'samplerate': 44100, 'bitdepth': 16, @@ -811,6 +832,9 @@ class MusepackTest(ReadWriteTestBase, unittest.TestCase): audio_properties = { 'length': 1.0, 'bitrate': 24023, + 'bitrate_mode': '', + 'encoder_info': '', + 'encoder_settings': '', 'format': u'Musepack', 'samplerate': 44100, 'bitdepth': 0, @@ -824,6 +848,9 @@ class WMATest(ReadWriteTestBase, ExtendedImageStructureTestMixin, audio_properties = { 'length': 1.0, 'bitrate': 128000, + 'bitrate_mode': '', + 'encoder_info': '', + 'encoder_settings': '', 'format': u'Windows Media', 'samplerate': 44100, 'bitdepth': 0, @@ -852,6 +879,9 @@ class OggTest(ReadWriteTestBase, ExtendedImageStructureTestMixin, audio_properties = { 'length': 1.0, 'bitrate': 48000, + 'bitrate_mode': '', + 'encoder_info': '', + 'encoder_settings': '', 'format': u'OGG', 'samplerate': 44100, 'bitdepth': 0, @@ -896,6 +926,9 @@ class FlacTest(ReadWriteTestBase, PartialTestMixin, audio_properties = { 'length': 1.0, 'bitrate': 108688, + 'bitrate_mode': '', + 'encoder_info': '', + 'encoder_settings': '', 'format': u'FLAC', 'samplerate': 44100, 'bitdepth': 16, @@ -909,6 +942,9 @@ class ApeTest(ReadWriteTestBase, ExtendedImageStructureTestMixin, audio_properties = { 'length': 1.0, 'bitrate': 112608, + 'bitrate_mode': '', + 'encoder_info': '', + 'encoder_settings': '', 'format': u'APE', 'samplerate': 44100, 'bitdepth': 16, @@ -921,6 +957,9 @@ class WavpackTest(ReadWriteTestBase, unittest.TestCase): audio_properties = { 'length': 1.0, 'bitrate': 109312, + 'bitrate_mode': '', + 'encoder_info': '', + 'encoder_settings': '', 'format': u'WavPack', 'samplerate': 44100, 'bitdepth': 16 if mutagen.version >= (1, 45, 0) else 0, @@ -933,6 +972,9 @@ class OpusTest(ReadWriteTestBase, unittest.TestCase): audio_properties = { 'length': 1.0, 'bitrate': 66792, + 'bitrate_mode': '', + 'encoder_info': '', + 'encoder_settings': '', 'format': u'Opus', 'samplerate': 48000, 'bitdepth': 0, @@ -945,6 +987,9 @@ class AIFFTest(ReadWriteTestBase, unittest.TestCase): audio_properties = { 'length': 1.0, 'bitrate': 705600, + 'bitrate_mode': '', + 'encoder_info': '', + 'encoder_settings': '', 'format': u'AIFF', 'samplerate': 44100, 'bitdepth': 16, @@ -957,6 +1002,9 @@ class WAVETest(ReadWriteTestBase, unittest.TestCase): audio_properties = { 'length': 1.0, 'bitrate': 88200, + 'bitrate_mode': '', + 'encoder_info': '', + 'encoder_settings': '', 'format': u'WAVE', 'samplerate': 44100, 'bitdepth': 16, @@ -1033,6 +1081,9 @@ class DSFTest(ReadWriteTestBase, unittest.TestCase): audio_properties = { 'length': 0.01, 'bitrate': 11289600, + 'bitrate_mode': '', + 'encoder_info': '', + 'encoder_settings': '', 'format': u'DSD Stream File', 'samplerate': 5644800, 'bitdepth': 1,