mirror of
https://github.com/beetbox/beets.git
synced 2025-12-14 12:35:19 +01:00
merge
This commit is contained in:
commit
c57f2d0b78
11 changed files with 87 additions and 32 deletions
1
.hgtags
1
.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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
4
setup.py
4
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',
|
||||
|
|
|
|||
BIN
test/rsrc/full.opus
Normal file
BIN
test/rsrc/full.opus
Normal file
Binary file not shown.
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue