mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 16:42:42 +01:00
Update multiple plugins: pass the logger around
This commit is contained in:
parent
7d58a38428
commit
32673b87e7
10 changed files with 289 additions and 322 deletions
|
|
@ -16,13 +16,11 @@
|
||||||
"""
|
"""
|
||||||
import shlex
|
import shlex
|
||||||
|
|
||||||
from beets import logging
|
|
||||||
from beets.plugins import BeetsPlugin
|
from beets.plugins import BeetsPlugin
|
||||||
from beets.ui import decargs, print_obj, vararg_callback, Subcommand, UserError
|
from beets.ui import decargs, print_obj, vararg_callback, Subcommand, UserError
|
||||||
from beets.util import command_output, displayable_path, subprocess
|
from beets.util import command_output, displayable_path, subprocess
|
||||||
|
|
||||||
PLUGIN = 'duplicates'
|
PLUGIN = 'duplicates'
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def _process_item(item, lib, copy=False, move=False, delete=False,
|
def _process_item(item, lib, copy=False, move=False, delete=False,
|
||||||
|
|
@ -47,7 +45,7 @@ def _process_item(item, lib, copy=False, move=False, delete=False,
|
||||||
print_obj(item, lib, fmt=format)
|
print_obj(item, lib, fmt=format)
|
||||||
|
|
||||||
|
|
||||||
def _checksum(item, prog):
|
def _checksum(item, prog, log):
|
||||||
"""Run external `prog` on file path associated with `item`, cache
|
"""Run external `prog` on file path associated with `item`, cache
|
||||||
output as flexattr on a key that is the name of the program, and
|
output as flexattr on a key that is the name of the program, and
|
||||||
return the key, checksum tuple.
|
return the key, checksum tuple.
|
||||||
|
|
@ -73,7 +71,7 @@ def _checksum(item, prog):
|
||||||
return key, checksum
|
return key, checksum
|
||||||
|
|
||||||
|
|
||||||
def _group_by(objs, keys):
|
def _group_by(objs, keys, log):
|
||||||
"""Return a dictionary with keys arbitrary concatenations of attributes and
|
"""Return a dictionary with keys arbitrary concatenations of attributes and
|
||||||
values lists of objects (Albums or Items) with those keys.
|
values lists of objects (Albums or Items) with those keys.
|
||||||
"""
|
"""
|
||||||
|
|
@ -92,11 +90,11 @@ def _group_by(objs, keys):
|
||||||
return counts
|
return counts
|
||||||
|
|
||||||
|
|
||||||
def _duplicates(objs, keys, full):
|
def _duplicates(objs, keys, full, log):
|
||||||
"""Generate triples of keys, duplicate counts, and constituent objects.
|
"""Generate triples of keys, duplicate counts, and constituent objects.
|
||||||
"""
|
"""
|
||||||
offset = 0 if full else 1
|
offset = 0 if full else 1
|
||||||
for k, objs in _group_by(objs, keys).iteritems():
|
for k, objs in _group_by(objs, keys, log).iteritems():
|
||||||
if len(objs) > 1:
|
if len(objs) > 1:
|
||||||
yield (k, len(objs) - offset, objs[offset:])
|
yield (k, len(objs) - offset, objs[offset:])
|
||||||
|
|
||||||
|
|
@ -214,12 +212,13 @@ class DuplicatesPlugin(BeetsPlugin):
|
||||||
'duplicates: "checksum" option must be a command'
|
'duplicates: "checksum" option must be a command'
|
||||||
)
|
)
|
||||||
for i in items:
|
for i in items:
|
||||||
k, _ = _checksum(i, checksum)
|
k, _ = self._checksum(i, checksum, self._log)
|
||||||
keys = [k]
|
keys = [k]
|
||||||
|
|
||||||
for obj_id, obj_count, objs in _duplicates(items,
|
for obj_id, obj_count, objs in _duplicates(items,
|
||||||
keys=keys,
|
keys=keys,
|
||||||
full=full):
|
full=full,
|
||||||
|
log=self._log):
|
||||||
if obj_id: # Skip empty IDs.
|
if obj_id: # Skip empty IDs.
|
||||||
for o in objs:
|
for o in objs:
|
||||||
_process_item(o, lib,
|
_process_item(o, lib,
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,6 @@ from beets import plugins
|
||||||
from beets import ui
|
from beets import ui
|
||||||
from beets.util import displayable_path
|
from beets.util import displayable_path
|
||||||
from beets import config
|
from beets import config
|
||||||
from beets import logging
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def split_on_feat(artist):
|
def split_on_feat(artist):
|
||||||
|
|
@ -46,69 +43,6 @@ def contains_feat(title):
|
||||||
return bool(re.search(plugins.feat_tokens(), title, flags=re.IGNORECASE))
|
return bool(re.search(plugins.feat_tokens(), title, flags=re.IGNORECASE))
|
||||||
|
|
||||||
|
|
||||||
def update_metadata(item, feat_part, drop_feat):
|
|
||||||
"""Choose how to add new artists to the title and set the new
|
|
||||||
metadata. Also, print out messages about any changes that are made.
|
|
||||||
If `drop_feat` is set, then do not add the artist to the title; just
|
|
||||||
remove it from the artist field.
|
|
||||||
"""
|
|
||||||
# In all cases, update the artist fields.
|
|
||||||
log.info(u'artist: {0} -> {1}', item.artist, item.albumartist)
|
|
||||||
item.artist = item.albumartist
|
|
||||||
if item.artist_sort:
|
|
||||||
# Just strip the featured artist from the sort name.
|
|
||||||
item.artist_sort, _ = split_on_feat(item.artist_sort)
|
|
||||||
|
|
||||||
# Only update the title if it does not already contain a featured
|
|
||||||
# artist and if we do not drop featuring information.
|
|
||||||
if not drop_feat and not contains_feat(item.title):
|
|
||||||
new_title = u"{0} feat. {1}".format(item.title, feat_part)
|
|
||||||
log.info(u'title: {0} -> {1}', item.title, new_title)
|
|
||||||
item.title = new_title
|
|
||||||
|
|
||||||
|
|
||||||
def ft_in_title(item, drop_feat):
|
|
||||||
"""Look for featured artists in the item's artist fields and move
|
|
||||||
them to the title.
|
|
||||||
"""
|
|
||||||
artist = item.artist.strip()
|
|
||||||
albumartist = item.albumartist.strip()
|
|
||||||
|
|
||||||
# Check whether there is a featured artist on this track and the
|
|
||||||
# artist field does not exactly match the album artist field. In
|
|
||||||
# that case, we attempt to move the featured artist to the title.
|
|
||||||
_, featured = split_on_feat(artist)
|
|
||||||
if featured and albumartist != artist and albumartist:
|
|
||||||
log.info(displayable_path(item.path))
|
|
||||||
feat_part = None
|
|
||||||
|
|
||||||
# Look for the album artist in the artist field. If it's not
|
|
||||||
# present, give up.
|
|
||||||
albumartist_split = artist.split(albumartist, 1)
|
|
||||||
if len(albumartist_split) <= 1:
|
|
||||||
log.info('album artist not present in artist')
|
|
||||||
|
|
||||||
# If the last element of the split (the right-hand side of the
|
|
||||||
# album artist) is nonempty, then it probably contains the
|
|
||||||
# featured artist.
|
|
||||||
elif albumartist_split[-1] != '':
|
|
||||||
# Extract the featured artist from the right-hand side.
|
|
||||||
_, feat_part = split_on_feat(albumartist_split[-1])
|
|
||||||
|
|
||||||
# Otherwise, if there's nothing on the right-hand side, look for a
|
|
||||||
# featuring artist on the left-hand side.
|
|
||||||
else:
|
|
||||||
lhs, rhs = split_on_feat(albumartist_split[0])
|
|
||||||
if rhs:
|
|
||||||
feat_part = lhs
|
|
||||||
|
|
||||||
# If we have a featuring artist, move it to the title.
|
|
||||||
if feat_part:
|
|
||||||
update_metadata(item, feat_part, drop_feat)
|
|
||||||
else:
|
|
||||||
log.info(u'no featuring artists found')
|
|
||||||
|
|
||||||
|
|
||||||
class FtInTitlePlugin(plugins.BeetsPlugin):
|
class FtInTitlePlugin(plugins.BeetsPlugin):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(FtInTitlePlugin, self).__init__()
|
super(FtInTitlePlugin, self).__init__()
|
||||||
|
|
@ -138,7 +72,7 @@ class FtInTitlePlugin(plugins.BeetsPlugin):
|
||||||
write = config['import']['write'].get(bool)
|
write = config['import']['write'].get(bool)
|
||||||
|
|
||||||
for item in lib.items(ui.decargs(args)):
|
for item in lib.items(ui.decargs(args)):
|
||||||
ft_in_title(item, drop_feat)
|
self.ft_in_title(item, drop_feat)
|
||||||
item.store()
|
item.store()
|
||||||
if write:
|
if write:
|
||||||
item.try_write()
|
item.try_write()
|
||||||
|
|
@ -152,5 +86,66 @@ class FtInTitlePlugin(plugins.BeetsPlugin):
|
||||||
drop_feat = self.config['drop'].get(bool)
|
drop_feat = self.config['drop'].get(bool)
|
||||||
|
|
||||||
for item in task.imported_items():
|
for item in task.imported_items():
|
||||||
ft_in_title(item, drop_feat)
|
self.ft_in_title(item, drop_feat)
|
||||||
item.store()
|
item.store()
|
||||||
|
|
||||||
|
def update_metadata(self, item, feat_part, drop_feat):
|
||||||
|
"""Choose how to add new artists to the title and set the new
|
||||||
|
metadata. Also, print out messages about any changes that are made.
|
||||||
|
If `drop_feat` is set, then do not add the artist to the title; just
|
||||||
|
remove it from the artist field.
|
||||||
|
"""
|
||||||
|
# In all cases, update the artist fields.
|
||||||
|
self._log.info(u'artist: {0} -> {1}', item.artist, item.albumartist)
|
||||||
|
item.artist = item.albumartist
|
||||||
|
if item.artist_sort:
|
||||||
|
# Just strip the featured artist from the sort name.
|
||||||
|
item.artist_sort, _ = split_on_feat(item.artist_sort)
|
||||||
|
|
||||||
|
# Only update the title if it does not already contain a featured
|
||||||
|
# artist and if we do not drop featuring information.
|
||||||
|
if not drop_feat and not contains_feat(item.title):
|
||||||
|
new_title = u"{0} feat. {1}".format(item.title, feat_part)
|
||||||
|
self._log.info(u'title: {0} -> {1}', item.title, new_title)
|
||||||
|
item.title = new_title
|
||||||
|
|
||||||
|
def ft_in_title(self, item, drop_feat):
|
||||||
|
"""Look for featured artists in the item's artist fields and move
|
||||||
|
them to the title.
|
||||||
|
"""
|
||||||
|
artist = item.artist.strip()
|
||||||
|
albumartist = item.albumartist.strip()
|
||||||
|
|
||||||
|
# Check whether there is a featured artist on this track and the
|
||||||
|
# artist field does not exactly match the album artist field. In
|
||||||
|
# that case, we attempt to move the featured artist to the title.
|
||||||
|
_, featured = split_on_feat(artist)
|
||||||
|
if featured and albumartist != artist and albumartist:
|
||||||
|
self._log.info(displayable_path(item.path))
|
||||||
|
feat_part = None
|
||||||
|
|
||||||
|
# Look for the album artist in the artist field. If it's not
|
||||||
|
# present, give up.
|
||||||
|
albumartist_split = artist.split(albumartist, 1)
|
||||||
|
if len(albumartist_split) <= 1:
|
||||||
|
self._log.info('album artist not present in artist')
|
||||||
|
|
||||||
|
# If the last element of the split (the right-hand side of the
|
||||||
|
# album artist) is nonempty, then it probably contains the
|
||||||
|
# featured artist.
|
||||||
|
elif albumartist_split[-1] != '':
|
||||||
|
# Extract the featured artist from the right-hand side.
|
||||||
|
_, feat_part = split_on_feat(albumartist_split[-1])
|
||||||
|
|
||||||
|
# Otherwise, if there's nothing on the right-hand side, look for a
|
||||||
|
# featuring artist on the left-hand side.
|
||||||
|
else:
|
||||||
|
lhs, rhs = split_on_feat(albumartist_split[0])
|
||||||
|
if rhs:
|
||||||
|
feat_part = lhs
|
||||||
|
|
||||||
|
# If we have a featuring artist, move it to the title.
|
||||||
|
if feat_part:
|
||||||
|
self.update_metadata(item, feat_part, drop_feat)
|
||||||
|
else:
|
||||||
|
self._log.info(u'no featuring artists found')
|
||||||
|
|
|
||||||
|
|
@ -17,56 +17,12 @@
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from beets import logging
|
|
||||||
from beets.plugins import BeetsPlugin
|
from beets.plugins import BeetsPlugin
|
||||||
from beets import ui
|
from beets import ui
|
||||||
from beets import mediafile
|
from beets import mediafile
|
||||||
from beets.util import displayable_path, normpath, syspath
|
from beets.util import displayable_path, normpath, syspath
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def run(lib, opts, args):
|
|
||||||
"""Print tag info or library data for each file referenced by args.
|
|
||||||
|
|
||||||
Main entry point for the `beet info ARGS...` command.
|
|
||||||
|
|
||||||
If an argument is a path pointing to an existing file, then the tags
|
|
||||||
of that file are printed. All other arguments are considered
|
|
||||||
queries, and for each item matching all those queries the tags from
|
|
||||||
the file are printed.
|
|
||||||
|
|
||||||
If `opts.summarize` is true, the function merges all tags into one
|
|
||||||
dictionary and only prints that. If two files have different values
|
|
||||||
for the same tag, the value is set to '[various]'
|
|
||||||
"""
|
|
||||||
if opts.library:
|
|
||||||
data_collector = library_data
|
|
||||||
else:
|
|
||||||
data_collector = tag_data
|
|
||||||
|
|
||||||
first = True
|
|
||||||
summary = {}
|
|
||||||
for data_emitter in data_collector(lib, ui.decargs(args)):
|
|
||||||
try:
|
|
||||||
data = data_emitter()
|
|
||||||
except mediafile.UnreadableFileError as ex:
|
|
||||||
log.error(u'cannot read file: {0}', ex.message)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if opts.summarize:
|
|
||||||
update_summary(summary, data)
|
|
||||||
else:
|
|
||||||
if not first:
|
|
||||||
ui.print_()
|
|
||||||
print_data(data)
|
|
||||||
first = False
|
|
||||||
|
|
||||||
if opts.summarize:
|
|
||||||
print_data(summary)
|
|
||||||
|
|
||||||
|
|
||||||
def tag_data(lib, args):
|
def tag_data(lib, args):
|
||||||
query = []
|
query = []
|
||||||
for arg in args:
|
for arg in args:
|
||||||
|
|
@ -143,9 +99,48 @@ class InfoPlugin(BeetsPlugin):
|
||||||
|
|
||||||
def commands(self):
|
def commands(self):
|
||||||
cmd = ui.Subcommand('info', help='show file metadata')
|
cmd = ui.Subcommand('info', help='show file metadata')
|
||||||
cmd.func = run
|
cmd.func = self.run
|
||||||
cmd.parser.add_option('-l', '--library', action='store_true',
|
cmd.parser.add_option('-l', '--library', action='store_true',
|
||||||
help='show library fields instead of tags')
|
help='show library fields instead of tags')
|
||||||
cmd.parser.add_option('-s', '--summarize', action='store_true',
|
cmd.parser.add_option('-s', '--summarize', action='store_true',
|
||||||
help='summarize the tags of all files')
|
help='summarize the tags of all files')
|
||||||
return [cmd]
|
return [cmd]
|
||||||
|
|
||||||
|
def run(self, lib, opts, args):
|
||||||
|
"""Print tag info or library data for each file referenced by args.
|
||||||
|
|
||||||
|
Main entry point for the `beet info ARGS...` command.
|
||||||
|
|
||||||
|
If an argument is a path pointing to an existing file, then the tags
|
||||||
|
of that file are printed. All other arguments are considered
|
||||||
|
queries, and for each item matching all those queries the tags from
|
||||||
|
the file are printed.
|
||||||
|
|
||||||
|
If `opts.summarize` is true, the function merges all tags into one
|
||||||
|
dictionary and only prints that. If two files have different values
|
||||||
|
for the same tag, the value is set to '[various]'
|
||||||
|
"""
|
||||||
|
if opts.library:
|
||||||
|
data_collector = library_data
|
||||||
|
else:
|
||||||
|
data_collector = tag_data
|
||||||
|
|
||||||
|
first = True
|
||||||
|
summary = {}
|
||||||
|
for data_emitter in data_collector(lib, ui.decargs(args)):
|
||||||
|
try:
|
||||||
|
data = data_emitter()
|
||||||
|
except mediafile.UnreadableFileError as ex:
|
||||||
|
self._log.error(u'cannot read file: {0}', ex.message)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if opts.summarize:
|
||||||
|
update_summary(summary, data)
|
||||||
|
else:
|
||||||
|
if not first:
|
||||||
|
ui.print_()
|
||||||
|
print_data(data)
|
||||||
|
first = False
|
||||||
|
|
||||||
|
if opts.summarize:
|
||||||
|
print_data(summary)
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,7 @@ import traceback
|
||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
from beets.plugins import BeetsPlugin
|
from beets.plugins import BeetsPlugin
|
||||||
from beets import config, logging
|
from beets import config
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
FUNC_NAME = u'__INLINE_FUNC__'
|
FUNC_NAME = u'__INLINE_FUNC__'
|
||||||
|
|
||||||
|
|
@ -49,55 +47,6 @@ def _compile_func(body):
|
||||||
return env[FUNC_NAME]
|
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
|
|
||||||
Item, and returns a Unicode string. If the expression cannot be
|
|
||||||
compiled, then an error is logged and this function returns None.
|
|
||||||
"""
|
|
||||||
# First, try compiling as a single function.
|
|
||||||
try:
|
|
||||||
code = compile(u'({0})'.format(python_code), 'inline', 'eval')
|
|
||||||
except SyntaxError:
|
|
||||||
# Fall back to a function body.
|
|
||||||
try:
|
|
||||||
func = _compile_func(python_code)
|
|
||||||
except SyntaxError:
|
|
||||||
log.error(u'syntax error in inline field definition:\n{0}',
|
|
||||||
traceback.format_exc())
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
is_expr = False
|
|
||||||
else:
|
|
||||||
is_expr = True
|
|
||||||
|
|
||||||
def _dict_for(obj):
|
|
||||||
out = dict(obj)
|
|
||||||
if album:
|
|
||||||
out['items'] = list(obj.items())
|
|
||||||
return out
|
|
||||||
|
|
||||||
if is_expr:
|
|
||||||
# For expressions, just evaluate and return the result.
|
|
||||||
def _expr_func(obj):
|
|
||||||
values = _dict_for(obj)
|
|
||||||
try:
|
|
||||||
return eval(code, values)
|
|
||||||
except Exception as exc:
|
|
||||||
raise InlineError(python_code, exc)
|
|
||||||
return _expr_func
|
|
||||||
else:
|
|
||||||
# For function bodies, invoke the function with values as global
|
|
||||||
# variables.
|
|
||||||
def _func_func(obj):
|
|
||||||
func.__globals__.update(_dict_for(obj))
|
|
||||||
try:
|
|
||||||
return func()
|
|
||||||
except Exception as exc:
|
|
||||||
raise InlineError(python_code, exc)
|
|
||||||
return _func_func
|
|
||||||
|
|
||||||
|
|
||||||
class InlinePlugin(BeetsPlugin):
|
class InlinePlugin(BeetsPlugin):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(InlinePlugin, self).__init__()
|
super(InlinePlugin, self).__init__()
|
||||||
|
|
@ -112,13 +61,61 @@ class InlinePlugin(BeetsPlugin):
|
||||||
for key, view in itertools.chain(config['item_fields'].items(),
|
for key, view in itertools.chain(config['item_fields'].items(),
|
||||||
config['pathfields'].items()):
|
config['pathfields'].items()):
|
||||||
self._log.debug(u'adding item field {0}', key)
|
self._log.debug(u'adding item field {0}', key)
|
||||||
func = compile_inline(view.get(unicode), False)
|
func = self.compile_inline(view.get(unicode), False)
|
||||||
if func is not None:
|
if func is not None:
|
||||||
self.template_fields[key] = func
|
self.template_fields[key] = func
|
||||||
|
|
||||||
# Album fields.
|
# Album fields.
|
||||||
for key, view in config['album_fields'].items():
|
for key, view in config['album_fields'].items():
|
||||||
self._log.debug(u'adding album field {0}', key)
|
self._log.debug(u'adding album field {0}', key)
|
||||||
func = compile_inline(view.get(unicode), True)
|
func = self.compile_inline(view.get(unicode), True)
|
||||||
if func is not None:
|
if func is not None:
|
||||||
self.album_template_fields[key] = func
|
self.album_template_fields[key] = func
|
||||||
|
|
||||||
|
def compile_inline(self, 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
|
||||||
|
Item, and returns a Unicode string. If the expression cannot be
|
||||||
|
compiled, then an error is logged and this function returns None.
|
||||||
|
"""
|
||||||
|
# First, try compiling as a single function.
|
||||||
|
try:
|
||||||
|
code = compile(u'({0})'.format(python_code), 'inline', 'eval')
|
||||||
|
except SyntaxError:
|
||||||
|
# Fall back to a function body.
|
||||||
|
try:
|
||||||
|
func = _compile_func(python_code)
|
||||||
|
except SyntaxError:
|
||||||
|
self._log.error(u'syntax error in inline field definition:\n'
|
||||||
|
u'{0}', traceback.format_exc())
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
is_expr = False
|
||||||
|
else:
|
||||||
|
is_expr = True
|
||||||
|
|
||||||
|
def _dict_for(obj):
|
||||||
|
out = dict(obj)
|
||||||
|
if album:
|
||||||
|
out['items'] = list(obj.items())
|
||||||
|
return out
|
||||||
|
|
||||||
|
if is_expr:
|
||||||
|
# For expressions, just evaluate and return the result.
|
||||||
|
def _expr_func(obj):
|
||||||
|
values = _dict_for(obj)
|
||||||
|
try:
|
||||||
|
return eval(code, values)
|
||||||
|
except Exception as exc:
|
||||||
|
raise InlineError(python_code, exc)
|
||||||
|
return _expr_func
|
||||||
|
else:
|
||||||
|
# For function bodies, invoke the function with values as global
|
||||||
|
# variables.
|
||||||
|
def _func_func(obj):
|
||||||
|
func.__globals__.update(_dict_for(obj))
|
||||||
|
try:
|
||||||
|
return func()
|
||||||
|
except Exception as exc:
|
||||||
|
raise InlineError(python_code, exc)
|
||||||
|
return _func_func
|
||||||
|
|
|
||||||
|
|
@ -24,14 +24,12 @@ import pylast
|
||||||
import os
|
import os
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from beets import logging
|
|
||||||
from beets import plugins
|
from beets import plugins
|
||||||
from beets import ui
|
from beets import ui
|
||||||
from beets.util import normpath, plurality
|
from beets.util import normpath, plurality
|
||||||
from beets import config
|
from beets import config
|
||||||
from beets import library
|
from beets import library
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
LASTFM = pylast.LastFMNetwork(api_key=plugins.LASTFM_KEY)
|
LASTFM = pylast.LastFMNetwork(api_key=plugins.LASTFM_KEY)
|
||||||
|
|
||||||
|
|
@ -53,40 +51,8 @@ def deduplicate(seq):
|
||||||
return [x for x in seq if x not in seen and not seen.add(x)]
|
return [x for x in seq if x not in seen and not seen.add(x)]
|
||||||
|
|
||||||
|
|
||||||
# Core genre identification routine.
|
|
||||||
|
|
||||||
def _tags_for(obj, min_weight=None):
|
|
||||||
"""Given a pylast entity (album or track), return a list of
|
|
||||||
tag names for that entity. Return an empty list if the entity is
|
|
||||||
not found or another error occurs.
|
|
||||||
|
|
||||||
If `min_weight` is specified, tags are filtered by weight.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Work around an inconsistency in pylast where
|
|
||||||
# Album.get_top_tags() does not return TopItem instances.
|
|
||||||
# https://code.google.com/p/pylast/issues/detail?id=85
|
|
||||||
if isinstance(obj, pylast.Album):
|
|
||||||
res = super(pylast.Album, obj).get_top_tags()
|
|
||||||
else:
|
|
||||||
res = obj.get_top_tags()
|
|
||||||
except PYLAST_EXCEPTIONS as exc:
|
|
||||||
log.debug(u'last.fm error: {0}', exc)
|
|
||||||
return []
|
|
||||||
|
|
||||||
# Filter by weight (optionally).
|
|
||||||
if min_weight:
|
|
||||||
res = [el for el in res if (el.weight or 0) >= min_weight]
|
|
||||||
|
|
||||||
# Get strings from tags.
|
|
||||||
res = [el.item.get_name().lower() for el in res]
|
|
||||||
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
# Canonicalization tree processing.
|
# Canonicalization tree processing.
|
||||||
|
|
||||||
|
|
||||||
def flatten_tree(elem, path, branches):
|
def flatten_tree(elem, path, branches):
|
||||||
"""Flatten nested lists/dictionaries into lists of strings
|
"""Flatten nested lists/dictionaries into lists of strings
|
||||||
(branches).
|
(branches).
|
||||||
|
|
@ -225,7 +191,7 @@ class LastGenrePlugin(plugins.BeetsPlugin):
|
||||||
can be found. Ex. 'Electronic, House, Dance'
|
can be found. Ex. 'Electronic, House, Dance'
|
||||||
"""
|
"""
|
||||||
min_weight = self.config['min_weight'].get(int)
|
min_weight = self.config['min_weight'].get(int)
|
||||||
return self._resolve_genres(_tags_for(lastfm_obj, min_weight))
|
return self._resolve_genres(self._tags_for(lastfm_obj, min_weight))
|
||||||
|
|
||||||
def _is_allowed(self, genre):
|
def _is_allowed(self, genre):
|
||||||
"""Determine whether the genre is present in the whitelist,
|
"""Determine whether the genre is present in the whitelist,
|
||||||
|
|
@ -371,8 +337,8 @@ class LastGenrePlugin(plugins.BeetsPlugin):
|
||||||
|
|
||||||
for album in lib.albums(ui.decargs(args)):
|
for album in lib.albums(ui.decargs(args)):
|
||||||
album.genre, src = self._get_genre(album)
|
album.genre, src = self._get_genre(album)
|
||||||
log.info(u'genre for album {0} - {1} ({2}): {3}',
|
self._log.info(u'genre for album {0.albumartist} - {0.album} '
|
||||||
album.albumartist, album.album, src, album.genre)
|
u'({1}): {0.genre}', album, src)
|
||||||
album.store()
|
album.store()
|
||||||
|
|
||||||
for item in album.items():
|
for item in album.items():
|
||||||
|
|
@ -381,8 +347,8 @@ class LastGenrePlugin(plugins.BeetsPlugin):
|
||||||
if 'track' in self.sources:
|
if 'track' in self.sources:
|
||||||
item.genre, src = self._get_genre(item)
|
item.genre, src = self._get_genre(item)
|
||||||
item.store()
|
item.store()
|
||||||
log.info(u'genre for track {0} - {1} ({2}): {3}',
|
self._log.info(u'genre for track {0.artist} - {0.tit'
|
||||||
item.artist, item.title, src, item.genre)
|
u'le} ({1}): {0.genre}', item, src)
|
||||||
|
|
||||||
if write:
|
if write:
|
||||||
item.try_write()
|
item.try_write()
|
||||||
|
|
@ -395,20 +361,50 @@ class LastGenrePlugin(plugins.BeetsPlugin):
|
||||||
if task.is_album:
|
if task.is_album:
|
||||||
album = task.album
|
album = task.album
|
||||||
album.genre, src = self._get_genre(album)
|
album.genre, src = self._get_genre(album)
|
||||||
log.debug(u'added last.fm album genre ({0}): {1}',
|
self._log.debug(u'added last.fm album genre ({0}): {1}',
|
||||||
src, album.genre)
|
src, album.genre)
|
||||||
album.store()
|
album.store()
|
||||||
|
|
||||||
if 'track' in self.sources:
|
if 'track' in self.sources:
|
||||||
for item in album.items():
|
for item in album.items():
|
||||||
item.genre, src = self._get_genre(item)
|
item.genre, src = self._get_genre(item)
|
||||||
log.debug(u'added last.fm item genre ({0}): {1}',
|
self._log.debug(u'added last.fm item genre ({0}): {1}',
|
||||||
src, item.genre)
|
src, item.genre)
|
||||||
item.store()
|
item.store()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
item = task.item
|
item = task.item
|
||||||
item.genre, src = self._get_genre(item)
|
item.genre, src = self._get_genre(item)
|
||||||
log.debug(u'added last.fm item genre ({0}): {1}',
|
self._log.debug(u'added last.fm item genre ({0}): {1}',
|
||||||
src, item.genre)
|
src, item.genre)
|
||||||
item.store()
|
item.store()
|
||||||
|
|
||||||
|
def _tags_for(self, obj, min_weight=None):
|
||||||
|
"""Core genre identification routine.
|
||||||
|
|
||||||
|
Given a pylast entity (album or track), return a list of
|
||||||
|
tag names for that entity. Return an empty list if the entity is
|
||||||
|
not found or another error occurs.
|
||||||
|
|
||||||
|
If `min_weight` is specified, tags are filtered by weight.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Work around an inconsistency in pylast where
|
||||||
|
# Album.get_top_tags() does not return TopItem instances.
|
||||||
|
# https://code.google.com/p/pylast/issues/detail?id=85
|
||||||
|
if isinstance(obj, pylast.Album):
|
||||||
|
res = super(pylast.Album, obj).get_top_tags()
|
||||||
|
else:
|
||||||
|
res = obj.get_top_tags()
|
||||||
|
except PYLAST_EXCEPTIONS as exc:
|
||||||
|
self._log.debug(u'last.fm error: {0}', exc)
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Filter by weight (optionally).
|
||||||
|
if min_weight:
|
||||||
|
res = [el for el in res if (el.weight or 0) >= min_weight]
|
||||||
|
|
||||||
|
# Get strings from tags.
|
||||||
|
res = [el.item.get_name().lower() for el in res]
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,8 @@ from beets import ui
|
||||||
from beets import dbcore
|
from beets import dbcore
|
||||||
from beets import config
|
from beets import config
|
||||||
from beets import plugins
|
from beets import plugins
|
||||||
from beets import logging
|
|
||||||
from beets.dbcore import types
|
from beets.dbcore import types
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
API_URL = 'http://ws.audioscrobbler.com/2.0/'
|
API_URL = 'http://ws.audioscrobbler.com/2.0/'
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -43,13 +41,13 @@ class LastImportPlugin(plugins.BeetsPlugin):
|
||||||
cmd = ui.Subcommand('lastimport', help='import last.fm play-count')
|
cmd = ui.Subcommand('lastimport', help='import last.fm play-count')
|
||||||
|
|
||||||
def func(lib, opts, args):
|
def func(lib, opts, args):
|
||||||
import_lastfm(lib)
|
import_lastfm(lib, self._log)
|
||||||
|
|
||||||
cmd.func = func
|
cmd.func = func
|
||||||
return [cmd]
|
return [cmd]
|
||||||
|
|
||||||
|
|
||||||
def import_lastfm(lib):
|
def import_lastfm(lib, log):
|
||||||
user = config['lastfm']['user']
|
user = config['lastfm']['user']
|
||||||
per_page = config['lastimport']['per_page']
|
per_page = config['lastimport']['per_page']
|
||||||
|
|
||||||
|
|
@ -78,7 +76,8 @@ def import_lastfm(lib):
|
||||||
# It means nothing to us!
|
# It means nothing to us!
|
||||||
raise ui.UserError('Last.fm reported no data.')
|
raise ui.UserError('Last.fm reported no data.')
|
||||||
|
|
||||||
found, unknown = process_tracks(lib, page['tracks']['track'])
|
track = page['tracks']['track']
|
||||||
|
found, unknown = process_tracks(lib, track, log)
|
||||||
found_total += found
|
found_total += found
|
||||||
unknown_total += unknown
|
unknown_total += unknown
|
||||||
break
|
break
|
||||||
|
|
@ -112,7 +111,7 @@ def fetch_tracks(user, page, limit):
|
||||||
}).json()
|
}).json()
|
||||||
|
|
||||||
|
|
||||||
def process_tracks(lib, tracks):
|
def process_tracks(lib, tracks, log):
|
||||||
total = len(tracks)
|
total = len(tracks)
|
||||||
total_found = 0
|
total_found = 0
|
||||||
total_fails = 0
|
total_fails = 0
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ from __future__ import print_function
|
||||||
|
|
||||||
from beets.plugins import BeetsPlugin
|
from beets.plugins import BeetsPlugin
|
||||||
from beets.ui import Subcommand
|
from beets.ui import Subcommand
|
||||||
from beets import logging
|
|
||||||
from beets import ui
|
from beets import ui
|
||||||
from beets import config
|
from beets import config
|
||||||
import musicbrainzngs
|
import musicbrainzngs
|
||||||
|
|
@ -26,8 +25,6 @@ import re
|
||||||
SUBMISSION_CHUNK_SIZE = 200
|
SUBMISSION_CHUNK_SIZE = 200
|
||||||
UUID_REGEX = r'^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$'
|
UUID_REGEX = r'^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$'
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def mb_call(func, *args, **kwargs):
|
def mb_call(func, *args, **kwargs):
|
||||||
"""Call a MusicBrainz API function and catch exceptions.
|
"""Call a MusicBrainz API function and catch exceptions.
|
||||||
|
|
@ -54,48 +51,6 @@ def submit_albums(collection_id, release_ids):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def update_album_list(album_list):
|
|
||||||
"""Update the MusicBrainz colleciton from a list of Beets albums
|
|
||||||
"""
|
|
||||||
# Get the available collections.
|
|
||||||
collections = mb_call(musicbrainzngs.get_collections)
|
|
||||||
if not collections['collection-list']:
|
|
||||||
raise ui.UserError('no collections exist for user')
|
|
||||||
|
|
||||||
# Get the first release collection. MusicBrainz also has event
|
|
||||||
# collections, so we need to avoid adding to those.
|
|
||||||
for collection in collections['collection-list']:
|
|
||||||
if 'release-count' in collection:
|
|
||||||
collection_id = collection['id']
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise ui.UserError('No collection found.')
|
|
||||||
|
|
||||||
# Get a list of all the album IDs.
|
|
||||||
album_ids = []
|
|
||||||
for album in album_list:
|
|
||||||
aid = album.mb_albumid
|
|
||||||
if aid:
|
|
||||||
if re.match(UUID_REGEX, aid):
|
|
||||||
album_ids.append(aid)
|
|
||||||
else:
|
|
||||||
log.info(u'skipping invalid MBID: {0}', aid)
|
|
||||||
|
|
||||||
# Submit to MusicBrainz.
|
|
||||||
print('Updating MusicBrainz collection {0}...'.format(collection_id))
|
|
||||||
submit_albums(collection_id, album_ids)
|
|
||||||
print('...MusicBrainz collection updated.')
|
|
||||||
|
|
||||||
|
|
||||||
def update_collection(lib, opts, args):
|
|
||||||
update_album_list(lib.albums())
|
|
||||||
|
|
||||||
|
|
||||||
update_mb_collection_cmd = Subcommand('mbupdate',
|
|
||||||
help='Update MusicBrainz collection')
|
|
||||||
update_mb_collection_cmd.func = update_collection
|
|
||||||
|
|
||||||
|
|
||||||
class MusicBrainzCollectionPlugin(BeetsPlugin):
|
class MusicBrainzCollectionPlugin(BeetsPlugin):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(MusicBrainzCollectionPlugin, self).__init__()
|
super(MusicBrainzCollectionPlugin, self).__init__()
|
||||||
|
|
@ -108,10 +63,47 @@ class MusicBrainzCollectionPlugin(BeetsPlugin):
|
||||||
self._import_stages = [self.imported]
|
self._import_stages = [self.imported]
|
||||||
|
|
||||||
def commands(self):
|
def commands(self):
|
||||||
return [update_mb_collection_cmd]
|
mbupdate = Subcommand('mbupdate', help='Update MusicBrainz collection')
|
||||||
|
mbupdate.func = self.update_collection
|
||||||
|
return [mbupdate]
|
||||||
|
|
||||||
|
def update_collection(self, lib, opts, args):
|
||||||
|
self.update_album_list(lib.albums())
|
||||||
|
|
||||||
def imported(self, session, task):
|
def imported(self, session, task):
|
||||||
"""Add each imported album to the collection.
|
"""Add each imported album to the collection.
|
||||||
"""
|
"""
|
||||||
if task.is_album:
|
if task.is_album:
|
||||||
update_album_list([task.album])
|
self.update_album_list([task.album])
|
||||||
|
|
||||||
|
def update_album_list(self, album_list):
|
||||||
|
"""Update the MusicBrainz colleciton from a list of Beets albums
|
||||||
|
"""
|
||||||
|
# Get the available collections.
|
||||||
|
collections = mb_call(musicbrainzngs.get_collections)
|
||||||
|
if not collections['collection-list']:
|
||||||
|
raise ui.UserError('no collections exist for user')
|
||||||
|
|
||||||
|
# Get the first release collection. MusicBrainz also has event
|
||||||
|
# collections, so we need to avoid adding to those.
|
||||||
|
for collection in collections['collection-list']:
|
||||||
|
if 'release-count' in collection:
|
||||||
|
collection_id = collection['id']
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ui.UserError('No collection found.')
|
||||||
|
|
||||||
|
# Get a list of all the album IDs.
|
||||||
|
album_ids = []
|
||||||
|
for album in album_list:
|
||||||
|
aid = album.mb_albumid
|
||||||
|
if aid:
|
||||||
|
if re.match(UUID_REGEX, aid):
|
||||||
|
album_ids.append(aid)
|
||||||
|
else:
|
||||||
|
self._log.info(u'skipping invalid MBID: {0}', aid)
|
||||||
|
|
||||||
|
# Submit to MusicBrainz.
|
||||||
|
print('Updating MusicBrainz collection {0}...'.format(collection_id))
|
||||||
|
submit_albums(collection_id, album_ids)
|
||||||
|
print('...MusicBrainz collection updated.')
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,11 @@
|
||||||
|
|
||||||
"""List missing tracks.
|
"""List missing tracks.
|
||||||
"""
|
"""
|
||||||
from beets import logging
|
|
||||||
from beets.autotag import hooks
|
from beets.autotag import hooks
|
||||||
from beets.library import Item
|
from beets.library import Item
|
||||||
from beets.plugins import BeetsPlugin
|
from beets.plugins import BeetsPlugin
|
||||||
from beets.ui import decargs, print_obj, Subcommand
|
from beets.ui import decargs, print_obj, Subcommand
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def _missing_count(album):
|
def _missing_count(album):
|
||||||
"""Return number of missing items in `album`.
|
"""Return number of missing items in `album`.
|
||||||
|
|
@ -29,23 +26,6 @@ def _missing_count(album):
|
||||||
return (album.tracktotal or 0) - len(album.items())
|
return (album.tracktotal or 0) - len(album.items())
|
||||||
|
|
||||||
|
|
||||||
def _missing(album):
|
|
||||||
"""Query MusicBrainz to determine items missing from `album`.
|
|
||||||
"""
|
|
||||||
item_mbids = map(lambda x: x.mb_trackid, album.items())
|
|
||||||
|
|
||||||
if len([i for i in album.items()]) < album.tracktotal:
|
|
||||||
# fetch missing items
|
|
||||||
# TODO: Implement caching that without breaking other stuff
|
|
||||||
album_info = hooks.album_for_mbid(album.mb_albumid)
|
|
||||||
for track_info in getattr(album_info, 'tracks', []):
|
|
||||||
if track_info.track_id not in item_mbids:
|
|
||||||
item = _item(track_info, album_info, album.id)
|
|
||||||
log.debug(u'track {1} in album {2}',
|
|
||||||
track_info.track_id, album_info.album_id)
|
|
||||||
yield item
|
|
||||||
|
|
||||||
|
|
||||||
def _item(track_info, album_info, album_id):
|
def _item(track_info, album_info, album_id):
|
||||||
"""Build and return `item` from `track_info` and `album info`
|
"""Build and return `item` from `track_info` and `album info`
|
||||||
objects. `item` is missing what fields cannot be obtained from
|
objects. `item` is missing what fields cannot be obtained from
|
||||||
|
|
@ -148,8 +128,24 @@ class MissingPlugin(BeetsPlugin):
|
||||||
print_obj(album, lib, fmt=fmt)
|
print_obj(album, lib, fmt=fmt)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
for item in _missing(album):
|
for item in self._missing(album):
|
||||||
print_obj(item, lib, fmt=fmt)
|
print_obj(item, lib, fmt=fmt)
|
||||||
|
|
||||||
self._command.func = _miss
|
self._command.func = _miss
|
||||||
return [self._command]
|
return [self._command]
|
||||||
|
|
||||||
|
def _missing(self, album):
|
||||||
|
"""Query MusicBrainz to determine items missing from `album`.
|
||||||
|
"""
|
||||||
|
item_mbids = map(lambda x: x.mb_trackid, album.items())
|
||||||
|
|
||||||
|
if len([i for i in album.items()]) < album.tracktotal:
|
||||||
|
# fetch missing items
|
||||||
|
# TODO: Implement caching that without breaking other stuff
|
||||||
|
album_info = hooks.album_for_mbid(album.mb_albumid)
|
||||||
|
for track_info in getattr(album_info, 'tracks', []):
|
||||||
|
if track_info.track_id not in item_mbids:
|
||||||
|
item = _item(track_info, album_info, album.id)
|
||||||
|
self._log.debug(u'track {1} in album {2}',
|
||||||
|
track_info.track_id, album_info.album_id)
|
||||||
|
yield item
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import select
|
||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from beets import logging
|
|
||||||
from beets import ui
|
from beets import ui
|
||||||
from beets import config
|
from beets import config
|
||||||
from beets import plugins
|
from beets import plugins
|
||||||
|
|
@ -27,8 +26,6 @@ from beets import library
|
||||||
from beets.util import displayable_path
|
from beets.util import displayable_path
|
||||||
from beets.dbcore import types
|
from beets.dbcore import types
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# If we lose the connection, how many times do we want to retry and how
|
# If we lose the connection, how many times do we want to retry and how
|
||||||
# much time should we wait between retries?
|
# much time should we wait between retries?
|
||||||
RETRIES = 10
|
RETRIES = 10
|
||||||
|
|
@ -56,7 +53,9 @@ class MPDClient(mpd.MPDClient):
|
||||||
|
|
||||||
|
|
||||||
class MPDClientWrapper(object):
|
class MPDClientWrapper(object):
|
||||||
def __init__(self):
|
def __init__(self, log):
|
||||||
|
self._log = log
|
||||||
|
|
||||||
self.music_directory = (
|
self.music_directory = (
|
||||||
config['mpdstats']['music_directory'].get(unicode))
|
config['mpdstats']['music_directory'].get(unicode))
|
||||||
|
|
||||||
|
|
@ -71,7 +70,7 @@ class MPDClientWrapper(object):
|
||||||
if host[0] in ['/', '~']:
|
if host[0] in ['/', '~']:
|
||||||
host = os.path.expanduser(host)
|
host = os.path.expanduser(host)
|
||||||
|
|
||||||
log.info(u'connecting to {0}:{1}', host, port)
|
self._log.info(u'connecting to {0}:{1}', host, port)
|
||||||
try:
|
try:
|
||||||
self.client.connect(host, port)
|
self.client.connect(host, port)
|
||||||
except socket.error as e:
|
except socket.error as e:
|
||||||
|
|
@ -99,7 +98,7 @@ class MPDClientWrapper(object):
|
||||||
try:
|
try:
|
||||||
return getattr(self.client, command)()
|
return getattr(self.client, command)()
|
||||||
except (select.error, mpd.ConnectionError) as err:
|
except (select.error, mpd.ConnectionError) as err:
|
||||||
log.error(u'{0}', err)
|
self._log.error(u'{0}', err)
|
||||||
|
|
||||||
if retries <= 0:
|
if retries <= 0:
|
||||||
# if we exited without breaking, we couldn't reconnect in time :(
|
# if we exited without breaking, we couldn't reconnect in time :(
|
||||||
|
|
@ -141,15 +140,16 @@ class MPDClientWrapper(object):
|
||||||
|
|
||||||
|
|
||||||
class MPDStats(object):
|
class MPDStats(object):
|
||||||
def __init__(self, lib):
|
def __init__(self, lib, log):
|
||||||
self.lib = lib
|
self.lib = lib
|
||||||
|
self._log = log
|
||||||
|
|
||||||
self.do_rating = config['mpdstats']['rating'].get(bool)
|
self.do_rating = config['mpdstats']['rating'].get(bool)
|
||||||
self.rating_mix = config['mpdstats']['rating_mix'].get(float)
|
self.rating_mix = config['mpdstats']['rating_mix'].get(float)
|
||||||
self.time_threshold = 10.0 # TODO: maybe add config option?
|
self.time_threshold = 10.0 # TODO: maybe add config option?
|
||||||
|
|
||||||
self.now_playing = None
|
self.now_playing = None
|
||||||
self.mpd = MPDClientWrapper()
|
self.mpd = MPDClientWrapper(log)
|
||||||
|
|
||||||
def rating(self, play_count, skip_count, rating, skipped):
|
def rating(self, play_count, skip_count, rating, skipped):
|
||||||
"""Calculate a new rating for a song based on play count, skip count,
|
"""Calculate a new rating for a song based on play count, skip count,
|
||||||
|
|
@ -171,10 +171,9 @@ class MPDStats(object):
|
||||||
if item:
|
if item:
|
||||||
return item
|
return item
|
||||||
else:
|
else:
|
||||||
log.info(u'item not found: {0}', displayable_path(path))
|
self._log.info(u'item not found: {0}', displayable_path(path))
|
||||||
|
|
||||||
@staticmethod
|
def update_item(self, item, attribute, value=None, increment=None):
|
||||||
def update_item(item, attribute, value=None, increment=None):
|
|
||||||
"""Update the beets item. Set attribute to value or increment the value
|
"""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
|
of attribute. If the increment argument is used the value is cast to
|
||||||
the corresponding type.
|
the corresponding type.
|
||||||
|
|
@ -190,10 +189,10 @@ class MPDStats(object):
|
||||||
item[attribute] = value
|
item[attribute] = value
|
||||||
item.store()
|
item.store()
|
||||||
|
|
||||||
log.debug(u'updated: {0} = {1} [{2}]',
|
self._log.debug(u'updated: {0} = {1} [{2}]',
|
||||||
attribute,
|
attribute,
|
||||||
item[attribute],
|
item[attribute],
|
||||||
displayable_path(item.path))
|
displayable_path(item.path))
|
||||||
|
|
||||||
def update_rating(self, item, skipped):
|
def update_rating(self, item, skipped):
|
||||||
"""Update the rating for a beets item.
|
"""Update the rating for a beets item.
|
||||||
|
|
@ -229,16 +228,16 @@ class MPDStats(object):
|
||||||
"""Updates the play count of a song.
|
"""Updates the play count of a song.
|
||||||
"""
|
"""
|
||||||
self.update_item(song['beets_item'], 'play_count', increment=1)
|
self.update_item(song['beets_item'], 'play_count', increment=1)
|
||||||
log.info(u'played {0}', displayable_path(song['path']))
|
self._log.info(u'played {0}', displayable_path(song['path']))
|
||||||
|
|
||||||
def handle_skipped(self, song):
|
def handle_skipped(self, song):
|
||||||
"""Updates the skip count of a song.
|
"""Updates the skip count of a song.
|
||||||
"""
|
"""
|
||||||
self.update_item(song['beets_item'], 'skip_count', increment=1)
|
self.update_item(song['beets_item'], 'skip_count', increment=1)
|
||||||
log.info(u'skipped {0}', displayable_path(song['path']))
|
self._log.info(u'skipped {0}', displayable_path(song['path']))
|
||||||
|
|
||||||
def on_stop(self, status):
|
def on_stop(self, status):
|
||||||
log.info(u'stop')
|
self._log.info(u'stop')
|
||||||
|
|
||||||
if self.now_playing:
|
if self.now_playing:
|
||||||
self.handle_song_change(self.now_playing)
|
self.handle_song_change(self.now_playing)
|
||||||
|
|
@ -246,7 +245,7 @@ class MPDStats(object):
|
||||||
self.now_playing = None
|
self.now_playing = None
|
||||||
|
|
||||||
def on_pause(self, status):
|
def on_pause(self, status):
|
||||||
log.info(u'pause')
|
self._log.info(u'pause')
|
||||||
self.now_playing = None
|
self.now_playing = None
|
||||||
|
|
||||||
def on_play(self, status):
|
def on_play(self, status):
|
||||||
|
|
@ -257,7 +256,7 @@ class MPDStats(object):
|
||||||
return
|
return
|
||||||
|
|
||||||
if is_url(path):
|
if is_url(path):
|
||||||
log.info(u'playing stream {0}', displayable_path(path))
|
self._log.info(u'playing stream {0}', displayable_path(path))
|
||||||
return
|
return
|
||||||
|
|
||||||
played, duration = map(int, status['time'].split(':', 1))
|
played, duration = map(int, status['time'].split(':', 1))
|
||||||
|
|
@ -266,7 +265,7 @@ class MPDStats(object):
|
||||||
if self.now_playing and self.now_playing['path'] != path:
|
if self.now_playing and self.now_playing['path'] != path:
|
||||||
self.handle_song_change(self.now_playing)
|
self.handle_song_change(self.now_playing)
|
||||||
|
|
||||||
log.info(u'playing {0}', displayable_path(path))
|
self._log.info(u'playing {0}', displayable_path(path))
|
||||||
|
|
||||||
self.now_playing = {
|
self.now_playing = {
|
||||||
'started': time.time(),
|
'started': time.time(),
|
||||||
|
|
@ -291,7 +290,7 @@ class MPDStats(object):
|
||||||
if handler:
|
if handler:
|
||||||
handler(status)
|
handler(status)
|
||||||
else:
|
else:
|
||||||
log.debug(u'unhandled status "{0}"', status)
|
self._log.debug(u'unhandled status "{0}"', status)
|
||||||
|
|
||||||
events = self.mpd.events()
|
events = self.mpd.events()
|
||||||
|
|
||||||
|
|
@ -344,7 +343,7 @@ class MPDStatsPlugin(plugins.BeetsPlugin):
|
||||||
config['mpd']['password'] = opts.password.decode('utf8')
|
config['mpd']['password'] = opts.password.decode('utf8')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
MPDStats(lib).run()
|
MPDStats(lib, self._log).run()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,10 @@
|
||||||
|
|
||||||
"""Send the results of a query to the configured music player as a playlist.
|
"""Send the results of a query to the configured music player as a playlist.
|
||||||
"""
|
"""
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
from beets.plugins import BeetsPlugin
|
from beets.plugins import BeetsPlugin
|
||||||
from beets.ui import Subcommand
|
from beets.ui import Subcommand
|
||||||
from beets import logging
|
|
||||||
from beets import config
|
from beets import config
|
||||||
from beets import ui
|
from beets import ui
|
||||||
from beets import util
|
from beets import util
|
||||||
|
|
@ -25,10 +26,8 @@ import platform
|
||||||
import shlex
|
import shlex
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
def play_music(lib, opts, args, log):
|
||||||
def play_music(lib, opts, args):
|
|
||||||
"""Execute query, create temporary playlist and execute player
|
"""Execute query, create temporary playlist and execute player
|
||||||
command passing that playlist.
|
command passing that playlist.
|
||||||
"""
|
"""
|
||||||
|
|
@ -133,5 +132,5 @@ class PlayPlugin(BeetsPlugin):
|
||||||
action='store_true', default=False,
|
action='store_true', default=False,
|
||||||
help='query and load albums rather than tracks'
|
help='query and load albums rather than tracks'
|
||||||
)
|
)
|
||||||
play_command.func = play_music
|
play_command.func = partial(play_music, log=self._log)
|
||||||
return [play_command]
|
return [play_command]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue