diff --git a/beetsplug/duplicates.py b/beetsplug/duplicates.py index fb6c1d3c1..6e4ad4f3c 100644 --- a/beetsplug/duplicates.py +++ b/beetsplug/duplicates.py @@ -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, diff --git a/beetsplug/ftintitle.py b/beetsplug/ftintitle.py index 22504a1fe..91aa9f33f 100644 --- a/beetsplug/ftintitle.py +++ b/beetsplug/ftintitle.py @@ -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') diff --git a/beetsplug/info.py b/beetsplug/info.py index fa6eb325f..2b1000405 100644 --- a/beetsplug/info.py +++ b/beetsplug/info.py @@ -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) diff --git a/beetsplug/inline.py b/beetsplug/inline.py index 078aff39c..e57f97bd5 100644 --- a/beetsplug/inline.py +++ b/beetsplug/inline.py @@ -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 diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index bee44ffc0..92395dd79 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -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 diff --git a/beetsplug/lastimport.py b/beetsplug/lastimport.py index dbed972a8..d39c1299d 100644 --- a/beetsplug/lastimport.py +++ b/beetsplug/lastimport.py @@ -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 diff --git a/beetsplug/mbcollection.py b/beetsplug/mbcollection.py index 52a0cef39..8535c5f86 100644 --- a/beetsplug/mbcollection.py +++ b/beetsplug/mbcollection.py @@ -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.') diff --git a/beetsplug/missing.py b/beetsplug/missing.py index d7fc5041e..0ebafdada 100644 --- a/beetsplug/missing.py +++ b/beetsplug/missing.py @@ -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 diff --git a/beetsplug/mpdstats.py b/beetsplug/mpdstats.py index 96167e2aa..6e47f990b 100644 --- a/beetsplug/mpdstats.py +++ b/beetsplug/mpdstats.py @@ -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 diff --git a/beetsplug/play.py b/beetsplug/play.py index e1d8d4fed..91339cffd 100644 --- a/beetsplug/play.py +++ b/beetsplug/play.py @@ -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]