Skip to content

Commit 24227d6

Browse files
committed
mediafile: optionally save ID3v2.3 tags
1 parent 64dcd28 commit 24227d6

File tree

2 files changed

+65
-1
lines changed

2 files changed

+65
-1
lines changed

beets/mediafile.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -930,7 +930,18 @@ def __init__(self, path):
930930
if self.mgfile.tags is None:
931931
self.mgfile.add_tags()
932932

933-
def save(self):
933+
def save(self, id3v23=False):
934+
"""Write the object's tags back to the file.
935+
936+
By default, MP3 files are saved with ID3v2.4 tags. You can use
937+
the older ID3v2.3 standard by specifying the `id3v23` option.
938+
"""
939+
if id3v23 and self.type == 'mp3':
940+
id3 = self.mgfile
941+
if hasattr(id3, 'tags'):
942+
# In case this is an MP3 object, not an ID3 object.
943+
id3 = id3.tags
944+
id3.update_to_v23()
934945
self.mgfile.save()
935946

936947
def delete(self):

test/test_mediafile.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from _common import unittest
2222
import beets.mediafile
2323

24+
2425
class EdgeTest(unittest.TestCase):
2526
def test_emptylist(self):
2627
# Some files have an ID3 frame that has a list with no elements.
@@ -67,6 +68,7 @@ def test_old_ape_version_bitrate(self):
6768
f = beets.mediafile.MediaFile(os.path.join(_common.RSRC, 'oldape.ape'))
6869
self.assertEqual(f.bitrate, 0)
6970

71+
7072
_sc = beets.mediafile._safe_cast
7173
class InvalidValueToleranceTest(unittest.TestCase):
7274
def test_packed_integer_with_extra_chars(self):
@@ -110,6 +112,7 @@ def test_safe_cast_special_chars_to_unicode(self):
110112
self.assertTrue(isinstance(us, unicode))
111113
self.assertTrue(us.startswith(u'caf'))
112114

115+
113116
class SafetyTest(unittest.TestCase):
114117
def _exccheck(self, fn, exc, data=''):
115118
fn = os.path.join(_common.RSRC, fn)
@@ -156,6 +159,7 @@ def test_broken_symlink(self):
156159
finally:
157160
os.unlink(fn)
158161

162+
159163
class SideEffectsTest(unittest.TestCase):
160164
def setUp(self):
161165
self.empty = os.path.join(_common.RSRC, 'empty.mp3')
@@ -166,6 +170,7 @@ def test_opening_tagless_file_leaves_untouched(self):
166170
new_mtime = os.stat(self.empty).st_mtime
167171
self.assertEqual(old_mtime, new_mtime)
168172

173+
169174
class EncodingTest(unittest.TestCase):
170175
def setUp(self):
171176
src = os.path.join(_common.RSRC, 'full.m4a')
@@ -183,10 +188,13 @@ def test_unicode_label_in_m4a(self):
183188
new_mf = beets.mediafile.MediaFile(self.path)
184189
self.assertEqual(new_mf.label, u'foo\xe8bar')
185190

191+
186192
class ZeroLengthMediaFile(beets.mediafile.MediaFile):
187193
@property
188194
def length(self):
189195
return 0.0
196+
197+
190198
class MissingAudioDataTest(unittest.TestCase):
191199
def setUp(self):
192200
super(MissingAudioDataTest, self).setUp()
@@ -197,6 +205,7 @@ def test_bitrate_with_zero_length(self):
197205
del self.mf.mgfile.info.bitrate # Not available directly.
198206
self.assertEqual(self.mf.bitrate, 0)
199207

208+
200209
class TypeTest(unittest.TestCase):
201210
def setUp(self):
202211
super(TypeTest, self).setUp()
@@ -223,6 +232,7 @@ def test_set_track_to_none(self):
223232
self.mf.track = None
224233
self.assertEqual(self.mf.track, 0)
225234

235+
226236
class SoundCheckTest(unittest.TestCase):
227237
def test_round_trip(self):
228238
data = beets.mediafile._sc_encode(1.0, 1.0)
@@ -242,8 +252,51 @@ def test_malformatted(self):
242252
self.assertEqual(gain, 0.0)
243253
self.assertEqual(peak, 0.0)
244254

255+
256+
class ID3v23Test(unittest.TestCase):
257+
def _make_test(self, ext='mp3'):
258+
src = os.path.join(_common.RSRC, 'full.{0}'.format(ext))
259+
self.path = os.path.join(_common.RSRC, 'test.{0}'.format(ext))
260+
shutil.copy(src, self.path)
261+
return beets.mediafile.MediaFile(self.path)
262+
263+
def _delete_test(self):
264+
os.remove(self.path)
265+
266+
def test_v24_year_tag(self):
267+
mf = self._make_test()
268+
try:
269+
mf.year = 2013
270+
mf.save(id3v23=False)
271+
frame = mf.mgfile['TDRC']
272+
self.assertTrue('2013' in str(frame))
273+
self.assertTrue('TYER' not in mf.mgfile)
274+
finally:
275+
self._delete_test()
276+
277+
def test_v23_year_tag(self):
278+
mf = self._make_test()
279+
try:
280+
mf.year = 2013
281+
mf.save(id3v23=True)
282+
frame = mf.mgfile['TYER']
283+
self.assertTrue('2013' in str(frame))
284+
self.assertTrue('TDRC' not in mf.mgfile)
285+
finally:
286+
self._delete_test()
287+
288+
def test_v23_on_non_mp3_is_noop(self):
289+
mf = self._make_test('m4a')
290+
try:
291+
mf.year = 2013
292+
mf.save(id3v23=True)
293+
finally:
294+
self._delete_test()
295+
296+
245297
def suite():
246298
return unittest.TestLoader().loadTestsFromName(__name__)
247299

300+
248301
if __name__ == '__main__':
249302
unittest.main(defaultTest='suite')

0 commit comments

Comments
 (0)