diff --git a/.hgtags b/.hgtags index 7b112e598..c2cd27735 100644 --- a/.hgtags +++ b/.hgtags @@ -24,3 +24,4 @@ b3f7b5267a2f7b46b826d087421d7f4569211240 v1.2.0 ecff182221ec32a9f6549ad3ce8d2ab4c3e5568a v1.2.0 bd7259ac13b54caecb1403f625688eb3eeeba8d6 v1.2.1 c6af5962e25b915ce538af1c0b53a89ceb340b04 v1.2.2 +87945a0e217591a842307fa11e161d4912598c32 v1.3.0 diff --git a/beets/__init__.py b/beets/__init__.py index 3e794c068..0edaa71b3 100644 --- a/beets/__init__.py +++ b/beets/__init__.py @@ -12,7 +12,7 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -__version__ = '1.3.0' +__version__ = '1.3.1' __author__ = 'Adrian Sampson ' import beets.library diff --git a/beets/mediafile.py b/beets/mediafile.py index 9b6234192..ec14f99e9 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -30,6 +30,7 @@ if no tag is present. If no value is available, the value will be false """ import mutagen import mutagen.mp3 +import mutagen.oggopus import mutagen.oggvorbis import mutagen.mp4 import mutagen.flac @@ -72,6 +73,7 @@ TYPES = { 'aac': 'AAC', 'alac': 'ALAC', 'ogg': 'OGG', + 'opus': 'Opus', 'flac': 'FLAC', 'ape': 'APE', 'wv': 'WavPack', @@ -283,7 +285,7 @@ class StorageStyle(object): - id3_lang: set the language field of the frame object. """ def __init__(self, key, list_elem=True, as_type=unicode, - packing=None, pack_pos=0, pack_type=int, + packing=None, pack_pos=0, pack_type=int, id3_desc=None, id3_frame_field='text', id3_lang=None, suffix=None, float_places=2): self.key = key @@ -741,7 +743,7 @@ class ImageField(object): return pictures[0].data or None else: return None - + elif obj.type == 'asf': if 'WM/Picture' in obj.mgfile: pictures = obj.mgfile['WM/Picture'] @@ -754,9 +756,9 @@ class ImageField(object): return None else: - # Here we're assuming everything but MP3, MPEG-4, and FLAC - # use the Xiph/Vorbis Comments standard. This may not be - # valid. http://wiki.xiph.org/VorbisComment#Cover_art + # Here we're assuming everything but MP3, MPEG-4, FLAC, and + # ASF/WMA use the Xiph/Vorbis Comments standard. This may + # not be valid. http://wiki.xiph.org/VorbisComment#Cover_art if 'metadata_block_picture' not in obj.mgfile: # Try legacy COVERART tags. @@ -773,6 +775,9 @@ class ImageField(object): else: return None + if not pic.data: + return None + return pic.data def __set__(self, obj, val): @@ -862,6 +867,7 @@ class MediaFile(object): mutagen.flac.error, mutagen.monkeysaudio.MonkeysAudioHeaderError, mutagen.mp4.error, + mutagen.oggopus.error, mutagen.oggvorbis.error, mutagen.ogg.error, mutagen.asf.error, @@ -904,6 +910,8 @@ class MediaFile(object): self.type = 'mp3' elif type(self.mgfile).__name__ == 'FLAC': self.type = 'flac' + elif type(self.mgfile).__name__ == 'OggOpus': + self.type = 'opus' elif type(self.mgfile).__name__ == 'OggVorbis': self.type = 'ogg' elif type(self.mgfile).__name__ == 'MonkeysAudio': @@ -1262,7 +1270,7 @@ class MediaFile(object): packing=packing.SC, pack_pos=0, pack_type=float)], mp4 = [StorageStyle('----:com.apple.iTunes:replaygain_track_gain', as_type=str, float_places=2, suffix=b' dB'), - StorageStyle('----:com.apple.iTunes:iTunNORM', + StorageStyle('----:com.apple.iTunes:iTunNORM', packing=packing.SC, pack_pos=0, pack_type=float)], etc = StorageStyle(u'REPLAYGAIN_TRACK_GAIN', float_places=2, suffix=u' dB'), @@ -1314,6 +1322,9 @@ class MediaFile(object): """The audio's sample rate (an int).""" if hasattr(self.mgfile.info, 'sample_rate'): return self.mgfile.info.sample_rate + elif self.type == 'opus': + # Opus is always 48kHz internally. + return 48000 return 0 @property diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index 2b342c989..d019ece77 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -34,8 +34,15 @@ def _embed(path, items, maxwidth=0): data = open(syspath(path), 'rb').read() kindstr = imghdr.what(None, data) - if kindstr not in ('jpeg', 'png'): - log.error('A file of type %s is not allowed as coverart.' % kindstr) + if kindstr is None: + log.error(u'Could not embed art of unkown type: {0}'.format( + displayable_path(path) + )) + return + elif kindstr not in ('jpeg', 'png'): + log.error(u'Image type {0} is not allowed as cover art: {1}'.format( + kindstr, displayable_path(path) + )) return # Add art to each file. @@ -110,8 +117,9 @@ def embed(lib, imagepath, query): log.error('No album matches query.') return - log.info('Embedding album art into %s - %s.' % \ - (album.albumartist, album.album)) + log.info(u'Embedding album art into {0.albumartist} - {0.album}.'.format( + album + )) _embed(imagepath, album.items(), config['embedart']['maxwidth'].get(int)) @@ -161,9 +169,8 @@ def extract(lib, outpath, query): return outpath += '.' + ext - log.info('Extracting album art from: %s - %s\n' - 'To: %s' % \ - (item.artist, item.title, outpath)) + log.info(u'Extracting album art from: {0.artist} - {0.title}\n' + u'To: {1}'.format(item, displayable_path(outpath))) with open(syspath(outpath), 'wb') as f: f.write(art) diff --git a/docs/changelog.rst b/docs/changelog.rst index d2df94cae..f035d1550 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,9 +1,24 @@ Changelog ========= -1.3.0 (in development) +1.3.1 (in development) ---------------------- +New stuff: + +* Add `Opus`_ audio support. Thanks to Rowan Lewis. + +And some fixes: + +* :doc:`/plugins/fetchart`: Better error message when the image file has an + unrecognized type. + +.. _Opus: http://www.opus-codec.org/ + + +1.3.0 (September 11, 2013) +-------------------------- + Albums and items now have **flexible attributes**. This means that, when you want to store information about your music in the beets database, you're no longer constrained to the set of fields it supports out of the box (title, @@ -33,6 +48,10 @@ match *nothing* instead of *everything*. So if you type ``beet ls fieldThatDoesNotExist:foo``, beets will now return no results, whereas previous versions would spit out a warning and then list your entire library. +There's more detail than you could ever need `on the beets blog`_. + +.. _on the beets blog: http://beets.radbox.org/blog/flexattr.html + 1.2.2 (August 27, 2013) ----------------------- @@ -1341,7 +1360,7 @@ issue involves correct ordering of autotagged albums. * BPD now uses a pure-Python socket library and no longer requires eventlet/greenlet (the latter of which is a C extension). For the curious, the - socket library in question is called `Bluelet`_. + socket library in question is called `Bluelet`_. * Non-autotagged imports are now resumable (just like autotagged imports). diff --git a/docs/conf.py b/docs/conf.py index 3a646837d..dba10592b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,7 +13,7 @@ project = u'beets' copyright = u'2012, Adrian Sampson' version = '1.3' -release = '1.3.0' +release = '1.3.1' pygments_style = 'sphinx' diff --git a/docs/guides/tagger.rst b/docs/guides/tagger.rst index 29bba2d89..489fe9807 100644 --- a/docs/guides/tagger.rst +++ b/docs/guides/tagger.rst @@ -61,8 +61,8 @@ all of these limitations. unidentified albums. * Currently, MP3, AAC, FLAC, ALAC, Ogg Vorbis, Monkey's Audio, WavPack, - Musepack, and Windows Media files are supported. (Do you use some other - format? `Let me know!`_) + Musepack, Windows Media, and Opus files are supported. (Do you use some + other format? `Let me know!`_) .. _Let me know!: mailto:adrian@radbox.org @@ -185,7 +185,7 @@ candidates), like so:: Candidates: 1. Panther - Yourself (66.8%) 2. Tav Falco's Panther Burns - Return of the Blue Panther (30.4%) - # selection (default 1), Skip, Use as-is, or Enter search, or aBort? + # selection (default 1), Skip, Use as-is, or Enter search, or aBort? Here, you have many of the same options as before, but you can also enter a number to choose one of the options that beets has found. Don't worry about diff --git a/docs/reference/config.rst b/docs/reference/config.rst index 36844b668..8a80b929f 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -237,7 +237,7 @@ copy Either ``yes`` or ``no``, indicating whether to **copy** files into the library directory when using ``beet import``. Defaults to ``yes``. Can be overridden with the ``-c`` and ``-C`` command-line options. - + The option is ignored if ``move`` is enabled (i.e., beets can move or copy files but it doesn't make sense to do both). @@ -246,7 +246,7 @@ move Either ``yes`` or ``no``, indicating whether to **move** files into the library directory when using ``beet import``. -Defaults to ``no``. +Defaults to ``no``. The effect is similar to the ``copy`` option but you end up with only one copy of the imported file. ("Moving" works even across filesystems; if @@ -347,14 +347,15 @@ instead of the main server. Use the ``host`` and ``ratelimit`` options under a ``musicbrainz:`` header, like so:: musicbrainz: - host: localhost + host: localhost:5000 ratelimit: 100 -The ``host`` key, of course, controls the Web server that will be contacted by -beets (default: musicbrainz.org). The ``ratelimit`` option, an integer, -controls the number of Web service requests per second (default: 1). **Do not -change the rate limit setting** if you're using the main MusicBrainz -server---on this public server, you're `limited`_ to one request per second. +The ``host`` key, of course, controls the Web server hostname (and port, +optionally) that will be contacted by beets (default: musicbrainz.org). The +``ratelimit`` option, an integer, controls the number of Web service requests +per second (default: 1). **Do not change the rate limit setting** if you're +using the main MusicBrainz server---on this public server, you're `limited`_ +to one request per second. .. _limited: http://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting .. _MusicBrainz: http://musicbrainz.org/ diff --git a/setup.py b/setup.py index d6201eba1..8a6c45f36 100755 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: -# +# # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. @@ -42,7 +42,7 @@ if 'sdist' in sys.argv: shutil.copytree(os.path.join(docdir, '_build', 'man'), mandir) setup(name='beets', - version='1.3.0', + version='1.3.1', description='music tagger and library organizer', author='Adrian Sampson', author_email='adrian@radbox.org', diff --git a/test/rsrc/full.opus b/test/rsrc/full.opus new file mode 100644 index 000000000..9a5534b22 Binary files /dev/null and b/test/rsrc/full.opus differ diff --git a/test/test_mediafile_basic.py b/test/test_mediafile_basic.py index 91f663556..3cbee09e7 100644 --- a/test/test_mediafile_basic.py +++ b/test/test_mediafile_basic.py @@ -173,6 +173,15 @@ READ_ONLY_CORRECT_DICTS = { 'channels': 1, }, + 'full.opus': { + 'length': 1.0, + 'bitrate': 57984, + 'format': 'Opus', + 'samplerate': 48000, + 'bitdepth': 0, + 'channels': 1, + }, + 'full.ape': { 'length': 1.0, 'bitrate': 112040, @@ -224,6 +233,7 @@ TEST_FILES = { 'mp3': ['full', 'partial', 'min'], 'flac': ['full', 'partial', 'min'], 'ogg': ['full'], + 'opus': ['full'], 'ape': ['full'], 'wv': ['full'], 'mpc': ['full'], @@ -264,6 +274,9 @@ class AllFilesMixin(object): def test_ogg(self): self._run('full', 'ogg') + def test_opus(self): + self._run('full', 'opus') + def test_ape(self): self._run('full', 'ape') @@ -293,7 +306,7 @@ class ReadingTest(unittest.TestCase, AllFilesMixin): self.assertAlmostEqual(got, correct, msg=message) else: self.assertEqual(got, correct, message) - + def _run(self, tagset, kind): correct_dict = CORRECT_DICTS[tagset] path = os.path.join(_common.RSRC, tagset + '.' + kind) @@ -389,7 +402,7 @@ class WritingTest(unittest.TestCase, AllFilesMixin): else: raise ValueError('unknown field type ' + \ str(type(correct_dict[field]))) - + # Make a copy of the file we'll work on. root, ext = os.path.splitext(path) tpath = root + '_test' + ext @@ -429,6 +442,9 @@ class ReadOnlyTest(unittest.TestCase): def test_ogg(self): self._run('full.ogg') + def test_opus(self): + self._run('full.opus') + def test_ape(self): self._run('full.ape')