This commit is contained in:
Adrian Sampson 2013-09-13 18:58:30 -07:00
commit c57f2d0b78
11 changed files with 87 additions and 32 deletions

View file

@ -24,3 +24,4 @@ b3f7b5267a2f7b46b826d087421d7f4569211240 v1.2.0
ecff182221ec32a9f6549ad3ce8d2ab4c3e5568a v1.2.0
bd7259ac13b54caecb1403f625688eb3eeeba8d6 v1.2.1
c6af5962e25b915ce538af1c0b53a89ceb340b04 v1.2.2
87945a0e217591a842307fa11e161d4912598c32 v1.3.0

View file

@ -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 <adrian@radbox.org>'
import beets.library

View file

@ -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

View file

@ -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)

View file

@ -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).

View file

@ -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'

View file

@ -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

View file

@ -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/

View file

@ -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',

BIN
test/rsrc/full.opus Normal file

Binary file not shown.

View file

@ -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')