diff --git a/beets/mediafile.py b/beets/mediafile.py index b31b638a1..9612399ca 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -382,6 +382,11 @@ class MediaField(object): if style.packing: out = Packed(out, style.packing)[style.pack_pos] + + # MPEG-4 freeform frames are (should be?) encoded as UTF-8. + if obj.type == 'mp4' and style.key.startswith('----:') and \ + isinstance(out, str): + out = out.decode('utf8') return _safe_cast(self.out_type, out) @@ -410,8 +415,8 @@ class MediaField(object): out = u'' # We trust that packed values are handled above. - # convert to correct storage type (irrelevant for - # packed values) + # Convert to correct storage type (irrelevant for + # packed values). if style.as_type == unicode: if out is None: out = u'' @@ -429,7 +434,13 @@ class MediaField(object): elif style.as_type in (bool, str): out = style.as_type(out) - # store the data + # MPEG-4 "freeform" (----) frames must be encoded as UTF-8 + # byte strings. + if obj.type == 'mp4' and style.key.startswith('----:') and \ + isinstance(out, unicode): + out = out.encode('utf8') + + # Store the data. self._storedata(obj, out, style) class CompositeDateField(object): diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 041981106..239294a74 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -180,7 +180,7 @@ CHAR_REPLACE = [ (re.compile(r'[\\/\?]|^\.'), '_'), (re.compile(r':'), '-'), ] -CHAR_REPLACE_WINDOWS = re.compile('["\*<>\|]|^\.|\.$| +$'), '_' +CHAR_REPLACE_WINDOWS = re.compile(r'["\*<>\|]|^\.|\.$| +$'), '_' def sanitize_path(path, pathmod=None): """Takes a path and makes sure that it is legal. Returns a new path. Only works with fragments; won't work reliably on Windows when a diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 97cf8bc20..5fa7cdd62 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -17,6 +17,7 @@ import unittest import os +import shutil import _common import beets.mediafile @@ -149,6 +150,23 @@ 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') + self.path = os.path.join(_common.RSRC, 'test.m4a') + shutil.copy(src, self.path) + + self.mf = beets.mediafile.MediaFile(self.path) + + def tearDown(self): + os.remove(self.path) + + def test_unicode_label_in_m4a(self): + self.mf.label = u'foo\xe8bar' + self.mf.save() + new_mf = beets.mediafile.MediaFile(self.path) + self.assertEqual(new_mf.label, u'foo\xe8bar') + def suite(): return unittest.TestLoader().loadTestsFromName(__name__)