Skip to content

Commit

Permalink
easyid3: support saving as v2.3. Fixes #188
Browse files Browse the repository at this point in the history
The EasyID3 class only supports working with a v2.4 file and frame
types. When saving we need update to v2.3 and then save. To
prevent the EasyID3 instance getting useless after saving we
save the state before and restore it afterwards.
  • Loading branch information
lazka committed Dec 22, 2016
1 parent fea7986 commit ccba9ef
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 8 deletions.
30 changes: 25 additions & 5 deletions mutagen/easyid3.py
Expand Up @@ -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


Expand Down Expand Up @@ -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))
Expand Down
19 changes: 19 additions & 0 deletions mutagen/id3/_tags.py
Expand Up @@ -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:
Expand Down
46 changes: 43 additions & 3 deletions tests/test_easyid3.py
Expand Up @@ -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

Expand All @@ -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"]
Expand All @@ -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)
Expand Down

0 comments on commit ccba9ef

Please sign in to comment.