diff --git a/beets/mediafile.py b/beets/mediafile.py index ec14f99e9..4c9070792 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -930,7 +930,18 @@ class MediaFile(object): if self.mgfile.tags is None: self.mgfile.add_tags() - def save(self): + def save(self, id3v23=False): + """Write the object's tags back to the file. + + By default, MP3 files are saved with ID3v2.4 tags. You can use + the older ID3v2.3 standard by specifying the `id3v23` option. + """ + if id3v23 and self.type == 'mp3': + id3 = self.mgfile + if hasattr(id3, 'tags'): + # In case this is an MP3 object, not an ID3 object. + id3 = id3.tags + id3.update_to_v23() self.mgfile.save() def delete(self): diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 71ebba3c4..c14b64ac4 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -21,6 +21,7 @@ import _common from _common import unittest import beets.mediafile + class EdgeTest(unittest.TestCase): def test_emptylist(self): # Some files have an ID3 frame that has a list with no elements. @@ -67,6 +68,7 @@ class EdgeTest(unittest.TestCase): f = beets.mediafile.MediaFile(os.path.join(_common.RSRC, 'oldape.ape')) self.assertEqual(f.bitrate, 0) + _sc = beets.mediafile._safe_cast class InvalidValueToleranceTest(unittest.TestCase): def test_packed_integer_with_extra_chars(self): @@ -110,6 +112,7 @@ class InvalidValueToleranceTest(unittest.TestCase): self.assertTrue(isinstance(us, unicode)) self.assertTrue(us.startswith(u'caf')) + class SafetyTest(unittest.TestCase): def _exccheck(self, fn, exc, data=''): fn = os.path.join(_common.RSRC, fn) @@ -156,6 +159,7 @@ class SafetyTest(unittest.TestCase): finally: os.unlink(fn) + class SideEffectsTest(unittest.TestCase): def setUp(self): self.empty = os.path.join(_common.RSRC, 'empty.mp3') @@ -166,6 +170,7 @@ class SideEffectsTest(unittest.TestCase): new_mtime = os.stat(self.empty).st_mtime self.assertEqual(old_mtime, new_mtime) + class EncodingTest(unittest.TestCase): def setUp(self): src = os.path.join(_common.RSRC, 'full.m4a') @@ -183,10 +188,13 @@ class EncodingTest(unittest.TestCase): new_mf = beets.mediafile.MediaFile(self.path) self.assertEqual(new_mf.label, u'foo\xe8bar') + class ZeroLengthMediaFile(beets.mediafile.MediaFile): @property def length(self): return 0.0 + + class MissingAudioDataTest(unittest.TestCase): def setUp(self): super(MissingAudioDataTest, self).setUp() @@ -197,6 +205,7 @@ class MissingAudioDataTest(unittest.TestCase): del self.mf.mgfile.info.bitrate # Not available directly. self.assertEqual(self.mf.bitrate, 0) + class TypeTest(unittest.TestCase): def setUp(self): super(TypeTest, self).setUp() @@ -223,6 +232,7 @@ class TypeTest(unittest.TestCase): self.mf.track = None self.assertEqual(self.mf.track, 0) + class SoundCheckTest(unittest.TestCase): def test_round_trip(self): data = beets.mediafile._sc_encode(1.0, 1.0) @@ -242,8 +252,51 @@ class SoundCheckTest(unittest.TestCase): self.assertEqual(gain, 0.0) self.assertEqual(peak, 0.0) + +class ID3v23Test(unittest.TestCase): + def _make_test(self, ext='mp3'): + src = os.path.join(_common.RSRC, 'full.{0}'.format(ext)) + self.path = os.path.join(_common.RSRC, 'test.{0}'.format(ext)) + shutil.copy(src, self.path) + return beets.mediafile.MediaFile(self.path) + + def _delete_test(self): + os.remove(self.path) + + def test_v24_year_tag(self): + mf = self._make_test() + try: + mf.year = 2013 + mf.save(id3v23=False) + frame = mf.mgfile['TDRC'] + self.assertTrue('2013' in str(frame)) + self.assertTrue('TYER' not in mf.mgfile) + finally: + self._delete_test() + + def test_v23_year_tag(self): + mf = self._make_test() + try: + mf.year = 2013 + mf.save(id3v23=True) + frame = mf.mgfile['TYER'] + self.assertTrue('2013' in str(frame)) + self.assertTrue('TDRC' not in mf.mgfile) + finally: + self._delete_test() + + def test_v23_on_non_mp3_is_noop(self): + mf = self._make_test('m4a') + try: + mf.year = 2013 + mf.save(id3v23=True) + finally: + self._delete_test() + + def suite(): return unittest.TestLoader().loadTestsFromName(__name__) + if __name__ == '__main__': unittest.main(defaultTest='suite')