mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 08:39:17 +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
|
||||
|
||||
from beets import logging
|
||||
from beets.plugins import BeetsPlugin
|
||||
from beets.ui import decargs, print_obj, vararg_callback, Subcommand, UserError
|
||||
from beets.util import command_output, displayable_path, subprocess
|
||||
|
||||
PLUGIN = 'duplicates'
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def _checksum(item, prog):
|
||||
def _checksum(item, prog, log):
|
||||
"""Run external `prog` on file path associated with `item`, cache
|
||||
output as flexattr on a key that is the name of the program, and
|
||||
return the key, checksum tuple.
|
||||
|
|
@ -73,7 +71,7 @@ def _checksum(item, prog):
|
|||
return key, checksum
|
||||
|
||||
|
||||
def _group_by(objs, keys):
|
||||
def _group_by(objs, keys, log):
|
||||
"""Return a dictionary with keys arbitrary concatenations of attributes and
|
||||
values lists of objects (Albums or Items) with those keys.
|
||||
"""
|
||||
|
|
@ -92,11 +90,11 @@ def _group_by(objs, keys):
|
|||
return counts
|
||||
|
||||
|
||||
def _duplicates(objs, keys, full):
|
||||
def _duplicates(objs, keys, full, log):
|
||||
"""Generate triples of keys, duplicate counts, and constituent objects.
|
||||
"""
|
||||
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:
|
||||
yield (k, len(objs) - offset, objs[offset:])
|
||||
|
||||
|
|
@ -214,12 +212,13 @@ class DuplicatesPlugin(BeetsPlugin):
|
|||
'duplicates: "checksum" option must be a command'
|
||||
)
|
||||
for i in items:
|
||||
k, _ = _checksum(i, checksum)
|
||||
k, _ = self._checksum(i, checksum, self._log)
|
||||
keys = [k]
|
||||
|
||||
for obj_id, obj_count, objs in _duplicates(items,
|
||||
keys=keys,
|
||||
full=full):
|
||||
full=full,
|
||||
log=self._log):
|
||||
if obj_id: # Skip empty IDs.
|
||||
for o in objs:
|
||||
_process_item(o, lib,
|
||||
|
|
|
|||
|
|
@ -20,9 +20,6 @@ from beets import plugins
|
|||
from beets import ui
|
||||
from beets.util import displayable_path
|
||||
from beets import config
|
||||
from beets import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def split_on_feat(artist):
|
||||
|
|
@ -46,69 +43,6 @@ def contains_feat(title):
|
|||
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):
|
||||
def __init__(self):
|
||||
super(FtInTitlePlugin, self).__init__()
|
||||
|
|
@ -138,7 +72,7 @@ class FtInTitlePlugin(plugins.BeetsPlugin):
|
|||
write = config['import']['write'].get(bool)
|
||||
|
||||
for item in lib.items(ui.decargs(args)):
|
||||
ft_in_title(item, drop_feat)
|
||||
self.ft_in_title(item, drop_feat)
|
||||
item.store()
|
||||
if write:
|
||||
item.try_write()
|
||||
|
|
@ -152,5 +86,66 @@ class FtInTitlePlugin(plugins.BeetsPlugin):
|
|||
drop_feat = self.config['drop'].get(bool)
|
||||
|
||||
for item in task.imported_items():
|
||||
ft_in_title(item, drop_feat)
|
||||
self.ft_in_title(item, drop_feat)
|
||||
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
|
||||
|
||||
from beets import logging
|
||||
from beets.plugins import BeetsPlugin
|
||||
from beets import ui
|
||||
from beets import mediafile
|
||||
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):
|
||||
query = []
|
||||
for arg in args:
|
||||
|
|
@ -143,9 +99,48 @@ class InfoPlugin(BeetsPlugin):
|
|||
|
||||
def commands(self):
|
||||
cmd = ui.Subcommand('info', help='show file metadata')
|
||||
cmd.func = run
|
||||
cmd.func = self.run
|
||||
cmd.parser.add_option('-l', '--library', action='store_true',
|
||||
help='show library fields instead of tags')
|
||||
cmd.parser.add_option('-s', '--summarize', action='store_true',
|
||||
help='summarize the tags of all files')
|
||||
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
|
||||
|
||||
from beets.plugins import BeetsPlugin
|
||||
from beets import config, logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
from beets import config
|
||||
|
||||
FUNC_NAME = u'__INLINE_FUNC__'
|
||||
|
||||
|
|
@ -49,55 +47,6 @@ def _compile_func(body):
|
|||
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):
|
||||
def __init__(self):
|
||||
super(InlinePlugin, self).__init__()
|
||||
|
|
@ -112,13 +61,61 @@ class InlinePlugin(BeetsPlugin):
|
|||
for key, view in itertools.chain(config['item_fields'].items(),
|
||||
config['pathfields'].items()):
|
||||
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:
|
||||
self.template_fields[key] = func
|
||||
|
||||
# Album fields.
|
||||
for key, view in config['album_fields'].items():
|
||||
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:
|
||||
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 yaml
|
||||
|
||||
from beets import logging
|
||||
from beets import plugins
|
||||
from beets import ui
|
||||
from beets.util import normpath, plurality
|
||||
from beets import config
|
||||
from beets import library
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
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)]
|
||||
|
||||
|
||||
# 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.
|
||||
|
||||
|
||||
def flatten_tree(elem, path, branches):
|
||||
"""Flatten nested lists/dictionaries into lists of strings
|
||||
(branches).
|
||||
|
|
@ -225,7 +191,7 @@ class LastGenrePlugin(plugins.BeetsPlugin):
|
|||
can be found. Ex. 'Electronic, House, Dance'
|
||||
"""
|
||||
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):
|
||||
"""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)):
|
||||
album.genre, src = self._get_genre(album)
|
||||
log.info(u'genre for album {0} - {1} ({2}): {3}',
|
||||
album.albumartist, album.album, src, album.genre)
|
||||
self._log.info(u'genre for album {0.albumartist} - {0.album} '
|
||||
u'({1}): {0.genre}', album, src)
|
||||
album.store()
|
||||
|
||||
for item in album.items():
|
||||
|
|
@ -381,8 +347,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}',
|
||||
item.artist, item.title, src, item.genre)
|
||||
self._log.info(u'genre for track {0.artist} - {0.tit'
|
||||
u'le} ({1}): {0.genre}', item, src)
|
||||
|
||||
if write:
|
||||
item.try_write()
|
||||
|
|
@ -395,20 +361,50 @@ class LastGenrePlugin(plugins.BeetsPlugin):
|
|||
if task.is_album:
|
||||
album = task.album
|
||||
album.genre, src = self._get_genre(album)
|
||||
log.debug(u'added last.fm album genre ({0}): {1}',
|
||||
src, album.genre)
|
||||
self._log.debug(u'added last.fm album genre ({0}): {1}',
|
||||
src, album.genre)
|
||||
album.store()
|
||||
|
||||
if 'track' in self.sources:
|
||||
for item in album.items():
|
||||
item.genre, src = self._get_genre(item)
|
||||
log.debug(u'added last.fm item genre ({0}): {1}',
|
||||
src, item.genre)
|
||||
self._log.debug(u'added last.fm item genre ({0}): {1}',
|
||||
src, item.genre)
|
||||
item.store()
|
||||
|
||||
else:
|
||||
item = task.item
|
||||
item.genre, src = self._get_genre(item)
|
||||
log.debug(u'added last.fm item genre ({0}): {1}',
|
||||
src, item.genre)
|
||||
self._log.debug(u'added last.fm item genre ({0}): {1}',
|
||||
src, item.genre)
|
||||
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 config
|
||||
from beets import plugins
|
||||
from beets import logging
|
||||
from beets.dbcore import types
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
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')
|
||||
|
||||
def func(lib, opts, args):
|
||||
import_lastfm(lib)
|
||||
import_lastfm(lib, self._log)
|
||||
|
||||
cmd.func = func
|
||||
return [cmd]
|
||||
|
||||
|
||||
def import_lastfm(lib):
|
||||
def import_lastfm(lib, log):
|
||||
user = config['lastfm']['user']
|
||||
per_page = config['lastimport']['per_page']
|
||||
|
||||
|
|
@ -78,7 +76,8 @@ def import_lastfm(lib):
|
|||
# It means nothing to us!
|
||||
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
|
||||
unknown_total += unknown
|
||||
break
|
||||
|
|
@ -112,7 +111,7 @@ def fetch_tracks(user, page, limit):
|
|||
}).json()
|
||||
|
||||
|
||||
def process_tracks(lib, tracks):
|
||||
def process_tracks(lib, tracks, log):
|
||||
total = len(tracks)
|
||||
total_found = 0
|
||||
total_fails = 0
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ from __future__ import print_function
|
|||
|
||||
from beets.plugins import BeetsPlugin
|
||||
from beets.ui import Subcommand
|
||||
from beets import logging
|
||||
from beets import ui
|
||||
from beets import config
|
||||
import musicbrainzngs
|
||||
|
|
@ -26,8 +25,6 @@ import re
|
|||
SUBMISSION_CHUNK_SIZE = 200
|
||||
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):
|
||||
"""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):
|
||||
def __init__(self):
|
||||
super(MusicBrainzCollectionPlugin, self).__init__()
|
||||
|
|
@ -108,10 +63,47 @@ class MusicBrainzCollectionPlugin(BeetsPlugin):
|
|||
self._import_stages = [self.imported]
|
||||
|
||||
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):
|
||||
"""Add each imported album to the collection.
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
from beets import logging
|
||||
from beets.autotag import hooks
|
||||
from beets.library import Item
|
||||
from beets.plugins import BeetsPlugin
|
||||
from beets.ui import decargs, print_obj, Subcommand
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _missing_count(album):
|
||||
"""Return number of missing items in `album`.
|
||||
|
|
@ -29,23 +26,6 @@ def _missing_count(album):
|
|||
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):
|
||||
"""Build and return `item` from `track_info` and `album info`
|
||||
objects. `item` is missing what fields cannot be obtained from
|
||||
|
|
@ -148,8 +128,24 @@ class MissingPlugin(BeetsPlugin):
|
|||
print_obj(album, lib, fmt=fmt)
|
||||
|
||||
else:
|
||||
for item in _missing(album):
|
||||
for item in self._missing(album):
|
||||
print_obj(item, lib, fmt=fmt)
|
||||
|
||||
self._command.func = _miss
|
||||
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 os
|
||||
|
||||
from beets import logging
|
||||
from beets import ui
|
||||
from beets import config
|
||||
from beets import plugins
|
||||
|
|
@ -27,8 +26,6 @@ from beets import library
|
|||
from beets.util import displayable_path
|
||||
from beets.dbcore import types
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# If we lose the connection, how many times do we want to retry and how
|
||||
# much time should we wait between retries?
|
||||
RETRIES = 10
|
||||
|
|
@ -56,7 +53,9 @@ class MPDClient(mpd.MPDClient):
|
|||
|
||||
|
||||
class MPDClientWrapper(object):
|
||||
def __init__(self):
|
||||
def __init__(self, log):
|
||||
self._log = log
|
||||
|
||||
self.music_directory = (
|
||||
config['mpdstats']['music_directory'].get(unicode))
|
||||
|
||||
|
|
@ -71,7 +70,7 @@ class MPDClientWrapper(object):
|
|||
if host[0] in ['/', '~']:
|
||||
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:
|
||||
self.client.connect(host, port)
|
||||
except socket.error as e:
|
||||
|
|
@ -99,7 +98,7 @@ class MPDClientWrapper(object):
|
|||
try:
|
||||
return getattr(self.client, command)()
|
||||
except (select.error, mpd.ConnectionError) as err:
|
||||
log.error(u'{0}', err)
|
||||
self._log.error(u'{0}', err)
|
||||
|
||||
if retries <= 0:
|
||||
# if we exited without breaking, we couldn't reconnect in time :(
|
||||
|
|
@ -141,15 +140,16 @@ class MPDClientWrapper(object):
|
|||
|
||||
|
||||
class MPDStats(object):
|
||||
def __init__(self, lib):
|
||||
def __init__(self, lib, log):
|
||||
self.lib = lib
|
||||
self._log = log
|
||||
|
||||
self.do_rating = config['mpdstats']['rating'].get(bool)
|
||||
self.rating_mix = config['mpdstats']['rating_mix'].get(float)
|
||||
self.time_threshold = 10.0 # TODO: maybe add config option?
|
||||
|
||||
self.now_playing = None
|
||||
self.mpd = MPDClientWrapper()
|
||||
self.mpd = MPDClientWrapper(log)
|
||||
|
||||
def rating(self, play_count, skip_count, rating, skipped):
|
||||
"""Calculate a new rating for a song based on play count, skip count,
|
||||
|
|
@ -171,10 +171,9 @@ class MPDStats(object):
|
|||
if item:
|
||||
return item
|
||||
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(item, attribute, value=None, increment=None):
|
||||
def update_item(self, 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.
|
||||
|
|
@ -190,10 +189,10 @@ class MPDStats(object):
|
|||
item[attribute] = value
|
||||
item.store()
|
||||
|
||||
log.debug(u'updated: {0} = {1} [{2}]',
|
||||
attribute,
|
||||
item[attribute],
|
||||
displayable_path(item.path))
|
||||
self._log.debug(u'updated: {0} = {1} [{2}]',
|
||||
attribute,
|
||||
item[attribute],
|
||||
displayable_path(item.path))
|
||||
|
||||
def update_rating(self, item, skipped):
|
||||
"""Update the rating for a beets item.
|
||||
|
|
@ -229,16 +228,16 @@ class MPDStats(object):
|
|||
"""Updates the play count of a song.
|
||||
"""
|
||||
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):
|
||||
"""Updates the skip count of a song.
|
||||
"""
|
||||
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):
|
||||
log.info(u'stop')
|
||||
self._log.info(u'stop')
|
||||
|
||||
if self.now_playing:
|
||||
self.handle_song_change(self.now_playing)
|
||||
|
|
@ -246,7 +245,7 @@ class MPDStats(object):
|
|||
self.now_playing = None
|
||||
|
||||
def on_pause(self, status):
|
||||
log.info(u'pause')
|
||||
self._log.info(u'pause')
|
||||
self.now_playing = None
|
||||
|
||||
def on_play(self, status):
|
||||
|
|
@ -257,7 +256,7 @@ class MPDStats(object):
|
|||
return
|
||||
|
||||
if is_url(path):
|
||||
log.info(u'playing stream {0}', displayable_path(path))
|
||||
self._log.info(u'playing stream {0}', displayable_path(path))
|
||||
return
|
||||
|
||||
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:
|
||||
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 = {
|
||||
'started': time.time(),
|
||||
|
|
@ -291,7 +290,7 @@ class MPDStats(object):
|
|||
if handler:
|
||||
handler(status)
|
||||
else:
|
||||
log.debug(u'unhandled status "{0}"', status)
|
||||
self._log.debug(u'unhandled status "{0}"', status)
|
||||
|
||||
events = self.mpd.events()
|
||||
|
||||
|
|
@ -344,7 +343,7 @@ class MPDStatsPlugin(plugins.BeetsPlugin):
|
|||
config['mpd']['password'] = opts.password.decode('utf8')
|
||||
|
||||
try:
|
||||
MPDStats(lib).run()
|
||||
MPDStats(lib, self._log).run()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -14,9 +14,10 @@
|
|||
|
||||
"""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.ui import Subcommand
|
||||
from beets import logging
|
||||
from beets import config
|
||||
from beets import ui
|
||||
from beets import util
|
||||
|
|
@ -25,10 +26,8 @@ import platform
|
|||
import shlex
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def play_music(lib, opts, args):
|
||||
def play_music(lib, opts, args, log):
|
||||
"""Execute query, create temporary playlist and execute player
|
||||
command passing that playlist.
|
||||
"""
|
||||
|
|
@ -133,5 +132,5 @@ class PlayPlugin(BeetsPlugin):
|
|||
action='store_true', default=False,
|
||||
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]
|
||||
|
|
|
|||
Loading…
Reference in a new issue