mirror of
https://github.com/beetbox/beets.git
synced 2025-12-16 05:34:47 +01:00
flake8 cleaning beetsplug/*
Remaining warnings are related to visual indentation.
This commit is contained in:
parent
1df6303222
commit
3ead936fe5
29 changed files with 209 additions and 112 deletions
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)):
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
@ -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]
|
||||
|
|
@ -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]
|
||||
|
|
@ -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__()
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ class Backend(object):
|
|||
|
||||
# mpgain/aacgain CLI tool backend.
|
||||
|
||||
|
||||
class CommandBackend(Backend):
|
||||
def __init__(self, config):
|
||||
config.add({
|
||||
|
|
|
|||
|
|
@ -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__()
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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''
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
Loading…
Reference in a new issue