flake8 cleaning beetsplug/*

Remaining warnings are related to visual indentation.
This commit is contained in:
Fabrice Laporte 2014-04-13 23:20:27 +02:00
parent 1df6303222
commit 3ead936fe5
29 changed files with 209 additions and 112 deletions

View file

@ -97,8 +97,8 @@ class BeatportRelease(BeatportObject):
else:
artist_str = "Various Artists"
return u"<BeatportRelease: {0} - {1} ({2})>".format(artist_str,
self.name,
self.catalog_number)
self.name,
self.catalog_number)
def __init__(self, data):
BeatportObject.__init__(self, data)
@ -129,7 +129,8 @@ class BeatportTrack(BeatportObject):
def __unicode__(self):
artist_str = ", ".join(x[1] for x in self.artists)
return u"<BeatportTrack: {0} - {1} ({2})>".format(artist_str, self.name,
return u"<BeatportTrack: {0} - {1} ({2})>".format(artist_str,
self.name,
self.mix_name)
def __init__(self, data):

View file

@ -81,6 +81,7 @@ def match_benchmark(lib, prof, query=None, album_id=None):
interval = timeit.timeit(_run_match, number=1)
print('match duration:', interval)
class BenchmarkPlugin(BeetsPlugin):
"""A plugin for performing some simple performance benchmarks.
"""
@ -91,7 +92,7 @@ class BenchmarkPlugin(BeetsPlugin):
action='store_true', default=False,
help='performance profiling')
aunique_bench_cmd.func = lambda lib, opts, args: \
aunique_benchmark(lib, opts.profile)
aunique_benchmark(lib, opts.profile)
match_bench_cmd = ui.Subcommand('bench_match',
help='benchmark for track matching')
@ -101,6 +102,6 @@ class BenchmarkPlugin(BeetsPlugin):
match_bench_cmd.parser.add_option('-i', '--id', default=None,
help='album ID to match against')
match_bench_cmd.func = lambda lib, opts, args: \
match_benchmark(lib, opts.profile, ui.decargs(args), opts.id)
match_benchmark(lib, opts.profile, ui.decargs(args), opts.id)
return [aunique_bench_cmd, match_bench_cmd]

View file

@ -76,7 +76,8 @@ global_log = logging.getLogger('beets')
# Gstreamer import error.
class NoGstreamerError(Exception): pass
class NoGstreamerError(Exception):
pass
# Error-handling, exceptions, parameter parsing.
@ -92,6 +93,7 @@ class BPDError(Exception):
self.index = index
template = Template(u'$resp [$code@$index] {$cmd_name} $message')
def response(self):
"""Returns a string to be used as the response code for the
erring command.
@ -101,23 +103,28 @@ class BPDError(Exception):
'index': self.index,
'cmd_name': self.cmd_name,
'message': self.message
})
})
def make_bpd_error(s_code, s_message):
"""Create a BPDError subclass for a static code and message.
"""
class NewBPDError(BPDError):
code = s_code
message = s_message
cmd_name = ''
index = 0
def __init__(self): pass
def __init__(self):
pass
return NewBPDError
ArgumentTypeError = make_bpd_error(ERROR_ARG, 'invalid type for argument')
ArgumentIndexError = make_bpd_error(ERROR_ARG, 'argument out of range')
ArgumentNotFoundError = make_bpd_error(ERROR_NO_EXIST, 'argument not found')
def cast_arg(t, val):
"""Attempts to call t on val, raising a ArgumentTypeError
on ValueError.
@ -133,14 +140,15 @@ def cast_arg(t, val):
except ValueError:
raise ArgumentTypeError()
class BPDClose(Exception):
"""Raised by a command invocation to indicate that the connection
should be closed.
"""
# Generic server infrastructure, implementing the basic protocol.
class BaseServer(object):
"""A MPD-compatible music player server.
@ -211,10 +219,10 @@ class BaseServer(object):
If there is only one song in the playlist it returns 0.
"""
if len(self.playlist) < 2:
return len(self.playlist)-1
new_index = self.random_obj.randint(0, len(self.playlist)-1)
return len(self.playlist) - 1
new_index = self.random_obj.randint(0, len(self.playlist) - 1)
while new_index == self.current_index:
new_index = self.random_obj.randint(0, len(self.playlist)-1)
new_index = self.random_obj.randint(0, len(self.playlist) - 1)
return new_index
def _succ_idx(self):
@ -226,7 +234,7 @@ class BaseServer(object):
return self.current_index
if self.random:
return self._random_idx()
return self.current_index+1
return self.current_index + 1
def _prev_idx(self):
"""Returns the index for the previous song to play.
@ -237,7 +245,7 @@ class BaseServer(object):
return self.current_index
if self.random:
return self._random_idx()
return self.current_index-1
return self.current_index - 1
def cmd_ping(self, conn):
"""Succeeds."""
@ -309,7 +317,7 @@ class BaseServer(object):
state = u'play'
yield u'state: ' + state
if self.current_index != -1: # i.e., paused or playing
if self.current_index != -1: # i.e., paused or playing
current_id = self._item_id(self.playlist[self.current_index])
yield u'song: ' + unicode(self.current_index)
yield u'songid: ' + unicode(current_id)
@ -360,9 +368,9 @@ class BaseServer(object):
raise ArgumentIndexError()
self.playlist_version += 1
if self.current_index == index: # Deleted playing song.
if self.current_index == index: # Deleted playing song.
self.cmd_stop(conn)
elif index < self.current_index: # Deleted before playing.
elif index < self.current_index: # Deleted before playing.
# Shift playing index down.
self.current_index -= 1
@ -437,6 +445,7 @@ class BaseServer(object):
except IndexError:
raise ArgumentIndexError()
yield self._item_info(track)
def cmd_playlistid(self, conn, track_id=-1):
return self.cmd_playlistinfo(conn, self._id_to_index(track_id))
@ -461,7 +470,7 @@ class BaseServer(object):
def cmd_currentsong(self, conn):
"""Sends information about the currently-playing song.
"""
if self.current_index != -1: # -1 means stopped.
if self.current_index != -1: # -1 means stopped.
track = self.playlist[self.current_index]
yield self._item_info(track)
@ -485,7 +494,7 @@ class BaseServer(object):
def cmd_pause(self, conn, state=None):
"""Set the pause state playback."""
if state is None:
self.paused = not self.paused # Toggle.
self.paused = not self.paused # Toggle.
else:
self.paused = cast_arg('intbool', state)
@ -496,14 +505,14 @@ class BaseServer(object):
if index < -1 or index > len(self.playlist):
raise ArgumentIndexError()
if index == -1: # No index specified: start where we are.
if not self.playlist: # Empty playlist: stop immediately.
if index == -1: # No index specified: start where we are.
if not self.playlist: # Empty playlist: stop immediately.
return self.cmd_stop(conn)
if self.current_index == -1: # No current song.
self.current_index = 0 # Start at the beginning.
if self.current_index == -1: # No current song.
self.current_index = 0 # Start at the beginning.
# If we have a current song, just stay there.
else: # Start with the specified index.
else: # Start with the specified index.
self.current_index = index
self.paused = False
@ -527,6 +536,7 @@ class BaseServer(object):
if index < 0 or index >= len(self.playlist):
raise ArgumentIndexError()
self.current_index = index
def cmd_seekid(self, conn, track_id, pos):
index = self._id_to_index(track_id)
return self.cmd_seek(conn, index, pos)
@ -537,6 +547,7 @@ class BaseServer(object):
heap = hpy().heap()
print(heap)
class Connection(object):
"""A connection between a client and the server. Handles input and
output from and to the client.
@ -557,7 +568,7 @@ class Connection(object):
if isinstance(lines, basestring):
lines = [lines]
out = NEWLINE.join(lines) + NEWLINE
log.debug(out[:-1]) # Don't log trailing newline.
log.debug(out[:-1]) # Don't log trailing newline.
if isinstance(out, unicode):
out = out.encode('utf8')
return self.sock.sendall(out)
@ -580,9 +591,9 @@ class Connection(object):
"""
yield self.send(HELLO)
clist = None # Initially, no command list is being constructed.
clist = None # Initially, no command list is being constructed.
while True:
line = yield self.sock.readline()
line = yield self.sock.readline()
if not line:
break
line = line.strip()
@ -594,7 +605,7 @@ class Connection(object):
# Command list already opened.
if line == CLIST_END:
yield bluelet.call(self.do_command(clist))
clist = None # Clear the command list.
clist = None # Clear the command list.
else:
clist.append(Command(line))
@ -619,6 +630,7 @@ class Connection(object):
return cls(server, sock).run()
return _handle
class Command(object):
"""A command issued by the client for processing by the server.
"""
@ -691,6 +703,7 @@ class CommandList(list):
server. May be verbose, in which case the response is delimited, or
not. Should be a list of `Command` objects.
"""
def __init__(self, sequence=None, verbose=False):
"""Create a new `CommandList` from the given sequence of
`Command`s. If `verbose`, this is a verbose command list.
@ -708,7 +721,7 @@ class CommandList(list):
yield bluelet.call(command.run(conn))
except BPDError as e:
# If the command failed, stop executing.
e.index = i # Give the error the correct index.
e.index = i # Give the error the correct index.
raise e
# Otherwise, possibly send the output delimeter if we're in a
@ -717,7 +730,6 @@ class CommandList(list):
yield conn.send(RESP_CLIST_VERBOSE)
# A subclass of the basic, protocol-handling server that actually plays
# music.
@ -750,7 +762,6 @@ class Server(BaseServer):
"""
self.cmd_next(None)
# Metadata helper functions.
def _item_info(self, item):
@ -783,7 +794,6 @@ class Server(BaseServer):
def _item_id(self, item):
return item.id
# Database updating.
def cmd_update(self, conn, path=u'/'):
@ -796,7 +806,6 @@ class Server(BaseServer):
print('... done.')
self.updated_time = time.time()
# Path (directory tree) browsing.
def _resolve_path(self, path):
@ -861,20 +870,22 @@ class Server(BaseServer):
for name, itemid in sorted(node.files.iteritems()):
newpath = self._path_join(basepath, name)
# "yield from"
for v in self._listall(newpath, itemid, info): yield v
for v in self._listall(newpath, itemid, info):
yield v
for name, subdir in sorted(node.dirs.iteritems()):
newpath = self._path_join(basepath, name)
yield u'directory: ' + newpath
for v in self._listall(newpath, subdir, info): yield v
for v in self._listall(newpath, subdir, info):
yield v
def cmd_listall(self, conn, path=u"/"):
"""Send the paths all items in the directory, recursively."""
return self._listall(path, self._resolve_path(path), False)
def cmd_listallinfo(self, conn, path=u"/"):
"""Send info on all the items in the directory, recursively."""
return self._listall(path, self._resolve_path(path), True)
# Playlist manipulation.
def _all_items(self, node):
@ -888,9 +899,11 @@ class Server(BaseServer):
# Recurse into a directory.
for name, itemid in sorted(node.files.iteritems()):
# "yield from"
for v in self._all_items(itemid): yield v
for v in self._all_items(itemid):
yield v
for name, subdir in sorted(node.dirs.iteritems()):
for v in self._all_items(subdir): yield v
for v in self._all_items(subdir):
yield v
def _add(self, path, send_id=False):
"""Adds a track or directory to the playlist, specified by the
@ -912,7 +925,6 @@ class Server(BaseServer):
"""Same as `cmd_add` but sends an id back to the client."""
return self._add(path, True)
# Server info.
def cmd_status(self, conn):
@ -921,7 +933,7 @@ class Server(BaseServer):
if self.current_index > -1:
item = self.playlist[self.current_index]
yield u'bitrate: ' + unicode(item.bitrate/1000)
yield u'bitrate: ' + unicode(item.bitrate / 1000)
# Missing 'audio'.
(pos, total) = self.player.time()
@ -929,7 +941,6 @@ class Server(BaseServer):
# Also missing 'updating_db'.
def cmd_stats(self, conn):
"""Sends some statistics about the library."""
with self.lib.transaction() as tx:
@ -944,12 +955,11 @@ class Server(BaseServer):
u'albums: ' + unicode(albums),
u'songs: ' + unicode(songs),
u'uptime: ' + unicode(int(time.time() - self.startup_time)),
u'playtime: ' + u'0', # Missing.
u'playtime: ' + u'0', # Missing.
u'db_playtime: ' + unicode(int(totaltime)),
u'db_update: ' + unicode(int(self.updated_time)),
)
# Searching.
tagtype_map = {
@ -965,7 +975,7 @@ class Server(BaseServer):
u'Composer': u'composer',
# Performer?
u'Disc': u'disc',
u'filename': u'path', # Suspect.
u'filename': u'path', # Suspect.
}
def cmd_tagtypes(self, conn):
@ -993,21 +1003,22 @@ class Server(BaseServer):
pairs specified. The any_query_type is used for queries of
type "any"; if None, then an error is thrown.
"""
if kv: # At least one key-value pair.
if kv: # At least one key-value pair.
queries = []
# Iterate pairwise over the arguments.
it = iter(kv)
for tag, value in zip(it, it):
if tag.lower() == u'any':
if any_query_type:
queries.append(any_query_type(value, ITEM_KEYS_WRITABLE, query_type))
queries.append(any_query_type(value,
ITEM_KEYS_WRITABLE, query_type))
else:
raise BPDError(ERROR_UNKNOWN, u'no such tagtype')
else:
_, key = self._tagtype_lookup(tag)
queries.append(query_type(key, value))
return dbcore.query.AndQuery(queries)
else: # No key-value pairs.
else: # No key-value pairs.
return dbcore.query.TrueQuery()
def cmd_search(self, conn, *kv):
@ -1056,7 +1067,6 @@ class Server(BaseServer):
yield u'songs: ' + unicode(songs)
yield u'playtime: ' + unicode(int(playtime))
# "Outputs." Just a dummy implementation because we don't control
# any outputs.
@ -1079,7 +1089,6 @@ class Server(BaseServer):
else:
raise ArgumentIndexError()
# Playback control. The functions below hook into the
# half-implementations provided by the base class. Together, they're
# enough to implement all normal playback functionality.
@ -1089,7 +1098,7 @@ class Server(BaseServer):
was_paused = self.paused
super(Server, self).cmd_play(conn, index)
if self.current_index > -1: # Not stopped.
if self.current_index > -1: # Not stopped.
if was_paused and not new_index:
# Just unpause.
self.player.play()
@ -1114,13 +1123,12 @@ class Server(BaseServer):
super(Server, self).cmd_seek(conn, index, pos)
self.player.seek(pos)
# Volume control.
def cmd_setvol(self, conn, vol):
vol = cast_arg(int, vol)
super(Server, self).cmd_setvol(conn, vol)
self.player.volume = float(vol)/100
self.player.volume = float(vol) / 100
# Beets plugin hooks.

View file

@ -29,6 +29,7 @@ import pygst
pygst.require('0.10')
import gst
class GstPlayer(object):
"""A music player abstracting GStreamer's Playbin element.
@ -138,6 +139,7 @@ class GstPlayer(object):
"""
# If we don't use the MainLoop, messages are never sent.
gobject.threads_init()
def start():
loop = gobject.MainLoop()
loop.run()
@ -150,8 +152,8 @@ class GstPlayer(object):
"""
fmt = gst.Format(gst.FORMAT_TIME)
try:
pos = self.player.query_position(fmt, None)[0]/(10**9)
length = self.player.query_duration(fmt, None)[0]/(10**9)
pos = self.player.query_position(fmt, None)[0] / (10 ** 9)
length = self.player.query_duration(fmt, None)[0] / (10 ** 9)
self.cached_time = (pos, length)
return (pos, length)
@ -172,7 +174,7 @@ class GstPlayer(object):
return
fmt = gst.Format(gst.FORMAT_TIME)
ns = position * 10**9 # convert to nanoseconds
ns = position * 10 ** 9 # convert to nanoseconds
self.player.seek_simple(fmt, gst.SEEK_FLAG_FLUSH, ns)
# save new cached time
@ -194,11 +196,13 @@ def play_simple(paths):
p.play_file(path)
p.block()
def play_complicated(paths):
"""Play the files in the path one after the other by using the
callback function to advance to the next song.
"""
my_paths = copy.copy(paths)
def next_song():
my_paths.pop(0)
p.play_file(my_paths[0])
@ -215,4 +219,3 @@ if __name__ == '__main__':
for p in sys.argv[1:]]
# play_simple(paths)
play_complicated(paths)

View file

@ -93,6 +93,7 @@ def acoustid_match(path):
# Plugin structure and autotagging logic.
def _all_releases(items):
"""Given an iterable of Items, determines (according to Acoustid)
which releases the items have in common. Generates release IDs.
@ -111,6 +112,7 @@ def _all_releases(items):
if float(count) / len(items) > COMMON_REL_THRESH:
yield release_id
class AcoustidPlugin(plugins.BeetsPlugin):
def track_distance(self, item, info):
dist = hooks.Distance()
@ -148,6 +150,7 @@ class AcoustidPlugin(plugins.BeetsPlugin):
def commands(self):
submit_cmd = ui.Subcommand('submit',
help='submit Acoustid fingerprints')
def submit_cmd_func(lib, opts, args):
try:
apikey = config['acoustid']['apikey'].get(unicode)
@ -157,7 +160,8 @@ class AcoustidPlugin(plugins.BeetsPlugin):
submit_cmd.func = submit_cmd_func
fingerprint_cmd = ui.Subcommand('fingerprint',
help='generate fingerprints for items without them')
help='generate fingerprints for items without them')
def fingerprint_cmd_func(lib, opts, args):
for item in lib.items(ui.decargs(args)):
fingerprint_item(item,
@ -169,6 +173,7 @@ class AcoustidPlugin(plugins.BeetsPlugin):
# Hooks into import process.
@AcoustidPlugin.listen('import_task_start')
def fingerprint_task(task, session):
"""Fingerprint each item in the task for later use during the
@ -178,6 +183,7 @@ def fingerprint_task(task, session):
for item in items:
acoustid_match(item.path)
@AcoustidPlugin.listen('import_task_apply')
def apply_acoustid_metadata(task, session):
"""Apply Acoustid metadata (fingerprint and ID) to the task's items.
@ -191,10 +197,12 @@ def apply_acoustid_metadata(task, session):
# UI commands.
def submit_items(userkey, items, chunksize=64):
"""Submit fingerprints for the items to the Acoustid server.
"""
data = [] # The running list of dictionaries to submit.
def submit_chunk():
"""Submit the current accumulated fingerprint data."""
log.info('submitting {0} fingerprints'.format(len(data)))

View file

@ -34,7 +34,9 @@ urllib3_logger.setLevel(logging.CRITICAL)
discogs_client.user_agent = 'beets/%s +http://beets.radbox.org/' % \
beets.__version__
class DiscogsPlugin(BeetsPlugin):
def __init__(self):
super(DiscogsPlugin, self).__init__()
self.config.add({
@ -72,7 +74,7 @@ class DiscogsPlugin(BeetsPlugin):
# of an input string as to avoid confusion with other metadata plugins.
# An optional bracket can follow the integer, as this is how discogs
# displays the release ID on its webpage.
match = re.search(r'(^|\[*r|discogs\.com/.+/release/)(\d+)($|\])',
match = re.search(r'(^|\[*r|discogs\.com/.+/release/)(\d+)($|\])',
album_id)
if not match:
return None
@ -112,10 +114,10 @@ class DiscogsPlugin(BeetsPlugin):
album = re.sub(r' +', ' ', result.title)
album_id = result.data['id']
artist, artist_id = self.get_artist(result.data['artists'])
# Use `.data` to access the tracklist directly instead of the convenient
# `.tracklist` property, which will strip out useful artist information
# and leave us with skeleton `Artist` objects that will each make an API
# call just to get the same data back.
# Use `.data` to access the tracklist directly instead of the
# convenient `.tracklist` property, which will strip out useful artist
# information and leave us with skeleton `Artist` objects that will
# each make an API call just to get the same data back.
tracks = self.get_tracks(result.data['tracklist'])
albumtype = ', '.join(
result.data['formats'][0].get('descriptions', [])) or None
@ -172,7 +174,7 @@ class DiscogsPlugin(BeetsPlugin):
index += 1
tracks.append(self.get_track_info(track, index))
else:
index_tracks[index+1] = track['title']
index_tracks[index + 1] = track['title']
# Fix up medium and medium_index for each track. Discogs position is
# unreliable, but tracks are in order.
@ -217,8 +219,8 @@ class DiscogsPlugin(BeetsPlugin):
artist, artist_id = self.get_artist(track.get('artists', []))
length = self.get_track_length(track['duration'])
return TrackInfo(title, track_id, artist, artist_id, length, index,
medium, medium_index, artist_sort=None, disctitle=None,
artist_credit=None)
medium, medium_index, artist_sort=None,
disctitle=None, artist_credit=None)
def get_track_index(self, position):
"""Returns the medium and medium index for a discogs track position.

View file

@ -178,6 +178,7 @@ class DuplicatesPlugin(BeetsPlugin):
' attribute')
def commands(self):
def _dup(lib, opts, args):
self.config.set_args(opts)
fmt = self.config['format'].get()

View file

@ -193,7 +193,6 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin):
values['id'] = song.id
return values
# "Profile" (ID-based) lookup.
def profile(self, item):
@ -221,7 +220,6 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin):
buckets=['id:musicbrainz', 'audio_summary'])
return self._flatten_song(self._pick_song(songs, item))
# "Search" (metadata-based) lookup.
def search(self, item):
@ -233,7 +231,6 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin):
'audio_summary'])
return self._flatten_song(self._pick_song(songs, item))
# "Identify" (fingerprinting) lookup.
def fingerprint(self, item):
@ -276,7 +273,6 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin):
return self._flatten_song(max(songs, key=lambda s: s.score))
# "Analyze" (upload the audio itself) method.
def convert(self, item):
@ -370,7 +366,6 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin):
return self._flatten_song(pick)
return from_track # Fall back to track metadata.
# Shared top-level logic.
def fetch_song(self, item):
@ -427,7 +422,6 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin):
item.try_write()
item.store()
# Automatic (on-import) metadata fetching.
def imported(self, session, task):
@ -438,7 +432,6 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin):
if song:
self.apply_metadata(item, song)
# Explicit command invocation.
def requires_update(self, item):

View file

@ -136,6 +136,7 @@ class EchoNestTempoPlugin(BeetsPlugin):
cmd.parser.add_option('-p', '--print', dest='printbpm',
action='store_true', default=False,
help='print tempo (bpm) to console')
def func(lib, opts, args):
# The "write to files" option corresponds to the
# import_write config value.

View file

@ -26,6 +26,7 @@ from beets import config
log = logging.getLogger('beets')
def _embed(path, items, maxwidth=0):
"""Embed an image file, located at `path`, into each item.
"""
@ -54,6 +55,7 @@ def _embed(path, items, maxwidth=0):
f.images = [image]
f.save(config['id3v23'].get(bool))
class EmbedCoverArtPlugin(BeetsPlugin):
"""Allows albumart to be embedded into the actual files.
"""
@ -75,6 +77,7 @@ class EmbedCoverArtPlugin(BeetsPlugin):
help='embed image files into file metadata')
embed_cmd.parser.add_option('-f', '--file', metavar='PATH',
help='the image file to embed')
def embed_func(lib, opts, args):
if opts.file:
imagepath = normpath(opts.file)
@ -88,6 +91,7 @@ class EmbedCoverArtPlugin(BeetsPlugin):
help='extract an image from file metadata')
extract_cmd.parser.add_option('-o', dest='outpath',
help='image output file')
def extract_func(lib, opts, args):
outpath = normpath(opts.outpath or 'cover')
extract(lib, outpath, decargs(args))
@ -96,14 +100,17 @@ class EmbedCoverArtPlugin(BeetsPlugin):
# Clear command.
clear_cmd = ui.Subcommand('clearart',
help='remove images from file metadata')
def clear_func(lib, opts, args):
clear(lib, decargs(args))
clear_cmd.func = clear_func
return [embed_cmd, extract_cmd, clear_cmd]
# "embedart" command with --file argument.
def embed(lib, imagepath, query):
"""'embedart' command with --file argument.
"""
albums = lib.albums(query)
for i_album in albums:
album = i_album
@ -118,8 +125,10 @@ def embed(lib, imagepath, query):
_embed(imagepath, album.items(),
config['embedart']['maxwidth'].get(int))
# "embedart" command without explicit file.
def embed_current(lib, query):
"""'embedart' command without explicit file.
"""
albums = lib.albums(query)
for album in albums:
if not album.artpath:
@ -132,8 +141,10 @@ def embed_current(lib, query):
_embed(album.artpath, album.items(),
config['embedart']['maxwidth'].get(int))
# "extractart" command.
def extract(lib, outpath, query):
"""'extractart' command.
"""
item = lib.items(query).get()
if not item:
log.error('No item matches query.')
@ -166,8 +177,10 @@ def extract(lib, outpath, query):
with open(syspath(outpath), 'wb') as f:
f.write(art)
# "clearart" command.
def clear(lib, query):
"""'clearart' command.
"""
log.info('Clearing album art from items:')
for item in lib.items(query):
log.info(u'%s - %s' % (item.artist, item.title))
@ -181,9 +194,11 @@ def clear(lib, query):
mf.art = None
mf.save(config['id3v23'].get(bool))
# Automatically embed art into imported albums.
@EmbedCoverArtPlugin.listen('album_imported')
def album_imported(lib, album):
"""Automatically embed art into imported albums.
"""
if album.artpath and config['embedart']['auto']:
_embed(album.artpath, album.items(),
config['embedart']['maxwidth'].get(int))

View file

@ -72,11 +72,13 @@ def _fetch_image(url):
CAA_URL = 'http://coverartarchive.org/release/{mbid}/front-500.jpg'
CAA_GROUP_URL = 'http://coverartarchive.org/release-group/{mbid}/front-500.jpg'
def caa_art(release_id):
"""Return the Cover Art Archive URL given a MusicBrainz release ID.
"""
return CAA_URL.format(mbid=release_id)
def caa_group_art(release_group_id):
"""Return the Cover Art Archive release group URL given a MusicBrainz
release group ID.
@ -89,6 +91,7 @@ def caa_group_art(release_group_id):
AMAZON_URL = 'http://images.amazon.com/images/P/%s.%02i.LZZZZZZZ.jpg'
AMAZON_INDICES = (1, 2)
def art_for_asin(asin):
"""Generate URLs for an Amazon ID (ASIN) string."""
for index in AMAZON_INDICES:
@ -100,6 +103,7 @@ def art_for_asin(asin):
AAO_URL = 'http://www.albumart.org/index_detail.php'
AAO_PAT = r'href\s*=\s*"([^>"]*)"[^>]*title\s*=\s*"View larger image"'
def aao_art(asin):
"""Return art URL from AlbumArt.org given an ASIN."""
# Get the page from albumart.org.
@ -121,6 +125,7 @@ def aao_art(asin):
# Art from the filesystem.
def art_in_path(path, cover_names, cautious):
"""Look for album art files in a specified directory."""
if not os.path.isdir(path):
@ -152,6 +157,7 @@ def art_in_path(path, cover_names, cautious):
# Try each source in turn.
def _source_urls(album):
"""Generate possible source URLs for an album's art. The URLs are
not guaranteed to work so they each need to be attempted in turn.
@ -173,6 +179,7 @@ def _source_urls(album):
if url:
yield url
def art_for_album(album, paths, maxwidth=None, local_only=False):
"""Given an Album object, returns a path to downloaded art for the
album (or None if no art is found). If `maxwidth`, then images are
@ -209,6 +216,7 @@ def art_for_album(album, paths, maxwidth=None, local_only=False):
# PLUGIN LOGIC ###############################################################
def batch_fetch_art(lib, albums, force, maxwidth=None):
"""Fetch album art for each of the albums. This implements the manual
fetchart CLI command.
@ -233,6 +241,7 @@ def batch_fetch_art(lib, albums, force, maxwidth=None):
log.info(u'{0} - {1}: {2}'.format(album.albumartist, album.album,
message))
class FetchArtPlugin(BeetsPlugin):
def __init__(self):
super(FetchArtPlugin, self).__init__()
@ -255,9 +264,10 @@ class FetchArtPlugin(BeetsPlugin):
self.import_stages = [self.fetch_art]
self.register_listener('import_task_files', self.assign_art)
# Asynchronous; after music is added to the library.
def fetch_art(self, session, task):
"""Find art for the album being imported."""
# Asynchronous; after music is added to the library.
if task.is_album: # Only fetch art for full albums.
if task.choice_flag == importer.action.ASIS:
# For as-is imports, don't search Web sources for art.
@ -275,9 +285,10 @@ class FetchArtPlugin(BeetsPlugin):
if path:
self.art_paths[task] = path
# Synchronous; after music files are put in place.
def assign_art(self, session, task):
"""Place the discovered art in the filesystem."""
# Synchronous; after music files are put in place.
if task in self.art_paths:
path = self.art_paths.pop(task)
@ -289,12 +300,14 @@ class FetchArtPlugin(BeetsPlugin):
if src_removed:
task.prune(path)
# Manual album art fetching.
def commands(self):
# Manual album art fetching.
cmd = ui.Subcommand('fetchart', help='download album art')
cmd.parser.add_option('-f', '--force', dest='force',
action='store_true', default=False,
help='re-download art when already present')
def func(lib, opts, args):
batch_fetch_art(lib, lib.albums(ui.decargs(args)), opts.force,
self.maxwidth)

View file

@ -139,6 +139,7 @@ def apply_matches(d):
class FromFilenamePlugin(plugins.BeetsPlugin):
pass
@FromFilenamePlugin.listen('import_task_start')
def filename_task(task, session):
"""Examine each item in the task to see if we can extract a title

View file

@ -115,6 +115,7 @@ class FtInTitlePlugin(BeetsPlugin):
def commands(self):
cmd = ui.Subcommand('ftintitle',
help='move featured artists to the title field')
def func(lib, opts, args):
write = config['import']['write'].get(bool)
for item in lib.items(ui.decargs(args)):

View file

@ -25,6 +25,7 @@ from beets import config
M3U_DEFAULT_NAME = 'imported.m3u'
class ImportFeedsPlugin(BeetsPlugin):
def __init__(self):
super(ImportFeedsPlugin, self).__init__()
@ -50,6 +51,7 @@ class ImportFeedsPlugin(BeetsPlugin):
else:
self.config['relative_to'] = feeds_dir
def _get_feeds_dir(lib):
"""Given a Library object, return the path to the feeds directory to be
used (either in the library directory or an explicitly configured
@ -63,6 +65,7 @@ def _get_feeds_dir(lib):
os.makedirs(syspath(dirpath))
return dirpath
def _build_m3u_filename(basename):
"""Builds unique m3u filename by appending given basename to current
date."""
@ -75,6 +78,7 @@ def _build_m3u_filename(basename):
))
return path
def _write_m3u(m3u_path, items_paths):
"""Append relative paths to items into m3u file.
"""
@ -82,6 +86,7 @@ def _write_m3u(m3u_path, items_paths):
for path in items_paths:
f.write(path + '\n')
def _record_items(lib, basename, items):
"""Records relative paths to the given items for each feed format
"""
@ -117,15 +122,18 @@ def _record_items(lib, basename, items):
if not os.path.exists(syspath(dest)):
os.symlink(syspath(path), syspath(dest))
@ImportFeedsPlugin.listen('library_opened')
def library_opened(lib):
if config['importfeeds']['dir'].get() is None:
config['importfeeds']['dir'] = _get_feeds_dir(lib)
@ImportFeedsPlugin.listen('album_imported')
def album_imported(lib, album):
_record_items(lib, album.album, album.items())
@ImportFeedsPlugin.listen('item_imported')
def item_imported(lib, item):
_record_items(lib, item.title, [item])

View file

@ -64,8 +64,10 @@ def info(paths):
class InfoPlugin(BeetsPlugin):
def commands(self):
cmd = ui.Subcommand('info', help='show file metadata')
def func(lib, opts, args):
if not args:
raise ui.UserError('no file specified')

View file

@ -25,15 +25,17 @@ log = logging.getLogger('beets')
FUNC_NAME = u'__INLINE_FUNC__'
class InlineError(Exception):
"""Raised when a runtime error occurs in an inline expression.
"""
def __init__(self, code, exc):
super(InlineError, self).__init__(
(u"error in inline path field code:\n" \
(u"error in inline path field code:\n"
u"%s\n%s: %s") % (code, type(exc).__name__, unicode(exc))
)
def _compile_func(body):
"""Given Python code for a function body, return a compiled
callable that invokes that code.
@ -47,6 +49,7 @@ def _compile_func(body):
eval(code, env)
return env[FUNC_NAME]
def compile_inline(python_code, album):
"""Given a Python expression or function body, compile it as a path
field function. The returned function takes a single argument, an
@ -95,6 +98,7 @@ def compile_inline(python_code, album):
raise InlineError(python_code, exc)
return _func_func
class InlinePlugin(BeetsPlugin):
def __init__(self):
super(InlinePlugin, self).__init__()
@ -118,4 +122,4 @@ class InlinePlugin(BeetsPlugin):
log.debug(u'inline: adding album field %s' % key)
func = compile_inline(view.get(unicode), True)
if func is not None:
self.album_template_fields[key] = func
self.album_template_fields[key] = func

View file

@ -69,4 +69,4 @@ class KeyFinderPlugin(BeetsPlugin):
log.debug('added computed initial key {0} for {1}'
.format(key, util.displayable_path(item.path)))
item.try_write()
item.store()
item.store()

View file

@ -86,6 +86,7 @@ def _tags_for(obj):
))
return tags
def _is_allowed(genre):
"""Determine whether the genre is present in the whitelist,
returning a boolean.
@ -96,6 +97,7 @@ def _is_allowed(genre):
return True
return False
def _strings_to_genre(tags):
"""Given a list of strings, return a genre by joining them into a
single string and (optionally) canonicalizing each.
@ -118,6 +120,7 @@ def _strings_to_genre(tags):
tags[:config['lastgenre']['count'].get(int)]
)
def fetch_genre(lastfm_obj):
"""Return the genre for a pylast entity or None if no suitable genre
can be found. Ex. 'Electronic, House, Dance'
@ -127,6 +130,7 @@ def fetch_genre(lastfm_obj):
# Canonicalization tree processing.
def flatten_tree(elem, path, branches):
"""Flatten nested lists/dictionaries into lists of strings
(branches).
@ -143,6 +147,7 @@ def flatten_tree(elem, path, branches):
else:
branches.append(path + [unicode(elem)])
def find_parents(candidate, branches):
"""Find parents genre of a given genre, ordered from the closest to
the further parent.
@ -160,6 +165,7 @@ def find_parents(candidate, branches):
_genre_cache = {}
def _cached_lookup(entity, method, *args):
"""Get a genre based on the named entity using the callable `method`
whose arguments are given in the sequence `args`. The genre lookup
@ -177,22 +183,26 @@ def _cached_lookup(entity, method, *args):
_genre_cache[key] = genre
return genre
def fetch_album_genre(obj):
"""Return the album genre for this Item or Album.
"""
return _cached_lookup(u'album', LASTFM.get_album, obj.albumartist,
obj.album)
def fetch_album_artist_genre(obj):
"""Return the album artist genre for this Item or Album.
"""
return _cached_lookup(u'artist', LASTFM.get_artist, obj.albumartist)
def fetch_artist_genre(item):
"""Returns the track artist genre for this Item.
"""
return _cached_lookup(u'artist', LASTFM.get_artist, item.artist)
def fetch_track_genre(obj):
"""Returns the track genre for this Item.
"""
@ -206,6 +216,8 @@ options = {
'branches': None,
'c14n': False,
}
class LastGenrePlugin(plugins.BeetsPlugin):
def __init__(self):
super(LastGenrePlugin, self).__init__()
@ -336,6 +348,7 @@ class LastGenrePlugin(plugins.BeetsPlugin):
lastgenre_cmd.parser.add_option('-s', '--source', dest='source',
type='string',
help='genre source: artist, album, or track')
def lastgenre_func(lib, opts, args):
write = config['import']['write'].get(bool)
self.config.set_args(opts)
@ -353,9 +366,8 @@ class LastGenrePlugin(plugins.BeetsPlugin):
if 'track' in self.sources:
item.genre, src = self._get_genre(item)
item.store()
log.info(u'genre for track {0} - {1} ({2}): {3}'.format(
item.artist, item.title, src, item.genre
))
log.info(u'genre for track {0} - {1} ({2}): {3}'.
format(item.artist, item.title, src, item.genre))
if write:
item.try_write()

View file

@ -1,16 +1,16 @@
#Copyright (c) 2011, Jeffrey Aylesworth <jeffrey@aylesworth.ca>
# Copyright (c) 2011, Jeffrey Aylesworth <jeffrey@aylesworth.ca>
#
#Permission to use, copy, modify, and/or distribute this software for any
#purpose with or without fee is hereby granted, provided that the above
#copyright notice and this permission notice appear in all copies.
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
#THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
#WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
#MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
#ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
#WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
#ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
#OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
from __future__ import print_function
@ -28,6 +28,7 @@ UUID_REGEX = r'^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$'
log = logging.getLogger('beets.bpd')
def mb_call(func, *args, **kwargs):
"""Call a MusicBrainz API function and catch exceptions.
"""
@ -40,6 +41,7 @@ def mb_call(func, *args, **kwargs):
except musicbrainzngs.UsageError:
raise ui.UserError('MusicBrainz credentials missing')
def submit_albums(collection_id, release_ids):
"""Add all of the release IDs to the indicated collection. Multiple
requests are made if there are many release IDs to submit.
@ -51,6 +53,7 @@ def submit_albums(collection_id, release_ids):
collection_id, chunk
)
def update_collection(lib, opts, args):
# Get the collection to modify.
collections = mb_call(musicbrainzngs.get_collections)
@ -77,6 +80,7 @@ update_mb_collection_cmd = Subcommand('mbupdate',
help='Update MusicBrainz collection')
update_mb_collection_cmd.func = update_collection
class MusicBrainzCollectionPlugin(BeetsPlugin):
def __init__(self):
super(MusicBrainzCollectionPlugin, self).__init__()
@ -86,4 +90,4 @@ class MusicBrainzCollectionPlugin(BeetsPlugin):
)
def commands(self):
return [update_mb_collection_cmd]
return [update_mb_collection_cmd]

View file

@ -151,4 +151,4 @@ class MBSyncPlugin(BeetsPlugin):
default=config['import']['write'], dest='write',
help="don't write updated metadata to files")
cmd.func = mbsync_func
return [cmd]
return [cmd]

View file

@ -177,8 +177,8 @@ class MPDStats(object):
@staticmethod
def update_item(item, attribute, value=None, increment=None):
"""Update the beets item. Set attribute to value or increment the value
of attribute. If the increment argument is used the value is cast to the
corresponding type.
of attribute. If the increment argument is used the value is cast to
the corresponding type.
"""
if item is None:
return
@ -297,7 +297,8 @@ class MPDStats(object):
if handler:
handler(status)
else:
log.debug(u'mpdstats: unhandled status "{0}"'.format(status))
log.debug(u'mpdstats: unhandled status "{0}"'.
format(status))
events = self.mpd.events()
@ -347,4 +348,4 @@ class MPDStatsPlugin(plugins.BeetsPlugin):
pass
cmd.func = func
return [cmd]
return [cmd]

View file

@ -31,6 +31,7 @@ from beets import config
# once before beets exits.
database_changed = False
# No need to introduce a dependency on an MPD library for such a
# simple use case. Here's a simple socket abstraction to make things
# easier.
@ -64,6 +65,7 @@ class BufferedSocket(object):
def close(self):
self.sock.close()
def update_mpd(host='localhost', port=6600, password=None):
"""Sends the "update" command to the MPD server indicated,
possibly authenticating with a password first.
@ -94,6 +96,7 @@ def update_mpd(host='localhost', port=6600, password=None):
s.close()
print('... updated.')
class MPDUpdatePlugin(BeetsPlugin):
def __init__(self):
super(MPDUpdatePlugin, self).__init__()

View file

@ -23,6 +23,7 @@ from operator import attrgetter
from itertools import groupby
import collections
def random_item(lib, opts, args):
query = decargs(args)
if opts.path:
@ -82,6 +83,7 @@ random_cmd.parser.add_option('-e', '--equal-chance', action='store_true',
help='each artist has the same chance')
random_cmd.func = random_item
class Random(BeetsPlugin):
def commands(self):
return [random_cmd]

View file

@ -82,6 +82,7 @@ class Backend(object):
# mpgain/aacgain CLI tool backend.
class CommandBackend(Backend):
def __init__(self, config):
config.add({

View file

@ -26,6 +26,7 @@ from beets import config
log = logging.getLogger('beets')
def rewriter(field, rules):
"""Create a template field function that rewrites the given field
with the given rewriting rules. ``rules`` must be a list of
@ -41,6 +42,7 @@ def rewriter(field, rules):
return value
return fieldfunc
class RewritePlugin(BeetsPlugin):
def __init__(self):
super(RewritePlugin, self).__init__()

View file

@ -79,7 +79,7 @@ def update_playlists(lib):
item_path = item.path
if relative_to:
item_path = os.path.relpath(item.path, relative_to)
if not item_path in m3us[m3u_name]:
if item_path not in m3us[m3u_name]:
m3us[m3u_name].append(item_path)
# Now iterate through the m3us that we need to generate
for m3u in m3us:
@ -104,7 +104,7 @@ class SmartPlaylistPlugin(BeetsPlugin):
def update(lib, opts, args):
update_playlists(lib)
spl_update = ui.Subcommand('splupdate',
help='update the smart playlists')
help='update the smart playlists')
spl_update.func = update
return [spl_update]

View file

@ -25,6 +25,7 @@ PATTERN_THE = u'^[the]{3}\s'
PATTERN_A = u'^[a][n]?\s'
FORMAT = u'{0}, {1}'
class ThePlugin(BeetsPlugin):
_instance = None
@ -67,14 +68,12 @@ class ThePlugin(BeetsPlugin):
if not self.patterns:
self._log.warn(u'[the] no patterns defined!')
def unthe(self, text, pattern):
"""Moves pattern in the path format string or strips it
text -- text to handle
pattern -- regexp pattern (case ignore is already on)
strip -- if True, pattern will be removed
"""
if text:
r = re.compile(pattern, flags=re.IGNORECASE)
@ -103,4 +102,4 @@ class ThePlugin(BeetsPlugin):
self._log.debug(u'[the] \"{0}\" -> \"{1}\"'.format(text, r))
return r
else:
return u''
return u''

View file

@ -51,6 +51,7 @@ def _rep(obj, expand=False):
out['items'] = [_rep(item) for item in obj.items()]
return out
def json_generator(items, root):
"""Generator that dumps list of beets Items or Albums as JSON
@ -68,6 +69,7 @@ def json_generator(items, root):
yield json.dumps(_rep(item))
yield ']}'
def resource(name):
"""Decorates a function to handle RESTful HTTP requests for a resource.
"""
@ -88,6 +90,7 @@ def resource(name):
return responder
return make_responder
def resource_query(name):
"""Decorates a function to handle RESTful HTTP queries for resources.
"""
@ -100,6 +103,7 @@ def resource_query(name):
return responder
return make_responder
def resource_list(name):
"""Decorates a function to handle RESTful HTTP request for a list of
resources.
@ -148,6 +152,7 @@ app = flask.Flask(__name__)
app.url_map.converters['idlist'] = IdListConverter
app.url_map.converters['query'] = QueryConverter
@app.before_request
def before_request():
g.lib = app.config['lib']
@ -167,6 +172,7 @@ def get_item(id):
def all_items():
return g.lib.items()
@app.route('/item/<int:item_id>/file')
def item_file(item_id):
item = g.lib.get_item(item_id)
@ -175,6 +181,7 @@ def item_file(item_id):
response.headers['Content-Length'] = os.path.getsize(item.path)
return response
@app.route('/item/query/<query:queries>')
@resource_query('items')
def item_query(queries):
@ -188,17 +195,20 @@ def item_query(queries):
def get_album(id):
return g.lib.get_album(id)
@app.route('/album/')
@app.route('/album/query/')
@resource_list('albums')
def all_albums():
return g.lib.albums()
@app.route('/album/query/<query:queries>')
@resource_query('albums')
def album_query(queries):
return g.lib.albums(queries)
@app.route('/album/<int:album_id>/art')
def album_art(album_id):
album = g.lib.get_album(album_id)
@ -249,6 +259,7 @@ class WebPlugin(BeetsPlugin):
cmd = ui.Subcommand('web', help='start a Web interface')
cmd.parser.add_option('-d', '--debug', action='store_true',
default=False, help='debug mode')
def func(lib, opts, args):
args = ui.decargs(args)
if args:

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.
""" Clears tag fields in media files."""
""" Clears tag fields in media files."""
import re
import logging
@ -62,11 +62,11 @@ class ZeroPlugin(BeetsPlugin):
if task.choice_flag == action.ASIS and not self.warned:
log.warn(u'[zero] cannot zero in \"as-is\" mode')
self.warned = True
# TODO request write in as-is mode
# TODO request write in as-is mode
@classmethod
def match_patterns(cls, field, patterns):
"""Check if field (as string) is matching any of the patterns in
"""Check if field (as string) is matching any of the patterns in
the list.
"""
for p in patterns:
@ -89,4 +89,4 @@ class ZeroPlugin(BeetsPlugin):
if self.match_patterns(value, patterns):
log.debug(u'[zero] {0}: {1} -> None'.format(field, value))
setattr(item, field, None)
setattr(item, field, None)