diff --git a/mutagen/easyid3.py b/mutagen/easyid3.py index d23c0e13..339fd811 100644 --- a/mutagen/easyid3.py +++ b/mutagen/easyid3.py @@ -16,7 +16,7 @@ from ._compat import iteritems, text_type, PY2 from mutagen import Metadata -from mutagen._util import DictMixin, dict_match +from mutagen._util import DictMixin, dict_match, loadfile from mutagen.id3 import ID3, error, delete, ID3FileType @@ -172,10 +172,30 @@ def __init__(self, filename=None): load = property(lambda s: s.__id3.load, lambda s, v: setattr(s.__id3, 'load', v)) - def save(self, *args, **kwargs): - # ignore v2_version until we support 2.3 here - kwargs.pop("v2_version", None) - self.__id3.save(*args, **kwargs) + @loadfile(writable=True, create=True) + def save(self, filething, v1=1, v2_version=4, v23_sep='/', padding=None): + """save(filething=None, v1=1, v2_version=4, v23_sep='/', padding=None) + + Save changes to a file. + See :meth:`mutagen.id3.ID3.save` for more info. + """ + + if v2_version == 3: + # EasyID3 only works with v2.4 frames, so update_to_v23() would + # break things. We have to save a shallow copy of all tags + # and restore it after saving. Due to CHAP/CTOC copying has + # to be done recursively implemented in ID3Tags. + backup = self.__id3._copy() + try: + self.__id3.update_to_v23() + self.__id3.save( + filething, v1=v1, v2_version=v2_version, v23_sep=v23_sep, + padding=padding) + finally: + self.__id3._restore(backup) + else: + self.__id3.save(filething, v1=v1, v2_version=v2_version, + v23_sep=v23_sep, padding=padding) delete = property(lambda s: s.__id3.delete, lambda s, v: setattr(s.__id3, 'delete', v)) diff --git a/mutagen/id3/_tags.py b/mutagen/id3/_tags.py index eb24b5da..b95d9e68 100644 --- a/mutagen/id3/_tags.py +++ b/mutagen/id3/_tags.py @@ -478,6 +478,25 @@ def update_to_v23(self): for f in self.getall("CTOC"): f.sub_frames.update_to_v23() + def _copy(self): + """Creates a shallow copy of all tags""" + + items = self.items() + subs = {} + for f in (self.getall("CHAP") + self.getall("CTOC")): + subs[f.HashKey] = f.sub_frames._copy() + return (items, subs) + + def _restore(self, value): + """Restores the state copied with _copy()""" + + items, subs = value + self.clear() + for key, value in items: + self[key] = value + if key in subs: + value.sub_frames._restore(subs[key]) + def save_frame(frame, name=None, config=None): if config is None: diff --git a/tests/test_easyid3.py b/tests/test_easyid3.py index 990ecb2f..34269368 100644 --- a/tests/test_easyid3.py +++ b/tests/test_easyid3.py @@ -4,7 +4,7 @@ import pickle from mutagen import MutagenError -from mutagen.id3 import ID3FileType, ID3, RVA2 +from mutagen.id3 import ID3FileType, ID3, RVA2, CHAP, TDRC, CTOC from mutagen.easyid3 import EasyID3, error as ID3Error from mutagen._compat import PY3 @@ -21,6 +21,15 @@ def setUp(self): def tearDown(self): os.unlink(self.filename) + def test_load_filename(self): + self.id3.save(self.filename) + self.id3.load(self.filename) + assert self.id3.filename == self.filename + + path = os.path.join(DATA_DIR, 'silence-44-s.mp3') + new = EasyID3(path) + assert new.filename == path + def test_txxx_latin_first_then_non_latin(self): self.id3["performer"] = [u"foo"] self.id3["performer"] = [u"\u0243"] @@ -37,11 +46,42 @@ def test_remember_ctr(self): mp3.pprint() self.failUnless(isinstance(mp3.tags, EasyID3)) - def test_ignore_23(self): - self.id3["date"] = "2004" + def test_save_23(self): self.id3.save(self.filename, v2_version=3) + self.assertEqual(ID3(self.filename).version, (2, 3, 0)) + self.id3.save(self.filename, v2_version=4) self.assertEqual(ID3(self.filename).version, (2, 4, 0)) + def test_save_date_v23(self): + self.id3["date"] = "2004" + assert self.realid3.getall("TDRC")[0] == u"2004" + self.id3.save(self.filename, v2_version=3) + assert self.realid3.getall("TDRC")[0] == u"2004" + assert not self.realid3.getall("TYER") + new = ID3(self.filename, translate=False) + assert new.version == (2, 3, 0) + assert new.getall("TYER")[0] == u"2004" + + def test_save_v23_error_restore(self): + self.id3["date"] = "2004" + with self.assertRaises(MutagenError): + self.id3.save("", v2_version=3) + assert self.id3["date"] == ["2004"] + + def test_save_v23_recurse_restore(self): + self.realid3.add(CHAP(sub_frames=[TDRC(text="2006")])) + self.realid3.add(CTOC(sub_frames=[TDRC(text="2006")])) + self.id3.save(self.filename, v2_version=3) + + for frame_id in ["CHAP", "CTOC"]: + chap = self.realid3.getall(frame_id)[0] + assert chap.sub_frames.getall("TDRC")[0] == "2006" + new = ID3(self.filename, translate=False) + assert new.version == (2, 3, 0) + chap = new.getall(frame_id)[0] + assert not chap.sub_frames.getall("TDRC") + assert chap.sub_frames.getall("TYER")[0] == "2006" + def test_delete(self): self.id3["artist"] = "foobar" self.id3.save(self.filename)