From b40fb507c321b227de164809b4cea094163c5fb3 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Sat, 3 Jan 2015 16:35:19 +0100 Subject: [PATCH 01/86] Offer new-style formatting for logging beets.logging is a drop-in replacement for logging. Any logger created from beets.logging.getLogger() will use {}-style formatting instead of %-style, on python 2 and 3. --- beets/logging.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 beets/logging.py diff --git a/beets/logging.py b/beets/logging.py new file mode 100644 index 000000000..2783c5ff5 --- /dev/null +++ b/beets/logging.py @@ -0,0 +1,37 @@ +"""Allow {}-style formatting on python 2 and 3 + +Provide everything the "logging" module does, the only difference is that when +getLogger(name) instantiates a logger that logger uses {}-style formatting. +""" + +from __future__ import absolute_import +from copy import copy +from logging import * + + +# create a str.format-based logger +class StrFormatLogger(Logger): + class _LogMessage(object): + def __init__(self, msg, args, kwargs): + self.msg = msg + self.args = args + self.kwargs = kwargs + + def __str__(self): + return self.msg.format(*self.args, **self.kwargs) + + def _log(self, level, msg, args, exc_info=None, extra=None, **kwargs): + """Log 'msg.format(*args, **kwargs)""" + msg = self._LogMessage(msg, args, kwargs) + return super(StrFormatLogger, self)._log(level, msg, (), exc_info, extra) + + +my_manager = copy(Logger.manager) +my_manager.loggerClass = StrFormatLogger + + +def getLogger(name=None): + if name: + return my_manager.getLogger(name) + else: + return root From e75f9a703d49a08098d6a840d6bebaa82c521c0e Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Sat, 3 Jan 2015 17:46:23 +0100 Subject: [PATCH 02/86] Convert beets core to lazy logging --- beets/autotag/match.py | 30 ++++++------ beets/autotag/mb.py | 4 +- beets/importer.py | 98 ++++++++++++++++++---------------------- beets/library.py | 6 +-- beets/mediafile.py | 6 +-- beets/plugins.py | 6 +-- beets/ui/__init__.py | 22 ++++----- beets/ui/commands.py | 32 ++++++------- beets/util/artresizer.py | 22 ++++----- 9 files changed, 103 insertions(+), 123 deletions(-) diff --git a/beets/autotag/match.py b/beets/autotag/match.py index 2d1f20074..00556359f 100644 --- a/beets/autotag/match.py +++ b/beets/autotag/match.py @@ -267,7 +267,7 @@ def match_by_id(items): # If all album IDs are equal, look up the album. if bool(reduce(lambda x, y: x if x == y else (), albumids)): albumid = albumids[0] - log.debug(u'Searching for discovered album ID: {0}'.format(albumid)) + log.debug(u'Searching for discovered album ID: {0}', albumid) return hooks.album_for_mbid(albumid) else: log.debug(u'No album ID consensus.') @@ -330,7 +330,7 @@ def _add_candidate(items, results, info): checking the track count, ordering the items, checking for duplicates, and calculating the distance. """ - log.debug(u'Candidate: {0} - {1}'.format(info.artist, info.album)) + log.debug(u'Candidate: {0} - {1}', info.artist, info.album) # Discard albums with zero tracks. if not info.tracks: @@ -345,7 +345,7 @@ def _add_candidate(items, results, info): # Discard matches without required tags. for req_tag in config['match']['required'].as_str_seq(): if getattr(info, req_tag) is None: - log.debug(u'Ignored. Missing required tag: {0}'.format(req_tag)) + log.debug(u'Ignored. Missing required tag: {0}', req_tag) return # Find mapping between the items and the track info. @@ -358,10 +358,10 @@ def _add_candidate(items, results, info): penalties = [key for _, key in dist] for penalty in config['match']['ignored'].as_str_seq(): if penalty in penalties: - log.debug(u'Ignored. Penalty: {0}'.format(penalty)) + log.debug(u'Ignored. Penalty: {0}', penalty) return - log.debug(u'Success. Distance: {0}'.format(dist)) + log.debug(u'Success. Distance: {0}', dist) results[info.album_id] = hooks.AlbumMatch(dist, info, mapping, extra_items, extra_tracks) @@ -387,7 +387,7 @@ def tag_album(items, search_artist=None, search_album=None, likelies, consensus = current_metadata(items) cur_artist = likelies['artist'] cur_album = likelies['album'] - log.debug(u'Tagging {0} - {1}'.format(cur_artist, cur_album)) + log.debug(u'Tagging {0} - {1}', cur_artist, cur_album) # The output result (distance, AlbumInfo) tuples (keyed by MB album # ID). @@ -395,7 +395,7 @@ def tag_album(items, search_artist=None, search_album=None, # Search by explicit ID. if search_id is not None: - log.debug(u'Searching for album ID: {0}'.format(search_id)) + log.debug(u'Searching for album ID: {0}', search_id) search_cands = hooks.albums_for_id(search_id) # Use existing metadata or text search. @@ -405,7 +405,7 @@ def tag_album(items, search_artist=None, search_album=None, if id_info: _add_candidate(items, candidates, id_info) rec = _recommendation(candidates.values()) - log.debug(u'Album ID match recommendation is {0}'.format(str(rec))) + log.debug(u'Album ID match recommendation is {0}', str(rec)) if candidates and not config['import']['timid']: # If we have a very good MBID match, return immediately. # Otherwise, this match will compete against metadata-based @@ -418,20 +418,19 @@ def tag_album(items, search_artist=None, search_album=None, if not (search_artist and search_album): # No explicit search terms -- use current metadata. search_artist, search_album = cur_artist, cur_album - log.debug(u'Search terms: {0} - {1}'.format(search_artist, - search_album)) + log.debug(u'Search terms: {0} - {1}', search_artist, search_album) # Is this album likely to be a "various artist" release? va_likely = ((not consensus['artist']) or (search_artist.lower() in VA_ARTISTS) or any(item.comp for item in items)) - log.debug(u'Album might be VA: {0}'.format(str(va_likely))) + log.debug(u'Album might be VA: {0}', str(va_likely)) # Get the results from the data sources. search_cands = hooks.album_candidates(items, search_artist, search_album, va_likely) - log.debug(u'Evaluating {0} candidates.'.format(len(search_cands))) + log.debug(u'Evaluating {0} candidates.', len(search_cands)) for info in search_cands: _add_candidate(items, candidates, info) @@ -456,7 +455,7 @@ def tag_item(item, search_artist=None, search_title=None, # First, try matching by MusicBrainz ID. trackid = search_id or item.mb_trackid if trackid: - log.debug(u'Searching for track ID: {0}'.format(trackid)) + log.debug(u'Searching for track ID: {0}', trackid) for track_info in hooks.tracks_for_id(trackid): dist = track_distance(item, track_info, incl_artist=True) candidates[track_info.track_id] = \ @@ -477,8 +476,7 @@ def tag_item(item, search_artist=None, search_title=None, # Search terms. if not (search_artist and search_title): search_artist, search_title = item.artist, item.title - log.debug(u'Item search terms: {0} - {1}'.format(search_artist, - search_title)) + log.debug(u'Item search terms: {0} - {1}', search_artist, search_title) # Get and evaluate candidate metadata. for track_info in hooks.item_candidates(item, search_artist, search_title): @@ -486,7 +484,7 @@ def tag_item(item, search_artist=None, search_title=None, candidates[track_info.track_id] = hooks.TrackMatch(dist, track_info) # Sort by distance and return with recommendation. - log.debug(u'Found {0} candidates.'.format(len(candidates))) + log.debug(u'Found {0} candidates.', len(candidates)) candidates = sorted(candidates.itervalues()) rec = _recommendation(candidates) return candidates, rec diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index d063f6278..72662bd5c 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -374,7 +374,7 @@ def album_for_id(releaseid): """ albumid = _parse_id(releaseid) if not albumid: - log.debug(u'Invalid MBID ({0}).'.format(releaseid)) + log.debug(u'Invalid MBID ({0}).', releaseid) return try: res = musicbrainzngs.get_release_by_id(albumid, @@ -394,7 +394,7 @@ def track_for_id(releaseid): """ trackid = _parse_id(releaseid) if not trackid: - log.debug(u'Invalid MBID ({0}).'.format(releaseid)) + log.debug(u'Invalid MBID ({0}).', releaseid) return try: res = musicbrainzngs.get_recording_by_id(trackid, TRACK_INCLUDES) diff --git a/beets/importer.py b/beets/importer.py index 4a7bd997f..4aa39ff92 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -71,7 +71,7 @@ def _open_state(): # unpickling, including ImportError. We use a catch-all # exception to avoid enumerating them all (the docs don't even have a # full list!). - log.debug(u'state file could not be read: {0}'.format(exc)) + log.debug(u'state file could not be read: {0}', exc) return {} @@ -81,7 +81,7 @@ def _save_state(state): with open(config['statefile'].as_filename(), 'w') as f: pickle.dump(state, f) except IOError as exc: - log.error(u'state file could not be written: {0}'.format(exc)) + log.error(u'state file could not be written: {0}', exc) # Utilities for reading and writing the beets progress file, which @@ -347,8 +347,8 @@ class ImportSession(object): # Either accept immediately or prompt for input to decide. if self.want_resume is True or \ self.should_resume(toppath): - log.warn(u'Resuming interrupted import of {0}'.format( - util.displayable_path(toppath))) + log.warn(u'Resuming interrupted import of {0}', + util.displayable_path(toppath)) self._is_resuming[toppath] = True else: # Clear progress; we're starting from the top. @@ -481,13 +481,12 @@ class ImportTask(object): def remove_duplicates(self, lib): duplicate_items = self.duplicate_items(lib) - log.debug(u'removing {0} old duplicated items' - .format(len(duplicate_items))) + log.debug(u'removing {0} old duplicated items', len(duplicate_items)) for item in duplicate_items: item.remove() if lib.directory in util.ancestry(item.path): - log.debug(u'deleting duplicate {0}' - .format(util.displayable_path(item.path))) + log.debug(u'deleting duplicate {0}', + util.displayable_path(item.path)) util.remove(item.path) util.prune_dirs(os.path.dirname(item.path), lib.directory) @@ -686,12 +685,11 @@ class ImportTask(object): self.album.store() log.debug( u'Reimported album: added {0}, flexible ' - u'attributes {1} from album {2} for {3}'.format( - self.album.added, - replaced_album._values_flex.keys(), - replaced_album.id, - displayable_path(self.album.path), - ) + u'attributes {1} from album {2} for {3}', + self.album.added, + replaced_album._values_flex.keys(), + replaced_album.id, + displayable_path(self.album.path) ) for item in self.imported_items(): @@ -701,20 +699,18 @@ class ImportTask(object): item.added = dup_item.added log.debug( u'Reimported item added {0} ' - u'from item {1} for {2}'.format( - item.added, - dup_item.id, - displayable_path(item.path), - ) + u'from item {1} for {2}', + item.added, + dup_item.id, + displayable_path(item.path) ) item.update(dup_item._values_flex) log.debug( u'Reimported item flexible attributes {0} ' - u'from item {1} for {2}'.format( - dup_item._values_flex.keys(), - dup_item.id, - displayable_path(item.path), - ) + u'from item {1} for {2}', + dup_item._values_flex.keys(), + dup_item.id, + displayable_path(item.path) ) item.store() @@ -724,13 +720,12 @@ class ImportTask(object): """ for item in self.imported_items(): for dup_item in self.replaced_items[item]: - log.debug(u'Replacing item {0}: {1}' - .format(dup_item.id, - displayable_path(item.path))) + log.debug(u'Replacing item {0}: {1}', + dup_item.id, displayable_path(item.path)) dup_item.remove() - log.debug(u'{0} of {1} items replaced' - .format(sum(bool(l) for l in self.replaced_items.values()), - len(self.imported_items()))) + log.debug(u'{0} of {1} items replaced', + sum(bool(l) for l in self.replaced_items.values()), + len(self.imported_items())) def choose_match(self, session): """Ask the session which match should apply and apply it. @@ -1002,8 +997,8 @@ class ImportTaskFactory(object): def singleton(self, path): if self.session.already_imported(self.toppath, [path]): - log.debug(u'Skipping previously-imported path: {0}' - .format(displayable_path(path))) + log.debug(u'Skipping previously-imported path: {0}', + displayable_path(path)) self.skipped += 1 return None @@ -1026,8 +1021,8 @@ class ImportTaskFactory(object): dirs = list(set(os.path.dirname(p) for p in paths)) if self.session.already_imported(self.toppath, dirs): - log.debug(u'Skipping previously-imported path: {0}' - .format(displayable_path(dirs))) + log.debug(u'Skipping previously-imported path: {0}', + displayable_path(dirs)) self.skipped += 1 return None @@ -1055,14 +1050,10 @@ class ImportTaskFactory(object): # Silently ignore non-music files. pass elif isinstance(exc.reason, mediafile.UnreadableFileError): - log.warn(u'unreadable file: {0}'.format( - displayable_path(path)) - ) + log.warn(u'unreadable file: {0}', displayable_path(path)) else: - log.error(u'error reading {0}: {1}'.format( - displayable_path(path), - exc, - )) + log.error(u'error reading {0}: {1}', + displayable_path(path), exc) # Full-album pipeline stages. @@ -1086,13 +1077,13 @@ def read_tasks(session): "'copy' or 'move' to be enabled.") continue - log.debug(u'extracting archive {0}' - .format(displayable_path(toppath))) + log.debug(u'extracting archive {0}', + displayable_path(toppath)) archive_task = ArchiveImportTask(toppath) try: archive_task.extract() except Exception as exc: - log.error(u'extraction failed: {0}'.format(exc)) + log.error(u'extraction failed: {0}', exc) continue # Continue reading albums from the extracted directory. @@ -1112,12 +1103,12 @@ def read_tasks(session): yield archive_task if not imported: - log.warn(u'No files imported from {0}' - .format(displayable_path(user_toppath))) + log.warn(u'No files imported from {0}', + displayable_path(user_toppath)) # Show skipped directories. if skipped: - log.info(u'Skipped {0} directories.'.format(skipped)) + log.info(u'Skipped {0} directories.', skipped) def query_tasks(session): @@ -1133,8 +1124,8 @@ def query_tasks(session): else: # Search for albums. for album in session.lib.albums(session.query): - log.debug(u'yielding album {0}: {1} - {2}' - .format(album.id, album.albumartist, album.album)) + log.debug(u'yielding album {0}: {1} - {2}', + album.id, album.albumartist, album.album) items = list(album.items()) # Clear IDs from re-tagged items so they appear "fresh" when @@ -1159,7 +1150,7 @@ def lookup_candidates(session, task): return plugins.send('import_task_start', session=session, task=task) - log.debug(u'Looking up: {0}'.format(displayable_path(task.paths))) + log.debug(u'Looking up: {0}', displayable_path(task.paths)) task.lookup_candidates() @@ -1300,12 +1291,11 @@ def log_files(session, task): """A coroutine (pipeline stage) to log each file which will be imported """ if isinstance(task, SingletonImportTask): - log.info( - 'Singleton: {0}'.format(displayable_path(task.item['path']))) + log.info('Singleton: {0}', displayable_path(task.item['path'])) elif task.items: - log.info('Album {0}'.format(displayable_path(task.paths[0]))) + log.info('Album {0}', displayable_path(task.paths[0])) for item in task.items: - log.info(' {0}'.format(displayable_path(item['path']))) + log.info(' {0}', displayable_path(item['path'])) def group_albums(session): diff --git a/beets/library.py b/beets/library.py index 1de1bba56..a4213d8b3 100644 --- a/beets/library.py +++ b/beets/library.py @@ -837,9 +837,9 @@ class Album(LibModel): return new_art = util.unique_path(new_art) - log.debug(u'moving album art {0} to {1}' - .format(util.displayable_path(old_art), - util.displayable_path(new_art))) + log.debug(u'moving album art {0} to {1}', + util.displayable_path(old_art), + util.displayable_path(new_art)) if copy: util.copy(old_art, new_art) elif link: diff --git a/beets/mediafile.py b/beets/mediafile.py index 49ef10378..9106bbd71 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -1313,7 +1313,7 @@ class MediaFile(object): try: self.mgfile = mutagen.File(path) except unreadable_exc as exc: - log.debug(u'header parsing failed: {0}'.format(unicode(exc))) + log.debug(u'header parsing failed: {0}', unicode(exc)) raise UnreadableFileError(path) except IOError as exc: if type(exc) == IOError: @@ -1326,7 +1326,7 @@ class MediaFile(object): except Exception as exc: # Isolate bugs in Mutagen. log.debug(traceback.format_exc()) - log.error(u'uncaught Mutagen exception in open: {0}'.format(exc)) + log.error(u'uncaught Mutagen exception in open: {0}', exc) raise MutagenError(path, exc) if self.mgfile is None: @@ -1399,7 +1399,7 @@ class MediaFile(object): raise except Exception as exc: log.debug(traceback.format_exc()) - log.error(u'uncaught Mutagen exception in save: {0}'.format(exc)) + log.error(u'uncaught Mutagen exception in save: {0}', exc) raise MutagenError(self.path, exc) def delete(self): diff --git a/beets/plugins.py b/beets/plugins.py index 8611b92a6..c3058b617 100755 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -204,7 +204,7 @@ def load_plugins(names=()): except ImportError as exc: # Again, this is hacky: if exc.args[0].endswith(' ' + name): - log.warn(u'** plugin {0} not found'.format(name)) + log.warn(u'** plugin {0} not found', name) else: raise else: @@ -214,7 +214,7 @@ def load_plugins(names=()): _classes.add(obj) except: - log.warn(u'** error loading plugin {0}'.format(name)) + log.warn(u'** error loading plugin {0}', name) log.warn(traceback.format_exc()) @@ -398,7 +398,7 @@ def send(event, **arguments): Returns a list of return values from the handlers. """ - log.debug(u'Sending event: {0}'.format(event)) + log.debug(u'Sending event: {0}', event) for handler in event_handlers()[event]: # Don't break legacy plugins if we want to pass more arguments argspec = inspect.getargspec(handler).args diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index 8978ff547..4617e84fc 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -866,14 +866,14 @@ def _configure(options): config_path = config.user_config_path() if os.path.isfile(config_path): - log.debug(u'user configuration: {0}'.format( - util.displayable_path(config_path))) + log.debug(u'user configuration: {0}', + util.displayable_path(config_path)) else: - log.debug(u'no user configuration found at {0}'.format( - util.displayable_path(config_path))) + log.debug(u'no user configuration found at {0}', + util.displayable_path(config_path)) - log.debug(u'data directory: {0}' - .format(util.displayable_path(config.config_dir()))) + log.debug(u'data directory: {0}', + util.displayable_path(config.config_dir())) return config @@ -895,9 +895,9 @@ def _open_library(config): util.displayable_path(dbpath) )) log.debug(u'library database: {0}\n' - u'library directory: {1}' - .format(util.displayable_path(lib.path), - util.displayable_path(lib.directory))) + u'library directory: {1}', + util.displayable_path(lib.path), + util.displayable_path(lib.directory)) return lib @@ -945,7 +945,7 @@ def main(args=None): _raw_main(args) except UserError as exc: message = exc.args[0] if exc.args else None - log.error(u'error: {0}'.format(message)) + log.error(u'error: {0}', message) sys.exit(1) except util.HumanReadableException as exc: exc.log(log) @@ -957,7 +957,7 @@ def main(args=None): log.error(exc) sys.exit(1) except confit.ConfigError as exc: - log.error(u'configuration error: {0}'.format(exc)) + log.error(u'configuration error: {0}', exc) sys.exit(1) except IOError as exc: if exc.errno == errno.EPIPE: diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 546fe87d9..e55d41504 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -764,8 +764,8 @@ class TerminalImportSession(importer.ImportSession): """Decide what to do when a new album or item seems similar to one that's already in the library. """ - log.warn(u"This {0} is already in the library!" - .format("album" if task.is_album else "item")) + log.warn(u"This {0} is already in the library!", + ("album" if task.is_album else "item")) if config['import']['quiet']: # In quiet mode, don't prompt -- just skip. @@ -1014,16 +1014,16 @@ def update_items(lib, query, album, move, pretend): # Did the item change since last checked? if item.current_mtime() <= item.mtime: - log.debug(u'skipping {0} because mtime is up to date ({1})' - .format(displayable_path(item.path), item.mtime)) + log.debug(u'skipping {0} because mtime is up to date ({1})', + displayable_path(item.path), item.mtime) continue # Read new data. try: item.read() except library.ReadError as exc: - log.error(u'error reading {0}: {1}'.format( - displayable_path(item.path), exc)) + log.error(u'error reading {0}: {1}', + displayable_path(item.path), exc) continue # Special-case album artist when it matches track artist. (Hacky @@ -1065,7 +1065,7 @@ def update_items(lib, query, album, move, pretend): continue album = lib.get_album(album_id) if not album: # Empty albums have already been removed. - log.debug(u'emptied album {0}'.format(album_id)) + log.debug(u'emptied album {0}', album_id) continue first_item = album.items().get() @@ -1076,7 +1076,7 @@ def update_items(lib, query, album, move, pretend): # Move album art (and any inconsistent items). if move and lib.directory in ancestry(first_item.path): - log.debug(u'moving album {0}'.format(album_id)) + log.debug(u'moving album {0}', album_id) album.move() @@ -1298,8 +1298,7 @@ def modify_items(lib, mods, dels, query, write, move, album, confirm): if move: cur_path = obj.path if lib.directory in ancestry(cur_path): # In library? - log.debug(u'moving object {0}' - .format(displayable_path(cur_path))) + log.debug(u'moving object {0}', displayable_path(cur_path)) obj.move() obj.try_sync(write) @@ -1377,9 +1376,9 @@ def move_items(lib, dest, query, copy, album): action = 'Copying' if copy else 'Moving' entity = 'album' if album else 'item' - log.info(u'{0} {1} {2}s.'.format(action, len(objs), entity)) + log.info(u'{0} {1} {2}s.', action, len(objs), entity) for obj in objs: - log.debug(u'moving: {0}'.format(util.displayable_path(obj.path))) + log.debug(u'moving: {0}', util.displayable_path(obj.path)) obj.move(copy, basedir=dest) obj.store() @@ -1425,18 +1424,15 @@ def write_items(lib, query, pretend, force): for item in items: # Item deleted? if not os.path.exists(syspath(item.path)): - log.info(u'missing file: {0}'.format( - util.displayable_path(item.path) - )) + log.info(u'missing file: {0}', util.displayable_path(item.path)) continue # Get an Item object reflecting the "clean" (on-disk) state. try: clean_item = library.Item.from_path(item.path) except library.ReadError as exc: - log.error(u'error reading {0}: {1}'.format( - displayable_path(item.path), exc - )) + log.error(u'error reading {0}: {1}', + displayable_path(item.path), exc) continue # Check for and display changes. diff --git a/beets/util/artresizer.py b/beets/util/artresizer.py index f17fdc5b9..0145124c1 100644 --- a/beets/util/artresizer.py +++ b/beets/util/artresizer.py @@ -58,9 +58,8 @@ def pil_resize(maxwidth, path_in, path_out=None): """ path_out = path_out or temp_file_for(path_in) from PIL import Image - log.debug(u'artresizer: PIL resizing {0} to {1}'.format( - util.displayable_path(path_in), util.displayable_path(path_out) - )) + log.debug(u'artresizer: PIL resizing {0} to {1}', + util.displayable_path(path_in), util.displayable_path(path_out)) try: im = Image.open(util.syspath(path_in)) @@ -69,9 +68,8 @@ def pil_resize(maxwidth, path_in, path_out=None): im.save(path_out) return path_out except IOError: - log.error(u"PIL cannot create thumbnail for '{0}'".format( - util.displayable_path(path_in) - )) + log.error(u"PIL cannot create thumbnail for '{0}'", + util.displayable_path(path_in)) return path_in @@ -80,9 +78,8 @@ def im_resize(maxwidth, path_in, path_out=None): Return the output path of resized image. """ path_out = path_out or temp_file_for(path_in) - log.debug(u'artresizer: ImageMagick resizing {0} to {1}'.format( - util.displayable_path(path_in), util.displayable_path(path_out) - )) + log.debug(u'artresizer: ImageMagick resizing {0} to {1}', + util.displayable_path(path_in), util.displayable_path(path_out)) # "-resize widthxheight>" shrinks images with dimension(s) larger # than the corresponding width and/or height dimension(s). The > @@ -94,9 +91,8 @@ def im_resize(maxwidth, path_in, path_out=None): '-resize', '{0}x^>'.format(maxwidth), path_out ]) except subprocess.CalledProcessError: - log.warn(u'artresizer: IM convert failed for {0}'.format( - util.displayable_path(path_in) - )) + log.warn(u'artresizer: IM convert failed for {0}', + util.displayable_path(path_in)) return path_in return path_out @@ -134,7 +130,7 @@ class ArtResizer(object): specified, with an inferred method. """ self.method = self._check_method(method) - log.debug(u"artresizer: method is {0}".format(self.method)) + log.debug(u"artresizer: method is {0}", self.method) self.can_compare = self._can_compare() def resize(self, maxwidth, path_in, path_out=None): From 8cac47af2a2d004ba90d87e38d78a6486622c273 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Sun, 4 Jan 2015 11:41:17 +0100 Subject: [PATCH 03/86] Convert beets plugins to lazy logging --- beetsplug/beatport.py | 8 ++-- beetsplug/bpm.py | 6 +-- beetsplug/chroma.py | 53 +++++++++++------------- beetsplug/convert.py | 61 +++++++++++---------------- beetsplug/discogs.py | 16 ++++---- beetsplug/duplicates.py | 20 ++++----- beetsplug/echonest.py | 59 ++++++++++++-------------- beetsplug/embedart.py | 50 ++++++++++------------ beetsplug/fetchart.py | 24 +++++------ beetsplug/ftintitle.py | 5 +-- beetsplug/ihate.py | 7 ++-- beetsplug/importadded.py | 22 +++++----- beetsplug/importfeeds.py | 2 +- beetsplug/info.py | 2 +- beetsplug/inline.py | 9 ++-- beetsplug/keyfinder.py | 6 +-- beetsplug/lastgenre/__init__.py | 24 +++++------ beetsplug/lastimport.py | 64 +++++++++++------------------ beetsplug/lyrics.py | 29 +++++++------ beetsplug/mbcollection.py | 2 +- beetsplug/mbsync.py | 11 +++-- beetsplug/missing.py | 6 +-- beetsplug/mpdstats.py | 37 ++++++----------- beetsplug/play.py | 8 ++-- beetsplug/replaygain.py | 73 ++++++++++----------------------- beetsplug/rewrite.py | 2 +- beetsplug/scrub.py | 10 ++--- beetsplug/spotify.py | 24 ++++------- beetsplug/the.py | 6 +-- beetsplug/zero.py | 6 +-- 30 files changed, 272 insertions(+), 380 deletions(-) diff --git a/beetsplug/beatport.py b/beetsplug/beatport.py index b83aef2f7..8afefbefb 100644 --- a/beetsplug/beatport.py +++ b/beetsplug/beatport.py @@ -194,7 +194,7 @@ class BeatportPlugin(BeetsPlugin): try: return self._get_releases(query) except BeatportAPIError as e: - log.debug(u'Beatport API Error: {0} (query: {1})'.format(e, query)) + log.debug(u'Beatport API Error: {0} (query: {1})', e, query) return [] def item_candidates(self, item, artist, title): @@ -205,14 +205,14 @@ class BeatportPlugin(BeetsPlugin): try: return self._get_tracks(query) except BeatportAPIError as e: - log.debug(u'Beatport API Error: {0} (query: {1})'.format(e, query)) + log.debug(u'Beatport API Error: {0} (query: {1})', e, query) return [] def album_for_id(self, release_id): """Fetches a release by its Beatport ID and returns an AlbumInfo object or None if the release is not found. """ - log.debug(u'Searching Beatport for release {0}'.format(release_id)) + log.debug(u'Searching Beatport for release {0}', release_id) match = re.search(r'(^|beatport\.com/release/.+/)(\d+)$', release_id) if not match: return None @@ -224,7 +224,7 @@ class BeatportPlugin(BeetsPlugin): """Fetches a track by its Beatport ID and returns a TrackInfo object or None if the track is not found. """ - log.debug(u'Searching Beatport for track {0}'.format(str(track_id))) + log.debug(u'Searching Beatport for track {0}', track_id) match = re.search(r'(^|beatport\.com/track/.+/)(\d+)$', track_id) if not match: return None diff --git a/beetsplug/bpm.py b/beetsplug/bpm.py index d895ec5be..2189e2fae 100644 --- a/beetsplug/bpm.py +++ b/beetsplug/bpm.py @@ -73,15 +73,15 @@ class BPMPlugin(BeetsPlugin): item = items[0] if item['bpm']: - log.info(u'Found bpm {0}'.format(item['bpm'])) + log.info(u'Found bpm {0}', item['bpm']) if not overwrite: return log.info(u'Press Enter {0} times to the rhythm or Ctrl-D ' - u'to exit'.format(self.config['max_strokes'].get(int))) + u'to exit', self.config['max_strokes'].get(int)) new_bpm = bpm(self.config['max_strokes'].get(int)) item['bpm'] = int(new_bpm) if write: item.try_write() item.store() - log.info(u'Added new bpm {0}'.format(item['bpm'])) + log.info(u'Added new bpm {0}', item['bpm']) diff --git a/beetsplug/chroma.py b/beetsplug/chroma.py index 106d6df76..0ff86e220 100644 --- a/beetsplug/chroma.py +++ b/beetsplug/chroma.py @@ -64,19 +64,19 @@ def acoustid_match(path): try: duration, fp = acoustid.fingerprint_file(util.syspath(path)) except acoustid.FingerprintGenerationError as exc: - log.error(u'fingerprinting of {0} failed: {1}' - .format(util.displayable_path(repr(path)), str(exc))) + log.error(u'fingerprinting of {0} failed: {1}', + util.displayable_path(repr(path)), str(exc)) return None _fingerprints[path] = fp try: res = acoustid.lookup(API_KEY, fp, duration, meta='recordings releases') except acoustid.AcoustidError as exc: - log.debug(u'fingerprint matching {0} failed: {1}' - .format(util.displayable_path(repr(path)), str(exc))) + log.debug(u'fingerprint matching {0} failed: {1}', + util.displayable_path(repr(path)), exc) return None - log.debug(u'chroma: fingerprinted {0}' - .format(util.displayable_path(repr(path)))) + log.debug(u'chroma: fingerprinted {0}', + util.displayable_path(repr(path))) # Ensure the response is usable and parse it. if res['status'] != 'ok' or not res.get('results'): @@ -99,9 +99,8 @@ def acoustid_match(path): if 'releases' in recording: release_ids += [rel['id'] for rel in recording['releases']] - log.debug(u'chroma: matched recordings {0} on releases {1}'.format( - recording_ids, release_ids, - )) + log.debug(u'chroma: matched recordings {0} on releases {1}', + recording_ids, release_ids) _matches[path] = recording_ids, release_ids @@ -155,7 +154,7 @@ class AcoustidPlugin(plugins.BeetsPlugin): if album: albums.append(album) - log.debug(u'acoustid album candidates: {0}'.format(len(albums))) + log.debug(u'acoustid album candidates: {0}', len(albums)) return albums def item_candidates(self, item, artist, title): @@ -168,7 +167,7 @@ class AcoustidPlugin(plugins.BeetsPlugin): track = hooks.track_for_mbid(recording_id) if track: tracks.append(track) - log.debug(u'acoustid item candidates: {0}'.format(len(tracks))) + log.debug(u'acoustid item candidates: {0}', len(tracks)) return tracks def commands(self): @@ -230,11 +229,11 @@ def submit_items(userkey, items, chunksize=64): def submit_chunk(): """Submit the current accumulated fingerprint data.""" - log.info(u'submitting {0} fingerprints'.format(len(data))) + log.info(u'submitting {0} fingerprints', len(data)) try: acoustid.submit(API_KEY, userkey, data) except acoustid.AcoustidError as exc: - log.warn(u'acoustid submission error: {0}'.format(exc)) + log.warn(u'acoustid submission error: {0}', exc) del data[:] for item in items: @@ -279,34 +278,28 @@ def fingerprint_item(item, write=False): """ # Get a fingerprint and length for this track. if not item.length: - log.info(u'{0}: no duration available'.format( - util.displayable_path(item.path) - )) + log.info(u'{0}: no duration available', + util.displayable_path(item.path)) elif item.acoustid_fingerprint: if write: - log.info(u'{0}: fingerprint exists, skipping'.format( - util.displayable_path(item.path) - )) + log.info(u'{0}: fingerprint exists, skipping', + util.displayable_path(item.path)) else: - log.info(u'{0}: using existing fingerprint'.format( - util.displayable_path(item.path) - )) + log.info(u'{0}: using existing fingerprint', + util.displayable_path(item.path)) return item.acoustid_fingerprint else: - log.info(u'{0}: fingerprinting'.format( - util.displayable_path(item.path) - )) + log.info(u'{0}: fingerprinting', + util.displayable_path(item.path)) try: _, fp = acoustid.fingerprint_file(item.path) item.acoustid_fingerprint = fp if write: - log.info(u'{0}: writing fingerprint'.format( - util.displayable_path(item.path) - )) + log.info(u'{0}: writing fingerprint', + util.displayable_path(item.path)) item.try_write() if item._db: item.store() return item.acoustid_fingerprint except acoustid.FingerprintGenerationError as exc: - log.info(u'fingerprint generation failed: {0}' - .format(exc)) + log.info(u'fingerprint generation failed: {0}', exc) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 0b87fb71b..679ebcd72 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -92,7 +92,7 @@ def encode(command, source, dest, pretend=False): quiet = config['convert']['quiet'].get() if not quiet and not pretend: - log.info(u'Encoding {0}'.format(util.displayable_path(source))) + log.info(u'Encoding {0}', util.displayable_path(source)) # Substitute $source and $dest in the argument list. args = shlex.split(command) @@ -110,12 +110,11 @@ def encode(command, source, dest, pretend=False): util.command_output(args) except subprocess.CalledProcessError as exc: # Something went wrong (probably Ctrl+C), remove temporary files - log.info(u'Encoding {0} failed. Cleaning up...' - .format(util.displayable_path(source))) - log.debug(u'Command {0} exited with status {1}'.format( - exc.cmd.decode('utf8', 'ignore'), - exc.returncode, - )) + log.info(u'Encoding {0} failed. Cleaning up...', + util.displayable_path(source)) + log.debug(u'Command {0} exited with status {1}', + exc.cmd.decode('utf8', 'ignore'), + exc.returncode) util.remove(dest) util.prune_dirs(os.path.dirname(dest)) raise @@ -127,9 +126,8 @@ def encode(command, source, dest, pretend=False): ) if not quiet and not pretend: - log.info(u'Finished encoding {0}'.format( - util.displayable_path(source)) - ) + log.info(u'Finished encoding {0}', + util.displayable_path(source)) def should_transcode(item, format): @@ -173,21 +171,17 @@ def convert_item(dest_dir, keep_new, path_formats, format, pretend=False): util.mkdirall(dest) if os.path.exists(util.syspath(dest)): - log.info(u'Skipping {0} (target file exists)'.format( - util.displayable_path(item.path) - )) + log.info(u'Skipping {0} (target file exists)', + util.displayable_path(item.path)) continue if keep_new: if pretend: - log.info(u'mv {0} {1}'.format( - util.displayable_path(item.path), - util.displayable_path(original), - )) + log.info(u'mv {0} {1}', + util.displayable_path(item.path), + util.displayable_path(original)) else: - log.info(u'Moving to {0}'.format( - util.displayable_path(original)) - ) + log.info(u'Moving to {0}', util.displayable_path(original)) util.move(item.path, original) if should_transcode(item, format): @@ -197,15 +191,12 @@ def convert_item(dest_dir, keep_new, path_formats, format, pretend=False): continue else: if pretend: - log.info(u'cp {0} {1}'.format( - util.displayable_path(original), - util.displayable_path(converted), - )) + log.info(u'cp {0} {1}', + util.displayable_path(original), + util.displayable_path(converted)) else: # No transcoding necessary. - log.info(u'Copying {0}'.format( - util.displayable_path(item.path)) - ) + log.info(u'Copying {0}', util.displayable_path(item.path)) util.copy(original, converted) if pretend: @@ -281,19 +272,17 @@ def copy_album_art(album, dest_dir, path_formats, pretend=False): util.mkdirall(dest) if os.path.exists(util.syspath(dest)): - log.info(u'Skipping {0} (target file exists)'.format( - util.displayable_path(album.artpath) - )) + log.info(u'Skipping {0} (target file exists)', + util.displayable_path(album.artpath)) return if pretend: - log.info(u'cp {0} {1}'.format( - util.displayable_path(album.artpath), - util.displayable_path(dest), - )) + log.info(u'cp {0} {1}', + util.displayable_path(album.artpath), + util.displayable_path(dest)) else: - log.info(u'Copying cover art to {0}'.format( - util.displayable_path(dest))) + log.info(u'Copying cover art to {0}', + util.displayable_path(dest)) util.copy(album.artpath, dest) diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index 234d9b6cd..41c088225 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -89,7 +89,7 @@ class DiscogsPlugin(BeetsPlugin): raise beets.ui.UserError('Discogs authorization failed') # Save the token for later use. - log.debug('Discogs token {0}, secret {1}'.format(token, secret)) + log.debug('Discogs token {0}, secret {1}', token, secret) with open(self._tokenfile(), 'w') as f: json.dump({'token': token, 'secret': secret}, f) @@ -117,10 +117,10 @@ class DiscogsPlugin(BeetsPlugin): try: return self.get_albums(query) except DiscogsAPIError as e: - log.debug(u'Discogs API Error: {0} (query: {1})'.format(e, query)) + log.debug(u'Discogs API Error: {0} (query: {1})', e, query) return [] except ConnectionError as e: - log.debug(u'HTTP Connection Error: {0}'.format(e)) + log.debug(u'HTTP Connection Error: {0}', e) return [] def album_for_id(self, album_id): @@ -130,7 +130,7 @@ class DiscogsPlugin(BeetsPlugin): if not self.discogs_client: return - log.debug(u'Searching Discogs for release {0}'.format(str(album_id))) + log.debug(u'Searching Discogs for release {0}', album_id) # Discogs-IDs are simple integers. We only look for those at the end # of an input string as to avoid confusion with other metadata plugins. # An optional bracket can follow the integer, as this is how discogs @@ -145,11 +145,11 @@ class DiscogsPlugin(BeetsPlugin): getattr(result, 'title') except DiscogsAPIError as e: if e.message != '404 Not Found': - log.debug(u'Discogs API Error: {0} (query: {1})' - .format(e, result._uri)) + log.debug(u'Discogs API Error: {0} (query: {1})', + e, result._uri) return None except ConnectionError as e: - log.debug(u'HTTP Connection Error: {0}'.format(e)) + log.debug(u'HTTP Connection Error: {0}', e) return None return self.get_album_info(result) @@ -294,7 +294,7 @@ class DiscogsPlugin(BeetsPlugin): if match: medium, index = match.groups() else: - log.debug(u'Invalid Discogs position: {0}'.format(position)) + log.debug(u'Invalid Discogs position: {0}', position) medium = index = None return medium or None, index or None diff --git a/beetsplug/duplicates.py b/beetsplug/duplicates.py index 8e0af4ab7..5229ee5ca 100644 --- a/beetsplug/duplicates.py +++ b/beetsplug/duplicates.py @@ -56,20 +56,20 @@ def _checksum(item, prog): key = args[0] checksum = getattr(item, key, False) if not checksum: - log.debug(u'{0}: key {1} on item {2} not cached: computing checksum' - .format(PLUGIN, key, displayable_path(item.path))) + log.debug(u'{0}: key {1} on item {2} not cached: computing checksum', + PLUGIN, key, displayable_path(item.path)) try: checksum = command_output(args) setattr(item, key, checksum) item.store() - log.debug(u'{)}: computed checksum for {1} using {2}' - .format(PLUGIN, item.title, key)) + log.debug(u'{)}: computed checksum for {1} using {2}', + PLUGIN, item.title, key) except subprocess.CalledProcessError as e: - log.debug(u'{0}: failed to checksum {1}: {2}' - .format(PLUGIN, displayable_path(item.path), e)) + log.debug(u'{0}: failed to checksum {1}: {2}', + PLUGIN, displayable_path(item.path), e) else: - log.debug(u'{0}: key {1} on item {2} cached: not computing checksum' - .format(PLUGIN, key, displayable_path(item.path))) + log.debug(u'{0}: key {1} on item {2} cached: not computing checksum', + PLUGIN, key, displayable_path(item.path)) return key, checksum @@ -86,8 +86,8 @@ def _group_by(objs, keys): key = '\001'.join(values) counts[key].append(obj) else: - log.debug(u'{0}: all keys {1} on item {2} are null: skipping' - .format(PLUGIN, str(keys), displayable_path(obj.path))) + log.debug(u'{0}: all keys {1} on item {2} are null: skipping', + PLUGIN, str(keys), displayable_path(obj.path)) return counts diff --git a/beetsplug/echonest.py b/beetsplug/echonest.py index db440a44a..16ba6f3bd 100644 --- a/beetsplug/echonest.py +++ b/beetsplug/echonest.py @@ -154,23 +154,23 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): if e.code == 3: # reached access limit per minute log.debug(u'echonest: rate-limited on try {0}; ' - u'waiting {1} seconds' - .format(i + 1, RETRY_INTERVAL)) + u'waiting {1} seconds', + i + 1, RETRY_INTERVAL) time.sleep(RETRY_INTERVAL) elif e.code == 5: # specified identifier does not exist # no use in trying again. - log.debug(u'echonest: {0}'.format(e)) + log.debug(u'echonest: {0}', e) return None else: - log.error(u'echonest: {0}'.format(e.args[0][0])) + log.error(u'echonest: {0}', e.args[0][0]) return None except (pyechonest.util.EchoNestIOError, socket.error) as e: - log.warn(u'echonest: IO error: {0}'.format(e)) + log.warn(u'echonest: IO error: {0}', e) time.sleep(RETRY_INTERVAL) except Exception as e: # there was an error analyzing the track, status: error - log.debug(u'echonest: {0}'.format(e)) + log.debug(u'echonest: {0}', e) return None else: break @@ -292,10 +292,9 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): fd, dest = tempfile.mkstemp(u'.ogg') os.close(fd) - log.info(u'echonest: encoding {0} to {1}'.format( - util.displayable_path(source), - util.displayable_path(dest), - )) + log.info(u'echonest: encoding {0} to {1}', + util.displayable_path(source), + util.displayable_path(dest)) opts = [] for arg in CONVERT_COMMAND.split(): @@ -306,13 +305,12 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): try: util.command_output(opts) except (OSError, subprocess.CalledProcessError) as exc: - log.debug(u'echonest: encode failed: {0}'.format(exc)) + log.debug(u'echonest: encode failed: {0}', exc) util.remove(dest) return - log.info(u'echonest: finished encoding {0}'.format( - util.displayable_path(source)) - ) + log.info(u'echonest: finished encoding {0}', + util.displayable_path(source)) return dest def truncate(self, source): @@ -320,10 +318,9 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): fd, dest = tempfile.mkstemp(u'.ogg') os.close(fd) - log.info(u'echonest: truncating {0} to {1}'.format( - util.displayable_path(source), - util.displayable_path(dest), - )) + log.info(u'echonest: truncating {0} to {1}', + util.displayable_path(source), + util.displayable_path(dest)) opts = [] for arg in TRUNCATE_COMMAND.split(): @@ -334,13 +331,12 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): try: util.command_output(opts) except (OSError, subprocess.CalledProcessError) as exc: - log.debug(u'echonest: truncate failed: {0}'.format(exc)) + log.debug(u'echonest: truncate failed: {0}', exc) util.remove(dest) return - log.info(u'echonest: truncate encoding {0}'.format( - util.displayable_path(source)) - ) + log.info(u'echonest: truncate encoding {0}', + util.displayable_path(source)) return dest def analyze(self, item): @@ -411,13 +407,11 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): for method in methods: song = method(item) if song: - log.debug( - u'echonest: got song through {0}: {1} - {2} [{3}]'.format( - method.__name__, - item.artist, - item.title, - song.get('duration'), - ) + log.debug(u'echonest: got song through {0}: {1} - {2} [{3}]', + method.__name__, + item.artist, + item.title, + song.get('duration'), ) return song @@ -429,7 +423,7 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): for k, v in values.iteritems(): if k in ATTRIBUTES: field = ATTRIBUTES[k] - log.debug(u'echonest: metadata: {0} = {1}'.format(field, v)) + log.debug(u'echonest: metadata: {0} = {1}', field, v) if field == 'bpm': item[field] = int(v) else: @@ -441,7 +435,7 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): item['initial_key'] = key if 'id' in values: enid = values['id'] - log.debug(u'echonest: metadata: {0} = {1}'.format(ID_KEY, enid)) + log.debug(u'echonest: metadata: {0} = {1}', ID_KEY, enid) item[ID_KEY] = enid # Write and save. @@ -483,8 +477,7 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): self.config.set_args(opts) write = config['import']['write'].get(bool) for item in lib.items(ui.decargs(args)): - log.info(u'echonest: {0} - {1}'.format(item.artist, - item.title)) + log.info(u'echonest: {0} - {1}', item.artist, item.title) if self.config['force'] or self.requires_update(item): song = self.fetch_song(item) if song: diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index 49ed47928..3fdfd9055 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -122,20 +122,17 @@ def embed_item(item, imagepath, maxwidth=None, itempath=None, if not art: pass else: - log.debug(u'embedart: media file contained art already {0}'.format( - displayable_path(imagepath) - )) + log.debug(u'embedart: media file contained art already {0}', + displayable_path(imagepath)) return if maxwidth and not as_album: imagepath = resize_image(imagepath, maxwidth) try: - log.debug(u'embedart: embedding {0}'.format( - displayable_path(imagepath) - )) + log.debug(u'embedart: embedding {0}', displayable_path(imagepath)) item['images'] = [_mediafile_image(imagepath, maxwidth)] except IOError as exc: - log.error(u'embedart: could not read image file: {0}'.format(exc)) + log.error(u'embedart: could not read image file: {0}', exc) else: # We don't want to store the image in the database. item.try_write(itempath) @@ -147,19 +144,18 @@ def embed_album(album, maxwidth=None, quiet=False): """ imagepath = album.artpath if not imagepath: - log.info(u'No album art present: {0} - {1}'. - format(album.albumartist, album.album)) + log.info(u'No album art present: {0} - {1}', + album.albumartist, album.album) return if not os.path.isfile(syspath(imagepath)): - log.error(u'Album art not found at {0}' - .format(displayable_path(imagepath))) + log.error(u'Album art not found at {0}', displayable_path(imagepath)) return if maxwidth: imagepath = resize_image(imagepath, maxwidth) log.log( logging.DEBUG if quiet else logging.INFO, - u'Embedding album art into {0.albumartist} - {0.album}.'.format(album), + u'Embedding album art into {0.albumartist} - {0.album}.', album ) for item in album.items(): @@ -171,8 +167,7 @@ def embed_album(album, maxwidth=None, quiet=False): def resize_image(imagepath, maxwidth): """Returns path to an image resized to maxwidth. """ - log.info(u'Resizing album art to {0} pixels wide' - .format(maxwidth)) + log.info(u'Resizing album art to {0} pixels wide', maxwidth) imagepath = ArtResizer.shared.resize(maxwidth, syspath(imagepath)) return imagepath @@ -197,15 +192,14 @@ def check_art_similarity(item, imagepath, compare_threshold): stdout, stderr = proc.communicate() if proc.returncode: if proc.returncode != 1: - log.warn(u'embedart: IM phashes compare failed for {0}, \ - {1}'.format(displayable_path(imagepath), - displayable_path(art))) + log.warn(u'embedart: IM phashes compare failed for {0}, {1}', + displayable_path(imagepath), displayable_path(art)) return phashDiff = float(stderr) else: phashDiff = float(stdout) - log.info(u'embedart: compare PHASH score is {0}'.format(phashDiff)) + log.info(u'embedart: compare PHASH score is {0}', phashDiff) if phashDiff > compare_threshold: return False @@ -226,9 +220,8 @@ def get_art(item): try: mf = mediafile.MediaFile(syspath(item.path)) except mediafile.UnreadableFileError as exc: - log.error(u'Could not extract art from {0}: {1}'.format( - displayable_path(item.path), exc - )) + log.error(u'Could not extract art from {0}: {1}', + displayable_path(item.path), exc) return return mf.art @@ -244,8 +237,8 @@ def extract(outpath, item): art = get_art(item) if not art: - log.error(u'No album art present in {0} - {1}.' - .format(item.artist, item.title)) + log.error(u'No album art present in {0} - {1}.', + item.artist, item.title) return # Add an extension to the filename. @@ -255,8 +248,8 @@ def extract(outpath, item): return outpath += '.' + ext - log.info(u'Extracting album art from: {0.artist} - {0.title} ' - u'to: {1}'.format(item, displayable_path(outpath))) + log.info(u'Extracting album art from: {0.artist} - {0.title} to: {1}', + item, displayable_path(outpath)) with open(syspath(outpath), 'wb') as f: f.write(art) return outpath @@ -267,14 +260,13 @@ def extract(outpath, item): def clear(lib, query): log.info(u'Clearing album art from items:') for item in lib.items(query): - log.info(u'{0} - {1}'.format(item.artist, item.title)) + log.info(u'{0} - {1}', item.artist, item.title) try: mf = mediafile.MediaFile(syspath(item.path), config['id3v23'].get(bool)) except mediafile.UnreadableFileError as exc: - log.error(u'Could not clear art from {0}: {1}'.format( - displayable_path(item.path), exc - )) + log.error(u'Could not clear art from {0}: {1}', + displayable_path(item.path), exc) continue del mf.art mf.save() diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index b2a4620b1..e8c11b2f9 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -50,7 +50,7 @@ def _fetch_image(url): actually be an image. If so, returns a path to the downloaded image. Otherwise, returns None. """ - log.debug(u'fetchart: downloading art: {0}'.format(url)) + log.debug(u'fetchart: downloading art: {0}', url) try: with closing(requests_session.get(url, stream=True)) as resp: if 'Content-Type' not in resp.headers \ @@ -63,9 +63,8 @@ def _fetch_image(url): as fh: for chunk in resp.iter_content(): fh.write(chunk) - log.debug(u'fetchart: downloaded art to: {0}'.format( - util.displayable_path(fh.name) - )) + log.debug(u'fetchart: downloaded art to: {0}', + util.displayable_path(fh.name)) return fh.name except (IOError, requests.RequestException): log.debug(u'fetchart: error fetching art') @@ -117,7 +116,7 @@ def aao_art(album): # Get the page from albumart.org. try: resp = requests_session.get(AAO_URL, params={'asin': album.asin}) - log.debug(u'fetchart: scraped art URL: {0}'.format(resp.url)) + log.debug(u'fetchart: scraped art URL: {0}', resp.url) except requests.RequestException: log.debug(u'fetchart: error scraping art page') return @@ -172,7 +171,7 @@ def itunes_art(album): try: itunes_album = itunes.search_album(search_string)[0] except Exception as exc: - log.debug('fetchart: iTunes search failed: {0}'.format(exc)) + log.debug('fetchart: iTunes search failed: {0}', exc) return if itunes_album.get_artwork()['100']: @@ -216,16 +215,14 @@ def art_in_path(path, cover_names, cautious): cover_pat = r"(\b|_)({0})(\b|_)".format('|'.join(cover_names)) for fn in images: if re.search(cover_pat, os.path.splitext(fn)[0], re.I): - log.debug(u'fetchart: using well-named art file {0}'.format( - util.displayable_path(fn) - )) + log.debug(u'fetchart: using well-named art file {0}', + util.displayable_path(fn)) return os.path.join(path, fn) # Fall back to any image in the folder. if images and not cautious: - log.debug(u'fetchart: using fallback art file {0}'.format( - util.displayable_path(images[0]) - )) + log.debug(u'fetchart: using fallback art file {0}', + util.displayable_path(images[0])) return os.path.join(path, images[0]) @@ -315,8 +312,7 @@ def batch_fetch_art(lib, albums, force, maxwidth=None): else: message = ui.colorize('red', 'no art found') - log.info(u'{0} - {1}: {2}'.format(album.albumartist, album.album, - message)) + log.info(u'{0} - {1}: {2}', album.albumartist, album.album, message) class FetchArtPlugin(plugins.BeetsPlugin): diff --git a/beetsplug/ftintitle.py b/beetsplug/ftintitle.py index f28a1661c..8bd0974fe 100644 --- a/beetsplug/ftintitle.py +++ b/beetsplug/ftintitle.py @@ -52,8 +52,7 @@ def update_metadata(item, feat_part, drop_feat, loglevel=logging.DEBUG): remove it from the artist field. """ # In all cases, update the artist fields. - log.log(loglevel, u'artist: {0} -> {1}'.format( - item.artist, item.albumartist)) + log.log(loglevel, 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. @@ -63,7 +62,7 @@ def update_metadata(item, feat_part, drop_feat, loglevel=logging.DEBUG): # 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.log(loglevel, u'title: {0} -> {1}'.format(item.title, new_title)) + log.log(loglevel, u'title: {0} -> {1}', item.title, new_title) item.title = new_title diff --git a/beetsplug/ihate.py b/beetsplug/ihate.py index b55554d8a..c8c930833 100644 --- a/beetsplug/ihate.py +++ b/beetsplug/ihate.py @@ -72,12 +72,11 @@ class IHatePlugin(BeetsPlugin): self._log.debug(u'[ihate] processing your hate') if self.do_i_hate_this(task, skip_queries): task.choice_flag = action.SKIP - self._log.info(u'[ihate] skipped: {0}' - .format(summary(task))) + self._log.info(u'[ihate] skipped: {0}', summary(task)) return if self.do_i_hate_this(task, warn_queries): - self._log.info(u'[ihate] you maybe hate this: {0}' - .format(summary(task))) + self._log.info(u'[ihate] you maybe hate this: {0}', + summary(task)) else: self._log.debug(u'[ihate] nothing to do') else: diff --git a/beetsplug/importadded.py b/beetsplug/importadded.py index bf2de1300..728112690 100644 --- a/beetsplug/importadded.py +++ b/beetsplug/importadded.py @@ -75,8 +75,8 @@ def write_item_mtime(item, mtime): item's file. """ if mtime is None: - log.warn(u"No mtime to be preserved for item '{0}'" - .format(util.displayable_path(item.path))) + log.warn(u"No mtime to be preserved for item '{0}'", + util.displayable_path(item.path)) return # The file's mtime on disk must be in sync with the item's mtime @@ -97,17 +97,17 @@ def record_import_mtime(item, source, destination): """ mtime = os.stat(util.syspath(source)).st_mtime item_mtime[destination] = mtime - log.debug(u"Recorded mtime {0} for item '{1}' imported from '{2}'".format( - mtime, util.displayable_path(destination), - util.displayable_path(source))) + log.debug(u"Recorded mtime {0} for item '{1}' imported from '{2}'", + mtime, util.displayable_path(destination), + util.displayable_path(source)) @ImportAddedPlugin.listen('album_imported') def update_album_times(lib, album): if reimported_album(album): log.debug(u"Album '{0}' is reimported, skipping import of added dates" - u" for the album and its items." - .format(util.displayable_path(album.path))) + u" for the album and its items.", + util.displayable_path(album.path)) return album_mtimes = [] @@ -120,7 +120,7 @@ def update_album_times(lib, album): item.store() album.added = min(album_mtimes) log.debug(u"Import of album '{0}', selected album.added={1} from item" - u" file mtimes.".format(album.album, album.added)) + u" file mtimes.", album.album, album.added) album.store() @@ -128,13 +128,13 @@ def update_album_times(lib, album): def update_item_times(lib, item): if reimported_item(item): log.debug(u"Item '{0}' is reimported, skipping import of added " - u"date.".format(util.displayable_path(item.path))) + u"date.", util.displayable_path(item.path)) return mtime = item_mtime.pop(item.path, None) if mtime: item.added = mtime if config['importadded']['preserve_mtimes'].get(bool): write_item_mtime(item, mtime) - log.debug(u"Import of item '{0}', selected item.added={1}" - .format(util.displayable_path(item.path), item.added)) + log.debug(u"Import of item '{0}', selected item.added={1}", + util.displayable_path(item.path), item.added) item.store() diff --git a/beetsplug/importfeeds.py b/beetsplug/importfeeds.py index 0f1cd11c8..dd1bf87df 100644 --- a/beetsplug/importfeeds.py +++ b/beetsplug/importfeeds.py @@ -132,7 +132,7 @@ def _record_items(lib, basename, items): if 'echo' in formats: log.info("Location of imported music:") for path in paths: - log.info(" " + path) + log.info(" {0}", path) @ImportFeedsPlugin.listen('library_opened') diff --git a/beetsplug/info.py b/beetsplug/info.py index 180f35747..f593bef12 100644 --- a/beetsplug/info.py +++ b/beetsplug/info.py @@ -52,7 +52,7 @@ def run(lib, opts, args): try: data = data_emitter() except mediafile.UnreadableFileError as ex: - log.error(u'cannot read file: {0}'.format(ex.message)) + log.error(u'cannot read file: {0}', ex.message) continue if opts.summarize: diff --git a/beetsplug/inline.py b/beetsplug/inline.py index 1ce4eb788..0f2e9f1d4 100644 --- a/beetsplug/inline.py +++ b/beetsplug/inline.py @@ -64,9 +64,8 @@ def compile_inline(python_code, album): try: func = _compile_func(python_code) except SyntaxError: - log.error(u'syntax error in inline field definition:\n{0}'.format( - traceback.format_exc() - )) + log.error(u'syntax error in inline field definition:\n{0}', + traceback.format_exc()) return else: is_expr = False @@ -113,14 +112,14 @@ class InlinePlugin(BeetsPlugin): # Item fields. for key, view in itertools.chain(config['item_fields'].items(), config['pathfields'].items()): - log.debug(u'inline: adding item field {0}'.format(key)) + log.debug(u'inline: adding item field {0}', key) func = 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(): - log.debug(u'inline: adding album field {0}'.format(key)) + log.debug(u'inline: adding album field {0}', key) func = compile_inline(view.get(unicode), True) if func is not None: self.album_template_fields[key] = func diff --git a/beetsplug/keyfinder.py b/beetsplug/keyfinder.py index 2abae381d..499fa5201 100644 --- a/beetsplug/keyfinder.py +++ b/beetsplug/keyfinder.py @@ -62,11 +62,11 @@ class KeyFinderPlugin(BeetsPlugin): try: key = util.command_output([bin, '-f', item.path]) except (subprocess.CalledProcessError, OSError) as exc: - log.error(u'KeyFinder execution failed: {0}'.format(exc)) + log.error(u'KeyFinder execution failed: {0}', exc) continue item['initial_key'] = key - log.debug(u'added computed initial key {0} for {1}' - .format(key, util.displayable_path(item.path))) + log.debug(u'added computed initial key {0} for {1}', + key, util.displayable_path(item.path)) item.try_write() item.store() diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index 109aaae3c..c1c1c39ab 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -71,7 +71,7 @@ def _tags_for(obj, min_weight=None): else: res = obj.get_top_tags() except PYLAST_EXCEPTIONS as exc: - log.debug(u'last.fm error: {0}'.format(exc)) + log.debug(u'last.fm error: {0}', exc) return [] # Filter by weight (optionally). @@ -371,9 +371,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}'.format( - album.albumartist, album.album, src, album.genre - )) + log.info(u'genre for album {0} - {1} ({2}): {3}', + album.albumartist, album.album, src, album.genre) album.store() for item in album.items(): @@ -382,9 +381,8 @@ class LastGenrePlugin(plugins.BeetsPlugin): if 'track' in self.sources: item.genre, src = self._get_genre(item) item.store() - log.info(u'genre for track {0} - {1} ({2}): {3}' - .format(item.artist, item.title, src, - item.genre)) + log.info(u'genre for track {0} - {1} ({2}): {3}', + item.artist, item.title, src, item.genre) if write: item.try_write() @@ -397,20 +395,20 @@ 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}'.format( - src, album.genre)) + 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}'.format( - src, item.genre)) + 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}'.format( - src, item.genre)) + log.debug(u'added last.fm item genre ({0}): {1}', + src, item.genre) item.store() diff --git a/beetsplug/lastimport.py b/beetsplug/lastimport.py index 240aa90ca..7a723a36d 100644 --- a/beetsplug/lastimport.py +++ b/beetsplug/lastimport.py @@ -56,7 +56,7 @@ def import_lastfm(lib): if not user: raise ui.UserError('You must specify a user name for lastimport') - log.info('Fetching last.fm library for @{0}'.format(user)) + log.info('Fetching last.fm library for @{0}', user) page_total = 1 page_current = 0 @@ -65,10 +65,10 @@ def import_lastfm(lib): retry_limit = config['lastimport']['retry_limit'].get(int) # Iterate through a yet to be known page total count while page_current < page_total: - log.info('lastimport: Querying page #{0}{1}...'.format( - page_current + 1, - '/' + str(page_total) if page_total > 1 else '' - )) + log.info('lastimport: Querying page #{0}{1}...', + page_current + 1, + '/{}'.format(page_total) if page_total > 1 else '' + ) for retry in range(0, retry_limit): page = fetch_tracks(user, page_current + 1, per_page) @@ -84,27 +84,22 @@ def import_lastfm(lib): unknown_total += unknown break else: - log.error('lastimport: ERROR: unable to read page #{0}'.format( - page_current + 1 - )) + log.error('lastimport: ERROR: unable to read page #{0}', + page_current + 1) if retry < retry_limit: log.info( - 'lastimport: Retrying page #{0}... ({1}/{2} retry)' - .format(page_current + 1, retry + 1, retry_limit) + 'lastimport: Retrying page #{0}... ({1}/{2} retry)', + page_current + 1, retry + 1, retry_limit ) else: - log.error( - 'lastimport: FAIL: unable to fetch page #{0}, ' - 'tried {1} times'.format(page_current, retry + 1) - ) + log.error('lastimport: FAIL: unable to fetch page #{0}, ', + 'tried {1} times', page_current, retry + 1) page_current += 1 log.info('lastimport: ... done!') - log.info('lastimport: finished processing {0} song pages'.format( - page_total - )) - log.info('lastimport: {0} unknown play-counts'.format(unknown_total)) - log.info('lastimport: {0} play-counts imported'.format(found_total)) + log.info('lastimport: finished processing {0} song pages', page_total) + log.info('lastimport: {0} unknown play-counts', unknown_total) + log.info('lastimport: {0} play-counts imported', found_total) def fetch_tracks(user, page, limit): @@ -122,10 +117,8 @@ def process_tracks(lib, tracks): total = len(tracks) total_found = 0 total_fails = 0 - log.info( - 'lastimport: Received {0} tracks in this page, processing...' - .format(total) - ) + log.info('lastimport: Received {0} tracks in this page, processing...', + total) for num in xrange(0, total): song = '' @@ -136,8 +129,7 @@ def process_tracks(lib, tracks): if 'album' in tracks[num]: album = tracks[num]['album'].get('name', '').strip() - log.debug(u'lastimport: query: {0} - {1} ({2})' - .format(artist, title, album)) + log.debug(u'lastimport: query: {0} - {1} ({2})', artist, title, album) # First try to query by musicbrainz's trackid if trackid: @@ -148,7 +140,7 @@ def process_tracks(lib, tracks): # Otherwise try artist/title/album if not song: log.debug(u'lastimport: no match for mb_trackid {0}, trying by ' - u'artist/title/album'.format(trackid)) + u'artist/title/album', trackid) query = dbcore.AndQuery([ dbcore.query.SubstringQuery('artist', artist), dbcore.query.SubstringQuery('title', title), @@ -178,26 +170,20 @@ def process_tracks(lib, tracks): if song: count = int(song.get('play_count', 0)) new_count = int(tracks[num]['playcount']) - log.debug( - u'lastimport: match: {0} - {1} ({2}) ' - u'updating: play_count {3} => {4}'.format( - song.artist, song.title, song.album, count, new_count - ) + log.debug(u'lastimport: match: {0} - {1} ({2}) ' + u'updating: play_count {3} => {4}', + song.artist, song.title, song.album, count, new_count ) song['play_count'] = new_count song.store() total_found += 1 else: total_fails += 1 - log.info( - u'lastimport: - No match: {0} - {1} ({2})' - .format(artist, title, album) - ) + log.info(u'lastimport: - No match: {0} - {1} ({2})', + artist, title, album) if total_fails > 0: - log.info( - 'lastimport: Acquired {0}/{1} play-counts ({2} unknown)' - .format(total_found, total, total_fails) - ) + log.info('lastimport: Acquired {0}/{1} play-counts ({2} unknown)', + total_found, total, total_fails) return total_found, total_fails diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index a2ebe7c36..17bf4c4fc 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -63,12 +63,12 @@ def fetch_url(url): try: r = requests.get(url, verify=False) except requests.RequestException as exc: - log.debug(u'lyrics request failed: {0}'.format(exc)) + log.debug(u'lyrics request failed: {0}', exc) return if r.status_code == requests.codes.ok: return r.text else: - log.debug(u'failed to fetch: {0} ({1})'.format(url, r.status_code)) + log.debug(u'failed to fetch: {0} ({1})', url, r.status_code) def unescape(text): @@ -272,7 +272,7 @@ def slugify(text): text = unicodedata.normalize('NFKD', text).encode('ascii', 'ignore') text = unicode(re.sub('[-\s]+', ' ', text)) except UnicodeDecodeError: - log.exception(u"Failing to normalize '{0}'".format(text)) + log.exception(u"Failing to normalize '{0}'", text) return text @@ -323,7 +323,7 @@ def is_lyrics(text, artist=None): badTriggersOcc = [] nbLines = text.count('\n') if nbLines <= 1: - log.debug(u"Ignoring too short lyrics '{0}'".format(text)) + log.debug(u"Ignoring too short lyrics '{0}'", text) return False elif nbLines < 5: badTriggersOcc.append('too_short') @@ -341,7 +341,7 @@ def is_lyrics(text, artist=None): text, re.I)) if badTriggersOcc: - log.debug(u'Bad triggers detected: {0}'.format(badTriggersOcc)) + log.debug(u'Bad triggers detected: {0}', badTriggersOcc) return len(badTriggersOcc) < 2 @@ -409,7 +409,7 @@ def fetch_google(artist, title): data = json.load(data) if 'error' in data: reason = data['error']['errors'][0]['reason'] - log.debug(u'google lyrics backend error: {0}'.format(reason)) + log.debug(u'google lyrics backend error: {0}', reason) return if 'items' in data.keys(): @@ -424,7 +424,7 @@ def fetch_google(artist, title): continue if is_lyrics(lyrics, artist): - log.debug(u'got lyrics from {0}'.format(item['displayLink'])) + log.debug(u'got lyrics from {0}', item['displayLink']) return lyrics @@ -502,8 +502,8 @@ class LyricsPlugin(plugins.BeetsPlugin): """ # Skip if the item already has lyrics. if not force and item.lyrics: - log.log(loglevel, u'lyrics already present: {0} - {1}' - .format(item.artist, item.title)) + log.log(loglevel, u'lyrics already present: {0} - {1}', + item.artist, item.title) return lyrics = None @@ -515,11 +515,11 @@ class LyricsPlugin(plugins.BeetsPlugin): lyrics = u"\n\n---\n\n".join([l for l in lyrics if l]) if lyrics: - log.log(loglevel, u'fetched lyrics: {0} - {1}' - .format(item.artist, item.title)) + log.log(loglevel, u'fetched lyrics: {0} - {1}', + item.artist, item.title) else: - log.log(loglevel, u'lyrics not found: {0} - {1}' - .format(item.artist, item.title)) + log.log(loglevel, u'lyrics not found: {0} - {1}', + item.artist, item.title) fallback = self.config['fallback'].get() if fallback: lyrics = fallback @@ -539,6 +539,5 @@ class LyricsPlugin(plugins.BeetsPlugin): for backend in self.backends: lyrics = backend(artist, title) if lyrics: - log.debug(u'got lyrics from backend: {0}' - .format(backend.__name__)) + log.debug(u'got lyrics from backend: {0}', backend.__name__) return _scrape_strip_cruft(lyrics, True) diff --git a/beetsplug/mbcollection.py b/beetsplug/mbcollection.py index 610408232..3683c7584 100644 --- a/beetsplug/mbcollection.py +++ b/beetsplug/mbcollection.py @@ -79,7 +79,7 @@ def update_album_list(album_list): if re.match(UUID_REGEX, aid): album_ids.append(aid) else: - log.info(u'skipping invalid MBID: {0}'.format(aid)) + log.info(u'skipping invalid MBID: {0}', aid) # Submit to MusicBrainz. print('Updating MusicBrainz collection {0}...'.format(collection_id)) diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py index 989caeb99..c96d0cf71 100644 --- a/beetsplug/mbsync.py +++ b/beetsplug/mbsync.py @@ -31,14 +31,13 @@ def mbsync_singletons(lib, query, move, pretend, write): """ for item in lib.items(query + ['singleton:true']): if not item.mb_trackid: - log.info(u'Skipping singleton {0}: has no mb_trackid' - .format(item.title)) + log.info(u'Skipping singleton {0}: has no mb_trackid', item.title) continue # Get the MusicBrainz recording info. track_info = hooks.track_for_mbid(item.mb_trackid) if not track_info: - log.info(u'Recording ID not found: {0}'.format(item.mb_trackid)) + log.info(u'Recording ID not found: {0}', item.mb_trackid) continue # Apply. @@ -54,7 +53,7 @@ def mbsync_albums(lib, query, move, pretend, write): # Process matching albums. for a in lib.albums(query): if not a.mb_albumid: - log.info(u'Skipping album {0}: has no mb_albumid'.format(a.id)) + log.info(u'Skipping album {0}: has no mb_albumid', a.id) continue items = list(a.items()) @@ -62,7 +61,7 @@ def mbsync_albums(lib, query, move, pretend, write): # Get the MusicBrainz album information. album_info = hooks.album_for_mbid(a.mb_albumid) if not album_info: - log.info(u'Release ID not found: {0}'.format(a.mb_albumid)) + log.info(u'Release ID not found: {0}', a.mb_albumid) continue # Map recording MBIDs to their information. Recordings can appear @@ -109,7 +108,7 @@ def mbsync_albums(lib, query, move, pretend, write): # Move album art (and any inconsistent items). if move and lib.directory in util.ancestry(items[0].path): - log.debug(u'moving album {0}'.format(a.id)) + log.debug(u'moving album {0}', a.id) a.move() diff --git a/beetsplug/missing.py b/beetsplug/missing.py index 74f4d4b6c..439b1d384 100644 --- a/beetsplug/missing.py +++ b/beetsplug/missing.py @@ -43,10 +43,8 @@ def _missing(album): 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'{0}: track {1} in album {2}' - .format(PLUGIN, - track_info.track_id, - album_info.album_id)) + log.debug(u'{0}: track {1} in album {2}', + PLUGIN, track_info.track_id, album_info.album_id) yield item diff --git a/beetsplug/mpdstats.py b/beetsplug/mpdstats.py index c198445dc..2fddb6a8c 100644 --- a/beetsplug/mpdstats.py +++ b/beetsplug/mpdstats.py @@ -71,7 +71,7 @@ class MPDClientWrapper(object): if host[0] in ['/', '~']: host = os.path.expanduser(host) - log.info(u'mpdstats: connecting to {0}:{1}'.format(host, port)) + log.info(u'mpdstats: connecting to {0}:{1}', host, port) try: self.client.connect(host, port) except socket.error as e: @@ -99,7 +99,7 @@ class MPDClientWrapper(object): try: return getattr(self.client, command)() except (select.error, mpd.ConnectionError) as err: - log.error(u'mpdstats: {0}'.format(err)) + log.error(u'mpdstats: {0}', err) if retries <= 0: # if we exited without breaking, we couldn't reconnect in time :( @@ -171,9 +171,7 @@ class MPDStats(object): if item: return item else: - log.info(u'mpdstats: item not found: {0}'.format( - displayable_path(path) - )) + log.info(u'mpdstats: item not found: {0}', displayable_path(path)) @staticmethod def update_item(item, attribute, value=None, increment=None): @@ -192,11 +190,11 @@ class MPDStats(object): item[attribute] = value item.store() - log.debug(u'mpdstats: updated: {0} = {1} [{2}]'.format( - attribute, - item[attribute], - displayable_path(item.path), - )) + log.debug(u'mpdstats: updated: {0} = {1} [{2}]', + attribute, + item[attribute], + displayable_path(item.path), + ) def update_rating(self, item, skipped): """Update the rating for a beets item. @@ -232,17 +230,13 @@ class MPDStats(object): """Updates the play count of a song. """ self.update_item(song['beets_item'], 'play_count', increment=1) - log.info(u'mpdstats: played {0}'.format( - displayable_path(song['path']) - )) + log.info(u'mpdstats: 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'mpdstats: skipped {0}'.format( - displayable_path(song['path']) - )) + log.info(u'mpdstats: skipped {0}', displayable_path(song['path'])) def on_stop(self, status): log.info(u'mpdstats: stop') @@ -264,9 +258,7 @@ class MPDStats(object): return if is_url(path): - log.info(u'mpdstats: playing stream {0}'.format( - displayable_path(path) - )) + log.info(u'mpdstats: playing stream {0}', displayable_path(path)) return played, duration = map(int, status['time'].split(':', 1)) @@ -275,9 +267,7 @@ class MPDStats(object): if self.now_playing and self.now_playing['path'] != path: self.handle_song_change(self.now_playing) - log.info(u'mpdstats: playing {0}'.format( - displayable_path(path) - )) + log.info(u'mpdstats: playing {0}', displayable_path(path)) self.now_playing = { 'started': time.time(), @@ -302,8 +292,7 @@ class MPDStats(object): if handler: handler(status) else: - log.debug(u'mpdstats: unhandled status "{0}"'. - format(status)) + log.debug(u'mpdstats: unhandled status "{0}"', status) events = self.mpd.events() diff --git a/beetsplug/play.py b/beetsplug/play.py index 770b84284..609627d59 100644 --- a/beetsplug/play.py +++ b/beetsplug/play.py @@ -101,10 +101,10 @@ def play_music(lib, opts, args): # Invoke the command and log the output. output = util.command_output(command) if output: - log.debug(u'Output of {0}: {1}'.format( - util.displayable_path(command[0]), - output.decode('utf8', 'ignore'), - )) + log.debug(u'Output of {0}: {1}', + util.displayable_path(command[0]), + output.decode('utf8', 'ignore'), + ) else: log.debug(u'play: no output') diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 40b3a3a85..d7f8366f5 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -180,9 +180,9 @@ class CommandBackend(Backend): cmd = cmd + ['-d', str(self.gain_offset)] cmd = cmd + [syspath(i.path) for i in items] - log.debug(u'replaygain: analyzing {0} files'.format(len(items))) - log.debug(u"replaygain: executing {0}" - .format(" ".join(map(displayable_path, cmd)))) + log.debug(u'replaygain: analyzing {0} files', len(items)) + log.debug(u"replaygain: executing {0}", + " ".join(map(displayable_path, cmd))) output = call(cmd) log.debug(u'replaygain: analysis finished') results = self.parse_tool_output(output, @@ -199,7 +199,7 @@ class CommandBackend(Backend): for line in text.split('\n')[1:num_lines + 1]: parts = line.split('\t') if len(parts) != 6 or parts[0] == 'File': - log.debug(u'replaygain: bad tool output: {0}'.format(text)) + log.debug(u'replaygain: bad tool output: {0}', text) raise ReplayGainError('mp3gain failed') d = { 'file': parts[0], @@ -548,14 +548,8 @@ class AudioToolsBackend(Backend): # be obtained from an audiofile instance. rg_track_gain, rg_track_peak = rg.title_gain(audiofile.to_pcm()) - log.debug( - u'ReplayGain for track {0} - {1}: {2:.2f}, {3:.2f}'.format( - item.artist, - item.title, - rg_track_gain, - rg_track_peak - ) - ) + log.debug(u'ReplayGain for track {0} - {1}: {2:.2f}, {3:.2f}', + item.artist, item.title, rg_track_gain, rg_track_peak) return Gain(gain=rg_track_gain, peak=rg_track_peak) def compute_album_gain(self, album): @@ -563,12 +557,7 @@ class AudioToolsBackend(Backend): :rtype: :class:`AlbumGain` """ - log.debug( - u'Analysing album {0} - {1}'.format( - album.albumartist, - album.album - ) - ) + log.debug(u'Analysing album {0} - {1}', album.albumartist, album.album) # The first item is taken and opened to get the sample rate to # initialize the replaygain object. The object is used for all the @@ -584,26 +573,14 @@ class AudioToolsBackend(Backend): track_gains.append( Gain(gain=rg_track_gain, peak=rg_track_peak) ) - log.debug( - u'ReplayGain for track {0} - {1}: {2:.2f}, {3:.2f}'.format( - item.artist, - item.title, - rg_track_gain, - rg_track_peak - ) - ) + log.debug(u'ReplayGain for track {0} - {1}: {2:.2f}, {3:.2f}', + item.artist, item.title, rg_track_gain, rg_track_peak) # After getting the values for all tracks, it's possible to get the # album values. rg_album_gain, rg_album_peak = rg.album_gain() - log.debug( - u'ReplayGain for Album {0} - {1}: {2:.2f}, {3:.2f}'.format( - album.albumartist, - album.album, - rg_album_gain, - rg_album_peak - ) - ) + log.debug(u'ReplayGain for Album {0} - {1}: {2:.2f}, {3:.2f}', + album.albumartist, album.album, rg_album_gain, rg_album_peak) return AlbumGain( Gain(gain=rg_album_gain, peak=rg_album_peak), @@ -674,19 +651,16 @@ class ReplayGainPlugin(BeetsPlugin): item.rg_track_peak = track_gain.peak item.store() - log.debug(u'replaygain: applied track gain {0}, peak {1}'.format( - item.rg_track_gain, - item.rg_track_peak - )) + log.debug(u'replaygain: applied track gain {0}, peak {1}', + item.rg_track_gain, item.rg_track_peak) def store_album_gain(self, album, album_gain): album.rg_album_gain = album_gain.gain album.rg_album_peak = album_gain.peak album.store() - log.debug(u'replaygain: applied album gain {0}, peak {1}'.format( - album.rg_album_gain, - album.rg_album_peak)) + log.debug(u'replaygain: applied album gain {0}, peak {1}', + album.rg_album_gain, album.rg_album_peak) def handle_album(self, album, write): """Compute album and track replay gain store it in all of the @@ -697,12 +671,11 @@ class ReplayGainPlugin(BeetsPlugin): items, nothing is done. """ if not self.album_requires_gain(album): - log.info(u'Skipping album {0} - {1}'.format(album.albumartist, - album.album)) + log.info(u'Skipping album {0} - {1}', + album.albumartist, album.album) return - log.info(u'analyzing {0} - {1}'.format(album.albumartist, - album.album)) + log.info(u'analyzing {0} - {1}', album.albumartist, album.album) try: album_gain = self.backend_instance.compute_album_gain(album) @@ -721,7 +694,7 @@ class ReplayGainPlugin(BeetsPlugin): if write: item.try_write() except ReplayGainError as e: - log.info(u"ReplayGain error: {0}".format(e)) + log.info(u"ReplayGain error: {0}", e) except FatalReplayGainError as e: raise ui.UserError( u"Fatal replay gain error: {0}".format(e) @@ -735,12 +708,10 @@ class ReplayGainPlugin(BeetsPlugin): in the item, nothing is done. """ if not self.track_requires_gain(item): - log.info(u'Skipping track {0} - {1}' - .format(item.artist, item.title)) + log.info(u'Skipping track {0} - {1}', item.artist, item.title) return - log.info(u'analyzing {0} - {1}' - .format(item.artist, item.title)) + log.info(u'analyzing {0} - {1}', item.artist, item.title) try: track_gains = self.backend_instance.compute_track_gain([item]) @@ -755,7 +726,7 @@ class ReplayGainPlugin(BeetsPlugin): if write: item.try_write() except ReplayGainError as e: - log.info(u"ReplayGain error: {0}".format(e)) + log.info(u"ReplayGain error: {0}", e) except FatalReplayGainError as e: raise ui.UserError( u"Fatal replay gain error: {0}".format(e) diff --git a/beetsplug/rewrite.py b/beetsplug/rewrite.py index 55b705492..cbb9e2a74 100644 --- a/beetsplug/rewrite.py +++ b/beetsplug/rewrite.py @@ -59,7 +59,7 @@ class RewritePlugin(BeetsPlugin): if fieldname not in library.Item._fields: raise ui.UserError("invalid field name (%s) in rewriter" % fieldname) - log.debug(u'adding template field {0}'.format(key)) + log.debug(u'adding template field {0}', key) pattern = re.compile(pattern.lower()) rules[fieldname].append((pattern, value)) if fieldname == 'artist': diff --git a/beetsplug/scrub.py b/beetsplug/scrub.py index c53c27590..0b071a162 100644 --- a/beetsplug/scrub.py +++ b/beetsplug/scrub.py @@ -64,8 +64,7 @@ class ScrubPlugin(BeetsPlugin): # Walk through matching files and remove tags. for item in lib.items(ui.decargs(args)): - log.info(u'scrubbing: {0}'.format( - util.displayable_path(item.path))) + log.info(u'scrubbing: {0}', util.displayable_path(item.path)) # Get album art if we need to restore it. if opts.write: @@ -132,14 +131,13 @@ def _scrub(path): del f[tag] f.save() except IOError as exc: - log.error(u'could not scrub {0}: {1}'.format( - util.displayable_path(path), exc, - )) + log.error(u'could not scrub {0}: {1}', + util.displayable_path(path), exc) # Automatically embed art into imported albums. @ScrubPlugin.listen('write') def write_item(path): if not scrubbing and config['scrub']['auto']: - log.debug(u'auto-scrubbing {0}'.format(util.displayable_path(path))) + log.debug(u'auto-scrubbing {0}', util.displayable_path(path)) _scrub(path) diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index 7d424c828..33b29fd8d 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -63,8 +63,7 @@ class SpotifyPlugin(BeetsPlugin): self.config['show_failures'].set(True) if self.config['mode'].get() not in ['list', 'open']: - log.warn(u'{0} is not a valid mode' - .format(self.config['mode'].get())) + log.warn(u'{0} is not a valid mode', self.config['mode'].get()) return False self.opts = opts @@ -81,7 +80,7 @@ class SpotifyPlugin(BeetsPlugin): log.debug(u'Your beets query returned no items, skipping spotify') return - log.info(u'Processing {0} tracks...'.format(len(items))) + log.info(u'Processing {0} tracks...', len(items)) for item in items: @@ -113,8 +112,7 @@ class SpotifyPlugin(BeetsPlugin): try: r.raise_for_status() except HTTPError as e: - log.debug(u'URL returned a {0} error' - .format(e.response.status_code)) + log.debug(u'URL returned a {0} error', e.response.status_code) failures.append(search_url) continue @@ -130,33 +128,29 @@ class SpotifyPlugin(BeetsPlugin): # Simplest, take the first result chosen_result = None if len(r_data) == 1 or self.config['tiebreak'].get() == "first": - log.debug(u'Spotify track(s) found, count: {0}' - .format(len(r_data))) + log.debug(u'Spotify track(s) found, count: {0}', len(r_data)) chosen_result = r_data[0] elif len(r_data) > 1: # Use the popularity filter - log.debug(u'Most popular track chosen, count: {0}' - .format(len(r_data))) + log.debug(u'Most popular track chosen, count: {0}', len(r_data)) chosen_result = max(r_data, key=lambda x: x['popularity']) if chosen_result: results.append(chosen_result) else: - log.debug(u'No spotify track found: {0}'.format(search_url)) + log.debug(u'No spotify track found: {0}', search_url) failures.append(search_url) failure_count = len(failures) if failure_count > 0: if self.config['show_failures'].get(): - log.info(u'{0} track(s) did not match a Spotify ID:' - .format(failure_count)) + log.info(u'{0} track(s) did not match a Spotify ID:', failure_count) for track in failures: - log.info(u'track: {0}'.format(track)) + log.info(u'track: {0}', track) log.info(u'') else: log.warn(u'{0} track(s) did not match a Spotify ID;\n' - u'use --show-failures to display' - .format(failure_count)) + u'use --show-failures to display', failure_count) return results diff --git a/beetsplug/the.py b/beetsplug/the.py index 5bc50415a..fb441c572 100644 --- a/beetsplug/the.py +++ b/beetsplug/the.py @@ -56,11 +56,11 @@ class ThePlugin(BeetsPlugin): try: re.compile(p) except re.error: - self._log.error(u'[the] invalid pattern: {0}'.format(p)) + self._log.error(u'[the] invalid pattern: {0}', p) else: if not (p.startswith('^') or p.endswith('$')): self._log.warn(u'[the] warning: \"{0}\" will not ' - 'match string start/end'.format(p)) + 'match string start/end', p) if self.config['a']: self.patterns = [PATTERN_A] + self.patterns if self.config['the']: @@ -99,7 +99,7 @@ class ThePlugin(BeetsPlugin): r = self.unthe(text, p) if r != text: break - self._log.debug(u'[the] \"{0}\" -> \"{1}\"'.format(text, r)) + self._log.debug(u'[the] \"{0}\" -> \"{1}\"', text, r) return r else: return u'' diff --git a/beetsplug/zero.py b/beetsplug/zero.py index ed41511c8..ae164bef0 100644 --- a/beetsplug/zero.py +++ b/beetsplug/zero.py @@ -49,10 +49,10 @@ class ZeroPlugin(BeetsPlugin): for field in self.config['fields'].as_str_seq(): if field in ('id', 'path', 'album_id'): log.warn(u'[zero] field \'{0}\' ignored, zeroing ' - u'it would be dangerous'.format(field)) + u'it would be dangerous', field) continue if field not in MediaFile.fields(): - log.error(u'[zero] invalid field: {0}'.format(field)) + log.error(u'[zero] invalid field: {0}', field) continue try: @@ -97,5 +97,5 @@ class ZeroPlugin(BeetsPlugin): match = patterns is True if match: - log.debug(u'[zero] {0}: {1} -> None'.format(field, value)) + log.debug(u'[zero] {0}: {1} -> None', field, value) tags[field] = None From 6bdb02c7218c0b41ee13e17edd9c41a1557d6ad9 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Sun, 4 Jan 2015 12:25:24 +0100 Subject: [PATCH 04/86] Fix logging formatting string in duplicates plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit {)} → {0} --- beetsplug/duplicates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/duplicates.py b/beetsplug/duplicates.py index 5229ee5ca..4b15bf939 100644 --- a/beetsplug/duplicates.py +++ b/beetsplug/duplicates.py @@ -62,7 +62,7 @@ def _checksum(item, prog): checksum = command_output(args) setattr(item, key, checksum) item.store() - log.debug(u'{)}: computed checksum for {1} using {2}', + log.debug(u'{0}: computed checksum for {1} using {2}', PLUGIN, item.title, key) except subprocess.CalledProcessError as e: log.debug(u'{0}: failed to checksum {1}: {2}', From 7df8bef8b7201395886a23eaf925b46090763655 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Sun, 4 Jan 2015 16:19:54 +0100 Subject: [PATCH 05/86] =?UTF-8?q?Update=20logging=20imports:=20logging=20?= =?UTF-8?q?=E2=86=92=20beets.logging?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- beets/autotag/__init__.py | 2 +- beets/autotag/hooks.py | 2 +- beets/autotag/match.py | 2 +- beets/autotag/mb.py | 2 +- beets/importer.py | 2 +- beets/library.py | 2 +- beets/mediafile.py | 2 +- beets/plugins.py | 2 +- beets/ui/__init__.py | 2 +- beets/ui/commands.py | 2 +- beets/util/artresizer.py | 2 +- beetsplug/beatport.py | 2 +- beetsplug/bpd/__init__.py | 2 +- beetsplug/bpm.py | 2 +- beetsplug/bucket.py | 2 +- beetsplug/chroma.py | 2 +- beetsplug/convert.py | 2 +- beetsplug/discogs.py | 2 +- beetsplug/duplicates.py | 2 +- beetsplug/echonest.py | 2 +- beetsplug/embedart.py | 2 +- beetsplug/fetchart.py | 2 +- beetsplug/freedesktop.py | 2 +- beetsplug/ftintitle.py | 2 +- beetsplug/ihate.py | 2 +- beetsplug/importadded.py | 2 +- beetsplug/importfeeds.py | 2 +- beetsplug/info.py | 2 +- beetsplug/inline.py | 2 +- beetsplug/keyfinder.py | 2 +- beetsplug/lastgenre/__init__.py | 2 +- beetsplug/lastimport.py | 2 +- beetsplug/lyrics.py | 2 +- beetsplug/mbcollection.py | 2 +- beetsplug/mbsync.py | 2 +- beetsplug/missing.py | 2 +- beetsplug/mpdstats.py | 2 +- beetsplug/play.py | 2 +- beetsplug/replaygain.py | 2 +- beetsplug/rewrite.py | 2 +- beetsplug/scrub.py | 2 +- beetsplug/spotify.py | 2 +- beetsplug/the.py | 2 +- beetsplug/zero.py | 2 +- test/_common.py | 2 +- test/helper.py | 2 +- 46 files changed, 46 insertions(+), 46 deletions(-) diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py index 5ac2380db..cf7048008 100644 --- a/beets/autotag/__init__.py +++ b/beets/autotag/__init__.py @@ -14,7 +14,7 @@ """Facilities for automatically determining files' correct metadata. """ -import logging +from beets import logging from beets import config diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index beb3bd91b..43433a2cd 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -13,7 +13,7 @@ # included in all copies or substantial portions of the Software. """Glue between metadata sources and the matching logic.""" -import logging +from beets import logging from collections import namedtuple import re diff --git a/beets/autotag/match.py b/beets/autotag/match.py index 00556359f..39a788c74 100644 --- a/beets/autotag/match.py +++ b/beets/autotag/match.py @@ -18,7 +18,7 @@ releases and tracks. from __future__ import division import datetime -import logging +from beets import logging import re from munkres import Munkres diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index 72662bd5c..e0f6088f6 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -14,7 +14,7 @@ """Searches for albums in the MusicBrainz database. """ -import logging +from beets import logging import musicbrainzngs import re import traceback diff --git a/beets/importer.py b/beets/importer.py index 4aa39ff92..f3b8975c5 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -19,7 +19,7 @@ from __future__ import print_function import os import re -import logging +from beets import logging import pickle import itertools from collections import defaultdict diff --git a/beets/library.py b/beets/library.py index a4213d8b3..d205e009b 100644 --- a/beets/library.py +++ b/beets/library.py @@ -16,7 +16,7 @@ """ import os import sys -import logging +from beets import logging import shlex import unicodedata import time diff --git a/beets/mediafile.py b/beets/mediafile.py index 9106bbd71..1bee05e5e 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -48,7 +48,7 @@ import math import struct import imghdr import os -import logging +from beets import logging import traceback import enum diff --git a/beets/plugins.py b/beets/plugins.py index c3058b617..917baf8a1 100755 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -14,7 +14,7 @@ """Support for beets plugins.""" -import logging +from beets import logging import traceback import inspect import re diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index 4617e84fc..09dec6a78 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -23,7 +23,7 @@ import optparse import textwrap import sys from difflib import SequenceMatcher -import logging +from beets import logging import sqlite3 import errno import re diff --git a/beets/ui/commands.py b/beets/ui/commands.py index e55d41504..14b0a6afd 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -17,7 +17,7 @@ interface. """ from __future__ import print_function -import logging +from beets import logging import os import time import codecs diff --git a/beets/util/artresizer.py b/beets/util/artresizer.py index 0145124c1..3bac1539f 100644 --- a/beets/util/artresizer.py +++ b/beets/util/artresizer.py @@ -20,7 +20,7 @@ import subprocess import os import re from tempfile import NamedTemporaryFile -import logging +from beets import logging from beets import util # Resizing methods diff --git a/beetsplug/beatport.py b/beetsplug/beatport.py index 8afefbefb..c431841cf 100644 --- a/beetsplug/beatport.py +++ b/beetsplug/beatport.py @@ -14,7 +14,7 @@ """Adds Beatport release and track search support to the autotagger """ -import logging +from beets import logging import re from datetime import datetime, timedelta diff --git a/beetsplug/bpd/__init__.py b/beetsplug/bpd/__init__.py index 7b550487c..e654fa6fd 100644 --- a/beetsplug/bpd/__init__.py +++ b/beetsplug/bpd/__init__.py @@ -21,7 +21,7 @@ from __future__ import print_function import re from string import Template import traceback -import logging +from beets import logging import random import time diff --git a/beetsplug/bpm.py b/beetsplug/bpm.py index 2189e2fae..d5d324bf7 100644 --- a/beetsplug/bpm.py +++ b/beetsplug/bpm.py @@ -15,7 +15,7 @@ """Determine BPM by pressing a key to the rhythm.""" import time -import logging +from beets import logging from beets import ui from beets.plugins import BeetsPlugin diff --git a/beetsplug/bucket.py b/beetsplug/bucket.py index 64dcdf6ee..86ec6be6c 100644 --- a/beetsplug/bucket.py +++ b/beetsplug/bucket.py @@ -16,7 +16,7 @@ """ from datetime import datetime -import logging +from beets import logging import re import string from itertools import tee, izip diff --git a/beetsplug/chroma.py b/beetsplug/chroma.py index 0ff86e220..a339b68a6 100644 --- a/beetsplug/chroma.py +++ b/beetsplug/chroma.py @@ -22,7 +22,7 @@ from beets import config from beets.util import confit from beets.autotag import hooks import acoustid -import logging +from beets import logging from collections import defaultdict API_KEY = '1vOwZtEn' diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 679ebcd72..cd07dfb22 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -14,7 +14,7 @@ """Converts tracks or albums to external directory """ -import logging +from beets import logging import os import threading import subprocess diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index 41c088225..17c1b05f9 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -22,7 +22,7 @@ from discogs_client import Release, Client from discogs_client.exceptions import DiscogsAPIError from requests.exceptions import ConnectionError import beets -import logging +from beets import logging import re import time import json diff --git a/beetsplug/duplicates.py b/beetsplug/duplicates.py index 4b15bf939..adbbbcf98 100644 --- a/beetsplug/duplicates.py +++ b/beetsplug/duplicates.py @@ -15,7 +15,7 @@ """List duplicate tracks or albums. """ import shlex -import logging +from beets import logging from beets.plugins import BeetsPlugin from beets.ui import decargs, print_obj, vararg_callback, Subcommand, UserError diff --git a/beetsplug/echonest.py b/beetsplug/echonest.py index 16ba6f3bd..7ebe2c0c3 100644 --- a/beetsplug/echonest.py +++ b/beetsplug/echonest.py @@ -15,7 +15,7 @@ """Fetch a variety of acoustic metrics from The Echo Nest. """ import time -import logging +from beets import logging import socket import os import tempfile diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index 3fdfd9055..72e8b17fc 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -14,7 +14,7 @@ """Allows beets to embed album art into file metadata.""" import os.path -import logging +from beets import logging import imghdr import subprocess import platform diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index e8c11b2f9..dfd4c9017 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -15,7 +15,7 @@ """Fetches album art. """ from contextlib import closing -import logging +from beets import logging import os import re from tempfile import NamedTemporaryFile diff --git a/beetsplug/freedesktop.py b/beetsplug/freedesktop.py index 0aea97c38..303394d34 100644 --- a/beetsplug/freedesktop.py +++ b/beetsplug/freedesktop.py @@ -20,7 +20,7 @@ from beets.ui import Subcommand from beets.ui import decargs import os -import logging +from beets import logging log = logging.getLogger('beets.freedesktop') diff --git a/beetsplug/ftintitle.py b/beetsplug/ftintitle.py index 8bd0974fe..c47ae37c1 100644 --- a/beetsplug/ftintitle.py +++ b/beetsplug/ftintitle.py @@ -18,7 +18,7 @@ from beets import plugins from beets import ui from beets.util import displayable_path from beets import config -import logging +from beets import logging import re log = logging.getLogger('beets') diff --git a/beetsplug/ihate.py b/beetsplug/ihate.py index c8c930833..ed7cbd954 100644 --- a/beetsplug/ihate.py +++ b/beetsplug/ihate.py @@ -14,7 +14,7 @@ """Warns you about things you hate (or even blocks import).""" -import logging +from beets import logging from beets.plugins import BeetsPlugin from beets.importer import action from beets.library import parse_query_string diff --git a/beetsplug/importadded.py b/beetsplug/importadded.py index 728112690..3bc991750 100644 --- a/beetsplug/importadded.py +++ b/beetsplug/importadded.py @@ -6,7 +6,7 @@ Reimported albums and items are skipped. from __future__ import unicode_literals, absolute_import, print_function -import logging +from beets import logging import os from beets import config diff --git a/beetsplug/importfeeds.py b/beetsplug/importfeeds.py index dd1bf87df..2697d9eca 100644 --- a/beetsplug/importfeeds.py +++ b/beetsplug/importfeeds.py @@ -19,7 +19,7 @@ one wants to manually add music to a player by its path. import datetime import os import re -import logging +from beets import logging from beets.plugins import BeetsPlugin from beets.util import normpath, syspath, bytestring_path diff --git a/beetsplug/info.py b/beetsplug/info.py index f593bef12..24930ab52 100644 --- a/beetsplug/info.py +++ b/beetsplug/info.py @@ -16,7 +16,7 @@ """ import os -import logging +from beets import logging from beets.plugins import BeetsPlugin from beets import ui diff --git a/beetsplug/inline.py b/beetsplug/inline.py index 0f2e9f1d4..ab886be20 100644 --- a/beetsplug/inline.py +++ b/beetsplug/inline.py @@ -14,7 +14,7 @@ """Allows inline path template customization code in the config file. """ -import logging +from beets import logging import traceback import itertools diff --git a/beetsplug/keyfinder.py b/beetsplug/keyfinder.py index 499fa5201..3f1398850 100644 --- a/beetsplug/keyfinder.py +++ b/beetsplug/keyfinder.py @@ -15,7 +15,7 @@ """Uses the `KeyFinder` program to add the `initial_key` field. """ -import logging +from beets import logging import subprocess from beets import ui diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index c1c1c39ab..497b0899b 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -20,7 +20,7 @@ and has been edited to remove some questionable entries. The scraper script used is available here: https://gist.github.com/1241307 """ -import logging +from beets import logging import pylast import os import yaml diff --git a/beetsplug/lastimport.py b/beetsplug/lastimport.py index 7a723a36d..138efbe1f 100644 --- a/beetsplug/lastimport.py +++ b/beetsplug/lastimport.py @@ -12,7 +12,7 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -import logging +from beets import logging import requests from beets import ui from beets import dbcore diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 17bf4c4fc..04e6d38a8 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -17,7 +17,7 @@ from __future__ import print_function import re -import logging +from beets import logging import requests import json import unicodedata diff --git a/beetsplug/mbcollection.py b/beetsplug/mbcollection.py index 3683c7584..9dd90f4cd 100644 --- a/beetsplug/mbcollection.py +++ b/beetsplug/mbcollection.py @@ -21,7 +21,7 @@ from beets import config import musicbrainzngs import re -import logging +from beets import logging SUBMISSION_CHUNK_SIZE = 200 UUID_REGEX = r'^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$' diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py index c96d0cf71..24823a229 100644 --- a/beetsplug/mbsync.py +++ b/beetsplug/mbsync.py @@ -14,7 +14,7 @@ """Update library's tags using MusicBrainz. """ -import logging +from beets import logging from beets.plugins import BeetsPlugin from beets import autotag, library, ui, util diff --git a/beetsplug/missing.py b/beetsplug/missing.py index 439b1d384..78142e167 100644 --- a/beetsplug/missing.py +++ b/beetsplug/missing.py @@ -14,7 +14,7 @@ """List missing tracks. """ -import logging +from beets import logging from beets.autotag import hooks from beets.library import Item diff --git a/beetsplug/mpdstats.py b/beetsplug/mpdstats.py index 2fddb6a8c..d8596c1a3 100644 --- a/beetsplug/mpdstats.py +++ b/beetsplug/mpdstats.py @@ -13,7 +13,7 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -import logging +from beets import logging import mpd import socket import select diff --git a/beetsplug/play.py b/beetsplug/play.py index 609627d59..1de61261c 100644 --- a/beetsplug/play.py +++ b/beetsplug/play.py @@ -21,7 +21,7 @@ from beets import ui from beets import util from os.path import relpath import platform -import logging +from beets import logging import shlex from tempfile import NamedTemporaryFile diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index d7f8366f5..7d8cc3520 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -12,7 +12,7 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -import logging +from beets import logging import subprocess import os import collections diff --git a/beetsplug/rewrite.py b/beetsplug/rewrite.py index cbb9e2a74..6a58ff490 100644 --- a/beetsplug/rewrite.py +++ b/beetsplug/rewrite.py @@ -16,7 +16,7 @@ formats. """ import re -import logging +from beets import logging from collections import defaultdict from beets.plugins import BeetsPlugin diff --git a/beetsplug/scrub.py b/beetsplug/scrub.py index 0b071a162..a6346a2f2 100644 --- a/beetsplug/scrub.py +++ b/beetsplug/scrub.py @@ -15,7 +15,7 @@ """Cleans extraneous metadata from files' tags via a command or automatically whenever tags are written. """ -import logging +from beets import logging from beets.plugins import BeetsPlugin from beets import ui diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index 33b29fd8d..273dd432b 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -2,7 +2,7 @@ from __future__ import print_function import re import webbrowser import requests -import logging +from beets import logging from beets.plugins import BeetsPlugin from beets.ui import decargs from beets import ui diff --git a/beetsplug/the.py b/beetsplug/the.py index fb441c572..d146b3e69 100644 --- a/beetsplug/the.py +++ b/beetsplug/the.py @@ -15,7 +15,7 @@ """Moves patterns in path formats (suitable for moving articles).""" import re -import logging +from beets import logging from beets.plugins import BeetsPlugin __author__ = 'baobab@heresiarch.info' diff --git a/beetsplug/zero.py b/beetsplug/zero.py index ae164bef0..a8c62d42c 100644 --- a/beetsplug/zero.py +++ b/beetsplug/zero.py @@ -15,7 +15,7 @@ """ Clears tag fields in media files.""" import re -import logging +from beets import logging from beets.plugins import BeetsPlugin from beets.mediafile import MediaFile from beets.importer import action diff --git a/test/_common.py b/test/_common.py index 64f2f7247..7341bc1a1 100644 --- a/test/_common.py +++ b/test/_common.py @@ -16,7 +16,7 @@ import time import sys import os -import logging +from beets import logging import tempfile import shutil from contextlib import contextmanager diff --git a/test/helper.py b/test/helper.py index 459e643a0..fdbe9f9bf 100644 --- a/test/helper.py +++ b/test/helper.py @@ -35,7 +35,7 @@ import os import os.path import shutil import subprocess -import logging +from beets import logging from tempfile import mkdtemp, mkstemp from contextlib import contextmanager from StringIO import StringIO From e2d3ba1c2318ac80675b4bdf489104a975f49902 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Sun, 4 Jan 2015 16:40:41 +0100 Subject: [PATCH 06/86] Fix 4 tests by str()-ing a logged exception --- beets/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/library.py b/beets/library.py index d205e009b..e97d335de 100644 --- a/beets/library.py +++ b/beets/library.py @@ -509,7 +509,7 @@ class Item(LibModel): self.write(path) return True except FileOperationError as exc: - log.error(exc) + log.error(str(exc)) return False def try_sync(self, write=None): From 408afa1b58dcd3a244534884604029beedfffe2e Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Sun, 4 Jan 2015 16:56:25 +0100 Subject: [PATCH 07/86] Add tests for beets.logging --- test/test_logging.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 test/test_logging.py diff --git a/test/test_logging.py b/test/test_logging.py new file mode 100644 index 000000000..61b0591c4 --- /dev/null +++ b/test/test_logging.py @@ -0,0 +1,38 @@ +"""Stupid tests that ensure logging works as expected""" +import logging as log +from StringIO import StringIO + +import beets.logging as blog +from _common import unittest, TestCase + + +class LoggingTest(TestCase): + def test_logging_management(self): + l1 = log.getLogger("foo123") + l2 = blog.getLogger("foo123") + self.assertEqual(l1, l2) + self.assertEqual(type(l1), log.Logger) + + l3 = blog.getLogger("bar123") + l4 = log.getLogger("bar123") + self.assertEqual(l3, l4) + self.assertEqual(type(l3), blog.StrFormatLogger) + + l5 = l3.getChild("shalala") + self.assertEqual(type(l5), blog.StrFormatLogger) + + def test_str_format_logging(self): + l = blog.getLogger("baz123") + stream = StringIO() + handler = log.StreamHandler(stream) + + l.addHandler(handler) + l.propagate = False + + l.warning("foo {} {bar}", "oof", bar="baz") + handler.flush() + self.assertTrue(stream.getvalue(), "foo oof baz") + + +if __name__ == '__main__': + unittest.main() From 52243269fc080af7094debc63121357035af9c82 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Sun, 4 Jan 2015 17:14:34 +0100 Subject: [PATCH 08/86] Make test/test_logging work on python 2.6 --- test/test_logging.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/test_logging.py b/test/test_logging.py index 61b0591c4..f63a63b63 100644 --- a/test/test_logging.py +++ b/test/test_logging.py @@ -34,5 +34,9 @@ class LoggingTest(TestCase): self.assertTrue(stream.getvalue(), "foo oof baz") +def suite(): + return unittest.TestLoader().loadTestsFromName(__name__) + + if __name__ == '__main__': - unittest.main() + unittest.main(defaultTest='suite') From d409da8753aab884aabf8f43314fd3ef61e65efa Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Sun, 4 Jan 2015 17:22:45 +0100 Subject: [PATCH 09/86] Fix docstring beets.logging.StrFormatLogger._log --- beets/logging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/logging.py b/beets/logging.py index 2783c5ff5..b2e8bb5aa 100644 --- a/beets/logging.py +++ b/beets/logging.py @@ -21,7 +21,7 @@ class StrFormatLogger(Logger): return self.msg.format(*self.args, **self.kwargs) def _log(self, level, msg, args, exc_info=None, extra=None, **kwargs): - """Log 'msg.format(*args, **kwargs)""" + """Log msg.format(*args, **kwargs)""" msg = self._LogMessage(msg, args, kwargs) return super(StrFormatLogger, self)._log(level, msg, (), exc_info, extra) From f5c56667295373eed8f4a7692aada45dcadebfaf Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Sun, 4 Jan 2015 17:29:31 +0100 Subject: [PATCH 10/86] Attain pep8-cleanliness No more E12 or E501 --- beets/logging.py | 4 ++-- beets/ui/commands.py | 2 +- beets/util/artresizer.py | 2 +- beetsplug/echonest.py | 2 +- beetsplug/embedart.py | 5 +++-- beetsplug/lastimport.py | 6 ++---- beetsplug/mpdstats.py | 3 +-- beetsplug/play.py | 3 +-- beetsplug/spotify.py | 6 ++++-- 9 files changed, 16 insertions(+), 17 deletions(-) diff --git a/beets/logging.py b/beets/logging.py index b2e8bb5aa..2168c6a95 100644 --- a/beets/logging.py +++ b/beets/logging.py @@ -22,8 +22,8 @@ class StrFormatLogger(Logger): def _log(self, level, msg, args, exc_info=None, extra=None, **kwargs): """Log msg.format(*args, **kwargs)""" - msg = self._LogMessage(msg, args, kwargs) - return super(StrFormatLogger, self)._log(level, msg, (), exc_info, extra) + m = self._LogMessage(msg, args, kwargs) + return super(StrFormatLogger, self)._log(level, m, (), exc_info, extra) my_manager = copy(Logger.manager) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 14b0a6afd..5883f37a3 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1023,7 +1023,7 @@ def update_items(lib, query, album, move, pretend): item.read() except library.ReadError as exc: log.error(u'error reading {0}: {1}', - displayable_path(item.path), exc) + displayable_path(item.path), exc) continue # Special-case album artist when it matches track artist. (Hacky diff --git a/beets/util/artresizer.py b/beets/util/artresizer.py index 3bac1539f..101f5711e 100644 --- a/beets/util/artresizer.py +++ b/beets/util/artresizer.py @@ -69,7 +69,7 @@ def pil_resize(maxwidth, path_in, path_out=None): return path_out except IOError: log.error(u"PIL cannot create thumbnail for '{0}'", - util.displayable_path(path_in)) + util.displayable_path(path_in)) return path_in diff --git a/beetsplug/echonest.py b/beetsplug/echonest.py index 7ebe2c0c3..809a3fae2 100644 --- a/beetsplug/echonest.py +++ b/beetsplug/echonest.py @@ -412,7 +412,7 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): item.artist, item.title, song.get('duration'), - ) + ) return song def apply_metadata(self, item, values, write=False): diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index 72e8b17fc..a7df5a2a0 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -192,8 +192,9 @@ def check_art_similarity(item, imagepath, compare_threshold): stdout, stderr = proc.communicate() if proc.returncode: if proc.returncode != 1: - log.warn(u'embedart: IM phashes compare failed for {0}, {1}', - displayable_path(imagepath), displayable_path(art)) + log.warn(u'embedart: IM hashes compare failed for ' + u'{0}, {1}', displayable_path(imagepath), + displayable_path(art)) return phashDiff = float(stderr) else: diff --git a/beetsplug/lastimport.py b/beetsplug/lastimport.py index 138efbe1f..9f9011dc3 100644 --- a/beetsplug/lastimport.py +++ b/beetsplug/lastimport.py @@ -67,8 +67,7 @@ def import_lastfm(lib): while page_current < page_total: log.info('lastimport: Querying page #{0}{1}...', page_current + 1, - '/{}'.format(page_total) if page_total > 1 else '' - ) + '/{}'.format(page_total) if page_total > 1 else '') for retry in range(0, retry_limit): page = fetch_tracks(user, page_current + 1, per_page) @@ -172,8 +171,7 @@ def process_tracks(lib, tracks): new_count = int(tracks[num]['playcount']) log.debug(u'lastimport: match: {0} - {1} ({2}) ' u'updating: play_count {3} => {4}', - song.artist, song.title, song.album, count, new_count - ) + song.artist, song.title, song.album, count, new_count) song['play_count'] = new_count song.store() total_found += 1 diff --git a/beetsplug/mpdstats.py b/beetsplug/mpdstats.py index d8596c1a3..6c0be72c2 100644 --- a/beetsplug/mpdstats.py +++ b/beetsplug/mpdstats.py @@ -193,8 +193,7 @@ class MPDStats(object): log.debug(u'mpdstats: updated: {0} = {1} [{2}]', attribute, item[attribute], - displayable_path(item.path), - ) + displayable_path(item.path)) def update_rating(self, item, skipped): """Update the rating for a beets item. diff --git a/beetsplug/play.py b/beetsplug/play.py index 1de61261c..687760ff4 100644 --- a/beetsplug/play.py +++ b/beetsplug/play.py @@ -103,8 +103,7 @@ def play_music(lib, opts, args): if output: log.debug(u'Output of {0}: {1}', util.displayable_path(command[0]), - output.decode('utf8', 'ignore'), - ) + output.decode('utf8', 'ignore')) else: log.debug(u'play: no output') diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index 273dd432b..89b4a2d62 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -132,7 +132,8 @@ class SpotifyPlugin(BeetsPlugin): chosen_result = r_data[0] elif len(r_data) > 1: # Use the popularity filter - log.debug(u'Most popular track chosen, count: {0}', len(r_data)) + log.debug(u'Most popular track chosen, count: {0}', + len(r_data)) chosen_result = max(r_data, key=lambda x: x['popularity']) if chosen_result: @@ -144,7 +145,8 @@ class SpotifyPlugin(BeetsPlugin): failure_count = len(failures) if failure_count > 0: if self.config['show_failures'].get(): - log.info(u'{0} track(s) did not match a Spotify ID:', failure_count) + log.info(u'{0} track(s) did not match a Spotify ID:', + failure_count) for track in failures: log.info(u'track: {0}', track) log.info(u'') From 4ba4f53053e121208401e2c8bf8704577f604a11 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Sun, 4 Jan 2015 17:50:05 +0100 Subject: [PATCH 11/86] Silence flake8 on logging.* import --- beets/logging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/logging.py b/beets/logging.py index 2168c6a95..9d9afdab3 100644 --- a/beets/logging.py +++ b/beets/logging.py @@ -6,7 +6,7 @@ getLogger(name) instantiates a logger that logger uses {}-style formatting. from __future__ import absolute_import from copy import copy -from logging import * +from logging import * # noqa # create a str.format-based logger From 1f3fc60badb2db35a28d4f105f8b7be6ee54fde8 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 4 Jan 2015 17:35:16 -0800 Subject: [PATCH 12/86] Add missing space (fix #1199) --- beets/ui/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 546fe87d9..c0fb699e9 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1517,7 +1517,7 @@ def config_edit(): try: os.execlp(*args) except OSError: - raise ui.UserError("Could not edit configuration. Please" + raise ui.UserError("Could not edit configuration. Please " "set the EDITOR environment variable.") From e13e7ed727fb5cf50e2b10d98e2a9b937e5153ab Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 4 Jan 2015 17:46:08 -0800 Subject: [PATCH 13/86] config -e: Parse arguments in $EDITOR (fix #1200) --- beets/ui/commands.py | 8 +++++++- docs/changelog.rst | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index c0fb699e9..4dfac11c8 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -23,6 +23,7 @@ import time import codecs import platform import re +import shlex import beets from beets import ui @@ -1503,7 +1504,12 @@ def config_edit(): if 'EDITOR' in os.environ: editor = os.environ['EDITOR'] - args = [editor, editor, path] + try: + editor = shlex.split(editor) + except ValueError: # Malformed shell tokens. + editor = [editor] + args = editor + [path] + args.insert(1, args[0]) elif platform.system() == 'Darwin': args = ['open', 'open', '-n', path] elif platform.system() == 'Windows': diff --git a/docs/changelog.rst b/docs/changelog.rst index 0446300cc..e9b6123ed 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -68,6 +68,8 @@ Fixed: twice in the artist string. Thanks to Marc Addeo. :bug:`1179` :bug:`1181` * :doc:`/plugins/lastgenre`: Match songs more robustly when they contain dashes. Thanks to :user:`djl`. :bug:`1156` +* The :ref:`config-cmd` command can now use ``$EDITOR`` variables with + arguments. .. _API changes: http://developer.echonest.com/forums/thread/3650 .. _Plex: https://plex.tv/ From 45db9304577063b336e11904be9c16d916249343 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Mon, 5 Jan 2015 07:59:53 +0100 Subject: [PATCH 14/86] embedart: restore an apparent typo (which wasn't) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hashes → phashes --- beetsplug/embedart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index a7df5a2a0..b88fcf366 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -192,7 +192,7 @@ def check_art_similarity(item, imagepath, compare_threshold): stdout, stderr = proc.communicate() if proc.returncode: if proc.returncode != 1: - log.warn(u'embedart: IM hashes compare failed for ' + log.warn(u'embedart: IM phashes compare failed for ' u'{0}, {1}', displayable_path(imagepath), displayable_path(art)) return From 06f0e1dee1d7d1076dd37e2e56d70c2e322eaf25 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Mon, 5 Jan 2015 09:15:18 +0100 Subject: [PATCH 15/86] Port beets.logging to python 2.6 Multiple hacks :-) --- beets/logging.py | 37 ++++++++++++++++++++++++++++++++++++- test/test_logging.py | 8 ++++---- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/beets/logging.py b/beets/logging.py index 9d9afdab3..a213f1e20 100644 --- a/beets/logging.py +++ b/beets/logging.py @@ -2,11 +2,15 @@ Provide everything the "logging" module does, the only difference is that when getLogger(name) instantiates a logger that logger uses {}-style formatting. + +It requires special hacks for python 2.6 due to logging.Logger being an old- +style class and having no loggerClass attribute. """ from __future__ import absolute_import from copy import copy from logging import * # noqa +import sys # create a str.format-based logger @@ -23,8 +27,21 @@ class StrFormatLogger(Logger): def _log(self, level, msg, args, exc_info=None, extra=None, **kwargs): """Log msg.format(*args, **kwargs)""" m = self._LogMessage(msg, args, kwargs) - return super(StrFormatLogger, self)._log(level, m, (), exc_info, extra) + return Logger._log(self, level, m, (), exc_info, extra) + # we cannot call super(StrFormatLogger, self) because it is not + # allowed on old-style classes (py2) which Logger is in python 2.6 + # moreover we cannot make StrFormatLogger a new-style class (by + # declaring 'class StrFormatLogger(Logger, object)' because the class- + # patching stmt 'logger.__class__ = StrFormatLogger' would not work: + # both prev & new __class__ values must be either old- or new- style, + # no mixing allowed. + if sys.version_info[:2] == (2, 6): + def getChild(self, suffix): + """Shameless copy from cpython's Lib/logging/__init__.py""" + if self.root is not self: + suffix = '.'.join((self.name, suffix)) + return self.manager.getLogger(suffix) my_manager = copy(Logger.manager) my_manager.loggerClass = StrFormatLogger @@ -35,3 +52,21 @@ def getLogger(name=None): return my_manager.getLogger(name) else: return root + + +if sys.version_info[:2] == (2, 6): + # no Manager.loggerClass so we dynamically change the logger class + # we must be careful to do that on new loggers only to avoid side-effects. + # Wrap Manager.getLogger + old_getLogger = my_manager.getLogger + + def new_getLogger(name): + change_its_type = not isinstance(my_manager.loggerDict.get(name), + Logger) + # it either does not exist or is a placeholder + logger = old_getLogger(name) + if change_its_type: + logger.__class__ = StrFormatLogger + return logger + + my_manager.getLogger = new_getLogger diff --git a/test/test_logging.py b/test/test_logging.py index f63a63b63..0d2eb7291 100644 --- a/test/test_logging.py +++ b/test/test_logging.py @@ -11,15 +11,15 @@ class LoggingTest(TestCase): l1 = log.getLogger("foo123") l2 = blog.getLogger("foo123") self.assertEqual(l1, l2) - self.assertEqual(type(l1), log.Logger) + self.assertEqual(l1.__class__, log.Logger) l3 = blog.getLogger("bar123") l4 = log.getLogger("bar123") self.assertEqual(l3, l4) - self.assertEqual(type(l3), blog.StrFormatLogger) + self.assertEqual(l3.__class__, blog.StrFormatLogger) l5 = l3.getChild("shalala") - self.assertEqual(type(l5), blog.StrFormatLogger) + self.assertEqual(l5.__class__, blog.StrFormatLogger) def test_str_format_logging(self): l = blog.getLogger("baz123") @@ -29,7 +29,7 @@ class LoggingTest(TestCase): l.addHandler(handler) l.propagate = False - l.warning("foo {} {bar}", "oof", bar="baz") + l.warning("foo {0} {bar}", "oof", bar="baz") handler.flush() self.assertTrue(stream.getvalue(), "foo oof baz") From fa696beef2145031e0fa83519e315259a1fbc1eb Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Mon, 5 Jan 2015 09:53:56 +0100 Subject: [PATCH 16/86] beets.logging: fix root logger access logging does not export 'root', so we must use Logger.root --- beets/logging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/logging.py b/beets/logging.py index a213f1e20..ec594698e 100644 --- a/beets/logging.py +++ b/beets/logging.py @@ -51,7 +51,7 @@ def getLogger(name=None): if name: return my_manager.getLogger(name) else: - return root + return Logger.root if sys.version_info[:2] == (2, 6): From a8b0454bfb79721d00cd83bda00fa5e7e83a5fe5 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Mon, 5 Jan 2015 09:55:05 +0100 Subject: [PATCH 17/86] Minor fix in beets.logging docstring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit formatting → logging --- beets/logging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/logging.py b/beets/logging.py index ec594698e..48c9d4625 100644 --- a/beets/logging.py +++ b/beets/logging.py @@ -1,4 +1,4 @@ -"""Allow {}-style formatting on python 2 and 3 +"""Allow {}-style logging on python 2 and 3 Provide everything the "logging" module does, the only difference is that when getLogger(name) instantiates a logger that logger uses {}-style formatting. From 30f158a95e1bd264592c786e8a6d5083e929061d Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Mon, 5 Jan 2015 10:05:21 +0100 Subject: [PATCH 18/86] Move "from beets import logging" statements Move the import next to other beets-related imports --- beets/autotag/__init__.py | 2 +- beets/autotag/hooks.py | 2 +- beets/autotag/match.py | 2 +- beets/autotag/mb.py | 2 +- beets/importer.py | 2 +- beets/library.py | 3 ++- beets/mediafile.py | 2 +- beets/plugins.py | 2 +- beets/ui/__init__.py | 2 +- beets/ui/commands.py | 2 +- beets/util/artresizer.py | 1 + beetsplug/beatport.py | 2 +- beetsplug/bpd/__init__.py | 2 +- beetsplug/bpm.py | 3 +-- beetsplug/bucket.py | 3 ++- beetsplug/chroma.py | 2 +- beetsplug/convert.py | 3 +-- beetsplug/discogs.py | 2 +- beetsplug/duplicates.py | 2 +- beetsplug/echonest.py | 3 +-- beetsplug/embedart.py | 2 +- beetsplug/fetchart.py | 2 +- beetsplug/freedesktop.py | 2 +- beetsplug/ftintitle.py | 3 ++- beetsplug/importadded.py | 2 +- beetsplug/importfeeds.py | 3 +-- beetsplug/info.py | 2 +- beetsplug/inline.py | 3 +-- beetsplug/keyfinder.py | 2 +- beetsplug/lastgenre/__init__.py | 2 +- beetsplug/lastimport.py | 2 +- beetsplug/lyrics.py | 2 +- beetsplug/mbcollection.py | 2 +- beetsplug/mbsync.py | 4 +--- beetsplug/missing.py | 1 - beetsplug/mpdstats.py | 2 +- beetsplug/play.py | 2 +- beetsplug/replaygain.py | 2 +- beetsplug/rewrite.py | 2 +- beetsplug/scrub.py | 2 +- beetsplug/spotify.py | 3 +-- test/_common.py | 3 +-- test/helper.py | 2 +- 43 files changed, 45 insertions(+), 51 deletions(-) diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py index cf7048008..3fa98758c 100644 --- a/beets/autotag/__init__.py +++ b/beets/autotag/__init__.py @@ -14,8 +14,8 @@ """Facilities for automatically determining files' correct metadata. """ -from beets import logging +from beets import logging from beets import config # Parts of external interface. diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 43433a2cd..5118212b4 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -13,10 +13,10 @@ # included in all copies or substantial portions of the Software. """Glue between metadata sources and the matching logic.""" -from beets import logging from collections import namedtuple import re +from beets import logging from beets import plugins from beets import config from beets.autotag import mb diff --git a/beets/autotag/match.py b/beets/autotag/match.py index 39a788c74..d51cd4fb1 100644 --- a/beets/autotag/match.py +++ b/beets/autotag/match.py @@ -18,10 +18,10 @@ releases and tracks. from __future__ import division import datetime -from beets import logging import re from munkres import Munkres +from beets import logging from beets import plugins from beets import config from beets.util import plurality diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index e0f6088f6..7c598a17f 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -14,12 +14,12 @@ """Searches for albums in the MusicBrainz database. """ -from beets import logging import musicbrainzngs import re import traceback from urlparse import urljoin +from beets import logging import beets.autotag.hooks import beets from beets import util diff --git a/beets/importer.py b/beets/importer.py index f3b8975c5..a0f100749 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -19,7 +19,6 @@ from __future__ import print_function import os import re -from beets import logging import pickle import itertools from collections import defaultdict @@ -28,6 +27,7 @@ from bisect import insort, bisect_left from contextlib import contextmanager import shutil +from beets import logging from beets import autotag from beets import library from beets import dbcore diff --git a/beets/library.py b/beets/library.py index e97d335de..180f029cb 100644 --- a/beets/library.py +++ b/beets/library.py @@ -16,12 +16,13 @@ """ import os import sys -from beets import logging import shlex import unicodedata import time import re from unidecode import unidecode + +from beets import logging from beets.mediafile import MediaFile, MutagenError, UnreadableFileError from beets import plugins from beets import util diff --git a/beets/mediafile.py b/beets/mediafile.py index 1bee05e5e..3e3d2aa23 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -48,10 +48,10 @@ import math import struct import imghdr import os -from beets import logging import traceback import enum +from beets import logging from beets.util import displayable_path diff --git a/beets/plugins.py b/beets/plugins.py index 917baf8a1..a975145db 100755 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -14,7 +14,6 @@ """Support for beets plugins.""" -from beets import logging import traceback import inspect import re @@ -22,6 +21,7 @@ from collections import defaultdict import beets +from beets import logging from beets import mediafile PLUGIN_NAMESPACE = 'beetsplug' diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index 09dec6a78..a69b98ac2 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -23,7 +23,6 @@ import optparse import textwrap import sys from difflib import SequenceMatcher -from beets import logging import sqlite3 import errno import re @@ -31,6 +30,7 @@ import struct import traceback import os.path +from beets import logging from beets import library from beets import plugins from beets import util diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 5883f37a3..8de9ab1bc 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -17,7 +17,6 @@ interface. """ from __future__ import print_function -from beets import logging import os import time import codecs @@ -37,6 +36,7 @@ from beets.util import syspath, normpath, ancestry, displayable_path from beets.util.functemplate import Template from beets import library from beets import config +from beets import logging from beets.util.confit import _package_path VARIOUS_ARTISTS = u'Various Artists' diff --git a/beets/util/artresizer.py b/beets/util/artresizer.py index 101f5711e..5b51392bd 100644 --- a/beets/util/artresizer.py +++ b/beets/util/artresizer.py @@ -20,6 +20,7 @@ import subprocess import os import re from tempfile import NamedTemporaryFile + from beets import logging from beets import util diff --git a/beetsplug/beatport.py b/beetsplug/beatport.py index c431841cf..39054aab1 100644 --- a/beetsplug/beatport.py +++ b/beetsplug/beatport.py @@ -14,12 +14,12 @@ """Adds Beatport release and track search support to the autotagger """ -from beets import logging import re from datetime import datetime, timedelta import requests +from beets import logging from beets.autotag.hooks import AlbumInfo, TrackInfo, Distance from beets.plugins import BeetsPlugin diff --git a/beetsplug/bpd/__init__.py b/beetsplug/bpd/__init__.py index e654fa6fd..b0b8ce6c2 100644 --- a/beetsplug/bpd/__init__.py +++ b/beetsplug/bpd/__init__.py @@ -21,13 +21,13 @@ from __future__ import print_function import re from string import Template import traceback -from beets import logging import random import time import beets from beets.plugins import BeetsPlugin import beets.ui +from beets import logging from beets import vfs from beets.util import bluelet from beets.library import Item diff --git a/beetsplug/bpm.py b/beetsplug/bpm.py index d5d324bf7..028af7eae 100644 --- a/beetsplug/bpm.py +++ b/beetsplug/bpm.py @@ -15,9 +15,8 @@ """Determine BPM by pressing a key to the rhythm.""" import time -from beets import logging -from beets import ui +from beets import ui, logging from beets.plugins import BeetsPlugin log = logging.getLogger('beets') diff --git a/beetsplug/bucket.py b/beetsplug/bucket.py index 86ec6be6c..68d520395 100644 --- a/beetsplug/bucket.py +++ b/beetsplug/bucket.py @@ -16,10 +16,11 @@ """ from datetime import datetime -from beets import logging import re import string from itertools import tee, izip + +from beets import logging from beets import plugins, ui log = logging.getLogger('beets') diff --git a/beetsplug/chroma.py b/beetsplug/chroma.py index a339b68a6..485bfba61 100644 --- a/beetsplug/chroma.py +++ b/beetsplug/chroma.py @@ -19,10 +19,10 @@ from beets import plugins from beets import ui from beets import util from beets import config +from beets import logging from beets.util import confit from beets.autotag import hooks import acoustid -from beets import logging from collections import defaultdict API_KEY = '1vOwZtEn' diff --git a/beetsplug/convert.py b/beetsplug/convert.py index cd07dfb22..c9b83c03f 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -14,7 +14,6 @@ """Converts tracks or albums to external directory """ -from beets import logging import os import threading import subprocess @@ -22,7 +21,7 @@ import tempfile import shlex from string import Template -from beets import ui, util, plugins, config +from beets import logging, ui, util, plugins, config from beets.plugins import BeetsPlugin from beetsplug.embedart import embed_item from beets.util.confit import ConfigTypeError diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index 17c1b05f9..e3a55fbdd 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -15,6 +15,7 @@ """Adds Discogs album search support to the autotagger. Requires the discogs-client library. """ +from beets import logging from beets.autotag.hooks import AlbumInfo, TrackInfo, Distance from beets.plugins import BeetsPlugin from beets.util import confit @@ -22,7 +23,6 @@ from discogs_client import Release, Client from discogs_client.exceptions import DiscogsAPIError from requests.exceptions import ConnectionError import beets -from beets import logging import re import time import json diff --git a/beetsplug/duplicates.py b/beetsplug/duplicates.py index adbbbcf98..1d7b6f9f1 100644 --- a/beetsplug/duplicates.py +++ b/beetsplug/duplicates.py @@ -15,8 +15,8 @@ """List duplicate tracks or albums. """ import shlex -from beets import logging +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 diff --git a/beetsplug/echonest.py b/beetsplug/echonest.py index 809a3fae2..e38814437 100644 --- a/beetsplug/echonest.py +++ b/beetsplug/echonest.py @@ -15,14 +15,13 @@ """Fetch a variety of acoustic metrics from The Echo Nest. """ import time -from beets import logging import socket import os import tempfile from string import Template import subprocess -from beets import util, config, plugins, ui +from beets import util, config, plugins, ui, logging from beets.dbcore import types import pyechonest import pyechonest.song diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index b88fcf366..7f04cd1fb 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -14,12 +14,12 @@ """Allows beets to embed album art into file metadata.""" import os.path -from beets import logging import imghdr import subprocess import platform from tempfile import NamedTemporaryFile +from beets import logging from beets.plugins import BeetsPlugin from beets import mediafile from beets import ui diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index dfd4c9017..3b0059817 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -15,13 +15,13 @@ """Fetches album art. """ from contextlib import closing -from beets import logging import os import re from tempfile import NamedTemporaryFile import requests +from beets import logging from beets import plugins from beets import importer from beets import ui diff --git a/beetsplug/freedesktop.py b/beetsplug/freedesktop.py index 303394d34..3f3307c82 100644 --- a/beetsplug/freedesktop.py +++ b/beetsplug/freedesktop.py @@ -15,12 +15,12 @@ """Creates freedesktop.org-compliant .directory files on an album level. """ +from beets import logging from beets.plugins import BeetsPlugin from beets.ui import Subcommand from beets.ui import decargs import os -from beets import logging log = logging.getLogger('beets.freedesktop') diff --git a/beetsplug/ftintitle.py b/beetsplug/ftintitle.py index c47ae37c1..f36cfb348 100644 --- a/beetsplug/ftintitle.py +++ b/beetsplug/ftintitle.py @@ -14,12 +14,13 @@ """Moves "featured" artists to the title from the artist field. """ +import re + from beets import plugins from beets import ui from beets.util import displayable_path from beets import config from beets import logging -import re log = logging.getLogger('beets') diff --git a/beetsplug/importadded.py b/beetsplug/importadded.py index 3bc991750..8cb590111 100644 --- a/beetsplug/importadded.py +++ b/beetsplug/importadded.py @@ -6,9 +6,9 @@ Reimported albums and items are skipped. from __future__ import unicode_literals, absolute_import, print_function -from beets import logging import os +from beets import logging from beets import config from beets import util from beets.plugins import BeetsPlugin diff --git a/beetsplug/importfeeds.py b/beetsplug/importfeeds.py index 2697d9eca..4ac1dda2b 100644 --- a/beetsplug/importfeeds.py +++ b/beetsplug/importfeeds.py @@ -19,11 +19,10 @@ one wants to manually add music to a player by its path. import datetime import os import re -from beets import logging from beets.plugins import BeetsPlugin from beets.util import normpath, syspath, bytestring_path -from beets import config +from beets import config, logging M3U_DEFAULT_NAME = 'imported.m3u' log = logging.getLogger('beets') diff --git a/beetsplug/info.py b/beetsplug/info.py index 24930ab52..30cccb1b2 100644 --- a/beetsplug/info.py +++ b/beetsplug/info.py @@ -16,8 +16,8 @@ """ import os -from beets import logging +from beets import logging from beets.plugins import BeetsPlugin from beets import ui from beets import mediafile diff --git a/beetsplug/inline.py b/beetsplug/inline.py index ab886be20..e7e7e0f41 100644 --- a/beetsplug/inline.py +++ b/beetsplug/inline.py @@ -14,12 +14,11 @@ """Allows inline path template customization code in the config file. """ -from beets import logging import traceback import itertools from beets.plugins import BeetsPlugin -from beets import config +from beets import config, logging log = logging.getLogger('beets') diff --git a/beetsplug/keyfinder.py b/beetsplug/keyfinder.py index 3f1398850..49830edef 100644 --- a/beetsplug/keyfinder.py +++ b/beetsplug/keyfinder.py @@ -15,9 +15,9 @@ """Uses the `KeyFinder` program to add the `initial_key` field. """ -from beets import logging import subprocess +from beets import logging from beets import ui from beets import util from beets.plugins import BeetsPlugin diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index 497b0899b..2b4acb590 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -20,11 +20,11 @@ and has been edited to remove some questionable entries. The scraper script used is available here: https://gist.github.com/1241307 """ -from beets import logging import pylast import os import yaml +from beets import logging from beets import plugins from beets import ui from beets.util import normpath, plurality diff --git a/beetsplug/lastimport.py b/beetsplug/lastimport.py index 9f9011dc3..087213031 100644 --- a/beetsplug/lastimport.py +++ b/beetsplug/lastimport.py @@ -12,12 +12,12 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from beets import logging import requests 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('beets') diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 04e6d38a8..a87715fe3 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -17,7 +17,6 @@ from __future__ import print_function import re -from beets import logging import requests import json import unicodedata @@ -26,6 +25,7 @@ import difflib import itertools from HTMLParser import HTMLParseError +from beets import logging from beets import plugins from beets import config, ui diff --git a/beetsplug/mbcollection.py b/beetsplug/mbcollection.py index 9dd90f4cd..764c4529f 100644 --- a/beetsplug/mbcollection.py +++ b/beetsplug/mbcollection.py @@ -16,12 +16,12 @@ 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 import re -from beets import logging SUBMISSION_CHUNK_SIZE = 200 UUID_REGEX = r'^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$' diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py index 24823a229..105122639 100644 --- a/beetsplug/mbsync.py +++ b/beetsplug/mbsync.py @@ -14,10 +14,8 @@ """Update library's tags using MusicBrainz. """ -from beets import logging - from beets.plugins import BeetsPlugin -from beets import autotag, library, ui, util +from beets import autotag, library, ui, util, logging from beets.autotag import hooks from beets import config from collections import defaultdict diff --git a/beetsplug/missing.py b/beetsplug/missing.py index 78142e167..2ebe3edf8 100644 --- a/beetsplug/missing.py +++ b/beetsplug/missing.py @@ -15,7 +15,6 @@ """List missing tracks. """ from beets import logging - from beets.autotag import hooks from beets.library import Item from beets.plugins import BeetsPlugin diff --git a/beetsplug/mpdstats.py b/beetsplug/mpdstats.py index 6c0be72c2..5a5d27b4e 100644 --- a/beetsplug/mpdstats.py +++ b/beetsplug/mpdstats.py @@ -13,13 +13,13 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from beets import logging import mpd import socket import select import time import os +from beets import logging from beets import ui from beets import config from beets import plugins diff --git a/beetsplug/play.py b/beetsplug/play.py index 687760ff4..7f9ff3ce9 100644 --- a/beetsplug/play.py +++ b/beetsplug/play.py @@ -16,12 +16,12 @@ """ 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 from os.path import relpath import platform -from beets import logging import shlex from tempfile import NamedTemporaryFile diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 7d8cc3520..5d50f61c5 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -12,7 +12,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from beets import logging import subprocess import os import collections @@ -20,6 +19,7 @@ import itertools import sys import warnings +from beets import logging from beets import ui from beets.plugins import BeetsPlugin from beets.util import syspath, command_output, displayable_path diff --git a/beetsplug/rewrite.py b/beetsplug/rewrite.py index 6a58ff490..8a59bdbfd 100644 --- a/beetsplug/rewrite.py +++ b/beetsplug/rewrite.py @@ -16,10 +16,10 @@ formats. """ import re -from beets import logging from collections import defaultdict from beets.plugins import BeetsPlugin +from beets import logging from beets import ui from beets import library diff --git a/beetsplug/scrub.py b/beetsplug/scrub.py index a6346a2f2..7c5b097cb 100644 --- a/beetsplug/scrub.py +++ b/beetsplug/scrub.py @@ -15,8 +15,8 @@ """Cleans extraneous metadata from files' tags via a command or automatically whenever tags are written. """ -from beets import logging +from beets import logging from beets.plugins import BeetsPlugin from beets import ui from beets import util diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index 89b4a2d62..704660894 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -2,10 +2,9 @@ from __future__ import print_function import re import webbrowser import requests -from beets import logging from beets.plugins import BeetsPlugin from beets.ui import decargs -from beets import ui +from beets import ui, logging from requests.exceptions import HTTPError log = logging.getLogger('beets') diff --git a/test/_common.py b/test/_common.py index 7341bc1a1..b222566b7 100644 --- a/test/_common.py +++ b/test/_common.py @@ -16,7 +16,6 @@ import time import sys import os -from beets import logging import tempfile import shutil from contextlib import contextmanager @@ -30,7 +29,7 @@ except ImportError: # Mangle the search path to include the beets sources. sys.path.insert(0, '..') import beets.library -from beets import importer +from beets import importer, logging from beets.ui import commands import beets diff --git a/test/helper.py b/test/helper.py index fdbe9f9bf..4dafb4c39 100644 --- a/test/helper.py +++ b/test/helper.py @@ -35,13 +35,13 @@ import os import os.path import shutil import subprocess -from beets import logging from tempfile import mkdtemp, mkstemp from contextlib import contextmanager from StringIO import StringIO from enum import Enum import beets +from beets import logging from beets import config import beets.plugins from beets.library import Library, Item, Album From 59f5fc7f7ff413587299ef648d3ead49fabc84f0 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 5 Jan 2015 10:16:10 -0800 Subject: [PATCH 19/86] Remove nose dependency --- test/test_bucket.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/test/test_bucket.py b/test/test_bucket.py index 060c360ad..c65116663 100644 --- a/test/test_bucket.py +++ b/test/test_bucket.py @@ -15,7 +15,6 @@ """Tests for the 'bucket' plugin.""" -from nose.tools import raises from _common import unittest from beetsplug import bucket from beets import config, ui @@ -129,26 +128,24 @@ class BucketPluginTest(unittest.TestCase, TestHelper): self.assertEqual(self.plugin._tmpl_bucket('…and Oceans'), 'A - D') self.assertEqual(self.plugin._tmpl_bucket('Eagles'), 'E - L') - @raises(ui.UserError) def test_bad_alpha_range_def(self): - """If bad alpha range definition, a UserError is raised""" - self._setup_config(bucket_alpha=['$%']) - self.assertEqual(self.plugin._tmpl_bucket('errol'), 'E') + """If bad alpha range definition, a UserError is raised.""" + with self.assertRaises(ui.UserError): + self._setup_config(bucket_alpha=['$%']) - @raises(ui.UserError) def test_bad_year_range_def_no4digits(self): """If bad year range definition, a UserError is raised. - Range origin must be expressed on 4 digits.""" - self._setup_config(bucket_year=['62-64']) - # from year must be expressed on 4 digits - self.assertEqual(self.plugin._tmpl_bucket('1963'), '62-64') + Range origin must be expressed on 4 digits. + """ + with self.assertRaises(ui.UserError): + self._setup_config(bucket_year=['62-64']) - @raises(ui.UserError) def test_bad_year_range_def_nodigits(self): """If bad year range definition, a UserError is raised. - At least the range origin must be declared.""" - self._setup_config(bucket_year=['nodigits']) - self.assertEqual(self.plugin._tmpl_bucket('1963'), '62-64') + At least the range origin must be declared. + """ + with self.assertRaises(ui.UserError): + self._setup_config(bucket_year=['nodigits']) def suite(): From 331d50d6ee7fefd0f474b0105ae1e86621546575 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 5 Jan 2015 12:19:45 -0800 Subject: [PATCH 20/86] Finish changelog for 1.3.10 release --- docs/changelog.rst | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index e9b6123ed..8f4470ddd 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,38 +1,48 @@ Changelog ========= -1.3.10 (in development) ------------------------ +1.3.10 (January 5, 2015) +------------------------ + +This version adds a healthy helping of new features and fixes a critical +MPEG-4--related bug. There are more lyrics sources, there new plugins for +managing permissions and integrating with `Plex`_, and the importer has a new +``--pretend`` flag that shows which music *would* be imported. One backwards-compatibility note: the :doc:`/plugins/lyrics` now requires the `requests`_ library. If you use this plugin, you will need to install the library by typing ``pip install requests`` or the equivalent for your OS. -New: +Also, as an advance warning, this will be one of the last releases to support +Python 2.6. If you have a system that cannot run Python 2.7, please consider +upgrading soon. + +The new features are: -* :doc:`/plugins/lyrics`: Add `Musixmatch`_ source and introduce a new - ``sources`` config option that lets you choose exactly where to look for - lyrics and in which order. -* :doc:`/plugins/lyrics`: Add brazilian and hispanic sources to Google custom - search engine. * A new :doc:`/plugins/permissions` makes it easy to fix permissions on music files as they are imported. Thanks to :user:`xsteadfastx`. :bug:`1098` * A new :doc:`/plugins/plexupdate` lets you notify a `Plex`_ server when the database changes. Thanks again to xsteadfastx. :bug:`1120` +* The :ref:`import-cmd` command now has a ``--pretend`` flag that lists the + files that will be imported. Thanks to :user:`mried`. :bug:`1162` +* :doc:`/plugins/lyrics`: Add `Musixmatch`_ source and introduce a new + ``sources`` config option that lets you choose exactly where to look for + lyrics and in which order. +* :doc:`/plugins/lyrics`: Add Brazilian and Spanish sources to Google custom + search engine. * Add a warning when importing a directory that contains no music. :bug:`1116` :bug:`1127` * :doc:`/plugins/zero`: Can now remove embedded images. :bug:`1129` :bug:`1100` * The :ref:`config-cmd` command can now be used to edit the configuration even when it has syntax errors. :bug:`1123` :bug:`1128` * :doc:`/plugins/lyrics`: Added a new ``force`` config option. :bug:`1150` -* The :ref:`import-cmd` command now has a ``--pretend`` flag that lists the - files that will be imported. Thanks to :user:`mried`. :bug:`1162` -Fixed: +As usual, there are loads of little fixes and improvements: -* :doc:`/plugins/lyrics`: Avoid fetching truncated lyrics from the Google - backed by merging text blocks separated by empty ``
`` before scraping. * Fix a new crash with the latest version of Mutagen (1.26). +* :doc:`/plugins/lyrics`: Avoid fetching truncated lyrics from the Google + backed by merging text blocks separated by empty ``
`` tags before + scraping. * We now print a better error message when the database file is corrupted. * :doc:`/plugins/discogs`: Only prompt for authentication when running the :ref:`import-cmd` command. :bug:`1123` From 1fc7b70763fcfb6ce39f65e453df5d6a2810407a Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 5 Jan 2015 12:21:18 -0800 Subject: [PATCH 21/86] Version bump: 1.3.11 --- beets/__init__.py | 2 +- docs/changelog.rst | 6 ++++++ docs/conf.py | 2 +- setup.py | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/beets/__init__.py b/beets/__init__.py index 609acd6fc..db0b38a2c 100644 --- a/beets/__init__.py +++ b/beets/__init__.py @@ -12,7 +12,7 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -__version__ = '1.3.10' +__version__ = '1.3.11' __author__ = 'Adrian Sampson ' import beets.library diff --git a/docs/changelog.rst b/docs/changelog.rst index 8f4470ddd..5936d1237 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,12 @@ Changelog ========= +1.3.11 (in development) +----------------------- + +Changelog goes here! + + 1.3.10 (January 5, 2015) ------------------------ diff --git a/docs/conf.py b/docs/conf.py index 2ce36bc66..82fc15da8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,7 +12,7 @@ project = u'beets' copyright = u'2012, Adrian Sampson' version = '1.3' -release = '1.3.10' +release = '1.3.11' pygments_style = 'sphinx' diff --git a/setup.py b/setup.py index e1b94eefe..53f5137e9 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ if 'sdist' in sys.argv: setup( name='beets', - version='1.3.10', + version='1.3.11', description='music tagger and library organizer', author='Adrian Sampson', author_email='adrian@radbox.org', From 4ef52e62657b9c0ba44ddc00e3ae5cc30095a41a Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 5 Jan 2015 17:39:13 -0800 Subject: [PATCH 22/86] Minor formatting fixes for logging overhaul #1198 --- beets/logging.py | 49 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/beets/logging.py b/beets/logging.py index 48c9d4625..27f0620ed 100644 --- a/beets/logging.py +++ b/beets/logging.py @@ -1,10 +1,23 @@ -"""Allow {}-style logging on python 2 and 3 +# This file is part of beets. +# Copyright 2015, Adrian Sampson. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. -Provide everything the "logging" module does, the only difference is that when -getLogger(name) instantiates a logger that logger uses {}-style formatting. +"""A drop-in replacement for the standard-library `logging` module that +allows {}-style log formatting on Python 2 and 3. -It requires special hacks for python 2.6 due to logging.Logger being an old- -style class and having no loggerClass attribute. +Provides everything the "logging" module does. The only difference is +that when getLogger(name) instantiates a logger that logger uses +{}-style formatting. """ from __future__ import absolute_import @@ -13,7 +26,12 @@ from logging import * # noqa import sys -# create a str.format-based logger +# We need special hacks for Python 2.6 due to logging.Logger being an +# old- style class and having no loggerClass attribute. +PY26 = sys.version_info[:2] == (2, 6) + + +# Create a `str.format`-based logger. class StrFormatLogger(Logger): class _LogMessage(object): def __init__(self, msg, args, kwargs): @@ -28,15 +46,15 @@ class StrFormatLogger(Logger): """Log msg.format(*args, **kwargs)""" m = self._LogMessage(msg, args, kwargs) return Logger._log(self, level, m, (), exc_info, extra) - # we cannot call super(StrFormatLogger, self) because it is not - # allowed on old-style classes (py2) which Logger is in python 2.6 - # moreover we cannot make StrFormatLogger a new-style class (by + # We cannot call super(StrFormatLogger, self) because it is not + # allowed on old-style classes (py2), which Logger is in python 2.6. + # Moreover, we cannot make StrFormatLogger a new-style class (by # declaring 'class StrFormatLogger(Logger, object)' because the class- # patching stmt 'logger.__class__ = StrFormatLogger' would not work: - # both prev & new __class__ values must be either old- or new- style, + # both prev & new __class__ values must be either old- or new- style; # no mixing allowed. - if sys.version_info[:2] == (2, 6): + if PY26: def getChild(self, suffix): """Shameless copy from cpython's Lib/logging/__init__.py""" if self.root is not self: @@ -54,10 +72,11 @@ def getLogger(name=None): return Logger.root -if sys.version_info[:2] == (2, 6): - # no Manager.loggerClass so we dynamically change the logger class - # we must be careful to do that on new loggers only to avoid side-effects. - # Wrap Manager.getLogger +# On Python 2.6, there is no Manager.loggerClass so we dynamically +# change the logger class. We must be careful to do that on new loggers +# only to avoid side-effects. +if PY26: + # Wrap Manager.getLogger. old_getLogger = my_manager.getLogger def new_getLogger(name): From fd2c57f736de1bb8f750c431ddf0f7a636a454f4 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Mon, 5 Jan 2015 17:43:22 +0100 Subject: [PATCH 23/86] Make beets.util.str2bool more idiomatic --- beets/util/__init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 38cecd703..047eea005 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -544,10 +544,7 @@ def truncate_path(path, length=MAX_FILENAME_LENGTH): def str2bool(value): """Returns a boolean reflecting a human-entered string.""" - if value.lower() in ('yes', '1', 'true', 't', 'y'): - return True - else: - return False + return value.lower() in ('yes', '1', 'true', 't', 'y') def as_string(value): From b8211a3c4c10028e7e5c83f96dff8f98ae892f38 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Mon, 5 Jan 2015 10:37:56 +0100 Subject: [PATCH 24/86] Every plugin uses its own logger logging.getLogger(__name__) everywhere! Several loggers prefixed every log message with [logername], which we delete here. --- beetsplug/beatport.py | 8 +++--- beetsplug/bpd/__init__.py | 2 +- beetsplug/bpm.py | 2 +- beetsplug/bucket.py | 2 +- beetsplug/chroma.py | 10 +++---- beetsplug/convert.py | 2 +- beetsplug/discogs.py | 11 ++++---- beetsplug/duplicates.py | 2 +- beetsplug/echonest.py | 49 ++++++++++++++++----------------- beetsplug/embedart.py | 18 ++++++------ beetsplug/fetchart.py | 28 +++++++++---------- beetsplug/freedesktop.py | 4 +-- beetsplug/ftintitle.py | 2 +- beetsplug/ihate.py | 12 ++++---- beetsplug/importadded.py | 2 +- beetsplug/importfeeds.py | 2 +- beetsplug/info.py | 2 +- beetsplug/inline.py | 6 ++-- beetsplug/keyfinder.py | 4 +-- beetsplug/lastgenre/__init__.py | 2 +- beetsplug/lastimport.py | 35 ++++++++++++----------- beetsplug/lyrics.py | 2 +- beetsplug/mbcollection.py | 2 +- beetsplug/mbsync.py | 2 +- beetsplug/missing.py | 7 ++--- beetsplug/mpdstats.py | 24 ++++++++-------- beetsplug/play.py | 4 +-- beetsplug/replaygain.py | 17 ++++++------ beetsplug/rewrite.py | 2 +- beetsplug/scrub.py | 2 +- beetsplug/spotify.py | 2 +- beetsplug/the.py | 12 ++++---- beetsplug/zero.py | 12 ++++---- 33 files changed, 144 insertions(+), 149 deletions(-) diff --git a/beetsplug/beatport.py b/beetsplug/beatport.py index 39054aab1..e769f82c5 100644 --- a/beetsplug/beatport.py +++ b/beetsplug/beatport.py @@ -23,7 +23,7 @@ from beets import logging from beets.autotag.hooks import AlbumInfo, TrackInfo, Distance from beets.plugins import BeetsPlugin -log = logging.getLogger('beets') +log = logging.getLogger(__name__) class BeatportAPIError(Exception): @@ -205,14 +205,14 @@ class BeatportPlugin(BeetsPlugin): try: return self._get_tracks(query) except BeatportAPIError as e: - log.debug(u'Beatport API Error: {0} (query: {1})', e, query) + log.debug(u'API Error: {0} (query: {1})', e, query) return [] def album_for_id(self, release_id): """Fetches a release by its Beatport ID and returns an AlbumInfo object or None if the release is not found. """ - log.debug(u'Searching Beatport for release {0}', release_id) + log.debug(u'Searching for release {0}', release_id) match = re.search(r'(^|beatport\.com/release/.+/)(\d+)$', release_id) if not match: return None @@ -224,7 +224,7 @@ class BeatportPlugin(BeetsPlugin): """Fetches a track by its Beatport ID and returns a TrackInfo object or None if the track is not found. """ - log.debug(u'Searching Beatport for track {0}', track_id) + log.debug(u'Searching for track {0}', track_id) match = re.search(r'(^|beatport\.com/track/.+/)(\d+)$', track_id) if not match: return None diff --git a/beetsplug/bpd/__init__.py b/beetsplug/bpd/__init__.py index b0b8ce6c2..6ed188656 100644 --- a/beetsplug/bpd/__init__.py +++ b/beetsplug/bpd/__init__.py @@ -71,7 +71,7 @@ SAFE_COMMANDS = ( ITEM_KEYS_WRITABLE = set(MediaFile.fields()).intersection(Item._fields.keys()) # Loggers. -log = logging.getLogger('beets.bpd') +log = logging.getLogger(__name__) global_log = logging.getLogger('beets') diff --git a/beetsplug/bpm.py b/beetsplug/bpm.py index 028af7eae..b8f42f950 100644 --- a/beetsplug/bpm.py +++ b/beetsplug/bpm.py @@ -19,7 +19,7 @@ import time from beets import ui, logging from beets.plugins import BeetsPlugin -log = logging.getLogger('beets') +log = logging.getLogger(__name__) def bpm(max_strokes): diff --git a/beetsplug/bucket.py b/beetsplug/bucket.py index 68d520395..e8f202875 100644 --- a/beetsplug/bucket.py +++ b/beetsplug/bucket.py @@ -23,7 +23,7 @@ from itertools import tee, izip from beets import logging from beets import plugins, ui -log = logging.getLogger('beets') +log = logging.getLogger(__name__) class BucketError(Exception): diff --git a/beetsplug/chroma.py b/beetsplug/chroma.py index 485bfba61..b5a1e2452 100644 --- a/beetsplug/chroma.py +++ b/beetsplug/chroma.py @@ -32,7 +32,7 @@ COMMON_REL_THRESH = 0.6 # How many tracks must have an album in common? MAX_RECORDINGS = 5 MAX_RELEASES = 5 -log = logging.getLogger('beets') +log = logging.getLogger(__name__) # Stores the Acoustid match information for each track. This is # populated when an import task begins and then used when searching for @@ -80,17 +80,17 @@ def acoustid_match(path): # Ensure the response is usable and parse it. if res['status'] != 'ok' or not res.get('results'): - log.debug(u'chroma: no match found') + log.debug(u'no match found') return None result = res['results'][0] # Best match. if result['score'] < SCORE_THRESH: - log.debug(u'chroma: no results above threshold') + log.debug(u'no results above threshold') return None _acoustids[path] = result['id'] # Get recording and releases from the result. if not result.get('recordings'): - log.debug(u'chroma: no recordings found') + log.debug(u'no recordings found') return None recording_ids = [] release_ids = [] @@ -99,7 +99,7 @@ def acoustid_match(path): if 'releases' in recording: release_ids += [rel['id'] for rel in recording['releases']] - log.debug(u'chroma: matched recordings {0} on releases {1}', + log.debug(u'matched recordings {0} on releases {1}', recording_ids, release_ids) _matches[path] = recording_ids, release_ids diff --git a/beetsplug/convert.py b/beetsplug/convert.py index c9b83c03f..3b0bb4244 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -26,7 +26,7 @@ from beets.plugins import BeetsPlugin from beetsplug.embedart import embed_item from beets.util.confit import ConfigTypeError -log = logging.getLogger('beets') +log = logging.getLogger(__name__) _fs_lock = threading.Lock() _temp_files = [] # Keep track of temporary transcoded files for deletion. diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index e3a55fbdd..8fe227726 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -27,7 +27,7 @@ import re import time import json -log = logging.getLogger('beets') +log = logging.getLogger(__name__) # Silence spurious INFO log lines generated by urllib3. urllib3_logger = logging.getLogger('requests.packages.urllib3') @@ -117,7 +117,7 @@ class DiscogsPlugin(BeetsPlugin): try: return self.get_albums(query) except DiscogsAPIError as e: - log.debug(u'Discogs API Error: {0} (query: {1})', e, query) + log.debug(u'API Error: {0} (query: {1})', e, query) return [] except ConnectionError as e: log.debug(u'HTTP Connection Error: {0}', e) @@ -130,7 +130,7 @@ class DiscogsPlugin(BeetsPlugin): if not self.discogs_client: return - log.debug(u'Searching Discogs for release {0}', album_id) + log.debug(u'Searching for release {0}', album_id) # Discogs-IDs are simple integers. We only look for those at the end # of an input string as to avoid confusion with other metadata plugins. # An optional bracket can follow the integer, as this is how discogs @@ -145,8 +145,7 @@ class DiscogsPlugin(BeetsPlugin): getattr(result, 'title') except DiscogsAPIError as e: if e.message != '404 Not Found': - log.debug(u'Discogs API Error: {0} (query: {1})', - e, result._uri) + log.debug(u'API Error: {0} (query: {1})', e, result._uri) return None except ConnectionError as e: log.debug(u'HTTP Connection Error: {0}', e) @@ -294,7 +293,7 @@ class DiscogsPlugin(BeetsPlugin): if match: medium, index = match.groups() else: - log.debug(u'Invalid Discogs position: {0}', position) + log.debug(u'Invalid position: {0}', position) medium = index = None return medium or None, index or None diff --git a/beetsplug/duplicates.py b/beetsplug/duplicates.py index 1d7b6f9f1..fb6c1d3c1 100644 --- a/beetsplug/duplicates.py +++ b/beetsplug/duplicates.py @@ -22,7 +22,7 @@ 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('beets') +log = logging.getLogger(__name__) def _process_item(item, lib, copy=False, move=False, delete=False, diff --git a/beetsplug/echonest.py b/beetsplug/echonest.py index e38814437..850a7e1be 100644 --- a/beetsplug/echonest.py +++ b/beetsplug/echonest.py @@ -27,7 +27,7 @@ import pyechonest import pyechonest.song import pyechonest.track -log = logging.getLogger('beets') +log = logging.getLogger(__name__) # If a request at the EchoNest fails, we want to retry the request RETRIES # times and wait between retries for RETRY_INTERVAL seconds. @@ -152,31 +152,30 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): except pyechonest.util.EchoNestAPIError as e: if e.code == 3: # reached access limit per minute - log.debug(u'echonest: rate-limited on try {0}; ' - u'waiting {1} seconds', + log.debug(u'rate-limited on try {0}; waiting {1} seconds', i + 1, RETRY_INTERVAL) time.sleep(RETRY_INTERVAL) elif e.code == 5: # specified identifier does not exist # no use in trying again. - log.debug(u'echonest: {0}', e) + log.debug(u'{0}', e) return None else: - log.error(u'echonest: {0}', e.args[0][0]) + log.error(u'{0}', e.args[0][0]) return None except (pyechonest.util.EchoNestIOError, socket.error) as e: - log.warn(u'echonest: IO error: {0}', e) + log.warn(u'IO error: {0}', e) time.sleep(RETRY_INTERVAL) except Exception as e: # there was an error analyzing the track, status: error - log.debug(u'echonest: {0}', e) + log.debug(u'{0}', e) return None else: break else: # If we exited the loop without breaking, then we used up all # our allotted retries. - log.error(u'echonest request failed repeatedly') + log.error(u'request failed repeatedly') return None return result @@ -187,7 +186,7 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): seconds, it's considered a match. """ if not songs: - log.debug(u'echonest: no songs found') + log.debug(u'no songs found') return pick = None @@ -225,13 +224,13 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): # Look up the Echo Nest ID based on the MBID. else: if not item.mb_trackid: - log.debug(u'echonest: no ID available') + log.debug(u'no ID available') return mbid = 'musicbrainz:track:{0}'.format(item.mb_trackid) track = self._echofun(pyechonest.track.track_from_id, identifier=mbid) if not track: - log.debug(u'echonest: lookup by MBID failed') + log.debug(u'lookup by MBID failed') return enid = track.song_id @@ -291,7 +290,7 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): fd, dest = tempfile.mkstemp(u'.ogg') os.close(fd) - log.info(u'echonest: encoding {0} to {1}', + log.info(u'encoding {0} to {1}', util.displayable_path(source), util.displayable_path(dest)) @@ -304,11 +303,11 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): try: util.command_output(opts) except (OSError, subprocess.CalledProcessError) as exc: - log.debug(u'echonest: encode failed: {0}', exc) + log.debug(u'encode failed: {0}', exc) util.remove(dest) return - log.info(u'echonest: finished encoding {0}', + log.info(u'finished encoding {0}', util.displayable_path(source)) return dest @@ -317,7 +316,7 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): fd, dest = tempfile.mkstemp(u'.ogg') os.close(fd) - log.info(u'echonest: truncating {0} to {1}', + log.info(u'truncating {0} to {1}', util.displayable_path(source), util.displayable_path(dest)) @@ -330,11 +329,11 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): try: util.command_output(opts) except (OSError, subprocess.CalledProcessError) as exc: - log.debug(u'echonest: truncate failed: {0}', exc) + log.debug(u'truncate failed: {0}', exc) util.remove(dest) return - log.info(u'echonest: truncate encoding {0}', + log.info(u'truncate encoding {0}', util.displayable_path(source)) return dest @@ -344,18 +343,18 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): """ prepared = self.prepare_upload(item) if not prepared: - log.debug(u'echonest: could not prepare file for upload') + log.debug(u'could not prepare file for upload') return source, tmp = prepared - log.info(u'echonest: uploading file, please be patient') + log.info(u'uploading file, please be patient') track = self._echofun(pyechonest.track.track_from_filename, filename=source) if tmp is not None: util.remove(tmp) if not track: - log.debug(u'echonest: failed to upload file') + log.debug(u'failed to upload file') return # Sometimes we have a track but no song. I guess this happens for @@ -406,7 +405,7 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): for method in methods: song = method(item) if song: - log.debug(u'echonest: got song through {0}: {1} - {2} [{3}]', + log.debug(u'got song through {0}: {1} - {2} [{3}]', method.__name__, item.artist, item.title, @@ -422,7 +421,7 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): for k, v in values.iteritems(): if k in ATTRIBUTES: field = ATTRIBUTES[k] - log.debug(u'echonest: metadata: {0} = {1}', field, v) + log.debug(u'metadata: {0} = {1}', field, v) if field == 'bpm': item[field] = int(v) else: @@ -434,7 +433,7 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): item['initial_key'] = key if 'id' in values: enid = values['id'] - log.debug(u'echonest: metadata: {0} = {1}', ID_KEY, enid) + log.debug(u'metadata: {0} = {1}', ID_KEY, enid) item[ID_KEY] = enid # Write and save. @@ -461,7 +460,7 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): for field in ATTRIBUTES.values(): if not item.get(field): return True - log.info(u'echonest: no update required') + log.info(u'no update required') return False def commands(self): @@ -476,7 +475,7 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): self.config.set_args(opts) write = config['import']['write'].get(bool) for item in lib.items(ui.decargs(args)): - log.info(u'echonest: {0} - {1}', item.artist, item.title) + log.info(u'{0} - {1}', item.artist, item.title) if self.config['force'] or self.requires_update(item): song = self.fetch_song(item) if song: diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index 7f04cd1fb..944f8c3b3 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -29,7 +29,7 @@ from beets.util.artresizer import ArtResizer from beets import config -log = logging.getLogger('beets') +log = logging.getLogger(__name__) class EmbedCoverArtPlugin(BeetsPlugin): @@ -46,12 +46,12 @@ class EmbedCoverArtPlugin(BeetsPlugin): if self.config['maxwidth'].get(int) and not ArtResizer.shared.local: self.config['maxwidth'] = 0 - log.warn(u"embedart: ImageMagick or PIL not found; " + log.warn(u"ImageMagick or PIL not found; " u"'maxwidth' option ignored") if self.config['compare_threshold'].get(int) and not \ ArtResizer.shared.can_compare: self.config['compare_threshold'] = 0 - log.warn(u"embedart: ImageMagick 6.8.7 or higher not installed; " + log.warn(u"ImageMagick 6.8.7 or higher not installed; " u"'compare_threshold' option ignored") def commands(self): @@ -122,17 +122,17 @@ def embed_item(item, imagepath, maxwidth=None, itempath=None, if not art: pass else: - log.debug(u'embedart: media file contained art already {0}', + log.debug(u'media file contained art already {0}', displayable_path(imagepath)) return if maxwidth and not as_album: imagepath = resize_image(imagepath, maxwidth) try: - log.debug(u'embedart: embedding {0}', displayable_path(imagepath)) + log.debug(u'embedding {0}', displayable_path(imagepath)) item['images'] = [_mediafile_image(imagepath, maxwidth)] except IOError as exc: - log.error(u'embedart: could not read image file: {0}', exc) + log.error(u'could not read image file: {0}', exc) else: # We don't want to store the image in the database. item.try_write(itempath) @@ -192,15 +192,15 @@ def check_art_similarity(item, imagepath, compare_threshold): stdout, stderr = proc.communicate() if proc.returncode: if proc.returncode != 1: - log.warn(u'embedart: IM phashes compare failed for ' - u'{0}, {1}', displayable_path(imagepath), + log.warn(u'IM phashes compare failed for {0}, {1}', + displayable_path(imagepath), displayable_path(art)) return phashDiff = float(stderr) else: phashDiff = float(stdout) - log.info(u'embedart: compare PHASH score is {0}', phashDiff) + log.info(u'compare PHASH score is {0}', phashDiff) if phashDiff > compare_threshold: return False diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index 3b0059817..f593f0e3a 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -39,7 +39,7 @@ IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg'] CONTENT_TYPES = ('image/jpeg',) DOWNLOAD_EXTENSION = '.jpg' -log = logging.getLogger('beets') +log = logging.getLogger(__name__) requests_session = requests.Session() requests_session.headers = {'User-Agent': 'beets'} @@ -50,12 +50,12 @@ def _fetch_image(url): actually be an image. If so, returns a path to the downloaded image. Otherwise, returns None. """ - log.debug(u'fetchart: downloading art: {0}', url) + log.debug(u'downloading art: {0}', url) try: with closing(requests_session.get(url, stream=True)) as resp: if 'Content-Type' not in resp.headers \ or resp.headers['Content-Type'] not in CONTENT_TYPES: - log.debug(u'fetchart: not an image') + log.debug(u'not an image') return # Generate a temporary file with the correct extension. @@ -63,11 +63,11 @@ def _fetch_image(url): as fh: for chunk in resp.iter_content(): fh.write(chunk) - log.debug(u'fetchart: downloaded art to: {0}', + log.debug(u'downloaded art to: {0}', util.displayable_path(fh.name)) return fh.name except (IOError, requests.RequestException): - log.debug(u'fetchart: error fetching art') + log.debug(u'error fetching art') # ART SOURCES ################################################################ @@ -116,9 +116,9 @@ def aao_art(album): # Get the page from albumart.org. try: resp = requests_session.get(AAO_URL, params={'asin': album.asin}) - log.debug(u'fetchart: scraped art URL: {0}', resp.url) + log.debug(u'scraped art URL: {0}', resp.url) except requests.RequestException: - log.debug(u'fetchart: error scraping art page') + log.debug(u'error scraping art page') return # Search the page for the image URL. @@ -127,7 +127,7 @@ def aao_art(album): image_url = m.group(1) yield image_url else: - log.debug(u'fetchart: no image found on page') + log.debug(u'no image found on page') # Google Images scraper. @@ -156,7 +156,7 @@ def google_art(album): for myUrl in dataInfo: yield myUrl['unescapedUrl'] except: - log.debug(u'fetchart: error scraping art page') + log.debug(u'error scraping art page') return @@ -171,7 +171,7 @@ def itunes_art(album): try: itunes_album = itunes.search_album(search_string)[0] except Exception as exc: - log.debug('fetchart: iTunes search failed: {0}', exc) + log.debug('iTunes search failed: {0}', exc) return if itunes_album.get_artwork()['100']: @@ -179,9 +179,9 @@ def itunes_art(album): big_url = small_url.replace('100x100', '1200x1200') yield big_url else: - log.debug(u'fetchart: album has no artwork in iTunes Store') + log.debug(u'album has no artwork in iTunes Store') except IndexError: - log.debug(u'fetchart: album not found in iTunes Store') + log.debug(u'album not found in iTunes Store') # Art from the filesystem. @@ -215,13 +215,13 @@ def art_in_path(path, cover_names, cautious): cover_pat = r"(\b|_)({0})(\b|_)".format('|'.join(cover_names)) for fn in images: if re.search(cover_pat, os.path.splitext(fn)[0], re.I): - log.debug(u'fetchart: using well-named art file {0}', + log.debug(u'using well-named art file {0}', util.displayable_path(fn)) return os.path.join(path, fn) # Fall back to any image in the folder. if images and not cautious: - log.debug(u'fetchart: using fallback art file {0}', + log.debug(u'using fallback art file {0}', util.displayable_path(images[0])) return os.path.join(path, images[0]) diff --git a/beetsplug/freedesktop.py b/beetsplug/freedesktop.py index 3f3307c82..cfe7c4cd7 100644 --- a/beetsplug/freedesktop.py +++ b/beetsplug/freedesktop.py @@ -22,7 +22,7 @@ from beets.ui import decargs import os -log = logging.getLogger('beets.freedesktop') +log = logging.getLogger(__name__) def process_query(lib, opts, args): @@ -37,7 +37,7 @@ def process_album(album): artfile = os.path.split(fullartpath)[1] create_file(albumpath, artfile) else: - log.debug(u'freedesktop: album has no art') + log.debug(u'album has no art') def create_file(albumpath, artfile): diff --git a/beetsplug/ftintitle.py b/beetsplug/ftintitle.py index f36cfb348..7c48a4653 100644 --- a/beetsplug/ftintitle.py +++ b/beetsplug/ftintitle.py @@ -22,7 +22,7 @@ from beets.util import displayable_path from beets import config from beets import logging -log = logging.getLogger('beets') +log = logging.getLogger(__name__) def split_on_feat(artist): diff --git a/beetsplug/ihate.py b/beetsplug/ihate.py index ed7cbd954..5d3d3a001 100644 --- a/beetsplug/ihate.py +++ b/beetsplug/ihate.py @@ -37,7 +37,7 @@ def summary(task): class IHatePlugin(BeetsPlugin): - _log = logging.getLogger('beets') + _log = logging.getLogger(__name__) def __init__(self): super(IHatePlugin, self).__init__() @@ -69,15 +69,15 @@ class IHatePlugin(BeetsPlugin): if task.choice_flag == action.APPLY: if skip_queries or warn_queries: - self._log.debug(u'[ihate] processing your hate') + self._log.debug(u'processing your hate') if self.do_i_hate_this(task, skip_queries): task.choice_flag = action.SKIP - self._log.info(u'[ihate] skipped: {0}', summary(task)) + self._log.info(u'skipped: {0}', summary(task)) return if self.do_i_hate_this(task, warn_queries): - self._log.info(u'[ihate] you maybe hate this: {0}', + self._log.info(u'you may hate this: {0}', summary(task)) else: - self._log.debug(u'[ihate] nothing to do') + self._log.debug(u'nothing to do') else: - self._log.debug(u'[ihate] user made a decision, nothing to do') + self._log.debug(u'user made a decision, nothing to do') diff --git a/beetsplug/importadded.py b/beetsplug/importadded.py index 8cb590111..00f2658b0 100644 --- a/beetsplug/importadded.py +++ b/beetsplug/importadded.py @@ -13,7 +13,7 @@ from beets import config from beets import util from beets.plugins import BeetsPlugin -log = logging.getLogger('beets') +log = logging.getLogger(__name__) class ImportAddedPlugin(BeetsPlugin): diff --git a/beetsplug/importfeeds.py b/beetsplug/importfeeds.py index 4ac1dda2b..5a5b8fbe0 100644 --- a/beetsplug/importfeeds.py +++ b/beetsplug/importfeeds.py @@ -25,7 +25,7 @@ from beets.util import normpath, syspath, bytestring_path from beets import config, logging M3U_DEFAULT_NAME = 'imported.m3u' -log = logging.getLogger('beets') +log = logging.getLogger(__name__) class ImportFeedsPlugin(BeetsPlugin): diff --git a/beetsplug/info.py b/beetsplug/info.py index 30cccb1b2..fa6eb325f 100644 --- a/beetsplug/info.py +++ b/beetsplug/info.py @@ -24,7 +24,7 @@ from beets import mediafile from beets.util import displayable_path, normpath, syspath -log = logging.getLogger('beets') +log = logging.getLogger(__name__) def run(lib, opts, args): diff --git a/beetsplug/inline.py b/beetsplug/inline.py index e7e7e0f41..7fa6037a3 100644 --- a/beetsplug/inline.py +++ b/beetsplug/inline.py @@ -20,7 +20,7 @@ import itertools from beets.plugins import BeetsPlugin from beets import config, logging -log = logging.getLogger('beets') +log = logging.getLogger(__name__) FUNC_NAME = u'__INLINE_FUNC__' @@ -111,14 +111,14 @@ class InlinePlugin(BeetsPlugin): # Item fields. for key, view in itertools.chain(config['item_fields'].items(), config['pathfields'].items()): - log.debug(u'inline: adding item field {0}', key) + log.debug(u'adding item field {0}', key) func = 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(): - log.debug(u'inline: adding album field {0}', key) + log.debug(u'adding album field {0}', key) func = compile_inline(view.get(unicode), True) if func is not None: self.album_template_fields[key] = func diff --git a/beetsplug/keyfinder.py b/beetsplug/keyfinder.py index 49830edef..6298ce6ca 100644 --- a/beetsplug/keyfinder.py +++ b/beetsplug/keyfinder.py @@ -23,7 +23,7 @@ from beets import util from beets.plugins import BeetsPlugin -log = logging.getLogger('beets') +log = logging.getLogger(__name__) class KeyFinderPlugin(BeetsPlugin): @@ -62,7 +62,7 @@ class KeyFinderPlugin(BeetsPlugin): try: key = util.command_output([bin, '-f', item.path]) except (subprocess.CalledProcessError, OSError) as exc: - log.error(u'KeyFinder execution failed: {0}', exc) + log.error(u'execution failed: {0}', exc) continue item['initial_key'] = key diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index 2b4acb590..8ef97c6d2 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -31,7 +31,7 @@ from beets.util import normpath, plurality from beets import config from beets import library -log = logging.getLogger('beets') +log = logging.getLogger(__name__) LASTFM = pylast.LastFMNetwork(api_key=plugins.LASTFM_KEY) diff --git a/beetsplug/lastimport.py b/beetsplug/lastimport.py index 087213031..dbed972a8 100644 --- a/beetsplug/lastimport.py +++ b/beetsplug/lastimport.py @@ -20,7 +20,7 @@ from beets import plugins from beets import logging from beets.dbcore import types -log = logging.getLogger('beets') +log = logging.getLogger(__name__) API_URL = 'http://ws.audioscrobbler.com/2.0/' @@ -65,7 +65,7 @@ def import_lastfm(lib): retry_limit = config['lastimport']['retry_limit'].get(int) # Iterate through a yet to be known page total count while page_current < page_total: - log.info('lastimport: Querying page #{0}{1}...', + log.info('Querying page #{0}{1}...', page_current + 1, '/{}'.format(page_total) if page_total > 1 else '') @@ -83,22 +83,22 @@ def import_lastfm(lib): unknown_total += unknown break else: - log.error('lastimport: ERROR: unable to read page #{0}', + log.error('ERROR: unable to read page #{0}', page_current + 1) if retry < retry_limit: log.info( - 'lastimport: Retrying page #{0}... ({1}/{2} retry)', + 'Retrying page #{0}... ({1}/{2} retry)', page_current + 1, retry + 1, retry_limit ) else: - log.error('lastimport: FAIL: unable to fetch page #{0}, ', + log.error('FAIL: unable to fetch page #{0}, ', 'tried {1} times', page_current, retry + 1) page_current += 1 - log.info('lastimport: ... done!') - log.info('lastimport: finished processing {0} song pages', page_total) - log.info('lastimport: {0} unknown play-counts', unknown_total) - log.info('lastimport: {0} play-counts imported', found_total) + log.info('... done!') + log.info('finished processing {0} song pages', page_total) + log.info('{0} unknown play-counts', unknown_total) + log.info('{0} play-counts imported', found_total) def fetch_tracks(user, page, limit): @@ -116,8 +116,7 @@ def process_tracks(lib, tracks): total = len(tracks) total_found = 0 total_fails = 0 - log.info('lastimport: Received {0} tracks in this page, processing...', - total) + log.info('Received {0} tracks in this page, processing...', total) for num in xrange(0, total): song = '' @@ -128,7 +127,7 @@ def process_tracks(lib, tracks): if 'album' in tracks[num]: album = tracks[num]['album'].get('name', '').strip() - log.debug(u'lastimport: query: {0} - {1} ({2})', artist, title, album) + log.debug(u'query: {0} - {1} ({2})', artist, title, album) # First try to query by musicbrainz's trackid if trackid: @@ -138,7 +137,7 @@ def process_tracks(lib, tracks): # Otherwise try artist/title/album if not song: - log.debug(u'lastimport: no match for mb_trackid {0}, trying by ' + log.debug(u'no match for mb_trackid {0}, trying by ' u'artist/title/album', trackid) query = dbcore.AndQuery([ dbcore.query.SubstringQuery('artist', artist), @@ -149,7 +148,7 @@ def process_tracks(lib, tracks): # If not, try just artist/title if not song: - log.debug(u'lastimport: no album match, trying by artist/title') + log.debug(u'no album match, trying by artist/title') query = dbcore.AndQuery([ dbcore.query.SubstringQuery('artist', artist), dbcore.query.SubstringQuery('title', title) @@ -159,7 +158,7 @@ def process_tracks(lib, tracks): # Last resort, try just replacing to utf-8 quote if not song: title = title.replace("'", u'\u2019') - log.debug(u'lastimport: no title match, trying utf-8 single quote') + log.debug(u'no title match, trying utf-8 single quote') query = dbcore.AndQuery([ dbcore.query.SubstringQuery('artist', artist), dbcore.query.SubstringQuery('title', title) @@ -169,7 +168,7 @@ def process_tracks(lib, tracks): if song: count = int(song.get('play_count', 0)) new_count = int(tracks[num]['playcount']) - log.debug(u'lastimport: match: {0} - {1} ({2}) ' + log.debug(u'match: {0} - {1} ({2}) ' u'updating: play_count {3} => {4}', song.artist, song.title, song.album, count, new_count) song['play_count'] = new_count @@ -177,11 +176,11 @@ def process_tracks(lib, tracks): total_found += 1 else: total_fails += 1 - log.info(u'lastimport: - No match: {0} - {1} ({2})', + log.info(u' - No match: {0} - {1} ({2})', artist, title, album) if total_fails > 0: - log.info('lastimport: Acquired {0}/{1} play-counts ({2} unknown)', + log.info('Acquired {0}/{1} play-counts ({2} unknown)', total_found, total, total_fails) return total_found, total_fails diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index a87715fe3..2f889ac8d 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -32,7 +32,7 @@ from beets import config, ui # Global logger. -log = logging.getLogger('beets') +log = logging.getLogger(__name__) DIV_RE = re.compile(r'<(/?)div>?', re.I) COMMENT_RE = re.compile(r'', re.S) diff --git a/beetsplug/mbcollection.py b/beetsplug/mbcollection.py index 764c4529f..870e337d9 100644 --- a/beetsplug/mbcollection.py +++ b/beetsplug/mbcollection.py @@ -26,7 +26,7 @@ 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('beets.bpd') +log = logging.getLogger(__name__) def mb_call(func, *args, **kwargs): diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py index 105122639..622277809 100644 --- a/beetsplug/mbsync.py +++ b/beetsplug/mbsync.py @@ -20,7 +20,7 @@ from beets.autotag import hooks from beets import config from collections import defaultdict -log = logging.getLogger('beets') +log = logging.getLogger(__name__) def mbsync_singletons(lib, query, move, pretend, write): diff --git a/beetsplug/missing.py b/beetsplug/missing.py index 2ebe3edf8..d7fc5041e 100644 --- a/beetsplug/missing.py +++ b/beetsplug/missing.py @@ -20,8 +20,7 @@ from beets.library import Item from beets.plugins import BeetsPlugin from beets.ui import decargs, print_obj, Subcommand -PLUGIN = 'missing' -log = logging.getLogger('beets') +log = logging.getLogger(__name__) def _missing_count(album): @@ -42,8 +41,8 @@ def _missing(album): 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'{0}: track {1} in album {2}', - PLUGIN, track_info.track_id, album_info.album_id) + 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 5a5d27b4e..96167e2aa 100644 --- a/beetsplug/mpdstats.py +++ b/beetsplug/mpdstats.py @@ -27,7 +27,7 @@ from beets import library from beets.util import displayable_path from beets.dbcore import types -log = logging.getLogger('beets') +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? @@ -71,7 +71,7 @@ class MPDClientWrapper(object): if host[0] in ['/', '~']: host = os.path.expanduser(host) - log.info(u'mpdstats: connecting to {0}:{1}', host, port) + log.info(u'connecting to {0}:{1}', host, port) try: self.client.connect(host, port) except socket.error as e: @@ -99,7 +99,7 @@ class MPDClientWrapper(object): try: return getattr(self.client, command)() except (select.error, mpd.ConnectionError) as err: - log.error(u'mpdstats: {0}', err) + log.error(u'{0}', err) if retries <= 0: # if we exited without breaking, we couldn't reconnect in time :( @@ -171,7 +171,7 @@ class MPDStats(object): if item: return item else: - log.info(u'mpdstats: item not found: {0}', displayable_path(path)) + log.info(u'item not found: {0}', displayable_path(path)) @staticmethod def update_item(item, attribute, value=None, increment=None): @@ -190,7 +190,7 @@ class MPDStats(object): item[attribute] = value item.store() - log.debug(u'mpdstats: updated: {0} = {1} [{2}]', + log.debug(u'updated: {0} = {1} [{2}]', attribute, item[attribute], displayable_path(item.path)) @@ -229,16 +229,16 @@ class MPDStats(object): """Updates the play count of a song. """ self.update_item(song['beets_item'], 'play_count', increment=1) - log.info(u'mpdstats: played {0}', displayable_path(song['path'])) + 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'mpdstats: skipped {0}', displayable_path(song['path'])) + log.info(u'skipped {0}', displayable_path(song['path'])) def on_stop(self, status): - log.info(u'mpdstats: stop') + log.info(u'stop') if self.now_playing: self.handle_song_change(self.now_playing) @@ -246,7 +246,7 @@ class MPDStats(object): self.now_playing = None def on_pause(self, status): - log.info(u'mpdstats: pause') + log.info(u'pause') self.now_playing = None def on_play(self, status): @@ -257,7 +257,7 @@ class MPDStats(object): return if is_url(path): - log.info(u'mpdstats: playing stream {0}', displayable_path(path)) + log.info(u'playing stream {0}', displayable_path(path)) return played, duration = map(int, status['time'].split(':', 1)) @@ -266,7 +266,7 @@ class MPDStats(object): if self.now_playing and self.now_playing['path'] != path: self.handle_song_change(self.now_playing) - log.info(u'mpdstats: playing {0}', displayable_path(path)) + log.info(u'playing {0}', displayable_path(path)) self.now_playing = { 'started': time.time(), @@ -291,7 +291,7 @@ class MPDStats(object): if handler: handler(status) else: - log.debug(u'mpdstats: unhandled status "{0}"', status) + log.debug(u'unhandled status "{0}"', status) events = self.mpd.events() diff --git a/beetsplug/play.py b/beetsplug/play.py index 7f9ff3ce9..e1d8d4fed 100644 --- a/beetsplug/play.py +++ b/beetsplug/play.py @@ -25,7 +25,7 @@ import platform import shlex from tempfile import NamedTemporaryFile -log = logging.getLogger('beets') +log = logging.getLogger(__name__) def play_music(lib, opts, args): @@ -105,7 +105,7 @@ def play_music(lib, opts, args): util.displayable_path(command[0]), output.decode('utf8', 'ignore')) else: - log.debug(u'play: no output') + log.debug(u'no output') ui.print_(u'Playing {0} {1}.'.format(len(selection), item_type)) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 5d50f61c5..6cf83161a 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -25,7 +25,7 @@ from beets.plugins import BeetsPlugin from beets.util import syspath, command_output, displayable_path from beets import config -log = logging.getLogger('beets.replaygain') +log = logging.getLogger(__name__) # Utilities. @@ -135,7 +135,7 @@ class CommandBackend(Backend): supported_items = filter(self.format_supported, album.items()) if len(supported_items) != len(album.items()): - log.debug(u'replaygain: tracks are of unsupported format') + log.debug(u'tracks are of unsupported format') return AlbumGain(None, []) output = self.compute_gain(supported_items, True) @@ -180,11 +180,10 @@ class CommandBackend(Backend): cmd = cmd + ['-d', str(self.gain_offset)] cmd = cmd + [syspath(i.path) for i in items] - log.debug(u'replaygain: analyzing {0} files', len(items)) - log.debug(u"replaygain: executing {0}", - " ".join(map(displayable_path, cmd))) + log.debug(u'analyzing {0} files', len(items)) + log.debug(u"executing {0}", " ".join(map(displayable_path, cmd))) output = call(cmd) - log.debug(u'replaygain: analysis finished') + log.debug(u'analysis finished') results = self.parse_tool_output(output, len(items) + (1 if is_album else 0)) @@ -199,7 +198,7 @@ class CommandBackend(Backend): for line in text.split('\n')[1:num_lines + 1]: parts = line.split('\t') if len(parts) != 6 or parts[0] == 'File': - log.debug(u'replaygain: bad tool output: {0}', text) + log.debug(u'bad tool output: {0}', text) raise ReplayGainError('mp3gain failed') d = { 'file': parts[0], @@ -651,7 +650,7 @@ class ReplayGainPlugin(BeetsPlugin): item.rg_track_peak = track_gain.peak item.store() - log.debug(u'replaygain: applied track gain {0}, peak {1}', + log.debug(u'applied track gain {0}, peak {1}', item.rg_track_gain, item.rg_track_peak) def store_album_gain(self, album, album_gain): @@ -659,7 +658,7 @@ class ReplayGainPlugin(BeetsPlugin): album.rg_album_peak = album_gain.peak album.store() - log.debug(u'replaygain: applied album gain {0}, peak {1}', + log.debug(u'applied album gain {0}, peak {1}', album.rg_album_gain, album.rg_album_peak) def handle_album(self, album, write): diff --git a/beetsplug/rewrite.py b/beetsplug/rewrite.py index 8a59bdbfd..8f005dda4 100644 --- a/beetsplug/rewrite.py +++ b/beetsplug/rewrite.py @@ -23,7 +23,7 @@ from beets import logging from beets import ui from beets import library -log = logging.getLogger('beets') +log = logging.getLogger(__name__) def rewriter(field, rules): diff --git a/beetsplug/scrub.py b/beetsplug/scrub.py index 7c5b097cb..0811da8bb 100644 --- a/beetsplug/scrub.py +++ b/beetsplug/scrub.py @@ -23,7 +23,7 @@ from beets import util from beets import config from beets import mediafile -log = logging.getLogger('beets') +log = logging.getLogger(__name__) _MUTAGEN_FORMATS = { 'asf': 'ASF', diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index 704660894..b50b06cb9 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -7,7 +7,7 @@ from beets.ui import decargs from beets import ui, logging from requests.exceptions import HTTPError -log = logging.getLogger('beets') +log = logging.getLogger(__name__) class SpotifyPlugin(BeetsPlugin): diff --git a/beetsplug/the.py b/beetsplug/the.py index d146b3e69..248431457 100644 --- a/beetsplug/the.py +++ b/beetsplug/the.py @@ -29,7 +29,7 @@ FORMAT = u'{0}, {1}' class ThePlugin(BeetsPlugin): _instance = None - _log = logging.getLogger('beets') + _log = logging.getLogger(__name__) the = True a = True @@ -56,17 +56,17 @@ class ThePlugin(BeetsPlugin): try: re.compile(p) except re.error: - self._log.error(u'[the] invalid pattern: {0}', p) + self._log.error(u'invalid pattern: {0}', p) else: if not (p.startswith('^') or p.endswith('$')): - self._log.warn(u'[the] warning: \"{0}\" will not ' - 'match string start/end', p) + self._log.warn(u'warning: \"{0}\" will not ' + u'match string start/end', p) if self.config['a']: self.patterns = [PATTERN_A] + self.patterns if self.config['the']: self.patterns = [PATTERN_THE] + self.patterns if not self.patterns: - self._log.warn(u'[the] no patterns defined!') + self._log.warn(u'no patterns defined!') def unthe(self, text, pattern): """Moves pattern in the path format string or strips it @@ -99,7 +99,7 @@ class ThePlugin(BeetsPlugin): r = self.unthe(text, p) if r != text: break - self._log.debug(u'[the] \"{0}\" -> \"{1}\"', text, r) + self._log.debug(u'\"{0}\" -> \"{1}\"', text, r) return r else: return u'' diff --git a/beetsplug/zero.py b/beetsplug/zero.py index a8c62d42c..aab798d26 100644 --- a/beetsplug/zero.py +++ b/beetsplug/zero.py @@ -24,7 +24,7 @@ from beets.util import confit __author__ = 'baobab@heresiarch.info' __version__ = '0.10' -log = logging.getLogger('beets') +log = logging.getLogger(__name__) class ZeroPlugin(BeetsPlugin): @@ -48,11 +48,11 @@ class ZeroPlugin(BeetsPlugin): for field in self.config['fields'].as_str_seq(): if field in ('id', 'path', 'album_id'): - log.warn(u'[zero] field \'{0}\' ignored, zeroing ' + log.warn(u'field \'{0}\' ignored, zeroing ' u'it would be dangerous', field) continue if field not in MediaFile.fields(): - log.error(u'[zero] invalid field: {0}', field) + log.error(u'invalid field: {0}', field) continue try: @@ -64,7 +64,7 @@ class ZeroPlugin(BeetsPlugin): def import_task_choice_event(self, session, task): """Listen for import_task_choice event.""" if task.choice_flag == action.ASIS and not self.warned: - log.warn(u'[zero] cannot zero in \"as-is\" mode') + log.warn(u'cannot zero in \"as-is\" mode') self.warned = True # TODO request write in as-is mode @@ -85,7 +85,7 @@ class ZeroPlugin(BeetsPlugin): by `self.patterns`. """ if not self.patterns: - log.warn(u'[zero] no fields, nothing to do') + log.warn(u'no fields, nothing to do') return for field, patterns in self.patterns.items(): @@ -97,5 +97,5 @@ class ZeroPlugin(BeetsPlugin): match = patterns is True if match: - log.debug(u'[zero] {0}: {1} -> None', field, value) + log.debug(u'{0}: {1} -> None', field, value) tags[field] = None From 6fb810289f113b2ae88c8e3293ba6eff69a93921 Mon Sep 17 00:00:00 2001 From: "Frederik \"Freso\" S. Olesen" Date: Tue, 6 Jan 2015 15:44:39 +0100 Subject: [PATCH 25/86] lastgenre: Add "comedy" to genre whitelist. See discussion from https://botbot.me/freenode/beets/msg/28875043/ --- beetsplug/lastgenre/genres.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/beetsplug/lastgenre/genres.txt b/beetsplug/lastgenre/genres.txt index ad344afec..5d1b28b10 100644 --- a/beetsplug/lastgenre/genres.txt +++ b/beetsplug/lastgenre/genres.txt @@ -275,6 +275,7 @@ coimbra fado coladeira colombianas combined rhythm +comedy comedy rap comedy rock comic opera From 054488e1ab0806822e3956b0ef8153e3604bc8b9 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 6 Jan 2015 16:32:43 +0100 Subject: [PATCH 26/86] Don't crash when no argument is given --- beets/ui/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index a69b98ac2..ebde7ebbf 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -924,7 +924,8 @@ def _raw_main(args, lib=None): # Special case for the `config --edit` command: bypass _setup so # that an invalid configuration does not prevent the editor from # starting. - if subargs[0] == 'config' and ('-e' in subargs or '--edit' in subargs): + if subargs and subargs[0] == 'config' \ + and ('-e' in subargs or '--edit' in subargs): from beets.ui.commands import config_edit return config_edit() From 3940f3a1c245b47af774ac9ab92446346297a3df Mon Sep 17 00:00:00 2001 From: "Frederik \"Freso\" S. Olesen" Date: Tue, 6 Jan 2015 19:50:00 +0100 Subject: [PATCH 27/86] lastgenre: Whitelist "humor" + "stand-up". https://botbot.me/freenode/beets/msg/28878482/ --- beetsplug/lastgenre/genres-tree.yaml | 2 ++ beetsplug/lastgenre/genres.txt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/beetsplug/lastgenre/genres-tree.yaml b/beetsplug/lastgenre/genres-tree.yaml index 6f701f829..89675ce2f 100644 --- a/beetsplug/lastgenre/genres-tree.yaml +++ b/beetsplug/lastgenre/genres-tree.yaml @@ -159,7 +159,9 @@ - comedy: - comedy music - comedy rock + - humor - parody music + - stand-up - country: - alternative country: - cowpunk diff --git a/beetsplug/lastgenre/genres.txt b/beetsplug/lastgenre/genres.txt index 5d1b28b10..d5e004af3 100644 --- a/beetsplug/lastgenre/genres.txt +++ b/beetsplug/lastgenre/genres.txt @@ -648,6 +648,7 @@ hua'er huasteco huayno hula +humor humppa hunguhungu hyangak @@ -1353,6 +1354,7 @@ sprechgesang square dance squee st. louis blues +stand-up steelband stoner metal stoner rock From daba9e770e5b1b17161c2743aa35ac0e97b1fbf9 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 6 Jan 2015 11:01:11 -0800 Subject: [PATCH 28/86] Fix #1204: insecure request warning in lyrics --- beetsplug/lyrics.py | 9 ++++++++- docs/changelog.rst | 5 ++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index a87715fe3..bffa8414c 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -23,6 +23,7 @@ import unicodedata import urllib import difflib import itertools +import warnings from HTMLParser import HTMLParseError from beets import logging @@ -61,7 +62,13 @@ def fetch_url(url): is unreachable. """ try: - r = requests.get(url, verify=False) + # Disable the InsecureRequestWarning that comes from using + # `verify=false`. + # https://github.com/kennethreitz/requests/issues/2214 + # We're not overly worried about the NSA MITMing our lyrics scraper. + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + r = requests.get(url, verify=False) except requests.RequestException as exc: log.debug(u'lyrics request failed: {0}', exc) return diff --git a/docs/changelog.rst b/docs/changelog.rst index 5936d1237..e77bf480f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,10 @@ Changelog 1.3.11 (in development) ----------------------- -Changelog goes here! +Fixes: + +* :doc:`/plugins/lyrics`: Silence a warning about insecure requests in the new + MusixMatch backend. :bug:`1204` 1.3.10 (January 5, 2015) From b027e48c3205485c9cc0b2c86a181dbfd6be62f1 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 6 Jan 2015 11:23:40 -0800 Subject: [PATCH 29/86] Changelog for #1206 --- docs/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index e77bf480f..27cf67380 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,6 +8,8 @@ Fixes: * :doc:`/plugins/lyrics`: Silence a warning about insecure requests in the new MusixMatch backend. :bug:`1204` +* :doc:`/plugins/lastgenre`: Add *comedy*, *humor*, and *stand-up* to the + built-in whitelist/canonicalization tree. :bug:`1206` 1.3.10 (January 5, 2015) From 0f1b5b2ca5565efc81ca80eff000b1b4f9bbdb44 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 6 Jan 2015 11:25:00 -0800 Subject: [PATCH 30/86] Changelog for #1207 (fix #1205) --- docs/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 27cf67380..01426c0a1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,8 @@ Fixes: MusixMatch backend. :bug:`1204` * :doc:`/plugins/lastgenre`: Add *comedy*, *humor*, and *stand-up* to the built-in whitelist/canonicalization tree. :bug:`1206` +* Fix a crash when ``beet`` is invoked without arguments. :bug:`1205` + :bug:`1207` 1.3.10 (January 5, 2015) From d38d264b6b31030968a452b0de0362cca7a982eb Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Mon, 5 Jan 2015 20:58:00 +0100 Subject: [PATCH 31/86] Add BeetsPlugin._log: per-plugin logger --- beets/plugins.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/beets/plugins.py b/beets/plugins.py index a975145db..ec68d7039 100755 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -61,6 +61,10 @@ class BeetsPlugin(object): if not self.album_template_fields: self.album_template_fields = {} + logger_name = '{0}.{1}'.format('beets', self.name) + self._log = logging.getLogger(logger_name) + self._log.setLevel(logging.WARNING) + def commands(self): """Should return a list of beets.ui.Subcommand objects for commands that should be added to beets' CLI. From 427f7e7035befe8188683be249a7f316fe06330a Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Mon, 5 Jan 2015 12:35:01 +0100 Subject: [PATCH 32/86] Automatic logger level changes on import Breaking changes: plugins should set set _import_stages instead of import_stages. From outside the latter is replaced by import_stages(). This is because it is now wrapped with log level-getting & -setting statements. --- beets/plugins.py | 22 ++++++++++++++++++---- beetsplug/convert.py | 2 +- beetsplug/echonest.py | 2 +- beetsplug/fetchart.py | 2 +- beetsplug/ftintitle.py | 2 +- beetsplug/keyfinder.py | 2 +- beetsplug/lastgenre/__init__.py | 2 +- beetsplug/lyrics.py | 2 +- beetsplug/mbcollection.py | 2 +- beetsplug/replaygain.py | 2 +- 10 files changed, 27 insertions(+), 13 deletions(-) diff --git a/beets/plugins.py b/beets/plugins.py index ec68d7039..b7d2f968b 100755 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -18,6 +18,7 @@ import traceback import inspect import re from collections import defaultdict +from functools import wraps import beets @@ -51,7 +52,7 @@ class BeetsPlugin(object): def __init__(self, name=None): """Perform one-time plugin setup. """ - self.import_stages = [] + self._import_stages = [] self.name = name or self.__module__.split('.')[-1] self.config = beets.config[self.name] if not self.template_funcs: @@ -63,7 +64,7 @@ class BeetsPlugin(object): logger_name = '{0}.{1}'.format('beets', self.name) self._log = logging.getLogger(logger_name) - self._log.setLevel(logging.WARNING) + self._log.setLevel(logging.INFO) def commands(self): """Should return a list of beets.ui.Subcommand objects for @@ -71,6 +72,20 @@ class BeetsPlugin(object): """ return () + def import_stages(self): + return [self._set_log_level(logging.WARNING, import_stage) + for import_stage in self._import_stages] + + def _set_log_level(self, log_level, func): + @wraps(func) + def wrapper(*args, **kwargs): + old_log_level = self._log.getEffectiveLevel() + self._log.setLevel(log_level) + result = func(*args, **kwargs) + self._log.setLevel(old_log_level) + return result + return wrapper + def queries(self): """Should return a dict mapping prefixes to Query subclasses. """ @@ -353,8 +368,7 @@ def import_stages(): """Get a list of import stage functions defined by plugins.""" stages = [] for plugin in find_plugins(): - if hasattr(plugin, 'import_stages'): - stages += plugin.import_stages + stages += plugin.import_stages() return stages diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 3b0bb4244..610591e94 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -365,7 +365,7 @@ class ConvertPlugin(BeetsPlugin): u'never_convert_lossy_files': False, u'copy_album_art': False, }) - self.import_stages = [self.auto_convert] + self._import_stages = [self.auto_convert] def commands(self): cmd = ui.Subcommand('convert', help='convert to external location') diff --git a/beetsplug/echonest.py b/beetsplug/echonest.py index 850a7e1be..e52f48ff5 100644 --- a/beetsplug/echonest.py +++ b/beetsplug/echonest.py @@ -139,7 +139,7 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): config['echonest']['apikey'].get(unicode) if self.config['auto']: - self.import_stages = [self.imported] + self._import_stages = [self.imported] def _echofun(self, func, **kwargs): """Wrapper for requests to the EchoNest API. Will retry up to diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index f593f0e3a..a1bb37480 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -336,7 +336,7 @@ class FetchArtPlugin(plugins.BeetsPlugin): self.maxwidth = self.config['maxwidth'].get(int) if self.config['auto']: # Enable two import hooks when fetching is enabled. - self.import_stages = [self.fetch_art] + self._import_stages = [self.fetch_art] self.register_listener('import_task_files', self.assign_art) available_sources = list(SOURCES_ALL) diff --git a/beetsplug/ftintitle.py b/beetsplug/ftintitle.py index 7c48a4653..b72113e4c 100644 --- a/beetsplug/ftintitle.py +++ b/beetsplug/ftintitle.py @@ -128,7 +128,7 @@ class FtInTitlePlugin(plugins.BeetsPlugin): help='drop featuring from artists and ignore title update') if self.config['auto']: - self.import_stages = [self.imported] + self._import_stages = [self.imported] def commands(self): diff --git a/beetsplug/keyfinder.py b/beetsplug/keyfinder.py index 6298ce6ca..726b85364 100644 --- a/beetsplug/keyfinder.py +++ b/beetsplug/keyfinder.py @@ -36,7 +36,7 @@ class KeyFinderPlugin(BeetsPlugin): u'overwrite': False, }) self.config['auto'].get(bool) - self.import_stages = [self.imported] + self._import_stages = [self.imported] def commands(self): cmd = ui.Subcommand('keyfinder', diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index 8ef97c6d2..bee44ffc0 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -145,7 +145,7 @@ class LastGenrePlugin(plugins.BeetsPlugin): """Setup plugin from config options """ if self.config['auto']: - self.import_stages = [self.imported] + self._import_stages = [self.imported] self._genre_cache = {} diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 2f889ac8d..3c413c49e 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -442,7 +442,7 @@ SOURCE_BACKENDS = { class LyricsPlugin(plugins.BeetsPlugin): def __init__(self): super(LyricsPlugin, self).__init__() - self.import_stages = [self.imported] + self._import_stages = [self.imported] self.config.add({ 'auto': True, 'google_API_key': None, diff --git a/beetsplug/mbcollection.py b/beetsplug/mbcollection.py index 870e337d9..52a0cef39 100644 --- a/beetsplug/mbcollection.py +++ b/beetsplug/mbcollection.py @@ -105,7 +105,7 @@ class MusicBrainzCollectionPlugin(BeetsPlugin): ) self.config.add({'auto': False}) if self.config['auto']: - self.import_stages = [self.imported] + self._import_stages = [self.imported] def commands(self): return [update_mb_collection_cmd] diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 6cf83161a..b1a593dab 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -601,7 +601,7 @@ class ReplayGainPlugin(BeetsPlugin): def __init__(self): super(ReplayGainPlugin, self).__init__() - self.import_stages = [self.imported] + self._import_stages = [self.imported] # default backend is 'command' for backward-compatibility. self.config.add({ From 1afe82fb41c82cff0876c35609d4fca7906063cd Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Mon, 5 Jan 2015 12:40:21 +0100 Subject: [PATCH 33/86] Make 2 plugins rely on auto log level mgmt ftintitle and title don't do manual management anymore. --- beetsplug/ftintitle.py | 20 ++++++++++---------- beetsplug/lyrics.py | 20 +++++++------------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/beetsplug/ftintitle.py b/beetsplug/ftintitle.py index b72113e4c..22504a1fe 100644 --- a/beetsplug/ftintitle.py +++ b/beetsplug/ftintitle.py @@ -46,14 +46,14 @@ def contains_feat(title): return bool(re.search(plugins.feat_tokens(), title, flags=re.IGNORECASE)) -def update_metadata(item, feat_part, drop_feat, loglevel=logging.DEBUG): +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.log(loglevel, u'artist: {0} -> {1}', item.artist, item.albumartist) + 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. @@ -63,11 +63,11 @@ def update_metadata(item, feat_part, drop_feat, loglevel=logging.DEBUG): # 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.log(loglevel, u'title: {0} -> {1}', item.title, new_title) + log.info(u'title: {0} -> {1}', item.title, new_title) item.title = new_title -def ft_in_title(item, drop_feat, loglevel=logging.DEBUG): +def ft_in_title(item, drop_feat): """Look for featured artists in the item's artist fields and move them to the title. """ @@ -79,14 +79,14 @@ def ft_in_title(item, drop_feat, loglevel=logging.DEBUG): # 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.log(loglevel, displayable_path(item.path)) + 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.log(loglevel, 'album artist not present in artist') + 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 @@ -104,9 +104,9 @@ def ft_in_title(item, drop_feat, loglevel=logging.DEBUG): # If we have a featuring artist, move it to the title. if feat_part: - update_metadata(item, feat_part, drop_feat, loglevel) + update_metadata(item, feat_part, drop_feat) else: - log.log(loglevel, u'no featuring artists found') + log.info(u'no featuring artists found') class FtInTitlePlugin(plugins.BeetsPlugin): @@ -138,7 +138,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, logging.INFO) + ft_in_title(item, drop_feat) item.store() if write: item.try_write() @@ -152,5 +152,5 @@ class FtInTitlePlugin(plugins.BeetsPlugin): drop_feat = self.config['drop'].get(bool) for item in task.imported_items(): - ft_in_title(item, drop_feat, logging.DEBUG) + ft_in_title(item, drop_feat) item.store() diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 3c413c49e..710cdd2e4 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -477,7 +477,7 @@ class LyricsPlugin(plugins.BeetsPlugin): write = config['import']['write'].get(bool) for item in lib.items(ui.decargs(args)): self.fetch_item_lyrics( - lib, logging.INFO, item, write, + lib, item, write, opts.force_refetch or self.config['force'], ) if opts.printlyr and item.lyrics: @@ -491,19 +491,15 @@ class LyricsPlugin(plugins.BeetsPlugin): """ if self.config['auto']: for item in task.imported_items(): - self.fetch_item_lyrics(session.lib, logging.DEBUG, item, + self.fetch_item_lyrics(session.lib, item, False, self.config['force']) - def fetch_item_lyrics(self, lib, loglevel, item, write, force): + def fetch_item_lyrics(self, lib, item, write, force): """Fetch and store lyrics for a single item. If ``write``, then the - lyrics will also be written to the file itself. The ``loglevel`` - parameter controls the visibility of the function's status log - messages. - """ + lyrics will also be written to the file itself.""" # Skip if the item already has lyrics. if not force and item.lyrics: - log.log(loglevel, u'lyrics already present: {0} - {1}', - item.artist, item.title) + log.info(u'lyrics already present: {0.artist} - {0.title}', item) return lyrics = None @@ -515,11 +511,9 @@ class LyricsPlugin(plugins.BeetsPlugin): lyrics = u"\n\n---\n\n".join([l for l in lyrics if l]) if lyrics: - log.log(loglevel, u'fetched lyrics: {0} - {1}', - item.artist, item.title) + log.info(u'fetched lyrics: {0} - {1}', item.artist, item.title) else: - log.log(loglevel, u'lyrics not found: {0} - {1}', - item.artist, item.title) + log.info(u'lyrics not found: {0} - {1}', item.artist, item.title) fallback = self.config['fallback'].get() if fallback: lyrics = fallback From 78ac338c2875854e0ea31fe6287784610273e1fd Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Mon, 5 Jan 2015 20:44:25 +0100 Subject: [PATCH 34/86] Delete _log definition where it was already used Easy switch to the new logger --- beetsplug/ihate.py | 6 +----- beetsplug/the.py | 2 -- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/beetsplug/ihate.py b/beetsplug/ihate.py index 5d3d3a001..37cf07f74 100644 --- a/beetsplug/ihate.py +++ b/beetsplug/ihate.py @@ -14,7 +14,6 @@ """Warns you about things you hate (or even blocks import).""" -from beets import logging from beets.plugins import BeetsPlugin from beets.importer import action from beets.library import parse_query_string @@ -37,8 +36,6 @@ def summary(task): class IHatePlugin(BeetsPlugin): - _log = logging.getLogger(__name__) - def __init__(self): super(IHatePlugin, self).__init__() self.register_listener('import_task_choice', @@ -75,8 +72,7 @@ class IHatePlugin(BeetsPlugin): self._log.info(u'skipped: {0}', summary(task)) return if self.do_i_hate_this(task, warn_queries): - self._log.info(u'you may hate this: {0}', - summary(task)) + self._log.info(u'you may hate this: {0}', summary(task)) else: self._log.debug(u'nothing to do') else: diff --git a/beetsplug/the.py b/beetsplug/the.py index 248431457..edc653787 100644 --- a/beetsplug/the.py +++ b/beetsplug/the.py @@ -15,7 +15,6 @@ """Moves patterns in path formats (suitable for moving articles).""" import re -from beets import logging from beets.plugins import BeetsPlugin __author__ = 'baobab@heresiarch.info' @@ -29,7 +28,6 @@ FORMAT = u'{0}, {1}' class ThePlugin(BeetsPlugin): _instance = None - _log = logging.getLogger(__name__) the = True a = True From 0617c0410f43c49a98b3505cd1e91c134c44a9a7 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 6 Jan 2015 10:45:53 +0100 Subject: [PATCH 35/86] Remove log declarations where it was unused Easiest conversions! --- beetsplug/bucket.py | 3 --- beetsplug/freedesktop.py | 3 --- 2 files changed, 6 deletions(-) diff --git a/beetsplug/bucket.py b/beetsplug/bucket.py index e8f202875..8d09f8988 100644 --- a/beetsplug/bucket.py +++ b/beetsplug/bucket.py @@ -20,11 +20,8 @@ import re import string from itertools import tee, izip -from beets import logging from beets import plugins, ui -log = logging.getLogger(__name__) - class BucketError(Exception): pass diff --git a/beetsplug/freedesktop.py b/beetsplug/freedesktop.py index cfe7c4cd7..aee180e49 100644 --- a/beetsplug/freedesktop.py +++ b/beetsplug/freedesktop.py @@ -15,15 +15,12 @@ """Creates freedesktop.org-compliant .directory files on an album level. """ -from beets import logging from beets.plugins import BeetsPlugin from beets.ui import Subcommand from beets.ui import decargs import os -log = logging.getLogger(__name__) - def process_query(lib, opts, args): for album in lib.albums(decargs(args)): From e14a54df055781000b362c7d71168ccfbcc4d58b Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Mon, 5 Jan 2015 21:26:56 +0100 Subject: [PATCH 36/86] Convert multiple plugins' logger usage (easy ones) Those plugins only called methods and no function, which eases the conversion. --- beetsplug/beatport.py | 11 +++---- beetsplug/bpm.py | 12 +++---- beetsplug/discogs.py | 15 ++++----- beetsplug/echonest.py | 72 ++++++++++++++++++++---------------------- beetsplug/inline.py | 4 +-- beetsplug/keyfinder.py | 10 ++---- beetsplug/rewrite.py | 5 +-- beetsplug/spotify.py | 43 +++++++++++++------------ beetsplug/zero.py | 15 ++++----- 9 files changed, 85 insertions(+), 102 deletions(-) diff --git a/beetsplug/beatport.py b/beetsplug/beatport.py index e769f82c5..cb3579750 100644 --- a/beetsplug/beatport.py +++ b/beetsplug/beatport.py @@ -19,12 +19,9 @@ from datetime import datetime, timedelta import requests -from beets import logging from beets.autotag.hooks import AlbumInfo, TrackInfo, Distance from beets.plugins import BeetsPlugin -log = logging.getLogger(__name__) - class BeatportAPIError(Exception): pass @@ -194,7 +191,7 @@ class BeatportPlugin(BeetsPlugin): try: return self._get_releases(query) except BeatportAPIError as e: - log.debug(u'Beatport API Error: {0} (query: {1})', e, query) + self._log.debug(u'Beatport API Error: {0} (query: {1})', e, query) return [] def item_candidates(self, item, artist, title): @@ -205,14 +202,14 @@ class BeatportPlugin(BeetsPlugin): try: return self._get_tracks(query) except BeatportAPIError as e: - log.debug(u'API Error: {0} (query: {1})', e, query) + self._log.debug(u'API Error: {0} (query: {1})', e, query) return [] def album_for_id(self, release_id): """Fetches a release by its Beatport ID and returns an AlbumInfo object or None if the release is not found. """ - log.debug(u'Searching for release {0}', release_id) + self._log.debug(u'Searching for release {0}', release_id) match = re.search(r'(^|beatport\.com/release/.+/)(\d+)$', release_id) if not match: return None @@ -224,7 +221,7 @@ class BeatportPlugin(BeetsPlugin): """Fetches a track by its Beatport ID and returns a TrackInfo object or None if the track is not found. """ - log.debug(u'Searching for track {0}', track_id) + self._log.debug(u'Searching for track {0}', track_id) match = re.search(r'(^|beatport\.com/track/.+/)(\d+)$', track_id) if not match: return None diff --git a/beetsplug/bpm.py b/beetsplug/bpm.py index b8f42f950..257113db6 100644 --- a/beetsplug/bpm.py +++ b/beetsplug/bpm.py @@ -16,11 +16,9 @@ import time -from beets import ui, logging +from beets import ui from beets.plugins import BeetsPlugin -log = logging.getLogger(__name__) - def bpm(max_strokes): """Returns average BPM (possibly of a playing song) @@ -72,15 +70,15 @@ class BPMPlugin(BeetsPlugin): item = items[0] if item['bpm']: - log.info(u'Found bpm {0}', item['bpm']) + self._log.info(u'Found bpm {0}', item['bpm']) if not overwrite: return - log.info(u'Press Enter {0} times to the rhythm or Ctrl-D ' - u'to exit', self.config['max_strokes'].get(int)) + self._log.info(u'Press Enter {0} times to the rhythm or Ctrl-D ' + u'to exit', self.config['max_strokes'].get(int)) new_bpm = bpm(self.config['max_strokes'].get(int)) item['bpm'] = int(new_bpm) if write: item.try_write() item.store() - log.info(u'Added new bpm {0}', item['bpm']) + self._log.info(u'Added new bpm {0}', item['bpm']) diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index 8fe227726..e2695688a 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -27,7 +27,6 @@ import re import time import json -log = logging.getLogger(__name__) # Silence spurious INFO log lines generated by urllib3. urllib3_logger = logging.getLogger('requests.packages.urllib3') @@ -89,7 +88,7 @@ class DiscogsPlugin(BeetsPlugin): raise beets.ui.UserError('Discogs authorization failed') # Save the token for later use. - log.debug('Discogs token {0}, secret {1}', token, secret) + self._log.debug('Discogs token {0}, secret {1}', token, secret) with open(self._tokenfile(), 'w') as f: json.dump({'token': token, 'secret': secret}, f) @@ -117,10 +116,10 @@ class DiscogsPlugin(BeetsPlugin): try: return self.get_albums(query) except DiscogsAPIError as e: - log.debug(u'API Error: {0} (query: {1})', e, query) + self._log.debug(u'API Error: {0} (query: {1})', e, query) return [] except ConnectionError as e: - log.debug(u'HTTP Connection Error: {0}', e) + self._log.debug(u'HTTP Connection Error: {0}', e) return [] def album_for_id(self, album_id): @@ -130,7 +129,7 @@ class DiscogsPlugin(BeetsPlugin): if not self.discogs_client: return - log.debug(u'Searching for release {0}', album_id) + self._log.debug(u'Searching for release {0}', album_id) # Discogs-IDs are simple integers. We only look for those at the end # of an input string as to avoid confusion with other metadata plugins. # An optional bracket can follow the integer, as this is how discogs @@ -145,10 +144,10 @@ class DiscogsPlugin(BeetsPlugin): getattr(result, 'title') except DiscogsAPIError as e: if e.message != '404 Not Found': - log.debug(u'API Error: {0} (query: {1})', e, result._uri) + self._log.debug(u'API Error: {0} (query: {1})', e, result._uri) return None except ConnectionError as e: - log.debug(u'HTTP Connection Error: {0}', e) + self._log.debug(u'HTTP Connection Error: {0}', e) return None return self.get_album_info(result) @@ -293,7 +292,7 @@ class DiscogsPlugin(BeetsPlugin): if match: medium, index = match.groups() else: - log.debug(u'Invalid position: {0}', position) + self._log.debug(u'Invalid position: {0}', position) medium = index = None return medium or None, index or None diff --git a/beetsplug/echonest.py b/beetsplug/echonest.py index e52f48ff5..c93901875 100644 --- a/beetsplug/echonest.py +++ b/beetsplug/echonest.py @@ -21,14 +21,12 @@ import tempfile from string import Template import subprocess -from beets import util, config, plugins, ui, logging +from beets import util, config, plugins, ui from beets.dbcore import types import pyechonest import pyechonest.song import pyechonest.track -log = logging.getLogger(__name__) - # If a request at the EchoNest fails, we want to retry the request RETRIES # times and wait between retries for RETRY_INTERVAL seconds. RETRIES = 10 @@ -152,30 +150,30 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): except pyechonest.util.EchoNestAPIError as e: if e.code == 3: # reached access limit per minute - log.debug(u'rate-limited on try {0}; waiting {1} seconds', - i + 1, RETRY_INTERVAL) + self._log.debug(u'rate-limited on try {0}; waiting {1} ' + u'seconds', i + 1, RETRY_INTERVAL) time.sleep(RETRY_INTERVAL) elif e.code == 5: # specified identifier does not exist # no use in trying again. - log.debug(u'{0}', e) + self._log.debug(u'{0}', e) return None else: - log.error(u'{0}', e.args[0][0]) + self._log.error(u'{0}', e.args[0][0]) return None except (pyechonest.util.EchoNestIOError, socket.error) as e: - log.warn(u'IO error: {0}', e) + self._log.warn(u'IO error: {0}', e) time.sleep(RETRY_INTERVAL) except Exception as e: # there was an error analyzing the track, status: error - log.debug(u'{0}', e) + self._log.debug(u'{0}', e) return None else: break else: # If we exited the loop without breaking, then we used up all # our allotted retries. - log.error(u'request failed repeatedly') + self._log.error(u'request failed repeatedly') return None return result @@ -186,7 +184,7 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): seconds, it's considered a match. """ if not songs: - log.debug(u'no songs found') + self._log.debug(u'no songs found') return pick = None @@ -224,13 +222,13 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): # Look up the Echo Nest ID based on the MBID. else: if not item.mb_trackid: - log.debug(u'no ID available') + self._log.debug(u'no ID available') return mbid = 'musicbrainz:track:{0}'.format(item.mb_trackid) track = self._echofun(pyechonest.track.track_from_id, identifier=mbid) if not track: - log.debug(u'lookup by MBID failed') + self._log.debug(u'lookup by MBID failed') return enid = track.song_id @@ -290,9 +288,9 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): fd, dest = tempfile.mkstemp(u'.ogg') os.close(fd) - log.info(u'encoding {0} to {1}', - util.displayable_path(source), - util.displayable_path(dest)) + self._log.info(u'encoding {0} to {1}', + util.displayable_path(source), + util.displayable_path(dest)) opts = [] for arg in CONVERT_COMMAND.split(): @@ -303,12 +301,11 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): try: util.command_output(opts) except (OSError, subprocess.CalledProcessError) as exc: - log.debug(u'encode failed: {0}', exc) + self._log.debug(u'encode failed: {0}', exc) util.remove(dest) return - log.info(u'finished encoding {0}', - util.displayable_path(source)) + self._log.info(u'finished encoding {0}', util.displayable_path(source)) return dest def truncate(self, source): @@ -316,9 +313,9 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): fd, dest = tempfile.mkstemp(u'.ogg') os.close(fd) - log.info(u'truncating {0} to {1}', - util.displayable_path(source), - util.displayable_path(dest)) + self._log.info(u'truncating {0} to {1}', + util.displayable_path(source), + util.displayable_path(dest)) opts = [] for arg in TRUNCATE_COMMAND.split(): @@ -329,12 +326,11 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): try: util.command_output(opts) except (OSError, subprocess.CalledProcessError) as exc: - log.debug(u'truncate failed: {0}', exc) + self._log.debug(u'truncate failed: {0}', exc) util.remove(dest) return - log.info(u'truncate encoding {0}', - util.displayable_path(source)) + self._log.info(u'truncate encoding {0}', util.displayable_path(source)) return dest def analyze(self, item): @@ -343,18 +339,18 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): """ prepared = self.prepare_upload(item) if not prepared: - log.debug(u'could not prepare file for upload') + self._log.debug(u'could not prepare file for upload') return source, tmp = prepared - log.info(u'uploading file, please be patient') + self._log.info(u'uploading file, please be patient') track = self._echofun(pyechonest.track.track_from_filename, filename=source) if tmp is not None: util.remove(tmp) if not track: - log.debug(u'failed to upload file') + self._log.debug(u'failed to upload file') return # Sometimes we have a track but no song. I guess this happens for @@ -405,12 +401,12 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): for method in methods: song = method(item) if song: - log.debug(u'got song through {0}: {1} - {2} [{3}]', - method.__name__, - item.artist, - item.title, - song.get('duration'), - ) + self._log.debug(u'got song through {0}: {1} - {2} [{3}]', + method.__name__, + item.artist, + item.title, + song.get('duration'), + ) return song def apply_metadata(self, item, values, write=False): @@ -421,7 +417,7 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): for k, v in values.iteritems(): if k in ATTRIBUTES: field = ATTRIBUTES[k] - log.debug(u'metadata: {0} = {1}', field, v) + self._log.debug(u'metadata: {0} = {1}', field, v) if field == 'bpm': item[field] = int(v) else: @@ -433,7 +429,7 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): item['initial_key'] = key if 'id' in values: enid = values['id'] - log.debug(u'metadata: {0} = {1}', ID_KEY, enid) + self._log.debug(u'metadata: {0} = {1}', ID_KEY, enid) item[ID_KEY] = enid # Write and save. @@ -460,7 +456,7 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): for field in ATTRIBUTES.values(): if not item.get(field): return True - log.info(u'no update required') + self._log.info(u'no update required') return False def commands(self): @@ -475,7 +471,7 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): self.config.set_args(opts) write = config['import']['write'].get(bool) for item in lib.items(ui.decargs(args)): - log.info(u'{0} - {1}', item.artist, item.title) + self._log.info(u'{0} - {1}', item.artist, item.title) if self.config['force'] or self.requires_update(item): song = self.fetch_song(item) if song: diff --git a/beetsplug/inline.py b/beetsplug/inline.py index 7fa6037a3..078aff39c 100644 --- a/beetsplug/inline.py +++ b/beetsplug/inline.py @@ -111,14 +111,14 @@ class InlinePlugin(BeetsPlugin): # Item fields. for key, view in itertools.chain(config['item_fields'].items(), config['pathfields'].items()): - log.debug(u'adding item field {0}', key) + self._log.debug(u'adding item field {0}', key) func = 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(): - log.debug(u'adding album field {0}', key) + self._log.debug(u'adding album field {0}', key) func = compile_inline(view.get(unicode), True) if func is not None: self.album_template_fields[key] = func diff --git a/beetsplug/keyfinder.py b/beetsplug/keyfinder.py index 726b85364..4f0380528 100644 --- a/beetsplug/keyfinder.py +++ b/beetsplug/keyfinder.py @@ -17,15 +17,11 @@ import subprocess -from beets import logging from beets import ui from beets import util from beets.plugins import BeetsPlugin -log = logging.getLogger(__name__) - - class KeyFinderPlugin(BeetsPlugin): def __init__(self): @@ -62,11 +58,11 @@ class KeyFinderPlugin(BeetsPlugin): try: key = util.command_output([bin, '-f', item.path]) except (subprocess.CalledProcessError, OSError) as exc: - log.error(u'execution failed: {0}', exc) + self._log.error(u'execution failed: {0}', exc) continue item['initial_key'] = key - log.debug(u'added computed initial key {0} for {1}', - key, util.displayable_path(item.path)) + self._log.debug(u'added computed initial key {0} for {1}', + key, util.displayable_path(item.path)) item.try_write() item.store() diff --git a/beetsplug/rewrite.py b/beetsplug/rewrite.py index 8f005dda4..9aaa05950 100644 --- a/beetsplug/rewrite.py +++ b/beetsplug/rewrite.py @@ -19,12 +19,9 @@ import re from collections import defaultdict from beets.plugins import BeetsPlugin -from beets import logging from beets import ui from beets import library -log = logging.getLogger(__name__) - def rewriter(field, rules): """Create a template field function that rewrites the given field @@ -59,7 +56,7 @@ class RewritePlugin(BeetsPlugin): if fieldname not in library.Item._fields: raise ui.UserError("invalid field name (%s) in rewriter" % fieldname) - log.debug(u'adding template field {0}', key) + self._log.debug(u'adding template field {0}', key) pattern = re.compile(pattern.lower()) rules[fieldname].append((pattern, value)) if fieldname == 'artist': diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index b50b06cb9..5833efb58 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -4,11 +4,9 @@ import webbrowser import requests from beets.plugins import BeetsPlugin from beets.ui import decargs -from beets import ui, logging +from beets import ui from requests.exceptions import HTTPError -log = logging.getLogger(__name__) - class SpotifyPlugin(BeetsPlugin): @@ -62,7 +60,8 @@ class SpotifyPlugin(BeetsPlugin): self.config['show_failures'].set(True) if self.config['mode'].get() not in ['list', 'open']: - log.warn(u'{0} is not a valid mode', self.config['mode'].get()) + self._log.warn(u'{0} is not a valid mode', + self.config['mode'].get()) return False self.opts = opts @@ -76,10 +75,11 @@ class SpotifyPlugin(BeetsPlugin): items = lib.items(query) if not items: - log.debug(u'Your beets query returned no items, skipping spotify') + self._log.debug(u'Your beets query returned no items, ' + u'skipping spotify') return - log.info(u'Processing {0} tracks...', len(items)) + self._log.info(u'Processing {0} tracks...', len(items)) for item in items: @@ -107,11 +107,12 @@ class SpotifyPlugin(BeetsPlugin): r = requests.get(self.base_url, params={ "q": search_url, "type": "track" }) - log.debug(r.url) + self._log.debug(r.url) try: r.raise_for_status() except HTTPError as e: - log.debug(u'URL returned a {0} error', e.response.status_code) + self._log.debug(u'URL returned a {0} error', + e.response.status_code) failures.append(search_url) continue @@ -127,31 +128,33 @@ class SpotifyPlugin(BeetsPlugin): # Simplest, take the first result chosen_result = None if len(r_data) == 1 or self.config['tiebreak'].get() == "first": - log.debug(u'Spotify track(s) found, count: {0}', len(r_data)) + self._log.debug(u'Spotify track(s) found, count: {0}', + len(r_data)) chosen_result = r_data[0] elif len(r_data) > 1: # Use the popularity filter - log.debug(u'Most popular track chosen, count: {0}', - len(r_data)) + self._log.debug(u'Most popular track chosen, count: {0}', + len(r_data)) chosen_result = max(r_data, key=lambda x: x['popularity']) if chosen_result: results.append(chosen_result) else: - log.debug(u'No spotify track found: {0}', search_url) + self._log.debug(u'No spotify track found: {0}', search_url) failures.append(search_url) failure_count = len(failures) if failure_count > 0: if self.config['show_failures'].get(): - log.info(u'{0} track(s) did not match a Spotify ID:', - failure_count) + self._log.info(u'{0} track(s) did not match a Spotify ID:', + failure_count) for track in failures: - log.info(u'track: {0}', track) - log.info(u'') + self._log.info(u'track: {0}', track) + self._log.info(u'') else: - log.warn(u'{0} track(s) did not match a Spotify ID;\n' - u'use --show-failures to display', failure_count) + self._log.warn(u'{0} track(s) did not match a Spotify ID;\n' + u'use --show-failures to display', + failure_count) return results @@ -159,7 +162,7 @@ class SpotifyPlugin(BeetsPlugin): if results: ids = map(lambda x: x['id'], results) if self.config['mode'].get() == "open": - log.info(u'Attempting to open Spotify with playlist') + self._log.info(u'Attempting to open Spotify with playlist') spotify_url = self.playlist_partial + ",".join(ids) webbrowser.open(spotify_url) @@ -167,4 +170,4 @@ class SpotifyPlugin(BeetsPlugin): for item in ids: print(unicode.encode(self.open_url + item)) else: - log.warn(u'No Spotify tracks found from beets query') + self._log.warn(u'No Spotify tracks found from beets query') diff --git a/beetsplug/zero.py b/beetsplug/zero.py index aab798d26..937d23bd3 100644 --- a/beetsplug/zero.py +++ b/beetsplug/zero.py @@ -15,7 +15,6 @@ """ Clears tag fields in media files.""" import re -from beets import logging from beets.plugins import BeetsPlugin from beets.mediafile import MediaFile from beets.importer import action @@ -24,8 +23,6 @@ from beets.util import confit __author__ = 'baobab@heresiarch.info' __version__ = '0.10' -log = logging.getLogger(__name__) - class ZeroPlugin(BeetsPlugin): @@ -48,11 +45,11 @@ class ZeroPlugin(BeetsPlugin): for field in self.config['fields'].as_str_seq(): if field in ('id', 'path', 'album_id'): - log.warn(u'field \'{0}\' ignored, zeroing ' - u'it would be dangerous', field) + self._log.warn(u'field \'{0}\' ignored, zeroing ' + u'it would be dangerous', field) continue if field not in MediaFile.fields(): - log.error(u'invalid field: {0}', field) + self._log.error(u'invalid field: {0}', field) continue try: @@ -64,7 +61,7 @@ class ZeroPlugin(BeetsPlugin): def import_task_choice_event(self, session, task): """Listen for import_task_choice event.""" if task.choice_flag == action.ASIS and not self.warned: - log.warn(u'cannot zero in \"as-is\" mode') + self._log.warn(u'cannot zero in \"as-is\" mode') self.warned = True # TODO request write in as-is mode @@ -85,7 +82,7 @@ class ZeroPlugin(BeetsPlugin): by `self.patterns`. """ if not self.patterns: - log.warn(u'no fields, nothing to do') + self._log.warn(u'no fields, nothing to do') return for field, patterns in self.patterns.items(): @@ -97,5 +94,5 @@ class ZeroPlugin(BeetsPlugin): match = patterns is True if match: - log.debug(u'{0}: {1} -> None', field, value) + self._log.debug(u'{0}: {1} -> None', field, value) tags[field] = None From 1be3dade049bcfc93887cce13dfacce0b1deadea Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Mon, 5 Jan 2015 21:10:46 +0100 Subject: [PATCH 37/86] =?UTF-8?q?Convert=20freedesktop=20plugin=20logging:?= =?UTF-8?q?=20func=20=E2=86=92=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This serves as an introduction for many plugin updates to come. Multiple functions have to turn into methods -- and this is one of the shortest plugins. --- beetsplug/freedesktop.py | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/beetsplug/freedesktop.py b/beetsplug/freedesktop.py index aee180e49..f862f6352 100644 --- a/beetsplug/freedesktop.py +++ b/beetsplug/freedesktop.py @@ -22,21 +22,6 @@ from beets.ui import decargs import os -def process_query(lib, opts, args): - for album in lib.albums(decargs(args)): - process_album(album) - - -def process_album(album): - albumpath = album.item_dir() - if album.artpath: - fullartpath = album.artpath - artfile = os.path.split(fullartpath)[1] - create_file(albumpath, artfile) - else: - log.debug(u'album has no art') - - def create_file(albumpath, artfile): file_contents = "[Desktop Entry]\nIcon=./" + artfile outfilename = os.path.join(albumpath, ".directory") @@ -58,11 +43,24 @@ class FreedesktopPlugin(BeetsPlugin): def commands(self): freedesktop_command = Subcommand("freedesktop", help="Create .directory files") - freedesktop_command.func = process_query + freedesktop_command.func = self.process_query return [freedesktop_command] def imported(self, lib, album): automatic = self.config['auto'].get(bool) if not automatic: return - process_album(album) + self.process_album(album) + + def process_query(self, lib, opts, args): + for album in lib.albums(decargs(args)): + self.process_album(album) + + def process_album(self, album): + albumpath = album.item_dir() + if album.artpath: + fullartpath = album.artpath + artfile = os.path.split(fullartpath)[1] + create_file(albumpath, artfile) + else: + self._log.debug(u'album has no art') From 38352f70588cbf4f21ca14b69597695c96929603 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Mon, 5 Jan 2015 21:29:25 +0100 Subject: [PATCH 38/86] Update chroma plugin logging: pas logger around The logger is passed as an argument to multiple function. This showcases the second possibility for using the new logging system. Several similar conversions to be expected. --- beetsplug/chroma.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/beetsplug/chroma.py b/beetsplug/chroma.py index b5a1e2452..2f8bf761b 100644 --- a/beetsplug/chroma.py +++ b/beetsplug/chroma.py @@ -19,11 +19,11 @@ from beets import plugins from beets import ui from beets import util from beets import config -from beets import logging from beets.util import confit from beets.autotag import hooks import acoustid from collections import defaultdict +from functools import partial API_KEY = '1vOwZtEn' SCORE_THRESH = 0.5 @@ -32,8 +32,6 @@ COMMON_REL_THRESH = 0.6 # How many tracks must have an album in common? MAX_RECORDINGS = 5 MAX_RELEASES = 5 -log = logging.getLogger(__name__) - # Stores the Acoustid match information for each track. This is # populated when an import task begins and then used when searching for # candidates. It maps audio file paths to (recording_ids, release_ids) @@ -57,7 +55,7 @@ def prefix(it, count): yield v -def acoustid_match(path): +def acoustid_match(log, path): """Gets metadata for a file from Acoustid and populates the _matches, _fingerprints, and _acoustids dictionaries accordingly. """ @@ -135,7 +133,8 @@ class AcoustidPlugin(plugins.BeetsPlugin): }) if self.config['auto']: - self.register_listener('import_task_start', fingerprint_task) + self.register_listener('import_task_start', + partial(fingerprint_task, self._log)) def track_distance(self, item, info): dist = hooks.Distance() @@ -154,7 +153,7 @@ class AcoustidPlugin(plugins.BeetsPlugin): if album: albums.append(album) - log.debug(u'acoustid album candidates: {0}', len(albums)) + self._log.debug(u'acoustid album candidates: {0}', len(albums)) return albums def item_candidates(self, item, artist, title): @@ -167,7 +166,7 @@ class AcoustidPlugin(plugins.BeetsPlugin): track = hooks.track_for_mbid(recording_id) if track: tracks.append(track) - log.debug(u'acoustid item candidates: {0}', len(tracks)) + self._log.debug(u'acoustid item candidates: {0}', len(tracks)) return tracks def commands(self): @@ -179,7 +178,7 @@ class AcoustidPlugin(plugins.BeetsPlugin): apikey = config['acoustid']['apikey'].get(unicode) except confit.NotFoundError: raise ui.UserError('no Acoustid user API key provided') - submit_items(apikey, lib.items(ui.decargs(args))) + submit_items(self._log, apikey, lib.items(ui.decargs(args))) submit_cmd.func = submit_cmd_func fingerprint_cmd = ui.Subcommand( @@ -189,7 +188,7 @@ class AcoustidPlugin(plugins.BeetsPlugin): def fingerprint_cmd_func(lib, opts, args): for item in lib.items(ui.decargs(args)): - fingerprint_item(item, + fingerprint_item(self._log, item, write=config['import']['write'].get(bool)) fingerprint_cmd.func = fingerprint_cmd_func @@ -199,13 +198,13 @@ class AcoustidPlugin(plugins.BeetsPlugin): # Hooks into import process. -def fingerprint_task(task, session): +def fingerprint_task(log, task, session): """Fingerprint each item in the task for later use during the autotagging candidate search. """ items = task.items if task.is_album else [task.item] for item in items: - acoustid_match(item.path) + acoustid_match(log, item.path) @AcoustidPlugin.listen('import_task_apply') @@ -222,7 +221,7 @@ def apply_acoustid_metadata(task, session): # UI commands. -def submit_items(userkey, items, chunksize=64): +def submit_items(log, userkey, items, chunksize=64): """Submit fingerprints for the items to the Acoustid server. """ data = [] # The running list of dictionaries to submit. @@ -269,7 +268,7 @@ def submit_items(userkey, items, chunksize=64): submit_chunk() -def fingerprint_item(item, write=False): +def fingerprint_item(log, item, write=False): """Get the fingerprint for an Item. If the item already has a fingerprint, it is not regenerated. If fingerprint generation fails, return None. If the items are associated with a library, they are From 7d58a38428258af9646b9bc4e854a398cc47b202 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 6 Jan 2015 11:03:57 +0100 Subject: [PATCH 39/86] Convert replaygain: pass logger to other classes This time the logger is passed to the other classes of the module, not to functions. --- beetsplug/replaygain.py | 63 +++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index b1a593dab..e0c692000 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -25,8 +25,6 @@ from beets.plugins import BeetsPlugin from beets.util import syspath, command_output, displayable_path from beets import config -log = logging.getLogger(__name__) - # Utilities. @@ -67,10 +65,11 @@ AlbumGain = collections.namedtuple("AlbumGain", "album_gain track_gains") class Backend(object): """An abstract class representing engine for calculating RG values. """ - def __init__(self, config): + def __init__(self, config, log): """Initialize the backend with the configuration view for the plugin. """ + self._log = log def compute_track_gain(self, items): raise NotImplementedError() @@ -85,7 +84,8 @@ class Backend(object): class CommandBackend(Backend): - def __init__(self, config): + def __init__(self, config, log): + super(CommandBackend, self).__init__(config, log) config.add({ 'command': u"", 'noclip': True, @@ -135,7 +135,7 @@ class CommandBackend(Backend): supported_items = filter(self.format_supported, album.items()) if len(supported_items) != len(album.items()): - log.debug(u'tracks are of unsupported format') + self._log.debug(u'tracks are of unsupported format') return AlbumGain(None, []) output = self.compute_gain(supported_items, True) @@ -180,10 +180,10 @@ class CommandBackend(Backend): cmd = cmd + ['-d', str(self.gain_offset)] cmd = cmd + [syspath(i.path) for i in items] - log.debug(u'analyzing {0} files', len(items)) - log.debug(u"executing {0}", " ".join(map(displayable_path, cmd))) + self._log.debug(u'analyzing {0} files', len(items)) + self._log.debug(u"executing {0}", " ".join(map(displayable_path, cmd))) output = call(cmd) - log.debug(u'analysis finished') + self._log.debug(u'analysis finished') results = self.parse_tool_output(output, len(items) + (1 if is_album else 0)) @@ -198,7 +198,7 @@ class CommandBackend(Backend): for line in text.split('\n')[1:num_lines + 1]: parts = line.split('\t') if len(parts) != 6 or parts[0] == 'File': - log.debug(u'bad tool output: {0}', text) + self._log.debug(u'bad tool output: {0}', text) raise ReplayGainError('mp3gain failed') d = { 'file': parts[0], @@ -467,7 +467,8 @@ class AudioToolsBackend(Backend): `_ and its capabilities to read more file formats and compute ReplayGain values using it replaygain module. """ - def __init__(self, config): + def __init__(self, config, log): + super(CommandBackend, self).__init__(config, log) self._import_audiotools() def _import_audiotools(self): @@ -547,8 +548,8 @@ class AudioToolsBackend(Backend): # be obtained from an audiofile instance. rg_track_gain, rg_track_peak = rg.title_gain(audiofile.to_pcm()) - log.debug(u'ReplayGain for track {0} - {1}: {2:.2f}, {3:.2f}', - item.artist, item.title, rg_track_gain, rg_track_peak) + self._log.debug(u'ReplayGain for track {0} - {1}: {2:.2f}, {3:.2f}', + item.artist, item.title, rg_track_gain, rg_track_peak) return Gain(gain=rg_track_gain, peak=rg_track_peak) def compute_album_gain(self, album): @@ -556,7 +557,7 @@ class AudioToolsBackend(Backend): :rtype: :class:`AlbumGain` """ - log.debug(u'Analysing album {0} - {1}', album.albumartist, album.album) + self._log.debug(u'Analysing album {0.albumartist} - {0.album}', album) # The first item is taken and opened to get the sample rate to # initialize the replaygain object. The object is used for all the @@ -572,14 +573,16 @@ class AudioToolsBackend(Backend): track_gains.append( Gain(gain=rg_track_gain, peak=rg_track_peak) ) - log.debug(u'ReplayGain for track {0} - {1}: {2:.2f}, {3:.2f}', - item.artist, item.title, rg_track_gain, rg_track_peak) + self._log.debug(u'ReplayGain for track {0.artist} - {0.title}: ' + u'{1:.2f}, {2:.2f}', + item, rg_track_gain, rg_track_peak) # After getting the values for all tracks, it's possible to get the # album values. rg_album_gain, rg_album_peak = rg.album_gain() - log.debug(u'ReplayGain for Album {0} - {1}: {2:.2f}, {3:.2f}', - album.albumartist, album.album, rg_album_gain, rg_album_peak) + self._log.debug(u'ReplayGain for album {0.albumartist} - {0.album}: ' + u'{1:.2f}, {2:.2f}', + album, rg_album_gain, rg_album_peak) return AlbumGain( Gain(gain=rg_album_gain, peak=rg_album_peak), @@ -650,16 +653,16 @@ class ReplayGainPlugin(BeetsPlugin): item.rg_track_peak = track_gain.peak item.store() - log.debug(u'applied track gain {0}, peak {1}', - item.rg_track_gain, item.rg_track_peak) + self._log.debug(u'applied track gain {0}, peak {1}', + item.rg_track_gain, item.rg_track_peak) def store_album_gain(self, album, album_gain): album.rg_album_gain = album_gain.gain album.rg_album_peak = album_gain.peak album.store() - log.debug(u'applied album gain {0}, peak {1}', - album.rg_album_gain, album.rg_album_peak) + self._log.debug(u'applied album gain {0}, peak {1}', + album.rg_album_gain, album.rg_album_peak) def handle_album(self, album, write): """Compute album and track replay gain store it in all of the @@ -670,11 +673,11 @@ class ReplayGainPlugin(BeetsPlugin): items, nothing is done. """ if not self.album_requires_gain(album): - log.info(u'Skipping album {0} - {1}', - album.albumartist, album.album) + self._log.info(u'Skipping album {0} - {1}', + album.albumartist, album.album) return - log.info(u'analyzing {0} - {1}', album.albumartist, album.album) + self._log.info(u'analyzing {0} - {1}', album.albumartist, album.album) try: album_gain = self.backend_instance.compute_album_gain(album) @@ -693,7 +696,7 @@ class ReplayGainPlugin(BeetsPlugin): if write: item.try_write() except ReplayGainError as e: - log.info(u"ReplayGain error: {0}", e) + self._log.info(u"ReplayGain error: {0}", e) except FatalReplayGainError as e: raise ui.UserError( u"Fatal replay gain error: {0}".format(e) @@ -707,10 +710,10 @@ class ReplayGainPlugin(BeetsPlugin): in the item, nothing is done. """ if not self.track_requires_gain(item): - log.info(u'Skipping track {0} - {1}', item.artist, item.title) + self._log.info(u'Skipping track {0.artist} - {0.title}', item) return - log.info(u'analyzing {0} - {1}', item.artist, item.title) + self._log.info(u'analyzing {0} - {1}', item.artist, item.title) try: track_gains = self.backend_instance.compute_track_gain([item]) @@ -725,7 +728,7 @@ class ReplayGainPlugin(BeetsPlugin): if write: item.try_write() except ReplayGainError as e: - log.info(u"ReplayGain error: {0}", e) + self._log.info(u"ReplayGain error: {0}", e) except FatalReplayGainError as e: raise ui.UserError( u"Fatal replay gain error: {0}".format(e) @@ -737,7 +740,7 @@ class ReplayGainPlugin(BeetsPlugin): if not self.automatic: return - log.setLevel(logging.WARN) + self._log.setLevel(logging.WARN) if task.is_album: self.handle_album(task.album, False) @@ -748,7 +751,7 @@ class ReplayGainPlugin(BeetsPlugin): """Return the "replaygain" ui subcommand. """ def func(lib, opts, args): - log.setLevel(logging.INFO) + self._log.setLevel(logging.INFO) write = config['import']['write'].get(bool) From 32673b87e79f037d50b3870d63596f918a661ff8 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 6 Jan 2015 11:25:51 +0100 Subject: [PATCH 40/86] Update multiple plugins: pass the logger around --- beetsplug/duplicates.py | 15 ++-- beetsplug/ftintitle.py | 131 +++++++++++++++----------------- beetsplug/info.py | 85 ++++++++++----------- beetsplug/inline.py | 105 +++++++++++++------------ beetsplug/lastgenre/__init__.py | 86 ++++++++++----------- beetsplug/lastimport.py | 11 ++- beetsplug/mbcollection.py | 86 ++++++++++----------- beetsplug/missing.py | 38 +++++---- beetsplug/mpdstats.py | 45 ++++++----- beetsplug/play.py | 9 +-- 10 files changed, 289 insertions(+), 322 deletions(-) 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] From 74e18afa94f7b60259d056fbd0f465c90286b3fd Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 6 Jan 2015 11:26:15 +0100 Subject: [PATCH 41/86] Convert bdp: pass around with static setter on classes --- beetsplug/bpd/__init__.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/beetsplug/bpd/__init__.py b/beetsplug/bpd/__init__.py index 6ed188656..a5de73339 100644 --- a/beetsplug/bpd/__init__.py +++ b/beetsplug/bpd/__init__.py @@ -71,7 +71,6 @@ SAFE_COMMANDS = ( ITEM_KEYS_WRITABLE = set(MediaFile.fields()).intersection(Item._fields.keys()) # Loggers. -log = logging.getLogger(__name__) global_log = logging.getLogger('beets') @@ -554,6 +553,8 @@ class Connection(object): """A connection between a client and the server. Handles input and output from and to the client. """ + _log = None + def __init__(self, server, sock): """Create a new connection for the accepted socket `client`. """ @@ -561,6 +562,10 @@ class Connection(object): self.sock = sock self.authenticated = False + @classmethod + def set_logger(cls, logger): + cls._log = logger + def send(self, lines): """Send lines, which which is either a single string or an iterable consisting of strings, to the client. A newline is @@ -570,7 +575,7 @@ class Connection(object): if isinstance(lines, basestring): lines = [lines] out = NEWLINE.join(lines) + NEWLINE - log.debug(out[:-1]) # Don't log trailing newline. + self._log.debug(out[:-1]) # Don't log trailing newline. if isinstance(out, unicode): out = out.encode('utf8') return self.sock.sendall(out) @@ -601,7 +606,7 @@ class Connection(object): line = line.strip() if not line: break - log.debug(line) + self._log.debug(line) if clist is not None: # Command list already opened. @@ -639,6 +644,7 @@ class Command(object): command_re = re.compile(r'^([^ \t]+)[ \t]*') arg_re = re.compile(r'"((?:\\"|[^"])+)"|([^ \t"]+)') + _log = None def __init__(self, s): """Creates a new `Command` from the given string, `s`, parsing @@ -660,6 +666,10 @@ class Command(object): arg = arg.decode('utf8') self.args.append(arg) + @classmethod + def set_logger(cls, logger): + cls._log = logger + def run(self, conn): """A coroutine that executes the command on the given connection. @@ -696,7 +706,7 @@ class Command(object): except Exception as e: # An "unintentional" error. Hide it from the client. - log.error(traceback.format_exc(e)) + self._log.error(traceback.format_exc(e)) raise BPDError(ERROR_SYSTEM, u'server error', self.name) @@ -1151,13 +1161,15 @@ class BPDPlugin(BeetsPlugin): 'password': u'', 'volume': VOLUME_MAX, }) + Connection.set_logger(self._log) + Server.set_logger(self._log) def start_bpd(self, lib, host, port, password, volume, debug): """Starts a BPD server.""" - if debug: - log.setLevel(logging.DEBUG) + if debug: # FIXME this should be managed by BeetsPlugin + self._log.setLevel(logging.DEBUG) else: - log.setLevel(logging.WARNING) + self._log.setLevel(logging.WARNING) try: server = Server(lib, host, port, password) server.cmd_setvol(None, volume) From 203b325ee7a44338261b66a2ab315f0032354fd9 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 6 Jan 2015 11:40:20 +0100 Subject: [PATCH 42/86] =?UTF-8?q?Convert=20mbsync=20logging:=20big=20funcs?= =?UTF-8?q?=20=E2=86=92=20methods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- beetsplug/mbsync.py | 203 ++++++++++++++++++++++---------------------- 1 file changed, 100 insertions(+), 103 deletions(-) diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py index 622277809..7bc26d954 100644 --- a/beetsplug/mbsync.py +++ b/beetsplug/mbsync.py @@ -15,100 +15,11 @@ """Update library's tags using MusicBrainz. """ from beets.plugins import BeetsPlugin -from beets import autotag, library, ui, util, logging +from beets import autotag, library, ui, util from beets.autotag import hooks from beets import config from collections import defaultdict -log = logging.getLogger(__name__) - - -def mbsync_singletons(lib, query, move, pretend, write): - """Retrieve and apply info from the autotagger for items matched by - query. - """ - for item in lib.items(query + ['singleton:true']): - if not item.mb_trackid: - log.info(u'Skipping singleton {0}: has no mb_trackid', item.title) - continue - - # Get the MusicBrainz recording info. - track_info = hooks.track_for_mbid(item.mb_trackid) - if not track_info: - log.info(u'Recording ID not found: {0}', item.mb_trackid) - continue - - # Apply. - with lib.transaction(): - autotag.apply_item_metadata(item, track_info) - apply_item_changes(lib, item, move, pretend, write) - - -def mbsync_albums(lib, query, move, pretend, write): - """Retrieve and apply info from the autotagger for albums matched by - query and their items. - """ - # Process matching albums. - for a in lib.albums(query): - if not a.mb_albumid: - log.info(u'Skipping album {0}: has no mb_albumid', a.id) - continue - - items = list(a.items()) - - # Get the MusicBrainz album information. - album_info = hooks.album_for_mbid(a.mb_albumid) - if not album_info: - log.info(u'Release ID not found: {0}', a.mb_albumid) - continue - - # Map recording MBIDs to their information. Recordings can appear - # multiple times on a release, so each MBID maps to a list of TrackInfo - # objects. - track_index = defaultdict(list) - for track_info in album_info.tracks: - track_index[track_info.track_id].append(track_info) - - # Construct a track mapping according to MBIDs. This should work - # for albums that have missing or extra tracks. If there are multiple - # copies of a recording, they are disambiguated using their disc and - # track number. - mapping = {} - for item in items: - candidates = track_index[item.mb_trackid] - if len(candidates) == 1: - mapping[item] = candidates[0] - else: - for c in candidates: - if c.medium_index == item.track and c.medium == item.disc: - mapping[item] = c - break - - # Apply. - with lib.transaction(): - autotag.apply_metadata(album_info, mapping) - changed = False - for item in items: - item_changed = ui.show_model_changes(item) - changed |= item_changed - if item_changed: - apply_item_changes(lib, item, move, pretend, write) - - if not changed: - # No change to any item. - continue - - if not pretend: - # Update album structure to reflect an item in it. - for key in library.Album.item_keys: - a[key] = items[0][key] - a.store() - - # Move album art (and any inconsistent items). - if move and lib.directory in util.ancestry(items[0].path): - log.debug(u'moving album {0}', a.id) - a.move() - def apply_item_changes(lib, item, move, pretend, write): """Store, move and write the item according to the arguments. @@ -123,18 +34,6 @@ def apply_item_changes(lib, item, move, pretend, write): item.store() -def mbsync_func(lib, opts, args): - """Command handler for the mbsync function. - """ - move = opts.move - pretend = opts.pretend - write = opts.write - query = ui.decargs(args) - - mbsync_singletons(lib, query, move, pretend, write) - mbsync_albums(lib, query, move, pretend, write) - - class MBSyncPlugin(BeetsPlugin): def __init__(self): super(MBSyncPlugin, self).__init__() @@ -150,5 +49,103 @@ class MBSyncPlugin(BeetsPlugin): cmd.parser.add_option('-W', '--nowrite', action='store_false', default=config['import']['write'], dest='write', help="don't write updated metadata to files") - cmd.func = mbsync_func + cmd.func = self.func return [cmd] + + def func(self, lib, opts, args): + """Command handler for the mbsync function. + """ + move = opts.move + pretend = opts.pretend + write = opts.write + query = ui.decargs(args) + + self.singletons(lib, query, move, pretend, write) + self.albums(lib, query, move, pretend, write) + + def singletons(self, lib, query, move, pretend, write): + """Retrieve and apply info from the autotagger for items matched by + query. + """ + for item in lib.items(query + ['singleton:true']): + if not item.mb_trackid: + self._log.info(u'Skipping singleton {0}: has no mb_trackid', + item.title) + continue + + # Get the MusicBrainz recording info. + track_info = hooks.track_for_mbid(item.mb_trackid) + if not track_info: + self._log.info(u'Recording ID not found: {0}', item.mb_trackid) + continue + + # Apply. + with lib.transaction(): + autotag.apply_item_metadata(item, track_info) + apply_item_changes(lib, item, move, pretend, write) + + def albums(self, lib, query, move, pretend, write): + """Retrieve and apply info from the autotagger for albums matched by + query and their items. + """ + # Process matching albums. + for a in lib.albums(query): + if not a.mb_albumid: + self._log.info(u'Skipping album {0}: has no mb_albumid', a.id) + continue + + items = list(a.items()) + + # Get the MusicBrainz album information. + album_info = hooks.album_for_mbid(a.mb_albumid) + if not album_info: + self._log.info(u'Release ID not found: {0}', a.mb_albumid) + continue + + # Map recording MBIDs to their information. Recordings can appear + # multiple times on a release, so each MBID maps to a list of + # TrackInfo objects. + track_index = defaultdict(list) + for track_info in album_info.tracks: + track_index[track_info.track_id].append(track_info) + + # Construct a track mapping according to MBIDs. This should work + # for albums that have missing or extra tracks. If there are + # multiple copies of a recording, they are disambiguated using + # their disc and track number. + mapping = {} + for item in items: + candidates = track_index[item.mb_trackid] + if len(candidates) == 1: + mapping[item] = candidates[0] + else: + for c in candidates: + if (c.medium_index == item.track and + c.medium == item.disc): + mapping[item] = c + break + + # Apply. + with lib.transaction(): + autotag.apply_metadata(album_info, mapping) + changed = False + for item in items: + item_changed = ui.show_model_changes(item) + changed |= item_changed + if item_changed: + apply_item_changes(lib, item, move, pretend, write) + + if not changed: + # No change to any item. + continue + + if not pretend: + # Update album structure to reflect an item in it. + for key in library.Album.item_keys: + a[key] = items[0][key] + a.store() + + # Move album art (and any inconsistent items). + if move and lib.directory in util.ancestry(items[0].path): + self._log.debug(u'moving album {0}', a.id) + a.move() From 860e7e1483d0e20b920bd71cb7b2fe0435cd831c Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 6 Jan 2015 16:54:15 +0100 Subject: [PATCH 43/86] =?UTF-8?q?Update=203=20plugins:=20func=20=E2=86=92?= =?UTF-8?q?=20methods,=20listeners?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - functions turn into method in order to have the logger object - registering the listener has to be updated too --- beetsplug/embedart.py | 349 +++++++++++++++++++-------------------- beetsplug/importfeeds.py | 146 ++++++++-------- beetsplug/scrub.py | 96 ++++++----- 3 files changed, 288 insertions(+), 303 deletions(-) diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index 944f8c3b3..527160200 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -29,9 +29,6 @@ from beets.util.artresizer import ArtResizer from beets import config -log = logging.getLogger(__name__) - - class EmbedCoverArtPlugin(BeetsPlugin): """Allows albumart to be embedded into the actual files. """ @@ -46,13 +43,15 @@ class EmbedCoverArtPlugin(BeetsPlugin): if self.config['maxwidth'].get(int) and not ArtResizer.shared.local: self.config['maxwidth'] = 0 - log.warn(u"ImageMagick or PIL not found; " - u"'maxwidth' option ignored") + self._log.warn(u"ImageMagick or PIL not found; " + u"'maxwidth' option ignored") if self.config['compare_threshold'].get(int) and not \ ArtResizer.shared.can_compare: self.config['compare_threshold'] = 0 - log.warn(u"ImageMagick 6.8.7 or higher not installed; " - u"'compare_threshold' option ignored") + self._log.warn(u"ImageMagick 6.8.7 or higher not installed; " + u"'compare_threshold' option ignored") + + self.register_listener('album_imported', self.album_imported) def commands(self): # Embed command. @@ -70,11 +69,11 @@ class EmbedCoverArtPlugin(BeetsPlugin): if opts.file: imagepath = normpath(opts.file) for item in lib.items(decargs(args)): - embed_item(item, imagepath, maxwidth, None, - compare_threshold, ifempty) + self.embed_item(item, imagepath, maxwidth, None, + compare_threshold, ifempty) else: for album in lib.albums(decargs(args)): - embed_album(album, maxwidth) + self.embed_album(album, maxwidth) embed_cmd.func = embed_func @@ -87,7 +86,7 @@ class EmbedCoverArtPlugin(BeetsPlugin): def extract_func(lib, opts, args): outpath = normpath(opts.outpath or 'cover') item = lib.items(decargs(args)).get() - extract(outpath, item) + self.extract(outpath, item) extract_cmd.func = extract_func # Clear command. @@ -95,179 +94,173 @@ class EmbedCoverArtPlugin(BeetsPlugin): help='remove images from file metadata') def clear_func(lib, opts, args): - clear(lib, decargs(args)) + self.clear(lib, decargs(args)) clear_cmd.func = clear_func return [embed_cmd, extract_cmd, clear_cmd] + def album_imported(self, lib, album): + """Automatically embed art into imported albums. + """ + if album.artpath and config['embedart']['auto']: + max_width = config['embedart']['maxwidth'].get(int) + self.embed_album(album, max_width, True) -@EmbedCoverArtPlugin.listen('album_imported') -def album_imported(lib, album): - """Automatically embed art into imported albums. - """ - if album.artpath and config['embedart']['auto']: - embed_album(album, config['embedart']['maxwidth'].get(int), True) - - -def embed_item(item, imagepath, maxwidth=None, itempath=None, - compare_threshold=0, ifempty=False, as_album=False): - """Embed an image into the item's media file. - """ - if compare_threshold: - if not check_art_similarity(item, imagepath, compare_threshold): - log.warn(u'Image not similar; skipping.') - return - if ifempty: - art = get_art(item) - if not art: - pass - else: - log.debug(u'media file contained art already {0}', - displayable_path(imagepath)) - return - if maxwidth and not as_album: - imagepath = resize_image(imagepath, maxwidth) - - try: - log.debug(u'embedding {0}', displayable_path(imagepath)) - item['images'] = [_mediafile_image(imagepath, maxwidth)] - except IOError as exc: - log.error(u'could not read image file: {0}', exc) - else: - # We don't want to store the image in the database. - item.try_write(itempath) - del item['images'] - - -def embed_album(album, maxwidth=None, quiet=False): - """Embed album art into all of the album's items. - """ - imagepath = album.artpath - if not imagepath: - log.info(u'No album art present: {0} - {1}', - album.albumartist, album.album) - return - if not os.path.isfile(syspath(imagepath)): - log.error(u'Album art not found at {0}', displayable_path(imagepath)) - return - if maxwidth: - imagepath = resize_image(imagepath, maxwidth) - - log.log( - logging.DEBUG if quiet else logging.INFO, - u'Embedding album art into {0.albumartist} - {0.album}.', album - ) - - for item in album.items(): - embed_item(item, imagepath, maxwidth, None, - config['embedart']['compare_threshold'].get(int), - config['embedart']['ifempty'].get(bool), as_album=True) - - -def resize_image(imagepath, maxwidth): - """Returns path to an image resized to maxwidth. - """ - log.info(u'Resizing album art to {0} pixels wide', maxwidth) - imagepath = ArtResizer.shared.resize(maxwidth, syspath(imagepath)) - return imagepath - - -def check_art_similarity(item, imagepath, compare_threshold): - """A boolean indicating if an image is similar to embedded item art. - """ - with NamedTemporaryFile(delete=True) as f: - art = extract(f.name, item) - - if art: - # Converting images to grayscale tends to minimize the weight - # of colors in the diff score - cmd = 'convert {0} {1} -colorspace gray MIFF:- | ' \ - 'compare -metric PHASH - null:'.format(syspath(imagepath), - syspath(art)) - - proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - close_fds=platform.system() != 'Windows', - shell=True) - stdout, stderr = proc.communicate() - if proc.returncode: - if proc.returncode != 1: - log.warn(u'IM phashes compare failed for {0}, {1}', - displayable_path(imagepath), - displayable_path(art)) - return - phashDiff = float(stderr) + def embed_item(self, item, imagepath, maxwidth=None, itempath=None, + compare_threshold=0, ifempty=False, as_album=False): + """Embed an image into the item's media file. + """ + if compare_threshold: + if not self.check_art_similarity(item, imagepath, + compare_threshold): + self._log.warn(u'Image not similar; skipping.') + return + if ifempty: + art = self.get_art(item) + if not art: + pass else: - phashDiff = float(stdout) + self._log.debug(u'media file contained art already {0}', + displayable_path(imagepath)) + return + if maxwidth and not as_album: + imagepath = self.resize_image(imagepath, maxwidth) - log.info(u'compare PHASH score is {0}', phashDiff) - if phashDiff > compare_threshold: - return False - - return True - - -def _mediafile_image(image_path, maxwidth=None): - """Return a `mediafile.Image` object for the path. - """ - - with open(syspath(image_path), 'rb') as f: - data = f.read() - return mediafile.Image(data, type=mediafile.ImageType.front) - - -def get_art(item): - # Extract the art. - try: - mf = mediafile.MediaFile(syspath(item.path)) - except mediafile.UnreadableFileError as exc: - log.error(u'Could not extract art from {0}: {1}', - displayable_path(item.path), exc) - return - - return mf.art - -# 'extractart' command. - - -def extract(outpath, item): - if not item: - log.error(u'No item matches query.') - return - - art = get_art(item) - - if not art: - log.error(u'No album art present in {0} - {1}.', - item.artist, item.title) - return - - # Add an extension to the filename. - ext = imghdr.what(None, h=art) - if not ext: - log.error(u'Unknown image type.') - return - outpath += '.' + ext - - log.info(u'Extracting album art from: {0.artist} - {0.title} to: {1}', - item, displayable_path(outpath)) - with open(syspath(outpath), 'wb') as f: - f.write(art) - return outpath - - -# 'clearart' command. - -def clear(lib, query): - log.info(u'Clearing album art from items:') - for item in lib.items(query): - log.info(u'{0} - {1}', item.artist, item.title) try: - mf = mediafile.MediaFile(syspath(item.path), - config['id3v23'].get(bool)) + self._log.debug(u'embedding {0}', displayable_path(imagepath)) + item['images'] = [self._mediafile_image(imagepath, maxwidth)] + except IOError as exc: + self._log.error(u'could not read image file: {0}', exc) + else: + # We don't want to store the image in the database. + item.try_write(itempath) + del item['images'] + + def embed_album(self, album, maxwidth=None, quiet=False): + """Embed album art into all of the album's items. + """ + imagepath = album.artpath + if not imagepath: + self._log.info(u'No album art present: {0} - {1}', + album.albumartist, album.album) + return + if not os.path.isfile(syspath(imagepath)): + self._log.error(u'Album art not found at {0}', + displayable_path(imagepath)) + return + if maxwidth: + imagepath = self.resize_image(imagepath, maxwidth) + + self._log.log( + logging.DEBUG if quiet else logging.INFO, + u'Embedding album art into {0.albumartist} - {0.album}.', album + ) + + for item in album.items(): + thresh = config['embedart']['compare_threshold'].get(int) + ifempty = config['embedart']['ifempty'].get(bool) + self.embed_item(item, imagepath, maxwidth, None, + thresh, ifempty, as_album=True) + + def resize_image(self, imagepath, maxwidth): + """Returns path to an image resized to maxwidth. + """ + self._log.info(u'Resizing album art to {0} pixels wide', maxwidth) + imagepath = ArtResizer.shared.resize(maxwidth, syspath(imagepath)) + return imagepath + + def check_art_similarity(self, item, imagepath, compare_threshold): + """A boolean indicating if an image is similar to embedded item art. + """ + with NamedTemporaryFile(delete=True) as f: + art = self.extract(f.name, item) + + if art: + # Converting images to grayscale tends to minimize the weight + # of colors in the diff score + cmd = 'convert {0} {1} -colorspace gray MIFF:- | ' \ + 'compare -metric PHASH - null:' \ + .format(syspath(imagepath), syspath(art)) + + is_windows = platform.system() != "Windows" + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + close_fds=is_windows, + shell=True) + stdout, stderr = proc.communicate() + if proc.returncode: + if proc.returncode != 1: + self._log.warn(u'IM phashes compare failed for {0}, ' + u'{1}', displayable_path(imagepath), + displayable_path(art)) + return + phashDiff = float(stderr) + else: + phashDiff = float(stdout) + + self._log.info(u'compare PHASH score is {0}', phashDiff) + if phashDiff > compare_threshold: + return False + + return True + + def _mediafile_image(self, image_path, maxwidth=None): + """Return a `mediafile.Image` object for the path. + """ + + with open(syspath(image_path), 'rb') as f: + data = f.read() + return mediafile.Image(data, type=mediafile.ImageType.front) + + def get_art(self, item): + # Extract the art. + try: + mf = mediafile.MediaFile(syspath(item.path)) except mediafile.UnreadableFileError as exc: - log.error(u'Could not clear art from {0}: {1}', - displayable_path(item.path), exc) - continue - del mf.art - mf.save() + self._log.error(u'Could not extract art from {0}: {1}', + displayable_path(item.path), exc) + return + + return mf.art + + # 'extractart' command. + + def extract(self, outpath, item): + if not item: + self._log.error(u'No item matches query.') + return + + art = self.get_art(item) + + if not art: + self._log.error(u'No album art present in {0} - {1}.', + item.artist, item.title) + return + + # Add an extension to the filename. + ext = imghdr.what(None, h=art) + if not ext: + self._log.error(u'Unknown image type.') + return + outpath += '.' + ext + + self._log.info(u'Extracting album art from: {0.artist} - {0.title} ' + u'to: {1}', item, displayable_path(outpath)) + with open(syspath(outpath), 'wb') as f: + f.write(art) + return outpath + + # 'clearart' command. + def clear(self, lib, query): + self._log.info(u'Clearing album art from items:') + for item in lib.items(query): + self._log.info(u'{0} - {1}', item.artist, item.title) + try: + mf = mediafile.MediaFile(syspath(item.path), + config['id3v23'].get(bool)) + except mediafile.UnreadableFileError as exc: + self._log.error(u'Could not clear art from {0}: {1}', + displayable_path(item.path), exc) + continue + del mf.art + mf.save() diff --git a/beetsplug/importfeeds.py b/beetsplug/importfeeds.py index 5a5b8fbe0..d173f81c8 100644 --- a/beetsplug/importfeeds.py +++ b/beetsplug/importfeeds.py @@ -22,36 +22,9 @@ import re from beets.plugins import BeetsPlugin from beets.util import normpath, syspath, bytestring_path -from beets import config, logging +from beets import config M3U_DEFAULT_NAME = 'imported.m3u' -log = logging.getLogger(__name__) - - -class ImportFeedsPlugin(BeetsPlugin): - def __init__(self): - super(ImportFeedsPlugin, self).__init__() - - self.config.add({ - 'formats': [], - 'm3u_name': u'imported.m3u', - 'dir': None, - 'relative_to': None, - 'absolute_path': False, - }) - - feeds_dir = self.config['dir'].get() - if feeds_dir: - feeds_dir = os.path.expanduser(bytestring_path(feeds_dir)) - self.config['dir'] = feeds_dir - if not os.path.exists(syspath(feeds_dir)): - os.makedirs(syspath(feeds_dir)) - - relative_to = self.config['relative_to'].get() - if relative_to: - self.config['relative_to'] = normpath(relative_to) - else: - self.config['relative_to'] = feeds_dir def _get_feeds_dir(lib): @@ -89,62 +62,85 @@ def _write_m3u(m3u_path, items_paths): f.write(path + '\n') -def _record_items(lib, basename, items): - """Records relative paths to the given items for each feed format - """ - feedsdir = bytestring_path(config['importfeeds']['dir'].as_filename()) - formats = config['importfeeds']['formats'].as_str_seq() - relative_to = config['importfeeds']['relative_to'].get() \ - or config['importfeeds']['dir'].as_filename() - relative_to = bytestring_path(relative_to) +class ImportFeedsPlugin(BeetsPlugin): + def __init__(self): + super(ImportFeedsPlugin, self).__init__() - paths = [] - for item in items: - if config['importfeeds']['absolute_path']: - paths.append(item.path) + self.config.add({ + 'formats': [], + 'm3u_name': u'imported.m3u', + 'dir': None, + 'relative_to': None, + 'absolute_path': False, + }) + + feeds_dir = self.config['dir'].get() + if feeds_dir: + feeds_dir = os.path.expanduser(bytestring_path(feeds_dir)) + self.config['dir'] = feeds_dir + if not os.path.exists(syspath(feeds_dir)): + os.makedirs(syspath(feeds_dir)) + + relative_to = self.config['relative_to'].get() + if relative_to: + self.config['relative_to'] = normpath(relative_to) else: - try: - relpath = os.path.relpath(item.path, relative_to) - except ValueError: - # On Windows, it is sometimes not possible to construct a - # relative path (if the files are on different disks). - relpath = item.path - paths.append(relpath) + self.config['relative_to'] = feeds_dir - if 'm3u' in formats: - basename = bytestring_path( - config['importfeeds']['m3u_name'].get(unicode) - ) - m3u_path = os.path.join(feedsdir, basename) - _write_m3u(m3u_path, paths) + self.register_listener('library_opened', self.library_opened) + self.register_listener('album_imported', self.album_imported) + self.register_listener('item_imported', self.item_imported) - if 'm3u_multi' in formats: - m3u_path = _build_m3u_filename(basename) - _write_m3u(m3u_path, paths) + def _record_items(self, lib, basename, items): + """Records relative paths to the given items for each feed format + """ + feedsdir = bytestring_path(config['importfeeds']['dir'].as_filename()) + formats = config['importfeeds']['formats'].as_str_seq() + relative_to = config['importfeeds']['relative_to'].get() \ + or config['importfeeds']['dir'].as_filename() + relative_to = bytestring_path(relative_to) - if 'link' in formats: - for path in paths: - dest = os.path.join(feedsdir, os.path.basename(path)) - if not os.path.exists(syspath(dest)): - os.symlink(syspath(path), syspath(dest)) + paths = [] + for item in items: + if config['importfeeds']['absolute_path']: + paths.append(item.path) + else: + try: + relpath = os.path.relpath(item.path, relative_to) + except ValueError: + # On Windows, it is sometimes not possible to construct a + # relative path (if the files are on different disks). + relpath = item.path + paths.append(relpath) - if 'echo' in formats: - log.info("Location of imported music:") - for path in paths: - log.info(" {0}", path) + if 'm3u' in formats: + basename = bytestring_path( + config['importfeeds']['m3u_name'].get(unicode) + ) + m3u_path = os.path.join(feedsdir, basename) + _write_m3u(m3u_path, paths) + if 'm3u_multi' in formats: + m3u_path = _build_m3u_filename(basename) + _write_m3u(m3u_path, paths) -@ImportFeedsPlugin.listen('library_opened') -def library_opened(lib): - if config['importfeeds']['dir'].get() is None: - config['importfeeds']['dir'] = _get_feeds_dir(lib) + if 'link' in formats: + for path in paths: + dest = os.path.join(feedsdir, os.path.basename(path)) + if not os.path.exists(syspath(dest)): + os.symlink(syspath(path), syspath(dest)) + if 'echo' in formats: + self._log.info("Location of imported music:") + for path in paths: + self._log.info(" {0}", path) -@ImportFeedsPlugin.listen('album_imported') -def album_imported(lib, album): - _record_items(lib, album.album, album.items()) + def library_opened(self, lib): + if self.config['dir'].get() is None: + self.config['dir'] = _get_feeds_dir(lib) + def album_imported(self, lib, album): + self._record_items(lib, album.album, album.items()) -@ImportFeedsPlugin.listen('item_imported') -def item_imported(lib, item): - _record_items(lib, item.title, [item]) + def item_imported(self, lib, item): + self._record_items(lib, item.title, [item]) diff --git a/beetsplug/scrub.py b/beetsplug/scrub.py index 0811da8bb..87625ecbd 100644 --- a/beetsplug/scrub.py +++ b/beetsplug/scrub.py @@ -16,15 +16,12 @@ automatically whenever tags are written. """ -from beets import logging from beets.plugins import BeetsPlugin from beets import ui from beets import util from beets import config from beets import mediafile -log = logging.getLogger(__name__) - _MUTAGEN_FORMATS = { 'asf': 'ASF', 'apev2': 'APEv2File', @@ -54,6 +51,7 @@ class ScrubPlugin(BeetsPlugin): self.config.add({ 'auto': True, }) + self.register_listener("write", self.write_item) def commands(self): def scrub_func(lib, opts, args): @@ -64,7 +62,8 @@ class ScrubPlugin(BeetsPlugin): # Walk through matching files and remove tags. for item in lib.items(ui.decargs(args)): - log.info(u'scrubbing: {0}', util.displayable_path(item.path)) + self._log.info(u'scrubbing: {0}', + util.displayable_path(item.path)) # Get album art if we need to restore it. if opts.write: @@ -73,14 +72,14 @@ class ScrubPlugin(BeetsPlugin): art = mf.art # Remove all tags. - _scrub(item.path) + self._scrub(item.path) # Restore tags, if enabled. if opts.write: - log.debug(u'writing new tags after scrub') + self._log.debug(u'writing new tags after scrub') item.try_write() if art: - log.info(u'restoring art') + self._log.info(u'restoring art') mf = mediafile.MediaFile(item.path) mf.art = art mf.save() @@ -95,49 +94,46 @@ class ScrubPlugin(BeetsPlugin): return [scrub_cmd] + @staticmethod + def _mutagen_classes(): + """Get a list of file type classes from the Mutagen module. + """ + classes = [] + for modname, clsname in _MUTAGEN_FORMATS.items(): + mod = __import__('mutagen.{0}'.format(modname), + fromlist=[clsname]) + classes.append(getattr(mod, clsname)) + return classes -def _mutagen_classes(): - """Get a list of file type classes from the Mutagen module. - """ - classes = [] - for modname, clsname in _MUTAGEN_FORMATS.items(): - mod = __import__('mutagen.{0}'.format(modname), - fromlist=[clsname]) - classes.append(getattr(mod, clsname)) - return classes + def _scrub(self, path): + """Remove all tags from a file. + """ + for cls in self._mutagen_classes(): + # Try opening the file with this type, but just skip in the + # event of any error. + try: + f = cls(util.syspath(path)) + except Exception: + continue + if f.tags is None: + continue + # Remove the tag for this type. + try: + f.delete() + except NotImplementedError: + # Some Mutagen metadata subclasses (namely, ASFTag) do not + # support .delete(), presumably because it is impossible to + # remove them. In this case, we just remove all the tags. + for tag in f.keys(): + del f[tag] + f.save() + except IOError as exc: + self._log.error(u'could not scrub {0}: {1}', + util.displayable_path(path), exc) -def _scrub(path): - """Remove all tags from a file. - """ - for cls in _mutagen_classes(): - # Try opening the file with this type, but just skip in the - # event of any error. - try: - f = cls(util.syspath(path)) - except Exception: - continue - if f.tags is None: - continue - - # Remove the tag for this type. - try: - f.delete() - except NotImplementedError: - # Some Mutagen metadata subclasses (namely, ASFTag) do not - # support .delete(), presumably because it is impossible to - # remove them. In this case, we just remove all the tags. - for tag in f.keys(): - del f[tag] - f.save() - except IOError as exc: - log.error(u'could not scrub {0}: {1}', - util.displayable_path(path), exc) - - -# Automatically embed art into imported albums. -@ScrubPlugin.listen('write') -def write_item(path): - if not scrubbing and config['scrub']['auto']: - log.debug(u'auto-scrubbing {0}', util.displayable_path(path)) - _scrub(path) + def write_item(self, path): + """Automatically embed art into imported albums.""" + if not scrubbing and config['scrub']['auto']: + self._log.debug(u'auto-scrubbing {0}', util.displayable_path(path)) + self._scrub(path) From 63041736e3a18a0ca7fe4efd1278796434f24aa4 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 6 Jan 2015 18:09:18 +0100 Subject: [PATCH 44/86] Convert lyrics plugin, with OO rewrite of backends --- beetsplug/lyrics.py | 416 ++++++++++++++++++++++---------------------- 1 file changed, 208 insertions(+), 208 deletions(-) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 710cdd2e4..70a1133c2 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -25,15 +25,10 @@ import difflib import itertools from HTMLParser import HTMLParseError -from beets import logging from beets import plugins from beets import config, ui -# Global logger. - -log = logging.getLogger(__name__) - DIV_RE = re.compile(r'<(/?)div>?', re.I) COMMENT_RE = re.compile(r'', re.S) TAG_RE = re.compile(r'<[^>]*>') @@ -56,21 +51,6 @@ URL_CHARACTERS = { # Utilities. -def fetch_url(url): - """Retrieve the content at a given URL, or return None if the source - is unreachable. - """ - try: - r = requests.get(url, verify=False) - except requests.RequestException as exc: - log.debug(u'lyrics request failed: {0}', exc) - return - if r.status_code == requests.codes.ok: - return r.text - else: - log.debug(u'failed to fetch: {0} ({1})', url, r.status_code) - - def unescape(text): """Resolves &#xxx; HTML entities (and some others).""" if isinstance(text, str): @@ -174,131 +154,110 @@ def search_pairs(item): return itertools.product(artists, multi_titles) -def _encode(s): - """Encode the string for inclusion in a URL (common to both - LyricsWiki and Lyrics.com). - """ - if isinstance(s, unicode): - for char, repl in URL_CHARACTERS.items(): - s = s.replace(char, repl) - s = s.encode('utf8', 'ignore') - return urllib.quote(s) +class Backend(object): + def __init__(self, log): + self._log = log -# Musixmatch + @staticmethod + def _encode(s): + """Encode the string for inclusion in a URL""" + if isinstance(s, unicode): + for char, repl in URL_CHARACTERS.items(): + s = s.replace(char, repl) + s = s.encode('utf8', 'ignore') + return urllib.quote(s) -MUSIXMATCH_URL_PATTERN = 'https://www.musixmatch.com/lyrics/%s/%s' + def build_url(self, artist, title): + return self.URL_PATTERN % (self._encode(artist.title()), + self._encode(title.title())) - -def fetch_musixmatch(artist, title): - url = MUSIXMATCH_URL_PATTERN % (_lw_encode(artist.title()), - _lw_encode(title.title())) - html = fetch_url(url) - if not html: - return - lyrics = extract_text_between(html, '"lyrics_body":', '"lyrics_language":') - return lyrics.strip(',"').replace('\\n', '\n') - -# LyricsWiki. - -LYRICSWIKI_URL_PATTERN = 'http://lyrics.wikia.com/%s:%s' - - -def _lw_encode(s): - s = re.sub(r'\s+', '_', s) - s = s.replace("<", "Less_Than") - s = s.replace(">", "Greater_Than") - s = s.replace("#", "Number_") - s = re.sub(r'[\[\{]', '(', s) - s = re.sub(r'[\]\}]', ')', s) - return _encode(s) - - -def fetch_lyricswiki(artist, title): - """Fetch lyrics from LyricsWiki.""" - url = LYRICSWIKI_URL_PATTERN % (_lw_encode(artist), _lw_encode(title)) - html = fetch_url(url) - if not html: - return - - lyrics = extract_text_in(html, u"
") - if lyrics and 'Unfortunately, we are not licensed' not in lyrics: - return lyrics - - -# Lyrics.com. - -LYRICSCOM_URL_PATTERN = 'http://www.lyrics.com/%s-lyrics-%s.html' -LYRICSCOM_NOT_FOUND = ( - 'Sorry, we do not have the lyric', - 'Submit Lyrics', -) - - -def _lc_encode(s): - s = re.sub(r'[^\w\s-]', '', s) - s = re.sub(r'\s+', '-', s) - return _encode(s).lower() - - -def fetch_lyricscom(artist, title): - """Fetch lyrics from Lyrics.com.""" - url = LYRICSCOM_URL_PATTERN % (_lc_encode(title), _lc_encode(artist)) - html = fetch_url(url) - if not html: - return - lyrics = extract_text_between(html, '
', '
') - if not lyrics: - return - for not_found_str in LYRICSCOM_NOT_FOUND: - if not_found_str in lyrics: + def fetch_url(self, url): + """Retrieve the content at a given URL, or return None if the source + is unreachable. + """ + try: + r = requests.get(url, verify=False) + except requests.RequestException as exc: + self._log.debug(u'lyrics request failed: {0}', exc) return + if r.status_code == requests.codes.ok: + return r.text + else: + self._log.debug(u'failed to fetch: {0} ({1})', url, r.status_code) - parts = lyrics.split('\n---\nLyrics powered by', 1) - if parts: - return parts[0] + def fetch(self, artist, title): + raise NotImplementedError() -# Optional Google custom search API backend. - -def slugify(text): - """Normalize a string and remove non-alphanumeric characters. - """ - text = re.sub(r"[-'_\s]", '_', text) - text = re.sub(r"_+", '_', text).strip('_') - pat = "([^,\(]*)\((.*?)\)" # Remove content within parentheses - text = re.sub(pat, '\g<1>', text).strip() - try: - text = unicodedata.normalize('NFKD', text).encode('ascii', 'ignore') - text = unicode(re.sub('[-\s]+', ' ', text)) - except UnicodeDecodeError: - log.exception(u"Failing to normalize '{0}'", text) - return text +class SymbolsReplaced(Backend): + @classmethod + def _encode(cls, s): + s = re.sub(r'\s+', '_', s) + s = s.replace("<", "Less_Than") + s = s.replace(">", "Greater_Than") + s = s.replace("#", "Number_") + s = re.sub(r'[\[\{]', '(', s) + s = re.sub(r'[\]\}]', ')', s) + return super(SymbolsReplaced, cls)._encode(s) -BY_TRANS = ['by', 'par', 'de', 'von'] -LYRICS_TRANS = ['lyrics', 'paroles', 'letras', 'liedtexte'] +class MusiXmatch(SymbolsReplaced): + URL_PATTERN = 'https://www.musixmatch.com/lyrics/%s/%s' + + def fetch(self, artist, title): + url = self.build_url(artist, title) + html = self.fetch_url(url) + if not html: + return + lyrics = extract_text_between(html, + '"lyrics_body":', '"lyrics_language":') + return lyrics.strip(',"').replace('\\n', '\n') -def is_page_candidate(urlLink, urlTitle, title, artist): - """Return True if the URL title makes it a good candidate to be a - page that contains lyrics of title by artist. - """ - title = slugify(title.lower()) - artist = slugify(artist.lower()) - sitename = re.search(u"//([^/]+)/.*", slugify(urlLink.lower())).group(1) - urlTitle = slugify(urlTitle.lower()) - # Check if URL title contains song title (exact match) - if urlTitle.find(title) != -1: - return True - # or try extracting song title from URL title and check if - # they are close enough - tokens = [by + '_' + artist for by in BY_TRANS] + \ - [artist, sitename, sitename.replace('www.', '')] + LYRICS_TRANS - songTitle = re.sub(u'(%s)' % u'|'.join(tokens), u'', urlTitle) - songTitle = songTitle.strip('_|') - typoRatio = .9 - return difflib.SequenceMatcher(None, songTitle, title).ratio() >= typoRatio +class LyricsWiki(SymbolsReplaced): + """Fetch lyrics from LyricsWiki.""" + URL_PATTERN = 'http://lyrics.wikia.com/%s:%s' + + def fetch(self, artist, title): + url = self.build_url(artist, title) + html = self.fetch_url(url) + if not html: + return + lyrics = extract_text_in(html, u"
") + if lyrics and 'Unfortunately, we are not licensed' not in lyrics: + return lyrics + + +class LyricsCom(Backend): + """Fetch lyrics from Lyrics.com.""" + URL_PATTERN = 'http://www.lyrics.com/%s-lyrics-%s.html' + NOT_FOUND = ( + 'Sorry, we do not have the lyric', + 'Submit Lyrics', + ) + + @classmethod + def _encode(cls, s): + s = re.sub(r'[^\w\s-]', '', s) + s = re.sub(r'\s+', '-', s) + return super(LyricsCom, cls)._encode(s).lower() + + def fetch(self, artist, title): + url = self.build_url(artist, title) + html = self.fetch_url(url) + if not html: + return + lyrics = extract_text_between(html, '
', '
') + if not lyrics: + return + for not_found_str in self.NOT_FOUND: + if not_found_str in lyrics: + return + + parts = lyrics.split('\n---\nLyrics powered by', 1) + if parts: + return parts[0] def remove_credits(text): @@ -315,36 +274,6 @@ def remove_credits(text): return text -def is_lyrics(text, artist=None): - """Determine whether the text seems to be valid lyrics. - """ - if not text: - return False - badTriggersOcc = [] - nbLines = text.count('\n') - if nbLines <= 1: - log.debug(u"Ignoring too short lyrics '{0}'", text) - return False - elif nbLines < 5: - badTriggersOcc.append('too_short') - else: - # Lyrics look legit, remove credits to avoid being penalized further - # down - text = remove_credits(text) - - badTriggers = ['lyrics', 'copyright', 'property', 'links'] - if artist: - badTriggersOcc += [artist] - - for item in badTriggers: - badTriggersOcc += [item] * len(re.findall(r'\W%s\W' % item, - text, re.I)) - - if badTriggersOcc: - log.debug(u'Bad triggers detected: {0}', badTriggersOcc) - return len(badTriggersOcc) < 2 - - def _scrape_strip_cruft(html, plain_text_out=False): """Clean up HTML """ @@ -396,50 +325,119 @@ def scrape_lyrics_from_html(html): return soup -def fetch_google(artist, title): - """Fetch lyrics from Google search results. - """ - query = u"%s %s" % (artist, title) - api_key = config['lyrics']['google_API_key'].get(unicode) - engine_id = config['lyrics']['google_engine_ID'].get(unicode) - url = u'https://www.googleapis.com/customsearch/v1?key=%s&cx=%s&q=%s' % \ - (api_key, engine_id, urllib.quote(query.encode('utf8'))) +class Google(Backend): + """Fetch lyrics from Google search results.""" + def is_lyrics(self, text, artist=None): + """Determine whether the text seems to be valid lyrics. + """ + if not text: + return False + badTriggersOcc = [] + nbLines = text.count('\n') + if nbLines <= 1: + self._log.debug(u"Ignoring too short lyrics '{0}'", text) + return False + elif nbLines < 5: + badTriggersOcc.append('too_short') + else: + # Lyrics look legit, remove credits to avoid being penalized + # further down + text = remove_credits(text) - data = urllib.urlopen(url) - data = json.load(data) - if 'error' in data: - reason = data['error']['errors'][0]['reason'] - log.debug(u'google lyrics backend error: {0}', reason) - return + badTriggers = ['lyrics', 'copyright', 'property', 'links'] + if artist: + badTriggersOcc += [artist] - if 'items' in data.keys(): - for item in data['items']: - urlLink = item['link'] - urlTitle = item.get('title', u'') - if not is_page_candidate(urlLink, urlTitle, title, artist): - continue - html = fetch_url(urlLink) - lyrics = scrape_lyrics_from_html(html) - if not lyrics: - continue + for item in badTriggers: + badTriggersOcc += [item] * len(re.findall(r'\W%s\W' % item, + text, re.I)) - if is_lyrics(lyrics, artist): - log.debug(u'got lyrics from {0}', item['displayLink']) - return lyrics + if badTriggersOcc: + self._log.debug(u'Bad triggers detected: {0}', badTriggersOcc) + return len(badTriggersOcc) < 2 + def slugify(self, text): + """Normalize a string and remove non-alphanumeric characters. + """ + text = re.sub(r"[-'_\s]", '_', text) + text = re.sub(r"_+", '_', text).strip('_') + pat = "([^,\(]*)\((.*?)\)" # Remove content within parentheses + text = re.sub(pat, '\g<1>', text).strip() + try: + text = unicodedata.normalize('NFKD', text).encode('ascii', + 'ignore') + text = unicode(re.sub('[-\s]+', ' ', text)) + except UnicodeDecodeError: + self._log.exception(u"Failing to normalize '{0}'", text) + return text -# Plugin logic. + BY_TRANS = ['by', 'par', 'de', 'von'] + LYRICS_TRANS = ['lyrics', 'paroles', 'letras', 'liedtexte'] -SOURCES = ['google', 'lyricwiki', 'lyrics.com', 'musixmatch'] -SOURCE_BACKENDS = { - 'google': fetch_google, - 'lyricwiki': fetch_lyricswiki, - 'lyrics.com': fetch_lyricscom, - 'musixmatch': fetch_musixmatch, -} + def is_page_candidate(self, urlLink, urlTitle, title, artist): + """Return True if the URL title makes it a good candidate to be a + page that contains lyrics of title by artist. + """ + title = self.slugify(title.lower()) + artist = self.slugify(artist.lower()) + sitename = re.search(u"//([^/]+)/.*", + self.slugify(urlLink.lower())).group(1) + urlTitle = self.slugify(urlTitle.lower()) + # Check if URL title contains song title (exact match) + if urlTitle.find(title) != -1: + return True + # or try extracting song title from URL title and check if + # they are close enough + tokens = [by + '_' + artist for by in self.BY_TRANS] + \ + [artist, sitename, sitename.replace('www.', '')] + \ + self.LYRICS_TRANS + songTitle = re.sub(u'(%s)' % u'|'.join(tokens), u'', urlTitle) + songTitle = songTitle.strip('_|') + typoRatio = .9 + ratio = difflib.SequenceMatcher(None, songTitle, title).ratio() + return ratio >= typoRatio + + def fetch(self, artist, title): + query = u"%s %s" % (artist, title) + api_key = config['lyrics']['google_API_key'].get(unicode) + engine_id = config['lyrics']['google_engine_ID'].get(unicode) + url = u'https://www.googleapis.com/customsearch/v1?key=%s&cx=%s&q=%s' % \ + (api_key, engine_id, urllib.quote(query.encode('utf8'))) + + data = urllib.urlopen(url) + data = json.load(data) + if 'error' in data: + reason = data['error']['errors'][0]['reason'] + self._log.debug(u'google lyrics backend error: {0}', reason) + return + + if 'items' in data.keys(): + for item in data['items']: + urlLink = item['link'] + urlTitle = item.get('title', u'') + if not self.is_page_candidate(urlLink, urlTitle, + title, artist): + continue + html = self.fetch_url(urlLink) + lyrics = scrape_lyrics_from_html(html) + if not lyrics: + continue + + if self.is_lyrics(lyrics, artist): + self._log.debug(u'got lyrics from {0}', + item['displayLink']) + return lyrics class LyricsPlugin(plugins.BeetsPlugin): + SOURCES = ['google', 'lyricwiki', 'lyrics.com', 'musixmatch'] + SOURCE_BACKENDS = { + 'google': Google, + 'lyricwiki': LyricsWiki, + 'lyrics.com': LyricsCom, + 'musixmatch': MusiXmatch, + } + def __init__(self): super(LyricsPlugin, self).__init__() self._import_stages = [self.imported] @@ -449,18 +447,18 @@ class LyricsPlugin(plugins.BeetsPlugin): 'google_engine_ID': u'009217259823014548361:lndtuqkycfu', 'fallback': None, 'force': False, - 'sources': SOURCES, + 'sources': self.SOURCES, }) - available_sources = list(SOURCES) + available_sources = list(self.SOURCES) if not self.config['google_API_key'].get() and \ - 'google' in SOURCES: + 'google' in self.SOURCES: available_sources.remove('google') self.config['sources'] = plugins.sanitize_choices( self.config['sources'].as_str_seq(), available_sources) self.backends = [] for key in self.config['sources'].as_str_seq(): - self.backends.append(SOURCE_BACKENDS[key]) + self.backends.append(self.SOURCE_BACKENDS[key](self._log)) def commands(self): cmd = ui.Subcommand('lyrics', help='fetch song lyrics') @@ -499,7 +497,8 @@ class LyricsPlugin(plugins.BeetsPlugin): lyrics will also be written to the file itself.""" # Skip if the item already has lyrics. if not force and item.lyrics: - log.info(u'lyrics already present: {0.artist} - {0.title}', item) + self._log.info(u'lyrics already present: {0.artist} - {0.title}', + item) return lyrics = None @@ -511,9 +510,9 @@ class LyricsPlugin(plugins.BeetsPlugin): lyrics = u"\n\n---\n\n".join([l for l in lyrics if l]) if lyrics: - log.info(u'fetched lyrics: {0} - {1}', item.artist, item.title) + self._log.info(u'fetched lyrics: {0.artist} - {0.title}', item) else: - log.info(u'lyrics not found: {0} - {1}', item.artist, item.title) + self._log.info(u'lyrics not found: {0.artist} - {0.title}', item) fallback = self.config['fallback'].get() if fallback: lyrics = fallback @@ -531,7 +530,8 @@ class LyricsPlugin(plugins.BeetsPlugin): None if no lyrics were found. """ for backend in self.backends: - lyrics = backend(artist, title) + lyrics = backend.fetch(artist, title) if lyrics: - log.debug(u'got lyrics from backend: {0}', backend.__name__) + self._log.debug(u'got lyrics from backend: {0}', + backend.__class__.__name__) return _scrape_strip_cruft(lyrics, True) From 11d5f93db1c95defe4477d58ef67aa5ce51ee599 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 6 Jan 2015 18:33:56 +0100 Subject: [PATCH 45/86] Convert importadded plugin Many listeners. Everything is now a method --- beetsplug/importadded.py | 189 ++++++++++++++++++--------------------- 1 file changed, 86 insertions(+), 103 deletions(-) diff --git a/beetsplug/importadded.py b/beetsplug/importadded.py index 00f2658b0..66639d143 100644 --- a/beetsplug/importadded.py +++ b/beetsplug/importadded.py @@ -8,13 +8,10 @@ from __future__ import unicode_literals, absolute_import, print_function import os -from beets import logging from beets import config from beets import util from beets.plugins import BeetsPlugin -log = logging.getLogger(__name__) - class ImportAddedPlugin(BeetsPlugin): def __init__(self): @@ -23,118 +20,104 @@ class ImportAddedPlugin(BeetsPlugin): 'preserve_mtimes': False, }) + # item.id for new items that were reimported + self.reimported_item_ids = None + # album.path for old albums that were replaced by a reimported album + self.replaced_album_paths = None + # item path in the library to the mtime of the source file + self.item_mtime = dict() -@ImportAddedPlugin.listen('import_task_start') -def check_config(task, session): - config['importadded']['preserve_mtimes'].get(bool) + register = self.register_listener + register('import_task_start', self.check_config) + register('import_task_start', self.record_if_inplace) + register('import_task_files', self.record_reimported) + register('before_item_moved', self.record_import_mtime) + register('item_copied', self.record_import_mtime) + register('item_linked', self.record_import_mtime) + register('album_imported', self.update_album_times) + register('item_imported', self.update_item_times) -# item.id for new items that were reimported -reimported_item_ids = None + def check_config(self, task, session): + self.config['preserve_mtimes'].get(bool) -# album.path for old albums that were replaced by a new reimported album -replaced_album_paths = None + def reimported_item(self, item): + return item.id in self.reimported_item_ids + def reimported_album(self, album): + return album.path in self.replaced_album_paths -def reimported_item(item): - return item.id in reimported_item_ids + def record_if_inplace(self, task, session): + if not (session.config['copy'] or session.config['move'] or + session.config['link']): + self._log.debug(u"In place import detected, recording mtimes from " + u"source paths") + for item in task.items: + self.record_import_mtime(item, item.path, item.path) + def record_reimported(self, task, session): + self.reimported_item_ids = set(item.id for item, replaced_items + in task.replaced_items.iteritems() + if replaced_items) + self.replaced_album_paths = set(task.replaced_albums.keys()) -def reimported_album(album): - return album.path in replaced_album_paths + def write_file_mtime(self, path, mtime): + """Write the given mtime to the destination path. + """ + stat = os.stat(util.syspath(path)) + os.utime(util.syspath(path), (stat.st_atime, mtime)) + def write_item_mtime(self, item, mtime): + """Write the given mtime to an item's `mtime` field and to the mtime + of the item's file. + """ + if mtime is None: + self._log.warn(u"No mtime to be preserved for item '{0}'", + util.displayable_path(item.path)) + return -@ImportAddedPlugin.listen('import_task_start') -def record_if_inplace(task, session): - if not (session.config['copy'] or session.config['move'] or - session.config['link']): - log.debug(u"In place import detected, recording mtimes from source" - u" paths") - for item in task.items: - record_import_mtime(item, item.path, item.path) + # The file's mtime on disk must be in sync with the item's mtime + self.write_file_mtime(util.syspath(item.path), mtime) + item.mtime = mtime + def record_import_mtime(self, item, source, destination): + """Record the file mtime of an item's path before its import. + """ + mtime = os.stat(util.syspath(source)).st_mtime + self.item_mtime[destination] = mtime + self._log.debug(u"Recorded mtime {0} for item '{1}' imported from " + u"'{2}'", mtime, util.displayable_path(destination), + util.displayable_path(source)) -@ImportAddedPlugin.listen('import_task_files') -def record_reimported(task, session): - global reimported_item_ids, replaced_album_paths - reimported_item_ids = set([item.id for item, replaced_items - in task.replaced_items.iteritems() - if replaced_items]) - replaced_album_paths = set(task.replaced_albums.keys()) + def update_album_times(self, lib, album): + if self.reimported_album(album): + self._log.debug(u"Album '{0}' is reimported, skipping import of " + u"added dates for the album and its items.", + util.displayable_path(album.path)) + return + album_mtimes = [] + for item in album.items(): + mtime = self.item_mtime.pop(item.path, None) + if mtime: + album_mtimes.append(mtime) + if config['importadded']['preserve_mtimes'].get(bool): + self.write_item_mtime(item, mtime) + item.store() + album.added = min(album_mtimes) + self._log.debug(u"Import of album '{0}', selected album.added={1} " + u"from item file mtimes.", album.album, album.added) + album.store() -def write_file_mtime(path, mtime): - """Write the given mtime to the destination path. - """ - stat = os.stat(util.syspath(path)) - os.utime(util.syspath(path), - (stat.st_atime, mtime)) - - -def write_item_mtime(item, mtime): - """Write the given mtime to an item's `mtime` field and to the mtime of the - item's file. - """ - if mtime is None: - log.warn(u"No mtime to be preserved for item '{0}'", - util.displayable_path(item.path)) - return - - # The file's mtime on disk must be in sync with the item's mtime - write_file_mtime(util.syspath(item.path), mtime) - item.mtime = mtime - - -# key: item path in the library -# value: the file mtime of the file the item was imported from -item_mtime = dict() - - -@ImportAddedPlugin.listen('before_item_moved') -@ImportAddedPlugin.listen('item_copied') -@ImportAddedPlugin.listen('item_linked') -def record_import_mtime(item, source, destination): - """Record the file mtime of an item's path before its import. - """ - mtime = os.stat(util.syspath(source)).st_mtime - item_mtime[destination] = mtime - log.debug(u"Recorded mtime {0} for item '{1}' imported from '{2}'", - mtime, util.displayable_path(destination), - util.displayable_path(source)) - - -@ImportAddedPlugin.listen('album_imported') -def update_album_times(lib, album): - if reimported_album(album): - log.debug(u"Album '{0}' is reimported, skipping import of added dates" - u" for the album and its items.", - util.displayable_path(album.path)) - return - - album_mtimes = [] - for item in album.items(): - mtime = item_mtime.pop(item.path, None) + def update_item_times(self, lib, item): + if self.reimported_item(item): + self._log.debug(u"Item '{0}' is reimported, skipping import of " + u"added date.", util.displayable_path(item.path)) + return + mtime = self.item_mtime.pop(item.path, None) if mtime: - album_mtimes.append(mtime) + item.added = mtime if config['importadded']['preserve_mtimes'].get(bool): - write_item_mtime(item, mtime) - item.store() - album.added = min(album_mtimes) - log.debug(u"Import of album '{0}', selected album.added={1} from item" - u" file mtimes.", album.album, album.added) - album.store() - - -@ImportAddedPlugin.listen('item_imported') -def update_item_times(lib, item): - if reimported_item(item): - log.debug(u"Item '{0}' is reimported, skipping import of added " - u"date.", util.displayable_path(item.path)) - return - mtime = item_mtime.pop(item.path, None) - if mtime: - item.added = mtime - if config['importadded']['preserve_mtimes'].get(bool): - write_item_mtime(item, mtime) - log.debug(u"Import of item '{0}', selected item.added={1}", - util.displayable_path(item.path), item.added) - item.store() + self.write_item_mtime(item, mtime) + self._log.debug(u"Import of item '{0}', selected item.added={1}", + util.displayable_path(item.path), item.added) + item.store() From 5c1cc6e7fc9f1c9d7e08fb5fef766511f4914219 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 6 Jan 2015 18:56:08 +0100 Subject: [PATCH 46/86] Convert the convert plugin to the new logging system --- beetsplug/convert.py | 496 ++++++++++++++++++++++--------------------- 1 file changed, 249 insertions(+), 247 deletions(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 610591e94..0e7838148 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -21,12 +21,11 @@ import tempfile import shlex from string import Template -from beets import logging, ui, util, plugins, config +from beets import ui, util, plugins, config from beets.plugins import BeetsPlugin from beetsplug.embedart import embed_item from beets.util.confit import ConfigTypeError -log = logging.getLogger(__name__) _fs_lock = threading.Lock() _temp_files = [] # Keep track of temporary transcoded files for deletion. @@ -82,53 +81,6 @@ def get_format(format=None): return (command.encode('utf8'), extension.encode('utf8')) -def encode(command, source, dest, pretend=False): - """Encode `source` to `dest` using command template `command`. - - Raises `subprocess.CalledProcessError` if the command exited with a - non-zero status code. - """ - quiet = config['convert']['quiet'].get() - - if not quiet and not pretend: - log.info(u'Encoding {0}', util.displayable_path(source)) - - # Substitute $source and $dest in the argument list. - args = shlex.split(command) - for i, arg in enumerate(args): - args[i] = Template(arg).safe_substitute({ - 'source': source, - 'dest': dest, - }) - - if pretend: - log.info(' '.join(args)) - return - - try: - util.command_output(args) - except subprocess.CalledProcessError as exc: - # Something went wrong (probably Ctrl+C), remove temporary files - log.info(u'Encoding {0} failed. Cleaning up...', - util.displayable_path(source)) - log.debug(u'Command {0} exited with status {1}', - exc.cmd.decode('utf8', 'ignore'), - exc.returncode) - util.remove(dest) - util.prune_dirs(os.path.dirname(dest)) - raise - except OSError as exc: - raise ui.UserError( - u"convert: could invoke '{0}': {1}".format( - ' '.join(args), exc - ) - ) - - if not quiet and not pretend: - log.info(u'Finished encoding {0}', - util.displayable_path(source)) - - def should_transcode(item, format): """Determine whether the item should be transcoded as part of conversion (i.e., its bitrate is high or it has the wrong format). @@ -141,195 +93,6 @@ def should_transcode(item, format): item.bitrate >= 1000 * maxbr -def convert_item(dest_dir, keep_new, path_formats, format, pretend=False): - command, ext = get_format(format) - item, original, converted = None, None, None - while True: - item = yield (item, original, converted) - dest = item.destination(basedir=dest_dir, path_formats=path_formats) - - # When keeping the new file in the library, we first move the - # current (pristine) file to the destination. We'll then copy it - # back to its old path or transcode it to a new path. - if keep_new: - original = dest - converted = item.path - if should_transcode(item, format): - converted = replace_ext(converted, ext) - else: - original = item.path - if should_transcode(item, format): - dest = replace_ext(dest, ext) - converted = dest - - # Ensure that only one thread tries to create directories at a - # time. (The existence check is not atomic with the directory - # creation inside this function.) - if not pretend: - with _fs_lock: - util.mkdirall(dest) - - if os.path.exists(util.syspath(dest)): - log.info(u'Skipping {0} (target file exists)', - util.displayable_path(item.path)) - continue - - if keep_new: - if pretend: - log.info(u'mv {0} {1}', - util.displayable_path(item.path), - util.displayable_path(original)) - else: - log.info(u'Moving to {0}', util.displayable_path(original)) - util.move(item.path, original) - - if should_transcode(item, format): - try: - encode(command, original, converted, pretend) - except subprocess.CalledProcessError: - continue - else: - if pretend: - log.info(u'cp {0} {1}', - util.displayable_path(original), - util.displayable_path(converted)) - else: - # No transcoding necessary. - log.info(u'Copying {0}', util.displayable_path(item.path)) - util.copy(original, converted) - - if pretend: - continue - - # Write tags from the database to the converted file. - item.try_write(path=converted) - - if keep_new: - # If we're keeping the transcoded file, read it again (after - # writing) to get new bitrate, duration, etc. - item.path = converted - item.read() - item.store() # Store new path and audio data. - - if config['convert']['embed']: - album = item.get_album() - if album and album.artpath: - embed_item(item, album.artpath, itempath=converted) - - if keep_new: - plugins.send('after_convert', item=item, - dest=dest, keepnew=True) - else: - plugins.send('after_convert', item=item, - dest=converted, keepnew=False) - - -def convert_on_import(lib, item): - """Transcode a file automatically after it is imported into the - library. - """ - format = config['convert']['format'].get(unicode).lower() - if should_transcode(item, format): - command, ext = get_format() - fd, dest = tempfile.mkstemp('.' + ext) - os.close(fd) - _temp_files.append(dest) # Delete the transcode later. - try: - encode(command, item.path, dest) - except subprocess.CalledProcessError: - return - item.path = dest - item.write() - item.read() # Load new audio information data. - item.store() - - -def copy_album_art(album, dest_dir, path_formats, pretend=False): - """Copies the associated cover art of the album. Album must have at least - one track. - """ - if not album or not album.artpath: - return - - album_item = album.items().get() - # Album shouldn't be empty. - if not album_item: - return - - # Get the destination of the first item (track) of the album, we use this - # function to format the path accordingly to path_formats. - dest = album_item.destination(basedir=dest_dir, path_formats=path_formats) - - # Remove item from the path. - dest = os.path.join(*util.components(dest)[:-1]) - - dest = album.art_destination(album.artpath, item_dir=dest) - if album.artpath == dest: - return - - if not pretend: - util.mkdirall(dest) - - if os.path.exists(util.syspath(dest)): - log.info(u'Skipping {0} (target file exists)', - util.displayable_path(album.artpath)) - return - - if pretend: - log.info(u'cp {0} {1}', - util.displayable_path(album.artpath), - util.displayable_path(dest)) - else: - log.info(u'Copying cover art to {0}', - util.displayable_path(dest)) - util.copy(album.artpath, dest) - - -def convert_func(lib, opts, args): - if not opts.dest: - opts.dest = config['convert']['dest'].get() - if not opts.dest: - raise ui.UserError('no convert destination set') - opts.dest = util.bytestring_path(opts.dest) - - if not opts.threads: - opts.threads = config['convert']['threads'].get(int) - - if config['convert']['paths']: - path_formats = ui.get_path_formats(config['convert']['paths']) - else: - path_formats = ui.get_path_formats() - - if not opts.format: - opts.format = config['convert']['format'].get(unicode).lower() - - pretend = opts.pretend if opts.pretend is not None else \ - config['convert']['pretend'].get(bool) - - if not pretend: - ui.commands.list_items(lib, ui.decargs(args), opts.album, None) - - if not (opts.yes or ui.input_yn("Convert? (Y/n)")): - return - - if opts.album: - albums = lib.albums(ui.decargs(args)) - items = (i for a in albums for i in a.items()) - if config['convert']['copy_album_art']: - for album in albums: - copy_album_art(album, opts.dest, path_formats, pretend) - else: - items = iter(lib.items(ui.decargs(args))) - convert = [convert_item(opts.dest, - opts.keep_new, - path_formats, - opts.format, - pretend) - for _ in range(opts.threads)] - pipe = util.pipeline.Pipeline([items, convert]) - pipe.run_parallel() - - class ConvertPlugin(BeetsPlugin): def __init__(self): super(ConvertPlugin, self).__init__() @@ -367,6 +130,8 @@ class ConvertPlugin(BeetsPlugin): }) self._import_stages = [self.auto_convert] + self.register_listener('import_task_files', self._cleanup) + def commands(self): cmd = ui.Subcommand('convert', help='convert to external location') cmd.parser.add_option('-p', '--pretend', action='store_true', @@ -385,19 +150,256 @@ class ConvertPlugin(BeetsPlugin): help='set the destination directory') cmd.parser.add_option('-y', '--yes', action='store_true', dest='yes', help='do not ask for confirmation') - cmd.func = convert_func + cmd.func = self.convert_func return [cmd] def auto_convert(self, config, task): if self.config['auto']: for item in task.imported_items(): - convert_on_import(config.lib, item) + self.convert_on_import(config.lib, item) + # Utilities converted from functions to methods on logging overhaul -@ConvertPlugin.listen('import_task_files') -def _cleanup(task, session): - for path in task.old_paths: - if path in _temp_files: - if os.path.isfile(path): - util.remove(path) - _temp_files.remove(path) + def encode(self, command, source, dest, pretend=False): + """Encode `source` to `dest` using command template `command`. + + Raises `subprocess.CalledProcessError` if the command exited with a + non-zero status code. + """ + quiet = config['convert']['quiet'].get() + + if not quiet and not pretend: + self._log.info(u'Encoding {0}', util.displayable_path(source)) + + # Substitute $source and $dest in the argument list. + args = shlex.split(command) + for i, arg in enumerate(args): + args[i] = Template(arg).safe_substitute({ + 'source': source, + 'dest': dest, + }) + + if pretend: + self._log.info(' '.join(args)) + return + + try: + util.command_output(args) + except subprocess.CalledProcessError as exc: + # Something went wrong (probably Ctrl+C), remove temporary files + self._log.info(u'Encoding {0} failed. Cleaning up...', + util.displayable_path(source)) + self._log.debug(u'Command {0} exited with status {1}', + exc.cmd.decode('utf8', 'ignore'), + exc.returncode) + util.remove(dest) + util.prune_dirs(os.path.dirname(dest)) + raise + except OSError as exc: + raise ui.UserError( + u"convert: could invoke '{0}': {1}".format( + ' '.join(args), exc + ) + ) + + if not quiet and not pretend: + self._log.info(u'Finished encoding {0}', + util.displayable_path(source)) + + def convert_item(self, dest_dir, keep_new, path_formats, format, + pretend=False): + command, ext = get_format(format) + item, original, converted = None, None, None + while True: + item = yield (item, original, converted) + dest = item.destination(basedir=dest_dir, + path_formats=path_formats) + + # When keeping the new file in the library, we first move the + # current (pristine) file to the destination. We'll then copy it + # back to its old path or transcode it to a new path. + if keep_new: + original = dest + converted = item.path + if should_transcode(item, format): + converted = replace_ext(converted, ext) + else: + original = item.path + if should_transcode(item, format): + dest = replace_ext(dest, ext) + converted = dest + + # Ensure that only one thread tries to create directories at a + # time. (The existence check is not atomic with the directory + # creation inside this function.) + if not pretend: + with _fs_lock: + util.mkdirall(dest) + + if os.path.exists(util.syspath(dest)): + self._log.info(u'Skipping {0} (target file exists)', + util.displayable_path(item.path)) + continue + + if keep_new: + if pretend: + self._log.info(u'mv {0} {1}', + util.displayable_path(item.path), + util.displayable_path(original)) + else: + self._log.info(u'Moving to {0}', + util.displayable_path(original)) + util.move(item.path, original) + + if should_transcode(item, format): + try: + self.encode(command, original, converted, pretend) + except subprocess.CalledProcessError: + continue + else: + if pretend: + self._log.info(u'cp {0} {1}', + util.displayable_path(original), + util.displayable_path(converted)) + else: + # No transcoding necessary. + self._log.info(u'Copying {0}', + util.displayable_path(item.path)) + util.copy(original, converted) + + if pretend: + continue + + # Write tags from the database to the converted file. + item.try_write(path=converted) + + if keep_new: + # If we're keeping the transcoded file, read it again (after + # writing) to get new bitrate, duration, etc. + item.path = converted + item.read() + item.store() # Store new path and audio data. + + if config['convert']['embed']: + album = item.get_album() + if album and album.artpath: + embed_item(item, album.artpath, itempath=converted) + + if keep_new: + plugins.send('after_convert', item=item, + dest=dest, keepnew=True) + else: + plugins.send('after_convert', item=item, + dest=converted, keepnew=False) + + def copy_album_art(self, album, dest_dir, path_formats, pretend=False): + """Copies the associated cover art of the album. Album must have at + least one track. + """ + if not album or not album.artpath: + return + + album_item = album.items().get() + # Album shouldn't be empty. + if not album_item: + return + + # Get the destination of the first item (track) of the album, we use + # this function to format the path accordingly to path_formats. + dest = album_item.destination(basedir=dest_dir, + path_formats=path_formats) + + # Remove item from the path. + dest = os.path.join(*util.components(dest)[:-1]) + + dest = album.art_destination(album.artpath, item_dir=dest) + if album.artpath == dest: + return + + if not pretend: + util.mkdirall(dest) + + if os.path.exists(util.syspath(dest)): + self._log.info(u'Skipping {0} (target file exists)', + util.displayable_path(album.artpath)) + return + + if pretend: + self._log.info(u'cp {0} {1}', + util.displayable_path(album.artpath), + util.displayable_path(dest)) + else: + self._log.info(u'Copying cover art to {0}', + util.displayable_path(dest)) + util.copy(album.artpath, dest) + + def convert_func(self, lib, opts, args): + if not opts.dest: + opts.dest = config['convert']['dest'].get() + if not opts.dest: + raise ui.UserError('no convert destination set') + opts.dest = util.bytestring_path(opts.dest) + + if not opts.threads: + opts.threads = config['convert']['threads'].get(int) + + if config['convert']['paths']: + path_formats = ui.get_path_formats(config['convert']['paths']) + else: + path_formats = ui.get_path_formats() + + if not opts.format: + opts.format = config['convert']['format'].get(unicode).lower() + + pretend = opts.pretend if opts.pretend is not None else \ + config['convert']['pretend'].get(bool) + + if not pretend: + ui.commands.list_items(lib, ui.decargs(args), opts.album, None) + + if not (opts.yes or ui.input_yn("Convert? (Y/n)")): + return + + if opts.album: + albums = lib.albums(ui.decargs(args)) + items = (i for a in albums for i in a.items()) + if config['convert']['copy_album_art']: + for album in albums: + self.copy_album_art(album, opts.dest, path_formats, + pretend) + else: + items = iter(lib.items(ui.decargs(args))) + convert = [self.convert_item(opts.dest, + opts.keep_new, + path_formats, + opts.format, + pretend) + for _ in range(opts.threads)] + pipe = util.pipeline.Pipeline([items, convert]) + pipe.run_parallel() + + def convert_on_import(self, lib, item): + """Transcode a file automatically after it is imported into the + library. + """ + format = config['convert']['format'].get(unicode).lower() + if should_transcode(item, format): + command, ext = get_format() + fd, dest = tempfile.mkstemp('.' + ext) + os.close(fd) + _temp_files.append(dest) # Delete the transcode later. + try: + self.encode(command, item.path, dest) + except subprocess.CalledProcessError: + return + item.path = dest + item.write() + item.read() # Load new audio information data. + item.store() + + def _cleanup(self, task, session): + for path in task.old_paths: + if path in _temp_files: + if os.path.isfile(path): + util.remove(path) + _temp_files.remove(path) From 8097ff8c1d32edce30b827d1f508f78ba8cc3e52 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 6 Jan 2015 19:19:30 +0100 Subject: [PATCH 47/86] Convert fetchart plugin, with OO rewrite of sources Art sources are now classes --- beetsplug/fetchart.py | 477 +++++++++++++++++++++--------------------- 1 file changed, 237 insertions(+), 240 deletions(-) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index a1bb37480..0d068e133 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -21,7 +21,6 @@ from tempfile import NamedTemporaryFile import requests -from beets import logging from beets import plugins from beets import importer from beets import ui @@ -39,191 +38,166 @@ IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg'] CONTENT_TYPES = ('image/jpeg',) DOWNLOAD_EXTENSION = '.jpg' -log = logging.getLogger(__name__) - requests_session = requests.Session() requests_session.headers = {'User-Agent': 'beets'} -def _fetch_image(url): - """Downloads an image from a URL and checks whether it seems to - actually be an image. If so, returns a path to the downloaded image. - Otherwise, returns None. - """ - log.debug(u'downloading art: {0}', url) - try: - with closing(requests_session.get(url, stream=True)) as resp: - if 'Content-Type' not in resp.headers \ - or resp.headers['Content-Type'] not in CONTENT_TYPES: - log.debug(u'not an image') - return - - # Generate a temporary file with the correct extension. - with NamedTemporaryFile(suffix=DOWNLOAD_EXTENSION, delete=False) \ - as fh: - for chunk in resp.iter_content(): - fh.write(chunk) - log.debug(u'downloaded art to: {0}', - util.displayable_path(fh.name)) - return fh.name - except (IOError, requests.RequestException): - log.debug(u'error fetching art') - - # ART SOURCES ################################################################ -# Cover Art Archive. +class ArtSource(object): + def __init__(self, log): + self._log = log -CAA_URL = 'http://coverartarchive.org/release/{mbid}/front-500.jpg' -CAA_GROUP_URL = 'http://coverartarchive.org/release-group/{mbid}/front-500.jpg' + def get(self, album): + raise NotImplementedError() -def caa_art(album): - """Return the Cover Art Archive and Cover Art Archive release group URLs - using album MusicBrainz release ID and release group ID. - """ - if album.mb_albumid: - yield CAA_URL.format(mbid=album.mb_albumid) - if album.mb_releasegroupid: - yield CAA_GROUP_URL.format(mbid=album.mb_releasegroupid) +class CoverArtArchive(ArtSource): + """Cover Art Archive""" + URL = 'http://coverartarchive.org/release/{mbid}/front-500.jpg' + GROUP_URL = 'http://coverartarchive.org/release-group/{mbid}/front-500.jpg' + + def get(self, album): + """Return the Cover Art Archive and Cover Art Archive release group URLs + using album MusicBrainz release ID and release group ID. + """ + if album.mb_albumid: + yield self.URL.format(mbid=album.mb_albumid) + if album.mb_releasegroupid: + yield self.GROUP_URL.format(mbid=album.mb_releasegroupid) -# Art from Amazon. +class Amazon(ArtSource): + URL = 'http://images.amazon.com/images/P/%s.%02i.LZZZZZZZ.jpg' + INDICES = (1, 2) -AMAZON_URL = 'http://images.amazon.com/images/P/%s.%02i.LZZZZZZZ.jpg' -AMAZON_INDICES = (1, 2) + def get(self, album): + """Generate URLs using Amazon ID (ASIN) string. + """ + if album.asin: + for index in self.INDICES: + yield self.URL % (album.asin, index) -def art_for_asin(album): - """Generate URLs using Amazon ID (ASIN) string. - """ - if album.asin: - for index in AMAZON_INDICES: - yield AMAZON_URL % (album.asin, index) +class AlbumArtOrg(ArtSource): + """AlbumArt.org scraper""" + URL = 'http://www.albumart.org/index_detail.php' + PAT = r'href\s*=\s*"([^>"]*)"[^>]*title\s*=\s*"View larger image"' - -# AlbumArt.org scraper. - -AAO_URL = 'http://www.albumart.org/index_detail.php' -AAO_PAT = r'href\s*=\s*"([^>"]*)"[^>]*title\s*=\s*"View larger image"' - - -def aao_art(album): - """Return art URL from AlbumArt.org using album ASIN. - """ - if not album.asin: - return - # Get the page from albumart.org. - try: - resp = requests_session.get(AAO_URL, params={'asin': album.asin}) - log.debug(u'scraped art URL: {0}', resp.url) - except requests.RequestException: - log.debug(u'error scraping art page') - return - - # Search the page for the image URL. - m = re.search(AAO_PAT, resp.text) - if m: - image_url = m.group(1) - yield image_url - else: - log.debug(u'no image found on page') - - -# Google Images scraper. - -GOOGLE_URL = 'https://ajax.googleapis.com/ajax/services/search/images' - - -def google_art(album): - """Return art URL from google.org given an album title and - interpreter. - """ - if not (album.albumartist and album.album): - return - search_string = (album.albumartist + ',' + album.album).encode('utf-8') - response = requests_session.get(GOOGLE_URL, params={ - 'v': '1.0', - 'q': search_string, - 'start': '0', - }) - - # Get results using JSON. - try: - results = response.json() - data = results['responseData'] - dataInfo = data['results'] - for myUrl in dataInfo: - yield myUrl['unescapedUrl'] - except: - log.debug(u'error scraping art page') - return - - -# Art from the iTunes Store. - -def itunes_art(album): - """Return art URL from iTunes Store given an album title. - """ - search_string = (album.albumartist + ' ' + album.album).encode('utf-8') - try: - # Isolate bugs in the iTunes library while searching. + def get(self, album): + """Return art URL from AlbumArt.org using album ASIN. + """ + if not album.asin: + return + # Get the page from albumart.org. try: - itunes_album = itunes.search_album(search_string)[0] - except Exception as exc: - log.debug('iTunes search failed: {0}', exc) + resp = requests_session.get(self.URL, params={'asin': album.asin}) + self._log.debug(u'scraped art URL: {0}', resp.url) + except requests.RequestException: + self._log.debug(u'error scraping art page') return - if itunes_album.get_artwork()['100']: - small_url = itunes_album.get_artwork()['100'] - big_url = small_url.replace('100x100', '1200x1200') - yield big_url + # Search the page for the image URL. + m = re.search(self.PAT, resp.text) + if m: + image_url = m.group(1) + yield image_url else: - log.debug(u'album has no artwork in iTunes Store') - except IndexError: - log.debug(u'album not found in iTunes Store') + self._log.debug(u'no image found on page') -# Art from the filesystem. +class GoogleImages(ArtSource): + URL = 'https://ajax.googleapis.com/ajax/services/search/images' + + def get(self, album): + """Return art URL from google.org given an album title and + interpreter. + """ + if not (album.albumartist and album.album): + return + search_string = (album.albumartist + ',' + album.album).encode('utf-8') + response = requests_session.get(self.URL, params={ + 'v': '1.0', + 'q': search_string, + 'start': '0', + }) + + # Get results using JSON. + try: + results = response.json() + data = results['responseData'] + dataInfo = data['results'] + for myUrl in dataInfo: + yield myUrl['unescapedUrl'] + except: + self._log.debug(u'error scraping art page') + return -def filename_priority(filename, cover_names): - """Sort order for image names. +class ITunesStore(ArtSource): + # Art from the iTunes Store. + def get(self, album): + """Return art URL from iTunes Store given an album title. + """ + search_string = (album.albumartist + ' ' + album.album).encode('utf-8') + try: + # Isolate bugs in the iTunes library while searching. + try: + itunes_album = itunes.search_album(search_string)[0] + except Exception as exc: + self._log.debug('iTunes search failed: {0}', exc) + return - Return indexes of cover names found in the image filename. This - means that images with lower-numbered and more keywords will have higher - priority. - """ - return [idx for (idx, x) in enumerate(cover_names) if x in filename] + if itunes_album.get_artwork()['100']: + small_url = itunes_album.get_artwork()['100'] + big_url = small_url.replace('100x100', '1200x1200') + yield big_url + else: + self._log.debug(u'album has no artwork in iTunes Store') + except IndexError: + self._log.debug(u'album not found in iTunes Store') -def art_in_path(path, cover_names, cautious): - """Look for album art files in a specified directory. - """ - if not os.path.isdir(path): - return +class FileSystem(ArtSource): + """Art from the filesystem""" + @staticmethod + def filename_priority(filename, cover_names): + """Sort order for image names. - # Find all files that look like images in the directory. - images = [] - for fn in os.listdir(path): - for ext in IMAGE_EXTENSIONS: - if fn.lower().endswith('.' + ext): - images.append(fn) + Return indexes of cover names found in the image filename. This + means that images with lower-numbered and more keywords will have + higher priority. + """ + return [idx for (idx, x) in enumerate(cover_names) if x in filename] - # Look for "preferred" filenames. - images = sorted(images, key=lambda x: filename_priority(x, cover_names)) - cover_pat = r"(\b|_)({0})(\b|_)".format('|'.join(cover_names)) - for fn in images: - if re.search(cover_pat, os.path.splitext(fn)[0], re.I): - log.debug(u'using well-named art file {0}', - util.displayable_path(fn)) - return os.path.join(path, fn) + def get(self, path, cover_names, cautious): + """Look for album art files in a specified directory. + """ + if not os.path.isdir(path): + return - # Fall back to any image in the folder. - if images and not cautious: - log.debug(u'using fallback art file {0}', - util.displayable_path(images[0])) - return os.path.join(path, images[0]) + # Find all files that look like images in the directory. + images = [] + for fn in os.listdir(path): + for ext in IMAGE_EXTENSIONS: + if fn.lower().endswith('.' + ext): + images.append(fn) + + # Look for "preferred" filenames. + images = sorted(images, + key=lambda x: self.filename_priority(x, cover_names)) + cover_pat = r"(\b|_)({0})(\b|_)".format('|'.join(cover_names)) + for fn in images: + if re.search(cover_pat, os.path.splitext(fn)[0], re.I): + self._log.debug(u'using well-named art file {0}', + util.displayable_path(fn)) + return os.path.join(path, fn) + + # Fall back to any image in the folder. + if images and not cautious: + self._log.debug(u'using fallback art file {0}', + util.displayable_path(images[0])) + return os.path.join(path, images[0]) # Try each source in turn. @@ -231,90 +205,16 @@ def art_in_path(path, cover_names, cautious): SOURCES_ALL = [u'coverart', u'itunes', u'amazon', u'albumart', u'google'] ART_FUNCS = { - u'coverart': caa_art, - u'itunes': itunes_art, - u'albumart': aao_art, - u'amazon': art_for_asin, - u'google': google_art, + u'coverart': CoverArtArchive, + u'itunes': ITunesStore, + u'albumart': AlbumArtOrg, + u'amazon': Amazon, + u'google': GoogleImages, } - -def _source_urls(album, sources=SOURCES_ALL): - """Generate possible source URLs for an album's art. The URLs are - not guaranteed to work so they each need to be attempted in turn. - This allows the main `art_for_album` function to abort iteration - through this sequence early to avoid the cost of scraping when not - necessary. - """ - for s in sources: - urls = ART_FUNCS[s](album) - for url in urls: - yield url - - -def art_for_album(album, paths, maxwidth=None, local_only=False): - """Given an Album object, returns a path to downloaded art for the - album (or None if no art is found). If `maxwidth`, then images are - resized to this maximum pixel size. If `local_only`, then only local - image files from the filesystem are returned; no network requests - are made. - """ - out = None - - # Local art. - cover_names = config['fetchart']['cover_names'].as_str_seq() - cover_names = map(util.bytestring_path, cover_names) - cautious = config['fetchart']['cautious'].get(bool) - if paths: - for path in paths: - out = art_in_path(path, cover_names, cautious) - if out: - break - - # Web art sources. - remote_priority = config['fetchart']['remote_priority'].get(bool) - if not local_only and (remote_priority or not out): - for url in _source_urls(album, - config['fetchart']['sources'].as_str_seq()): - if maxwidth: - url = ArtResizer.shared.proxy_url(maxwidth, url) - candidate = _fetch_image(url) - if candidate: - out = candidate - break - - if maxwidth and out: - out = ArtResizer.shared.resize(maxwidth, out) - return out - - # PLUGIN LOGIC ############################################################### -def batch_fetch_art(lib, albums, force, maxwidth=None): - """Fetch album art for each of the albums. This implements the manual - fetchart CLI command. - """ - for album in albums: - if album.artpath and not force: - message = 'has album art' - else: - # In ordinary invocations, look for images on the - # filesystem. When forcing, however, always go to the Web - # sources. - local_paths = None if force else [album.path] - - path = art_for_album(album, local_paths, maxwidth) - if path: - album.set_art(path, False) - album.store() - message = ui.colorize('green', 'found album art') - else: - message = ui.colorize('red', 'no art found') - - log.info(u'{0} - {1}: {2}', album.albumartist, album.album, message) - - class FetchArtPlugin(plugins.BeetsPlugin): def __init__(self): super(FetchArtPlugin, self).__init__() @@ -342,8 +242,10 @@ class FetchArtPlugin(plugins.BeetsPlugin): available_sources = list(SOURCES_ALL) if not HAVE_ITUNES and u'itunes' in available_sources: available_sources.remove(u'itunes') - self.config['sources'] = plugins.sanitize_choices( + sources_name = plugins.sanitize_choices( self.config['sources'].as_str_seq(), available_sources) + self.sources = [ART_FUNCS[s](self._log) for s in sources_name] + self.fs_source = FileSystem(self._log) # Asynchronous; after music is added to the library. def fetch_art(self, session, task): @@ -359,7 +261,7 @@ class FetchArtPlugin(plugins.BeetsPlugin): # For any other choices (e.g., TRACKS), do nothing. return - path = art_for_album(task.album, task.paths, self.maxwidth, local) + path = self.art_for_album(task.album, task.paths, local) if path: self.art_paths[task] = path @@ -386,7 +288,102 @@ class FetchArtPlugin(plugins.BeetsPlugin): help='re-download art when already present') def func(lib, opts, args): - batch_fetch_art(lib, lib.albums(ui.decargs(args)), opts.force, - self.maxwidth) + self.batch_fetch_art(lib, lib.albums(ui.decargs(args)), opts.force) cmd.func = func return [cmd] + + # Utilities converted from functions to methods on logging overhaul + + def _fetch_image(self, url): + """Downloads an image from a URL and checks whether it seems to + actually be an image. If so, returns a path to the downloaded image. + Otherwise, returns None. + """ + self._log.debug(u'downloading art: {0}', url) + try: + with closing(requests_session.get(url, stream=True)) as resp: + if 'Content-Type' not in resp.headers \ + or resp.headers['Content-Type'] not in CONTENT_TYPES: + self._log.debug(u'not an image') + return + + # Generate a temporary file with the correct extension. + with NamedTemporaryFile(suffix=DOWNLOAD_EXTENSION, + delete=False) as fh: + for chunk in resp.iter_content(): + fh.write(chunk) + self._log.debug(u'downloaded art to: {0}', + util.displayable_path(fh.name)) + return fh.name + except (IOError, requests.RequestException): + self._log.debug(u'error fetching art') + + def art_for_album(self, album, paths, local_only=False): + """Given an Album object, returns a path to downloaded art for the + album (or None if no art is found). If `maxwidth`, then images are + resized to this maximum pixel size. If `local_only`, then only local + image files from the filesystem are returned; no network requests + are made. + """ + out = None + + # Local art. + cover_names = config['fetchart']['cover_names'].as_str_seq() + cover_names = map(util.bytestring_path, cover_names) + cautious = config['fetchart']['cautious'].get(bool) + if paths: + for path in paths: + # FIXME + out = self.fs_source.get(path, cover_names, cautious) + if out: + break + + # Web art sources. + remote_priority = config['fetchart']['remote_priority'].get(bool) + if not local_only and (remote_priority or not out): + for url in self._source_urls(album): + if self.maxwidth: + url = ArtResizer.shared.proxy_url(self.maxwidth, url) + candidate = self._fetch_image(url) + if candidate: + out = candidate + break + + if self.maxwidth and out: + out = ArtResizer.shared.resize(self.maxwidth, out) + return out + + def batch_fetch_art(self, lib, albums, force): + """Fetch album art for each of the albums. This implements the manual + fetchart CLI command. + """ + for album in albums: + if album.artpath and not force: + message = 'has album art' + else: + # In ordinary invocations, look for images on the + # filesystem. When forcing, however, always go to the Web + # sources. + local_paths = None if force else [album.path] + + path = self.art_for_album(album, local_paths) + if path: + album.set_art(path, False) + album.store() + message = ui.colorize('green', 'found album art') + else: + message = ui.colorize('red', 'no art found') + + self._log.info(u'{0.albumartist} - {0.album}: {1}', album, message) + + def _source_urls(self, album): + """Generate possible source URLs for an album's art. The URLs are + not guaranteed to work so they each need to be attempted in turn. + This allows the main `art_for_album` function to abort iteration + through this sequence early to avoid the cost of scraping when not + necessary. + """ + for source in self.sources: + urls = source.get(album) + for url in urls: + yield url From 1f0932968e3a97e416a716d12d4306ed436be1d6 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 6 Jan 2015 20:58:56 +0100 Subject: [PATCH 48/86] Simple test fixes importfeeds, lastgenre --- test/test_importfeeds.py | 4 ++-- test/test_lastgenre.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/test/test_importfeeds.py b/test/test_importfeeds.py index bee7fa28a..f9646c175 100644 --- a/test/test_importfeeds.py +++ b/test/test_importfeeds.py @@ -6,7 +6,7 @@ import shutil from _common import unittest from beets import config from beets.library import Item, Album, Library -from beetsplug.importfeeds import album_imported, ImportFeedsPlugin +from beetsplug.importfeeds import ImportFeedsPlugin class ImportfeedsTestTest(unittest.TestCase): @@ -30,7 +30,7 @@ class ImportfeedsTestTest(unittest.TestCase): self.lib.add(album) self.lib.add(item) - album_imported(self.lib, album) + self.importfeeds.album_imported(self.lib, album) playlist_path = os.path.join(self.feeds_dir, os.listdir(self.feeds_dir)[0]) self.assertTrue(playlist_path.endswith('album_name.m3u')) diff --git a/test/test_lastgenre.py b/test/test_lastgenre.py index 9aaace237..ee893be91 100644 --- a/test/test_lastgenre.py +++ b/test/test_lastgenre.py @@ -157,9 +157,10 @@ class LastGenrePluginTest(unittest.TestCase, TestHelper): tag2.item = MockPylastElem(u'Rap') return [tag1, tag2] - res = lastgenre._tags_for(MockPylastObj()) + plugin = lastgenre.LastGenrePlugin() + res = plugin._tags_for(MockPylastObj()) self.assertEqual(res, [u'pop', u'rap']) - res = lastgenre._tags_for(MockPylastObj(), min_weight=50) + res = plugin._tags_for(MockPylastObj(), min_weight=50) self.assertEqual(res, [u'pop']) def test_get_genre(self): From 1f627112219600a100343826c9474bdbb0443c00 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 6 Jan 2015 19:48:03 +0100 Subject: [PATCH 49/86] Fix FetchArt tests --- test/test_art.py | 87 ++++++++++++++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 36 deletions(-) diff --git a/test/test_art.py b/test/test_art.py index 22ead0c04..b7169067c 100644 --- a/test/test_art.py +++ b/test/test_art.py @@ -26,9 +26,19 @@ from beets.autotag import AlbumInfo, AlbumMatch from beets import library from beets import importer from beets import config +from beets import logging -class FetchImageTest(_common.TestCase): +logger = logging.getLogger('beets.test_art') + + +class UseThePlugin(_common.TestCase): + def setUp(self): + super(UseThePlugin, self).setUp() + self.plugin = fetchart.FetchArtPlugin() + + +class FetchImageTest(UseThePlugin): @responses.activate def run(self, *args, **kwargs): super(FetchImageTest, self).run(*args, **kwargs) @@ -39,12 +49,12 @@ class FetchImageTest(_common.TestCase): def test_invalid_type_returns_none(self): self.mock_response('image/watercolour') - artpath = fetchart._fetch_image('http://example.com') + artpath = self.plugin._fetch_image('http://example.com') self.assertEqual(artpath, None) def test_jpeg_type_returns_path(self): self.mock_response('image/jpeg') - artpath = fetchart._fetch_image('http://example.com') + artpath = self.plugin._fetch_image('http://example.com') self.assertNotEqual(artpath, None) @@ -54,41 +64,42 @@ class FSArtTest(_common.TestCase): self.dpath = os.path.join(self.temp_dir, 'arttest') os.mkdir(self.dpath) + self.source = fetchart.FileSystem(logger) + def test_finds_jpg_in_directory(self): _common.touch(os.path.join(self.dpath, 'a.jpg')) - fn = fetchart.art_in_path(self.dpath, ('art',), False) + fn = self.source.get(self.dpath, ('art',), False) self.assertEqual(fn, os.path.join(self.dpath, 'a.jpg')) def test_appropriately_named_file_takes_precedence(self): _common.touch(os.path.join(self.dpath, 'a.jpg')) _common.touch(os.path.join(self.dpath, 'art.jpg')) - fn = fetchart.art_in_path(self.dpath, ('art',), False) + fn = self.source.get(self.dpath, ('art',), False) self.assertEqual(fn, os.path.join(self.dpath, 'art.jpg')) def test_non_image_file_not_identified(self): _common.touch(os.path.join(self.dpath, 'a.txt')) - fn = fetchart.art_in_path(self.dpath, ('art',), False) + fn = self.source.get(self.dpath, ('art',), False) self.assertEqual(fn, None) def test_cautious_skips_fallback(self): _common.touch(os.path.join(self.dpath, 'a.jpg')) - fn = fetchart.art_in_path(self.dpath, ('art',), True) + fn = self.source.get(self.dpath, ('art',), True) self.assertEqual(fn, None) def test_empty_dir(self): - fn = fetchart.art_in_path(self.dpath, ('art',), True) + fn = self.source.get(self.dpath, ('art',), True) self.assertEqual(fn, None) def test_precedence_amongst_correct_files(self): _common.touch(os.path.join(self.dpath, 'back.jpg')) _common.touch(os.path.join(self.dpath, 'front.jpg')) _common.touch(os.path.join(self.dpath, 'front-cover.jpg')) - fn = fetchart.art_in_path(self.dpath, - ('cover', 'front', 'back'), False) + fn = self.source.get(self.dpath, ('cover', 'front', 'back'), False) self.assertEqual(fn, os.path.join(self.dpath, 'front-cover.jpg')) -class CombinedTest(_common.TestCase): +class CombinedTest(UseThePlugin): ASIN = 'xxxx' MBID = 'releaseid' AMAZON_URL = 'http://images.amazon.com/images/P/{0}.01.LZZZZZZZ.jpg' \ @@ -103,9 +114,6 @@ class CombinedTest(_common.TestCase): self.dpath = os.path.join(self.temp_dir, 'arttest') os.mkdir(self.dpath) - # Set up configuration. - self.plugin = fetchart.FetchArtPlugin() - @responses.activate def run(self, *args, **kwargs): super(CombinedTest, self).run(*args, **kwargs) @@ -116,61 +124,61 @@ class CombinedTest(_common.TestCase): def test_main_interface_returns_amazon_art(self): self.mock_response(self.AMAZON_URL) album = _common.Bag(asin=self.ASIN) - artpath = fetchart.art_for_album(album, None) + artpath = self.plugin.art_for_album(album, None) self.assertNotEqual(artpath, None) def test_main_interface_returns_none_for_missing_asin_and_path(self): album = _common.Bag() - artpath = fetchart.art_for_album(album, None) + artpath = self.plugin.art_for_album(album, None) self.assertEqual(artpath, None) def test_main_interface_gives_precedence_to_fs_art(self): _common.touch(os.path.join(self.dpath, 'art.jpg')) self.mock_response(self.AMAZON_URL) album = _common.Bag(asin=self.ASIN) - artpath = fetchart.art_for_album(album, [self.dpath]) + artpath = self.plugin.art_for_album(album, [self.dpath]) self.assertEqual(artpath, os.path.join(self.dpath, 'art.jpg')) def test_main_interface_falls_back_to_amazon(self): self.mock_response(self.AMAZON_URL) album = _common.Bag(asin=self.ASIN) - artpath = fetchart.art_for_album(album, [self.dpath]) + artpath = self.plugin.art_for_album(album, [self.dpath]) self.assertNotEqual(artpath, None) self.assertFalse(artpath.startswith(self.dpath)) def test_main_interface_tries_amazon_before_aao(self): self.mock_response(self.AMAZON_URL) album = _common.Bag(asin=self.ASIN) - fetchart.art_for_album(album, [self.dpath]) + self.plugin.art_for_album(album, [self.dpath]) self.assertEqual(len(responses.calls), 1) self.assertEqual(responses.calls[0].request.url, self.AMAZON_URL) def test_main_interface_falls_back_to_aao(self): self.mock_response(self.AMAZON_URL, content_type='text/html') album = _common.Bag(asin=self.ASIN) - fetchart.art_for_album(album, [self.dpath]) + self.plugin.art_for_album(album, [self.dpath]) self.assertEqual(responses.calls[-1].request.url, self.AAO_URL) def test_main_interface_uses_caa_when_mbid_available(self): self.mock_response(self.CAA_URL) album = _common.Bag(mb_albumid=self.MBID, asin=self.ASIN) - artpath = fetchart.art_for_album(album, None) + artpath = self.plugin.art_for_album(album, None) self.assertNotEqual(artpath, None) self.assertEqual(len(responses.calls), 1) self.assertEqual(responses.calls[0].request.url, self.CAA_URL) def test_local_only_does_not_access_network(self): album = _common.Bag(mb_albumid=self.MBID, asin=self.ASIN) - artpath = fetchart.art_for_album(album, [self.dpath], - local_only=True) + artpath = self.plugin.art_for_album(album, [self.dpath], + local_only=True) self.assertEqual(artpath, None) self.assertEqual(len(responses.calls), 0) def test_local_only_gets_fs_image(self): _common.touch(os.path.join(self.dpath, 'art.jpg')) album = _common.Bag(mb_albumid=self.MBID, asin=self.ASIN) - artpath = fetchart.art_for_album(album, [self.dpath], - None, local_only=True) + artpath = self.plugin.art_for_album(album, [self.dpath], + local_only=True) self.assertEqual(artpath, os.path.join(self.dpath, 'art.jpg')) self.assertEqual(len(responses.calls), 0) @@ -179,6 +187,10 @@ class AAOTest(_common.TestCase): ASIN = 'xxxx' AAO_URL = 'http://www.albumart.org/index_detail.php?asin={0}'.format(ASIN) + def setUp(self): + super(AAOTest, self).setUp() + self.source = fetchart.AlbumArtOrg(logger) + @responses.activate def run(self, *args, **kwargs): super(AAOTest, self).run(*args, **kwargs) @@ -197,13 +209,13 @@ class AAOTest(_common.TestCase): """ self.mock_response(self.AAO_URL, body) album = _common.Bag(asin=self.ASIN) - res = fetchart.aao_art(album) + res = self.source.get(album) self.assertEqual(list(res)[0], 'TARGET_URL') def test_aao_scraper_returns_no_result_when_no_image_present(self): self.mock_response(self.AAO_URL, 'blah blah') album = _common.Bag(asin=self.ASIN) - res = fetchart.aao_art(album) + res = self.source.get(album) self.assertEqual(list(res), []) @@ -211,6 +223,10 @@ class GoogleImageTest(_common.TestCase): _google_url = 'https://ajax.googleapis.com/ajax/services/search/images' + def setUp(self): + super(GoogleImageTest, self).setUp() + self.source = fetchart.GoogleImages(logger) + @responses.activate def run(self, *args, **kwargs): super(GoogleImageTest, self).run(*args, **kwargs) @@ -224,31 +240,31 @@ class GoogleImageTest(_common.TestCase): json = """{"responseData": {"results": [{"unescapedUrl": "url_to_the_image"}]}}""" self.mock_response(self._google_url, json) - result_url = fetchart.google_art(album) + result_url = self.source.get(album) self.assertEqual(list(result_url)[0], 'url_to_the_image') def test_google_art_dont_finds_image(self): album = _common.Bag(albumartist="some artist", album="some album") json = """bla blup""" self.mock_response(self._google_url, json) - result_url = fetchart.google_art(album) + result_url = self.source.get(album) self.assertEqual(list(result_url), []) -class ArtImporterTest(_common.TestCase): +class ArtImporterTest(UseThePlugin): def setUp(self): super(ArtImporterTest, self).setUp() # Mock the album art fetcher to always return our test file. self.art_file = os.path.join(self.temp_dir, 'tmpcover.jpg') _common.touch(self.art_file) - self.old_afa = fetchart.art_for_album + self.old_afa = self.plugin.art_for_album self.afa_response = self.art_file - def art_for_album(i, p, maxwidth=None, local_only=False): + def art_for_album(i, p, local_only=False): return self.afa_response - fetchart.art_for_album = art_for_album + self.plugin.art_for_album = art_for_album # Test library. self.libpath = os.path.join(self.temp_dir, 'tmplib.blb') @@ -263,8 +279,7 @@ class ArtImporterTest(_common.TestCase): self.album = self.lib.add_album([self.i]) self.lib._connection().commit() - # The plugin and import configuration. - self.plugin = fetchart.FetchArtPlugin() + # The import configuration. self.session = _common.import_session(self.lib) # Import task for the coroutine. @@ -283,7 +298,7 @@ class ArtImporterTest(_common.TestCase): def tearDown(self): self.lib._connection().close() super(ArtImporterTest, self).tearDown() - fetchart.art_for_album = self.old_afa + self.plugin.art_for_album = self.old_afa def _fetch_art(self, should_exist): """Execute the fetch_art coroutine for the task and return the From 30562024d277f3001ace59335e626a8b9e81d397 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 6 Jan 2015 20:51:08 +0100 Subject: [PATCH 50/86] Fix Lyrics plugin tests --- test/test_lyrics.py | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/test/test_lyrics.py b/test/test_lyrics.py index 492229630..4a71058db 100644 --- a/test/test_lyrics.py +++ b/test/test_lyrics.py @@ -22,6 +22,11 @@ from _common import unittest from beetsplug import lyrics from beets.library import Item from beets.util import confit +from beets import logging + +log = logging.getLogger('beets.test_lyrics') +raw_backend = lyrics.Backend(log) +google = lyrics.Google(log) class LyricsPluginTest(unittest.TestCase): @@ -128,11 +133,12 @@ class LyricsPluginTest(unittest.TestCase): texts += ["""All material found on this site is property\n of mywickedsongtext brand"""] for t in texts: - self.assertFalse(lyrics.is_lyrics(t)) + self.assertFalse(google.is_lyrics(t)) def test_slugify(self): text = u"http://site.com/\xe7afe-au_lait(boisson)" - self.assertEqual(lyrics.slugify(text), 'http://site.com/cafe_au_lait') + self.assertEqual(google.slugify(text), + 'http://site.com/cafe_au_lait') def test_scrape_strip_cruft(self): text = u""" @@ -160,7 +166,7 @@ class LyricsPluginTest(unittest.TestCase): "one\ntwo\nthree") def test_missing_lyrics(self): - self.assertFalse(lyrics.is_lyrics(LYRICS_TEXTS['missing_texts'])) + self.assertFalse(google.is_lyrics(LYRICS_TEXTS['missing_texts'])) def url_to_filename(url): @@ -196,7 +202,7 @@ class MockFetchUrl(object): def is_lyrics_content_ok(title, text): """Compare lyrics text to expected lyrics for given title""" - keywords = LYRICS_TEXTS[lyrics.slugify(title)] + keywords = LYRICS_TEXTS[google.slugify(title)] return all(x in text.lower() for x in keywords) LYRICS_ROOT_DIR = os.path.join(_common.RSRC, 'lyrics') @@ -298,14 +304,14 @@ class LyricsGooglePluginTest(unittest.TestCase): if sys.version_info[:3] < (2, 7, 3): self.skipTest("Python's built-in HTML parser is not good enough") lyrics.LyricsPlugin() - lyrics.fetch_url = MockFetchUrl() + raw_backend.fetch_url = MockFetchUrl() def test_mocked_source_ok(self): """Test that lyrics of the mocked page are correctly scraped""" url = self.source['url'] + self.source['path'] if os.path.isfile(url_to_filename(url)): - res = lyrics.scrape_lyrics_from_html(lyrics.fetch_url(url)) - self.assertTrue(lyrics.is_lyrics(res), url) + res = lyrics.scrape_lyrics_from_html(raw_backend.fetch_url(url)) + self.assertTrue(google.is_lyrics(res), url) self.assertTrue(is_lyrics_content_ok(self.source['title'], res), url) @@ -317,21 +323,21 @@ class LyricsGooglePluginTest(unittest.TestCase): for s in GOOGLE_SOURCES: url = s['url'] + s['path'] if os.path.isfile(url_to_filename(url)): - res = lyrics.scrape_lyrics_from_html(lyrics.fetch_url(url)) - self.assertTrue(lyrics.is_lyrics(res), url) + res = lyrics.scrape_lyrics_from_html(raw_backend.fetch_url(url)) + self.assertTrue(google.is_lyrics(res), url) self.assertTrue(is_lyrics_content_ok(s['title'], res), url) def test_default_ok(self): """Test default engines with the default query""" if not check_lyrics_fetched(): self.skipTest("Run lyrics_download_samples.py script first.") - for (fun, s) in zip([lyrics.fetch_lyricswiki, - lyrics.fetch_lyricscom, - lyrics.fetch_musixmatch], DEFAULT_SOURCES): + for (source, s) in zip([lyrics.LyricsWiki, + lyrics.LyricsCom, + lyrics.MusiXmatch], DEFAULT_SOURCES): url = s['url'] + s['path'] if os.path.isfile(url_to_filename(url)): - res = fun(s['artist'], s['title']) - self.assertTrue(lyrics.is_lyrics(res), url) + res = source(log).fetch(s['artist'], s['title']) + self.assertTrue(google.is_lyrics(res), url) self.assertTrue(is_lyrics_content_ok(s['title'], res), url) def test_is_page_candidate_exact_match(self): @@ -340,10 +346,10 @@ class LyricsGooglePluginTest(unittest.TestCase): from bs4 import SoupStrainer, BeautifulSoup s = self.source url = unicode(s['url'] + s['path']) - html = lyrics.fetch_url(url) + html = raw_backend.fetch_url(url) soup = BeautifulSoup(html, "html.parser", parse_only=SoupStrainer('title')) - self.assertEqual(lyrics.is_page_candidate(url, soup.title.string, + self.assertEqual(google.is_page_candidate(url, soup.title.string, s['title'], s['artist']), True, url) @@ -355,11 +361,11 @@ class LyricsGooglePluginTest(unittest.TestCase): urlTitle = u'example.com | Beats song by John doe' # very small diffs (typo) are ok eg 'beats' vs 'beets' with same artist - self.assertEqual(lyrics.is_page_candidate(url, urlTitle, s['title'], + self.assertEqual(google.is_page_candidate(url, urlTitle, s['title'], s['artist']), True, url) # reject different title urlTitle = u'example.com | seets bong lyrics by John doe' - self.assertEqual(lyrics.is_page_candidate(url, urlTitle, s['title'], + self.assertEqual(google.is_page_candidate(url, urlTitle, s['title'], s['artist']), False, url) From f504c7868107c43fbddfbf636cebb7b5759468e0 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 6 Jan 2015 21:05:59 +0100 Subject: [PATCH 51/86] Fix usage of embedart by convert plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instanciation of EmbedCovertArtPlugin on the fly: there may be several instances → problem with the listeners it registers? --- beetsplug/convert.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 0e7838148..76694cc12 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -23,7 +23,7 @@ from string import Template from beets import ui, util, plugins, config from beets.plugins import BeetsPlugin -from beetsplug.embedart import embed_item +from beetsplug.embedart import EmbedCoverArtPlugin from beets.util.confit import ConfigTypeError _fs_lock = threading.Lock() @@ -283,7 +283,8 @@ class ConvertPlugin(BeetsPlugin): if config['convert']['embed']: album = item.get_album() if album and album.artpath: - embed_item(item, album.artpath, itempath=converted) + EmbedCoverArtPlugin().embed_item(item, album.artpath, + itempath=converted) if keep_new: plugins.send('after_convert', item=item, From df82e113e522cfa6c297d720fc51f7e37e67bb22 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 6 Jan 2015 21:20:43 +0100 Subject: [PATCH 52/86] Fix embedart tests --- test/test_embedart.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/test_embedart.py b/test/test_embedart.py index 818c0ab91..8e03c5eba 100644 --- a/test/test_embedart.py +++ b/test/test_embedart.py @@ -18,7 +18,7 @@ from _common import unittest from helper import TestHelper, capture_log from beets.mediafile import MediaFile -from beets import config +from beets import config, logging from beets.util import syspath from beets.util.artresizer import ArtResizer @@ -76,9 +76,10 @@ class EmbedartCliTest(unittest.TestCase, TestHelper): def test_art_file_missing(self): self.add_album_fixture() - with capture_log() as logs: + logging.getLogger('beets.embedart').setLevel(logging.DEBUG) + with capture_log('beets.embedart') as logs: self.run_command('embedart', '-f', '/doesnotexist') - self.assertIn(u'embedart: could not read image file:', ''.join(logs)) + self.assertIn(u'could not read image file:', ''.join(logs)) @require_artresizer_compare def test_reject_different_art(self): From b2582c5d6217963204ef5cd5847ef26f3a6a12a0 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 6 Jan 2015 17:15:55 +0100 Subject: [PATCH 53/86] MusicBrainz collection plugin: delete "print" uses --- beetsplug/mbcollection.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/beetsplug/mbcollection.py b/beetsplug/mbcollection.py index 8535c5f86..e5f62f489 100644 --- a/beetsplug/mbcollection.py +++ b/beetsplug/mbcollection.py @@ -12,8 +12,6 @@ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -from __future__ import print_function - from beets.plugins import BeetsPlugin from beets.ui import Subcommand from beets import ui @@ -104,6 +102,6 @@ class MusicBrainzCollectionPlugin(BeetsPlugin): self._log.info(u'skipping invalid MBID: {0}', aid) # Submit to MusicBrainz. - print('Updating MusicBrainz collection {0}...'.format(collection_id)) + self._log.info('Updating MusicBrainz collection {0}...', collection_id) submit_albums(collection_id, album_ids) - print('...MusicBrainz collection updated.') + self._log.info('...MusicBrainz collection updated.') From 974155f2bcd91ea3eae222990d96e3d4d5879e67 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 6 Jan 2015 22:10:30 +0100 Subject: [PATCH 54/86] Fix pep8 in test --- test/test_lyrics.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_lyrics.py b/test/test_lyrics.py index 4a71058db..6ed0c61cb 100644 --- a/test/test_lyrics.py +++ b/test/test_lyrics.py @@ -323,7 +323,8 @@ class LyricsGooglePluginTest(unittest.TestCase): for s in GOOGLE_SOURCES: url = s['url'] + s['path'] if os.path.isfile(url_to_filename(url)): - res = lyrics.scrape_lyrics_from_html(raw_backend.fetch_url(url)) + res = lyrics.scrape_lyrics_from_html( + raw_backend.fetch_url(url)) self.assertTrue(google.is_lyrics(res), url) self.assertTrue(is_lyrics_content_ok(s['title'], res), url) From 422a7f6063a9a9831cdb21fa3860d3b65682cea6 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 6 Jan 2015 22:13:17 +0100 Subject: [PATCH 55/86] replaygain: fix backend instanciation --- beetsplug/replaygain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index e0c692000..7fa42779d 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -628,7 +628,7 @@ class ReplayGainPlugin(BeetsPlugin): try: self.backend_instance = self.backends[backend_name]( - self.config + self.config, self._log ) except (ReplayGainError, FatalReplayGainError) as e: raise ui.UserError( From 1ca33a8a5275b17cc320568c6371057f994444b2 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Thu, 8 Jan 2015 10:10:42 +0100 Subject: [PATCH 56/86] Avoid using partial() for the listener callback partial() is not a instance of types.FunctionType so inspect.getargspec() (used in beets/plugins:422) fails on it. --- beetsplug/chroma.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/beetsplug/chroma.py b/beetsplug/chroma.py index 2f8bf761b..44ab25508 100644 --- a/beetsplug/chroma.py +++ b/beetsplug/chroma.py @@ -23,7 +23,6 @@ from beets.util import confit from beets.autotag import hooks import acoustid from collections import defaultdict -from functools import partial API_KEY = '1vOwZtEn' SCORE_THRESH = 0.5 @@ -133,8 +132,10 @@ class AcoustidPlugin(plugins.BeetsPlugin): }) if self.config['auto']: - self.register_listener('import_task_start', - partial(fingerprint_task, self._log)) + self.register_listener('import_task_start', self.fingerprint_task) + + def fingerprint_task(self, task, session): + return fingerprint_task(self._log, task, session) def track_distance(self, item, info): dist = hooks.Distance() From 2e1b0d589d13a964f9216a9ad4b841dba1ce48e1 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Thu, 8 Jan 2015 10:56:19 +0100 Subject: [PATCH 57/86] Fetchart: check that the art found is a file Fixes issue #1177 --- beetsplug/fetchart.py | 3 ++- test/test_fetchart.py | 22 ++++++++++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index 3b0059817..def10e83b 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -207,7 +207,8 @@ def art_in_path(path, cover_names, cautious): images = [] for fn in os.listdir(path): for ext in IMAGE_EXTENSIONS: - if fn.lower().endswith('.' + ext): + if fn.lower().endswith('.' + ext) and \ + os.path.isfile(os.path.join(path, fn)): images.append(fn) # Look for "preferred" filenames. diff --git a/test/test_fetchart.py b/test/test_fetchart.py index 5e36f9145..12c06ae2a 100644 --- a/test/test_fetchart.py +++ b/test/test_fetchart.py @@ -12,7 +12,7 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -import os.path +import os from _common import unittest from helper import TestHelper @@ -22,25 +22,31 @@ class FetchartCliTest(unittest.TestCase, TestHelper): def setUp(self): self.setup_beets() self.load_plugins('fetchart') + self.config['fetchart']['cover_names'] = 'c\xc3\xb6ver.jpg' + self.config['art_filename'] = 'mycover' + self.album = self.add_album() def tearDown(self): self.unload_plugins() self.teardown_beets() def test_set_art_from_folder(self): - self.config['fetchart']['cover_names'] = 'c\xc3\xb6ver.jpg' - self.config['art_filename'] = 'mycover' - album = self.add_album() - self.touch('c\xc3\xb6ver.jpg', dir=album.path, content='IMAGE') + self.touch('c\xc3\xb6ver.jpg', dir=self.album.path, content='IMAGE') self.run_command('fetchart') - cover_path = os.path.join(album.path, 'mycover.jpg') + cover_path = os.path.join(self.album.path, 'mycover.jpg') - album.load() - self.assertEqual(album['artpath'], cover_path) + self.album.load() + self.assertEqual(self.album['artpath'], cover_path) with open(cover_path, 'r') as f: self.assertEqual(f.read(), 'IMAGE') + def test_filesystem_does_not_pick_up_folder(self): + os.makedirs(os.path.join(self.album.path, 'mycover.jpg')) + self.run_command('fetchart') + self.album.load() + self.assertEqual(self.album['artpath'], None) + def suite(): return unittest.TestLoader().loadTestsFromName(__name__) From b7735bd3bf745b9d8595c4f331f132998c2d190f Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Thu, 8 Jan 2015 12:39:28 +0100 Subject: [PATCH 58/86] Don't register a natural song change twice mpd responds twice to an 'idle' command upon a 'natural' song change (i.e. not a skip). Checking the reason for mpd response avoids registering twice the song change. Fix issue #773 --- beetsplug/mpdstats.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/beetsplug/mpdstats.py b/beetsplug/mpdstats.py index 5a5d27b4e..d3b71becf 100644 --- a/beetsplug/mpdstats.py +++ b/beetsplug/mpdstats.py @@ -212,6 +212,8 @@ class MPDStats(object): To this end the difference between the song's supposed end time and the current time is calculated. If it's greater than a threshold, the song is considered skipped. + + Returns whether the change was manual (skipped previous song or not) """ diff = abs(song['remaining'] - (time.time() - song['started'])) @@ -225,6 +227,8 @@ class MPDStats(object): if self.do_rating: self.update_rating(song['beets_item'], skipped) + return skipped + def handle_played(self, song): """Updates the play count of a song. """ @@ -264,19 +268,24 @@ class MPDStats(object): remaining = duration - played if self.now_playing and self.now_playing['path'] != path: - self.handle_song_change(self.now_playing) + skipped = self.handle_song_change(self.now_playing) + # mpd responds twice on a natural new song start + going_to_happen_twice = not skipped + else: + going_to_happen_twice = False - log.info(u'mpdstats: playing {0}', displayable_path(path)) + if not going_to_happen_twice: + log.info(u'mpdstats: playing {0}', displayable_path(path)) - self.now_playing = { - 'started': time.time(), - 'remaining': remaining, - 'path': path, - 'beets_item': self.get_item(path), - } + self.now_playing = { + 'started': time.time(), + 'remaining': remaining, + 'path': path, + 'beets_item': self.get_item(path), + } - self.update_item(self.now_playing['beets_item'], - 'last_played', value=int(time.time())) + self.update_item(self.now_playing['beets_item'], + 'last_played', value=int(time.time())) def run(self): self.mpd.connect() From 23e6760e19735c7d74c84458627757c365c69582 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Thu, 8 Jan 2015 17:52:32 +0100 Subject: [PATCH 59/86] Partial rollback of bpd logging --- beetsplug/bpd/__init__.py | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/beetsplug/bpd/__init__.py b/beetsplug/bpd/__init__.py index a5de73339..c4f3fb379 100644 --- a/beetsplug/bpd/__init__.py +++ b/beetsplug/bpd/__init__.py @@ -71,6 +71,7 @@ SAFE_COMMANDS = ( ITEM_KEYS_WRITABLE = set(MediaFile.fields()).intersection(Item._fields.keys()) # Loggers. +log = logging.getLogger('beets.bpd') global_log = logging.getLogger('beets') @@ -553,8 +554,6 @@ class Connection(object): """A connection between a client and the server. Handles input and output from and to the client. """ - _log = None - def __init__(self, server, sock): """Create a new connection for the accepted socket `client`. """ @@ -562,10 +561,6 @@ class Connection(object): self.sock = sock self.authenticated = False - @classmethod - def set_logger(cls, logger): - cls._log = logger - def send(self, lines): """Send lines, which which is either a single string or an iterable consisting of strings, to the client. A newline is @@ -575,7 +570,7 @@ class Connection(object): if isinstance(lines, basestring): lines = [lines] out = NEWLINE.join(lines) + NEWLINE - self._log.debug(out[:-1]) # Don't log trailing newline. + log.debug(out[:-1]) # Don't log trailing newline. if isinstance(out, unicode): out = out.encode('utf8') return self.sock.sendall(out) @@ -606,7 +601,7 @@ class Connection(object): line = line.strip() if not line: break - self._log.debug(line) + log.debug(line) if clist is not None: # Command list already opened. @@ -644,7 +639,6 @@ class Command(object): command_re = re.compile(r'^([^ \t]+)[ \t]*') arg_re = re.compile(r'"((?:\\"|[^"])+)"|([^ \t"]+)') - _log = None def __init__(self, s): """Creates a new `Command` from the given string, `s`, parsing @@ -666,10 +660,6 @@ class Command(object): arg = arg.decode('utf8') self.args.append(arg) - @classmethod - def set_logger(cls, logger): - cls._log = logger - def run(self, conn): """A coroutine that executes the command on the given connection. @@ -706,7 +696,7 @@ class Command(object): except Exception as e: # An "unintentional" error. Hide it from the client. - self._log.error(traceback.format_exc(e)) + log.error(traceback.format_exc(e)) raise BPDError(ERROR_SYSTEM, u'server error', self.name) @@ -1161,8 +1151,6 @@ class BPDPlugin(BeetsPlugin): 'password': u'', 'volume': VOLUME_MAX, }) - Connection.set_logger(self._log) - Server.set_logger(self._log) def start_bpd(self, lib, host, port, password, volume, debug): """Starts a BPD server.""" From 4b1f0cbf48c18987d7bd78706d601c5d3d6ccacb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederik=20=E2=80=9CFreso=E2=80=9D=20S=2E=20Olesen?= Date: Thu, 8 Jan 2015 21:37:09 +0100 Subject: [PATCH 60/86] Happy 2015. ;) See 7a410f636b85aafb018f1fdcfad1ae4e4feb68c7 Command used: git grep -l 'Copyright 201'|xargs sed -i -E 's/Copyright 201./Copyright 2015/'` --- beet | 2 +- beets/__init__.py | 2 +- beets/autotag/__init__.py | 2 +- beets/autotag/hooks.py | 2 +- beets/autotag/match.py | 2 +- beets/autotag/mb.py | 2 +- beets/dbcore/__init__.py | 2 +- beets/dbcore/db.py | 2 +- beets/dbcore/query.py | 2 +- beets/dbcore/queryparse.py | 2 +- beets/dbcore/types.py | 2 +- beets/importer.py | 2 +- beets/library.py | 2 +- beets/mediafile.py | 2 +- beets/plugins.py | 2 +- beets/ui/__init__.py | 2 +- beets/ui/commands.py | 2 +- beets/util/__init__.py | 2 +- beets/util/artresizer.py | 2 +- beets/util/confit.py | 2 +- beets/util/enumeration.py | 2 +- beets/util/functemplate.py | 2 +- beets/util/pipeline.py | 2 +- beets/vfs.py | 2 +- beetsplug/__init__.py | 2 +- beetsplug/beatport.py | 2 +- beetsplug/bench.py | 2 +- beetsplug/bpd/__init__.py | 2 +- beetsplug/bpd/gstplayer.py | 2 +- beetsplug/bpm.py | 2 +- beetsplug/bucket.py | 2 +- beetsplug/chroma.py | 2 +- beetsplug/convert.py | 2 +- beetsplug/discogs.py | 2 +- beetsplug/duplicates.py | 2 +- beetsplug/echonest.py | 2 +- beetsplug/embedart.py | 2 +- beetsplug/fetchart.py | 2 +- beetsplug/freedesktop.py | 2 +- beetsplug/fromfilename.py | 2 +- beetsplug/ftintitle.py | 2 +- beetsplug/fuzzy.py | 2 +- beetsplug/ihate.py | 2 +- beetsplug/importfeeds.py | 2 +- beetsplug/info.py | 2 +- beetsplug/inline.py | 2 +- beetsplug/keyfinder.py | 2 +- beetsplug/lastgenre/__init__.py | 2 +- beetsplug/lastimport.py | 2 +- beetsplug/lyrics.py | 2 +- beetsplug/mbsync.py | 2 +- beetsplug/missing.py | 2 +- beetsplug/mpdstats.py | 2 +- beetsplug/mpdupdate.py | 2 +- beetsplug/play.py | 2 +- beetsplug/random.py | 2 +- beetsplug/replaygain.py | 2 +- beetsplug/rewrite.py | 2 +- beetsplug/scrub.py | 2 +- beetsplug/smartplaylist.py | 2 +- beetsplug/the.py | 2 +- beetsplug/types.py | 2 +- beetsplug/web/__init__.py | 2 +- beetsplug/web/static/jquery.js | 6 +++--- beetsplug/zero.py | 2 +- setup.py | 2 +- test/_common.py | 2 +- test/helper.py | 2 +- test/lyrics_download_samples.py | 2 +- test/test_art.py | 2 +- test/test_autotag.py | 2 +- test/test_bucket.py | 2 +- test/test_convert.py | 2 +- test/test_datequery.py | 2 +- test/test_dbcore.py | 2 +- test/test_echonest.py | 2 +- test/test_embedart.py | 2 +- test/test_fetchart.py | 2 +- test/test_files.py | 2 +- test/test_ftintitle.py | 2 +- test/test_importadded.py | 2 +- test/test_importer.py | 2 +- test/test_info.py | 2 +- test/test_keyfinder.py | 2 +- test/test_lastgenre.py | 2 +- test/test_library.py | 2 +- test/test_lyrics.py | 2 +- test/test_mb.py | 2 +- test/test_mbsync.py | 2 +- test/test_mediafile.py | 2 +- test/test_mediafile_edge.py | 2 +- test/test_pipeline.py | 2 +- test/test_player.py | 2 +- test/test_plugins.py | 2 +- test/test_query.py | 2 +- test/test_replaygain.py | 2 +- test/test_sort.py | 2 +- test/test_template.py | 2 +- test/test_types_plugin.py | 2 +- test/test_ui.py | 2 +- test/test_ui_importer.py | 2 +- test/test_vfs.py | 2 +- test/testall.py | 2 +- 103 files changed, 105 insertions(+), 105 deletions(-) diff --git a/beet b/beet index c5699a5ac..e30c8b6eb 100755 --- a/beet +++ b/beet @@ -1,7 +1,7 @@ #!/usr/bin/env python # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beets/__init__.py b/beets/__init__.py index db0b38a2c..be95bdcb4 100644 --- a/beets/__init__.py +++ b/beets/__init__.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py index 3fa98758c..7ed7ce6bc 100644 --- a/beets/autotag/__init__.py +++ b/beets/autotag/__init__.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 5118212b4..6b592e1de 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beets/autotag/match.py b/beets/autotag/match.py index d51cd4fb1..ea80ae111 100644 --- a/beets/autotag/match.py +++ b/beets/autotag/match.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index 7c598a17f..78f4cba7d 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beets/dbcore/__init__.py b/beets/dbcore/__init__.py index c364fdfc3..d08bd5013 100644 --- a/beets/dbcore/__init__.py +++ b/beets/dbcore/__init__.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beets/dbcore/db.py b/beets/dbcore/db.py index 0c786daa5..7017e3e62 100644 --- a/beets/dbcore/db.py +++ b/beets/dbcore/db.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 5a116eb2b..3ea37524a 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beets/dbcore/queryparse.py b/beets/dbcore/queryparse.py index 90963696b..89a6f5ca2 100644 --- a/beets/dbcore/queryparse.py +++ b/beets/dbcore/queryparse.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beets/dbcore/types.py b/beets/dbcore/types.py index 82346e704..c171a9310 100644 --- a/beets/dbcore/types.py +++ b/beets/dbcore/types.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beets/importer.py b/beets/importer.py index a0f100749..a47fe9a56 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beets/library.py b/beets/library.py index 180f029cb..9b3e4a238 100644 --- a/beets/library.py +++ b/beets/library.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beets/mediafile.py b/beets/mediafile.py index 3e3d2aa23..7522acf0a 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beets/plugins.py b/beets/plugins.py index a975145db..0c97bd3ca 100755 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index ebde7ebbf..291c768ec 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beets/ui/commands.py b/beets/ui/commands.py index f68a5e7c2..da96b1898 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 047eea005..f952a711a 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beets/util/artresizer.py b/beets/util/artresizer.py index 5b51392bd..09092bbe8 100644 --- a/beets/util/artresizer.py +++ b/beets/util/artresizer.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Fabrice Laporte +# Copyright 2015, Fabrice Laporte # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beets/util/confit.py b/beets/util/confit.py index de22e0adf..b157c35a2 100644 --- a/beets/util/confit.py +++ b/beets/util/confit.py @@ -1,5 +1,5 @@ # This file is part of Confit. -# Copyright 2014, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beets/util/enumeration.py b/beets/util/enumeration.py index e8cd0fe10..86e11874a 100644 --- a/beets/util/enumeration.py +++ b/beets/util/enumeration.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beets/util/functemplate.py b/beets/util/functemplate.py index 03e57c618..6d236c8a2 100644 --- a/beets/util/functemplate.py +++ b/beets/util/functemplate.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beets/util/pipeline.py b/beets/util/pipeline.py index d267789c8..9b4446d9f 100644 --- a/beets/util/pipeline.py +++ b/beets/util/pipeline.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beets/vfs.py b/beets/vfs.py index e940e21fe..f0032d63b 100644 --- a/beets/vfs.py +++ b/beets/vfs.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/__init__.py b/beetsplug/__init__.py index 98a7ffd5c..337f84daa 100644 --- a/beetsplug/__init__.py +++ b/beetsplug/__init__.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/beatport.py b/beetsplug/beatport.py index 39054aab1..a5ef7c4d4 100644 --- a/beetsplug/beatport.py +++ b/beetsplug/beatport.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/bench.py b/beetsplug/bench.py index 56695e7c4..80c9a39ec 100644 --- a/beetsplug/bench.py +++ b/beetsplug/bench.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/bpd/__init__.py b/beetsplug/bpd/__init__.py index b0b8ce6c2..8058ec44b 100644 --- a/beetsplug/bpd/__init__.py +++ b/beetsplug/bpd/__init__.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/bpd/gstplayer.py b/beetsplug/bpd/gstplayer.py index 275f34b5c..2e7c05201 100644 --- a/beetsplug/bpd/gstplayer.py +++ b/beetsplug/bpd/gstplayer.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/bpm.py b/beetsplug/bpm.py index 028af7eae..9126cf447 100644 --- a/beetsplug/bpm.py +++ b/beetsplug/bpm.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, aroquen +# Copyright 2015, aroquen # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/bucket.py b/beetsplug/bucket.py index 68d520395..b55ac3140 100644 --- a/beetsplug/bucket.py +++ b/beetsplug/bucket.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Fabrice Laporte. +# Copyright 2015, Fabrice Laporte. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/chroma.py b/beetsplug/chroma.py index 485bfba61..12f2eb614 100644 --- a/beetsplug/chroma.py +++ b/beetsplug/chroma.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/convert.py b/beetsplug/convert.py index c9b83c03f..f34216c9f 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Jakob Schnitzer. +# Copyright 2015, Jakob Schnitzer. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index e3a55fbdd..38387aea5 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/duplicates.py b/beetsplug/duplicates.py index 1d7b6f9f1..a1bffb16d 100644 --- a/beetsplug/duplicates.py +++ b/beetsplug/duplicates.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Pedro Silva. +# Copyright 2015, Pedro Silva. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/echonest.py b/beetsplug/echonest.py index e38814437..08d721cd6 100644 --- a/beetsplug/echonest.py +++ b/beetsplug/echonest.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index 7f04cd1fb..2f31475cd 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index 3b0059817..5e7ff856e 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/freedesktop.py b/beetsplug/freedesktop.py index 3f3307c82..db1f1aabf 100644 --- a/beetsplug/freedesktop.py +++ b/beetsplug/freedesktop.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Matt Lichtenberg. +# Copyright 2015, Matt Lichtenberg. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/fromfilename.py b/beetsplug/fromfilename.py index f3884f556..4121c40e3 100644 --- a/beetsplug/fromfilename.py +++ b/beetsplug/fromfilename.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Jan-Erik Dahlin +# Copyright 2015, Jan-Erik Dahlin # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/ftintitle.py b/beetsplug/ftintitle.py index f36cfb348..c4ff0934c 100644 --- a/beetsplug/ftintitle.py +++ b/beetsplug/ftintitle.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Verrus, +# Copyright 2015, Verrus, # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/fuzzy.py b/beetsplug/fuzzy.py index c6fa36a26..fcc59ef5a 100644 --- a/beetsplug/fuzzy.py +++ b/beetsplug/fuzzy.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Philippe Mongeau. +# Copyright 2015, Philippe Mongeau. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/ihate.py b/beetsplug/ihate.py index ed7cbd954..04bcee453 100644 --- a/beetsplug/ihate.py +++ b/beetsplug/ihate.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Blemjhoo Tezoulbr . +# Copyright 2015, Blemjhoo Tezoulbr . # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/importfeeds.py b/beetsplug/importfeeds.py index 4ac1dda2b..252c4343d 100644 --- a/beetsplug/importfeeds.py +++ b/beetsplug/importfeeds.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Fabrice Laporte. +# Copyright 2015, Fabrice Laporte. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/info.py b/beetsplug/info.py index 30cccb1b2..f38693b20 100644 --- a/beetsplug/info.py +++ b/beetsplug/info.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/inline.py b/beetsplug/inline.py index e7e7e0f41..55b0a5620 100644 --- a/beetsplug/inline.py +++ b/beetsplug/inline.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/keyfinder.py b/beetsplug/keyfinder.py index 49830edef..1c1406694 100644 --- a/beetsplug/keyfinder.py +++ b/beetsplug/keyfinder.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Thomas Scholtes. +# Copyright 2015, Thomas Scholtes. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index 2b4acb590..65ff3fe9c 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/lastimport.py b/beetsplug/lastimport.py index 087213031..804964b2b 100644 --- a/beetsplug/lastimport.py +++ b/beetsplug/lastimport.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Rafael Bodill http://github.com/rafi +# Copyright 2015, Rafael Bodill http://github.com/rafi # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index bffa8414c..05c9e6cc5 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py index 105122639..d44953a2c 100644 --- a/beetsplug/mbsync.py +++ b/beetsplug/mbsync.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Jakob Schnitzer. +# Copyright 2015, Jakob Schnitzer. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/missing.py b/beetsplug/missing.py index 2ebe3edf8..2bfc600e8 100644 --- a/beetsplug/missing.py +++ b/beetsplug/missing.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Pedro Silva. +# Copyright 2015, Pedro Silva. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/mpdstats.py b/beetsplug/mpdstats.py index 5a5d27b4e..115477276 100644 --- a/beetsplug/mpdstats.py +++ b/beetsplug/mpdstats.py @@ -1,6 +1,6 @@ # coding=utf-8 # This file is part of beets. -# Copyright 2013, Peter Schnebel and Johann Klähn. +# Copyright 2015, Peter Schnebel and Johann Klähn. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/mpdupdate.py b/beetsplug/mpdupdate.py index b5137237a..ff6e6dbe2 100644 --- a/beetsplug/mpdupdate.py +++ b/beetsplug/mpdupdate.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/play.py b/beetsplug/play.py index 7f9ff3ce9..3af36608f 100644 --- a/beetsplug/play.py +++ b/beetsplug/play.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, David Hamp-Gonsalves +# Copyright 2015, David Hamp-Gonsalves # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/random.py b/beetsplug/random.py index f594bd73b..2c4d0c000 100644 --- a/beetsplug/random.py +++ b/beetsplug/random.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Philippe Mongeau. +# Copyright 2015, Philippe Mongeau. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 5d50f61c5..42d8a2b4a 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Fabrice Laporte, Yevgeny Bezman, and Adrian Sampson. +# Copyright 2015, Fabrice Laporte, Yevgeny Bezman, and Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/rewrite.py b/beetsplug/rewrite.py index 8a59bdbfd..52780376e 100644 --- a/beetsplug/rewrite.py +++ b/beetsplug/rewrite.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/scrub.py b/beetsplug/scrub.py index 7c5b097cb..5449be7d8 100644 --- a/beetsplug/scrub.py +++ b/beetsplug/scrub.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/smartplaylist.py b/beetsplug/smartplaylist.py index 5cae4f385..368d516cc 100644 --- a/beetsplug/smartplaylist.py +++ b/beetsplug/smartplaylist.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Dang Mai . +# Copyright 2015, Dang Mai . # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/the.py b/beetsplug/the.py index d146b3e69..6519b3332 100644 --- a/beetsplug/the.py +++ b/beetsplug/the.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Blemjhoo Tezoulbr . +# Copyright 2015, Blemjhoo Tezoulbr . # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/types.py b/beetsplug/types.py index e351c8add..8a1e6c2d6 100644 --- a/beetsplug/types.py +++ b/beetsplug/types.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Thomas Scholtes. +# Copyright 2015, Thomas Scholtes. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index cf45d8c48..a60461689 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/beetsplug/web/static/jquery.js b/beetsplug/web/static/jquery.js index a9d6fe8c1..5b43a3ece 100644 --- a/beetsplug/web/static/jquery.js +++ b/beetsplug/web/static/jquery.js @@ -2,13 +2,13 @@ * jQuery JavaScript Library v1.7.1 * http://jquery.com/ * - * Copyright 2013, John Resig + * Copyright 2015, John Resig * Dual licensed under the MIT or GPL Version 2 licenses. * http://jquery.org/license * * Includes Sizzle.js * http://sizzlejs.com/ - * Copyright 2013, The Dojo Foundation + * Copyright 2015, The Dojo Foundation * Released under the MIT, BSD, and GPL Licenses. * * Date: Mon Nov 21 21:11:03 2011 -0500 @@ -3851,7 +3851,7 @@ jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblcl /*! * Sizzle CSS Selector Engine - * Copyright 2013, The Dojo Foundation + * Copyright 2015, The Dojo Foundation * Released under the MIT, BSD, and GPL Licenses. * More information: http://sizzlejs.com/ */ diff --git a/beetsplug/zero.py b/beetsplug/zero.py index a8c62d42c..10fed5672 100644 --- a/beetsplug/zero.py +++ b/beetsplug/zero.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Blemjhoo Tezoulbr . +# Copyright 2015, Blemjhoo Tezoulbr . # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/setup.py b/setup.py index 53f5137e9..802809a92 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # This file is part of beets. -# Copyright 2014, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/_common.py b/test/_common.py index b222566b7..3852ba2f0 100644 --- a/test/_common.py +++ b/test/_common.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/helper.py b/test/helper.py index 4dafb4c39..afa5d29d1 100644 --- a/test/helper.py +++ b/test/helper.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Thomas Scholtes. +# Copyright 2015, Thomas Scholtes. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/lyrics_download_samples.py b/test/lyrics_download_samples.py index 34f71c0d1..819d6f9f7 100644 --- a/test/lyrics_download_samples.py +++ b/test/lyrics_download_samples.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Fabrice Laporte +# Copyright 2015, Fabrice Laporte # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_art.py b/test/test_art.py index 22ead0c04..400ad2580 100644 --- a/test/test_art.py +++ b/test/test_art.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_autotag.py b/test/test_autotag.py index 4599d6df2..3405351e6 100644 --- a/test/test_autotag.py +++ b/test/test_autotag.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_bucket.py b/test/test_bucket.py index c65116663..38846e324 100644 --- a/test/test_bucket.py +++ b/test/test_bucket.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # This file is part of beets. -# Copyright 2014, Fabrice Laporte. +# Copyright 2015, Fabrice Laporte. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_convert.py b/test/test_convert.py index 05d643e3e..f3f8cbeff 100644 --- a/test/test_convert.py +++ b/test/test_convert.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Thomas Scholtes. +# Copyright 2015, Thomas Scholtes. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_datequery.py b/test/test_datequery.py index 61f6abe2e..06d857c0b 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_dbcore.py b/test/test_dbcore.py index c05dc1c5d..4882c4da5 100644 --- a/test/test_dbcore.py +++ b/test/test_dbcore.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_echonest.py b/test/test_echonest.py index d845b8a6b..a92bd4086 100644 --- a/test/test_echonest.py +++ b/test/test_echonest.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Thomas Scholtes +# Copyright 2015, Thomas Scholtes # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_embedart.py b/test/test_embedart.py index 818c0ab91..1f8eabaeb 100644 --- a/test/test_embedart.py +++ b/test/test_embedart.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Thomas Scholtes. +# Copyright 2015, Thomas Scholtes. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_fetchart.py b/test/test_fetchart.py index 5e36f9145..03eac2849 100644 --- a/test/test_fetchart.py +++ b/test/test_fetchart.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Thomas Scholtes. +# Copyright 2015, Thomas Scholtes. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_files.py b/test/test_files.py index 272a33fe7..03daf92e3 100644 --- a/test/test_files.py +++ b/test/test_files.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_ftintitle.py b/test/test_ftintitle.py index 77e416c5a..6b2e43b33 100644 --- a/test/test_ftintitle.py +++ b/test/test_ftintitle.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Fabrice Laporte. +# Copyright 2015, Fabrice Laporte. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_importadded.py b/test/test_importadded.py index b228ee24c..11c541ffa 100644 --- a/test/test_importadded.py +++ b/test/test_importadded.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Stig Inge Lea Bjornsen. +# Copyright 2015, Stig Inge Lea Bjornsen. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_importer.py b/test/test_importer.py index 89a65b74f..9a555c665 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_info.py b/test/test_info.py index 9c641e501..15f5936ce 100644 --- a/test/test_info.py +++ b/test/test_info.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Thomas Scholtes. +# Copyright 2015, Thomas Scholtes. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_keyfinder.py b/test/test_keyfinder.py index 5795002dc..a8428cbb3 100644 --- a/test/test_keyfinder.py +++ b/test/test_keyfinder.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Thomas Scholtes. +# Copyright 2015, Thomas Scholtes. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_lastgenre.py b/test/test_lastgenre.py index 9aaace237..130a92b36 100644 --- a/test/test_lastgenre.py +++ b/test/test_lastgenre.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Fabrice Laporte. +# Copyright 2015, Fabrice Laporte. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_library.py b/test/test_library.py index f4bdb15d6..717cc1aa1 100644 --- a/test/test_library.py +++ b/test/test_library.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_lyrics.py b/test/test_lyrics.py index 492229630..3cee65a00 100644 --- a/test/test_lyrics.py +++ b/test/test_lyrics.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Fabrice Laporte. +# Copyright 2015, Fabrice Laporte. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_mb.py b/test/test_mb.py index f41ec5510..2e5cd1aa3 100644 --- a/test/test_mb.py +++ b/test/test_mb.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_mbsync.py b/test/test_mbsync.py index 2a064a311..8dddf60c0 100644 --- a/test/test_mbsync.py +++ b/test/test_mbsync.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Thomas Scholtes. +# Copyright 2015, Thomas Scholtes. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 722b74484..c685cdd94 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_mediafile_edge.py b/test/test_mediafile_edge.py index 81b689fbc..fa12d1dda 100644 --- a/test/test_mediafile_edge.py +++ b/test/test_mediafile_edge.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_pipeline.py b/test/test_pipeline.py index 0c4de6836..917983a91 100644 --- a/test/test_pipeline.py +++ b/test/test_pipeline.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_player.py b/test/test_player.py index 966b98dac..147026963 100644 --- a/test/test_player.py +++ b/test/test_player.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_plugins.py b/test/test_plugins.py index eea162e90..0880e2d27 100644 --- a/test/test_plugins.py +++ b/test/test_plugins.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Thomas Scholtes. +# Copyright 2015, Thomas Scholtes. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_query.py b/test/test_query.py index f2ad3cb6e..879e9ca7d 100644 --- a/test/test_query.py +++ b/test/test_query.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_replaygain.py b/test/test_replaygain.py index bad779c69..aa0d19b23 100644 --- a/test/test_replaygain.py +++ b/test/test_replaygain.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Thomas Scholtes +# Copyright 2015, Thomas Scholtes # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_sort.py b/test/test_sort.py index be5706bca..f0b56d3ca 100644 --- a/test/test_sort.py +++ b/test/test_sort.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_template.py b/test/test_template.py index f14aed019..1bc0b2cd7 100644 --- a/test/test_template.py +++ b/test/test_template.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_types_plugin.py b/test/test_types_plugin.py index d175525be..697cda702 100644 --- a/test/test_types_plugin.py +++ b/test/test_types_plugin.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Thomas Scholtes. +# Copyright 2015, Thomas Scholtes. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_ui.py b/test/test_ui.py index 4ef5a9fcf..e32f9ed83 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2014, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_ui_importer.py b/test/test_ui_importer.py index 3a8b2696c..8006e4215 100644 --- a/test/test_ui_importer.py +++ b/test/test_ui_importer.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/test_vfs.py b/test/test_vfs.py index e31640aa2..ae8e7aef6 100644 --- a/test/test_vfs.py +++ b/test/test_vfs.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/test/testall.py b/test/testall.py index 70bf44f87..5fe8c5536 100755 --- a/test/testall.py +++ b/test/testall.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2015, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the From a7be9280650dea9da12b8285d81b9f0ae742e693 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Thu, 8 Jan 2015 13:41:00 -0800 Subject: [PATCH 61/86] Changelog for #1211, fix #1177 --- docs/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 01426c0a1..c32f58455 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -12,6 +12,8 @@ Fixes: built-in whitelist/canonicalization tree. :bug:`1206` * Fix a crash when ``beet`` is invoked without arguments. :bug:`1205` :bug:`1207` +* :doc:`/plugins/fetchart`: Do not attempt to import directories as album art. + :bug:`1177` :bug:`1211` 1.3.10 (January 5, 2015) From e9834ef51cdb36567ecc926c85358f7c3d0e681a Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Thu, 8 Jan 2015 13:44:28 -0800 Subject: [PATCH 62/86] Changelog for #1212, fix #773 --- docs/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index c32f58455..286c26abb 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -14,6 +14,8 @@ Fixes: :bug:`1207` * :doc:`/plugins/fetchart`: Do not attempt to import directories as album art. :bug:`1177` :bug:`1211` +* :doc:`/plugins/mpdstats`: Avoid double-counting some play events. :bug:`773` + :bug:`1212` 1.3.10 (January 5, 2015) From 2a527598bf7665238133f6fa63b0089ebd4ff3d4 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Fri, 9 Jan 2015 15:07:02 +0100 Subject: [PATCH 63/86] Add logging usage recommendations to docs --- docs/dev/plugins.rst | 45 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/docs/dev/plugins.rst b/docs/dev/plugins.rst index c79d49645..c0c285939 100644 --- a/docs/dev/plugins.rst +++ b/docs/dev/plugins.rst @@ -112,8 +112,23 @@ an example:: def loaded(): print 'Plugin loaded!' -Pass the name of the event in question to the ``listen`` decorator. The events -currently available are: +Pass the name of the event in question to the ``listen`` decorator. + +Note that if you want to access an attribute of your plugin (e.g. ``config`` or +``log``) you'll have to define a method and not a function. Here is the usual +registration process in this case:: + + from beets.plugins import BeetsPlugin + + class SomePlugin(BeetsPlugin): + def __init__(self): + super(SomePlugin, self).__init__() + self.register_listener('pluginload', self.loaded) + + def loaded(self): + self._log.info('Plugin loaded!') + +The events currently available are: * *pluginload*: called after all the plugins have been loaded after the ``beet`` command starts @@ -328,11 +343,11 @@ method. Here's an example plugin that provides a meaningless new field "foo":: - class FooPlugin(BeetsPlugin): + class fooplugin(beetsplugin): def __init__(self): - field = mediafile.MediaField( - mediafile.MP3DescStorageStyle(u'foo') - mediafile.StorageStyle(u'foo') + field = mediafile.mediafield( + mediafile.mp3descstoragestyle(u'foo') + mediafile.storagestyle(u'foo') ) self.add_media_field('foo', field) @@ -442,3 +457,21 @@ Specifying types has several advantages: from the command line. * User input for flexible fields may be validated and converted. + + +Log stuff +^^^^^^^^^ + +A plugin has a ``_log`` attribute which is a ``Logger`` instance. A plugin in +``beetsplug/myplugin.py`` will have a logger named ``beets.myplugin``. + +Logging uses {}-style formatting. Also note that logging should be lazy and not +eager, for example:: + + from beets import logging + + log = logging.getLogger('foo.bar') + log.info("I use the {0} syntax", "new") + log.info("I like the album {0.title} by {0.albumartist}, it is {1}", album, "great") + +You should use ``beets.logging`` and never ``logging``. From 0cf9956d5bb316bf8ff51d93ddd6657d79e13407 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 11 Jan 2015 12:10:52 -0800 Subject: [PATCH 64/86] Unicode logging formats, FFS (fix #1214) --- beets/importer.py | 6 +++--- docs/changelog.rst | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/beets/importer.py b/beets/importer.py index a47fe9a56..2ee143e4c 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -1291,11 +1291,11 @@ def log_files(session, task): """A coroutine (pipeline stage) to log each file which will be imported """ if isinstance(task, SingletonImportTask): - log.info('Singleton: {0}', displayable_path(task.item['path'])) + log.info(u'Singleton: {0}', displayable_path(task.item['path'])) elif task.items: - log.info('Album {0}', displayable_path(task.paths[0])) + log.info(u'Album {0}', displayable_path(task.paths[0])) for item in task.items: - log.info(' {0}', displayable_path(item['path'])) + log.info(u' {0}', displayable_path(item['path'])) def group_albums(session): diff --git a/docs/changelog.rst b/docs/changelog.rst index 286c26abb..5ca5a7c0d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -16,6 +16,8 @@ Fixes: :bug:`1177` :bug:`1211` * :doc:`/plugins/mpdstats`: Avoid double-counting some play events. :bug:`773` :bug:`1212` +* Fix a crash when the importer deals with Unicode metadata in ``--pretend`` + mode. :bug:`1214` 1.3.10 (January 5, 2015) From 65fc5f8062e0014826580825abe9da97e3ead31d Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 11 Jan 2015 12:18:33 -0800 Subject: [PATCH 65/86] Disable PyPy on Travis Something seems to be wrong: there's a "fatal RPython error" that I can't reproduce locally. --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 57051f833..fb2719187 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,8 +10,8 @@ matrix: env: {TOX_ENV: py26} - python: 2.7 env: {TOX_ENV: py27cov, COVERAGE: 1} - - python: pypy - env: {TOX_ENV: pypy} +# - python: pypy +# env: {TOX_ENV: pypy} - python: 2.7 env: {TOX_ENV: docs} - python: 2.7 From 24317fd4c7be4f254909389acf7c55f5d9760caf Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 11 Jan 2015 14:27:59 -0800 Subject: [PATCH 66/86] Rename BeetsPlugin._import_stages to import_stages For #1208. Restores backwards-compatibility and matches the development docs. --- beets/plugins.py | 15 +++++++++++---- beetsplug/convert.py | 2 +- beetsplug/echonest.py | 2 +- beetsplug/fetchart.py | 2 +- beetsplug/ftintitle.py | 2 +- beetsplug/keyfinder.py | 2 +- beetsplug/lastgenre/__init__.py | 2 +- beetsplug/lyrics.py | 2 +- beetsplug/mbcollection.py | 2 +- beetsplug/replaygain.py | 2 +- 10 files changed, 20 insertions(+), 13 deletions(-) diff --git a/beets/plugins.py b/beets/plugins.py index 12c8ad100..ac0025288 100755 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -52,7 +52,6 @@ class BeetsPlugin(object): def __init__(self, name=None): """Perform one-time plugin setup. """ - self._import_stages = [] self.name = name or self.__module__.split('.')[-1] self.config = beets.config[self.name] if not self.template_funcs: @@ -61,6 +60,7 @@ class BeetsPlugin(object): self.template_fields = {} if not self.album_template_fields: self.album_template_fields = {} + self.import_stages = [] logger_name = '{0}.{1}'.format('beets', self.name) self._log = logging.getLogger(logger_name) @@ -72,9 +72,16 @@ class BeetsPlugin(object): """ return () - def import_stages(self): + def get_import_stages(self): + """Return a list of functions that should be called as importer + pipelines stages. + + The callables are wrapped versions of the functions in + `self.import_stages`. Wrapping provides some bookkeeping for the + plugin: specifically, the logging level is adjusted to WARNING. + """ return [self._set_log_level(logging.WARNING, import_stage) - for import_stage in self._import_stages] + for import_stage in self.import_stages] def _set_log_level(self, log_level, func): @wraps(func) @@ -368,7 +375,7 @@ def import_stages(): """Get a list of import stage functions defined by plugins.""" stages = [] for plugin in find_plugins(): - stages += plugin.import_stages() + stages += plugin.get_import_stages() return stages diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 219baf1f3..899846e88 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -128,7 +128,7 @@ class ConvertPlugin(BeetsPlugin): u'never_convert_lossy_files': False, u'copy_album_art': False, }) - self._import_stages = [self.auto_convert] + self.import_stages = [self.auto_convert] self.register_listener('import_task_files', self._cleanup) diff --git a/beetsplug/echonest.py b/beetsplug/echonest.py index 22616385b..8c60de409 100644 --- a/beetsplug/echonest.py +++ b/beetsplug/echonest.py @@ -137,7 +137,7 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): config['echonest']['apikey'].get(unicode) if self.config['auto']: - self._import_stages = [self.imported] + self.import_stages = [self.imported] def _echofun(self, func, **kwargs): """Wrapper for requests to the EchoNest API. Will retry up to diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index 03d6ce18a..e17eb929e 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -237,7 +237,7 @@ class FetchArtPlugin(plugins.BeetsPlugin): self.maxwidth = self.config['maxwidth'].get(int) if self.config['auto']: # Enable two import hooks when fetching is enabled. - self._import_stages = [self.fetch_art] + self.import_stages = [self.fetch_art] self.register_listener('import_task_files', self.assign_art) available_sources = list(SOURCES_ALL) diff --git a/beetsplug/ftintitle.py b/beetsplug/ftintitle.py index daeadc1d6..839e4d16d 100644 --- a/beetsplug/ftintitle.py +++ b/beetsplug/ftintitle.py @@ -62,7 +62,7 @@ class FtInTitlePlugin(plugins.BeetsPlugin): help='drop featuring from artists and ignore title update') if self.config['auto']: - self._import_stages = [self.imported] + self.import_stages = [self.imported] def commands(self): diff --git a/beetsplug/keyfinder.py b/beetsplug/keyfinder.py index ef3b4cfb8..9ce81f36e 100644 --- a/beetsplug/keyfinder.py +++ b/beetsplug/keyfinder.py @@ -32,7 +32,7 @@ class KeyFinderPlugin(BeetsPlugin): u'overwrite': False, }) self.config['auto'].get(bool) - self._import_stages = [self.imported] + self.import_stages = [self.imported] def commands(self): cmd = ui.Subcommand('keyfinder', diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index f38d10fe1..54604ece6 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -111,7 +111,7 @@ class LastGenrePlugin(plugins.BeetsPlugin): """Setup plugin from config options """ if self.config['auto']: - self._import_stages = [self.imported] + self.import_stages = [self.imported] self._genre_cache = {} diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 51703a22b..a85ce3efa 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -448,7 +448,7 @@ class LyricsPlugin(plugins.BeetsPlugin): def __init__(self): super(LyricsPlugin, self).__init__() - self._import_stages = [self.imported] + self.import_stages = [self.imported] self.config.add({ 'auto': True, 'google_API_key': None, diff --git a/beetsplug/mbcollection.py b/beetsplug/mbcollection.py index e5f62f489..b7cac8024 100644 --- a/beetsplug/mbcollection.py +++ b/beetsplug/mbcollection.py @@ -58,7 +58,7 @@ class MusicBrainzCollectionPlugin(BeetsPlugin): ) self.config.add({'auto': False}) if self.config['auto']: - self._import_stages = [self.imported] + self.import_stages = [self.imported] def commands(self): mbupdate = Subcommand('mbupdate', help='Update MusicBrainz collection') diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 2781f116c..f7ca68c5b 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -604,7 +604,7 @@ class ReplayGainPlugin(BeetsPlugin): def __init__(self): super(ReplayGainPlugin, self).__init__() - self._import_stages = [self.imported] + self.import_stages = [self.imported] # default backend is 'command' for backward-compatibility. self.config.add({ From f871ef9e2123ca538c632f922e4efdfc2d12834c Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 11 Jan 2015 15:00:20 -0800 Subject: [PATCH 67/86] Verbosity affects plugins (#1208) This restores the -v flag affecting plugins. By default, plugin loggers use the NOTSET level, which just reuses the base `beets` logger level. The level is only auto-adjusted for the importer when not in verbose mode. --- beets/plugins.py | 17 +++++++++++++---- beetsplug/beatport.py | 2 +- beetsplug/info.py | 2 +- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/beets/plugins.py b/beets/plugins.py index ac0025288..2ae9090e4 100755 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -64,7 +64,7 @@ class BeetsPlugin(object): logger_name = '{0}.{1}'.format('beets', self.name) self._log = logging.getLogger(logger_name) - self._log.setLevel(logging.INFO) + self._log.setLevel(logging.NOTSET) # Use `beets` logger level. def commands(self): """Should return a list of beets.ui.Subcommand objects for @@ -84,12 +84,21 @@ class BeetsPlugin(object): for import_stage in self.import_stages] def _set_log_level(self, log_level, func): + """Wrap `func` to temporarily set this plugin's logger level to + `log_level` (and restore it after the function returns). + + The level is *not* adjusted when beets is in verbose + mode---i.e., the plugin logger continues to delegate to the base + beets logger. + """ @wraps(func) def wrapper(*args, **kwargs): - old_log_level = self._log.getEffectiveLevel() - self._log.setLevel(log_level) + if not beets.config['verbose']: + old_log_level = self._log.level + self._log.setLevel(log_level) result = func(*args, **kwargs) - self._log.setLevel(old_log_level) + if not beets.config['verbose']: + self._log.setLevel(old_log_level) return result return wrapper diff --git a/beetsplug/beatport.py b/beetsplug/beatport.py index 5c116455f..de71562a9 100644 --- a/beetsplug/beatport.py +++ b/beetsplug/beatport.py @@ -191,7 +191,7 @@ class BeatportPlugin(BeetsPlugin): try: return self._get_releases(query) except BeatportAPIError as e: - self._log.debug(u'Beatport API Error: {0} (query: {1})', e, query) + self._log.debug(u'API Error: {0} (query: {1})', e, query) return [] def item_candidates(self, item, artist, title): diff --git a/beetsplug/info.py b/beetsplug/info.py index fc7d05f38..27aeabf0f 100644 --- a/beetsplug/info.py +++ b/beetsplug/info.py @@ -131,7 +131,7 @@ class InfoPlugin(BeetsPlugin): try: data = data_emitter() except mediafile.UnreadableFileError as ex: - self._log.error(u'cannot read file: {0}', ex.message) + self._log.error(u'cannot read file: {0}', ex) continue if opts.summarize: From 6d5945daa437fee39c564796a245e69294be4369 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 11 Jan 2015 15:08:42 -0800 Subject: [PATCH 68/86] Use `logger.getChild` (#1208) Let the library do the string formatting for us. --- beets/plugins.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/beets/plugins.py b/beets/plugins.py index 2ae9090e4..6a704f414 100755 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -62,8 +62,7 @@ class BeetsPlugin(object): self.album_template_fields = {} self.import_stages = [] - logger_name = '{0}.{1}'.format('beets', self.name) - self._log = logging.getLogger(logger_name) + self._log = log.getChild(self.name) self._log.setLevel(logging.NOTSET) # Use `beets` logger level. def commands(self): From e83b6ae29764952186b038fac318c6308fb56569 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 11 Jan 2015 15:39:54 -0800 Subject: [PATCH 69/86] Logging (#1208): prefix plugin names, expand docs We now add the plugin name to messages in verbose mode. This may not be the best final policy, but it does help make the output more readable when many plugins are talking at once. --- beets/logging.py | 5 ++++- beets/plugins.py | 19 +++++++++++++++++++ docs/dev/plugins.rst | 29 ++++++++++++++++++----------- 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/beets/logging.py b/beets/logging.py index 27f0620ed..dd1a28cf2 100644 --- a/beets/logging.py +++ b/beets/logging.py @@ -31,8 +31,11 @@ import sys PY26 = sys.version_info[:2] == (2, 6) -# Create a `str.format`-based logger. class StrFormatLogger(Logger): + """A version of `Logger` that uses `str.format`-style formatting + instead of %-style formatting. + """ + class _LogMessage(object): def __init__(self, msg, args, kwargs): self.msg = msg diff --git a/beets/plugins.py b/beets/plugins.py index 6a704f414..ff89c856b 100755 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -42,6 +42,23 @@ class PluginConflictException(Exception): """ +class PluginLogFilter(logging.Filter): + """A logging filter that identifies the plugin that emitted a log + message. + """ + def __init__(self, plugin): + self.prefix = u'{0}: '.format(plugin.name) + + def filter(self, record): + if hasattr(record.msg, 'msg') and isinstance(record.msg.msg, + basestring): + # A _LogMessage from our hacked-up Logging replacement. + record.msg.msg = self.prefix + record.msg.msg + elif isinstance(record.msg, basestring): + record.msg = self.prefix + record.msg + return True + + # Managing the plugins themselves. class BeetsPlugin(object): @@ -64,6 +81,8 @@ class BeetsPlugin(object): self._log = log.getChild(self.name) self._log.setLevel(logging.NOTSET) # Use `beets` logger level. + if beets.config['verbose']: + self._log.addFilter(PluginLogFilter(self)) def commands(self): """Should return a list of beets.ui.Subcommand objects for diff --git a/docs/dev/plugins.rst b/docs/dev/plugins.rst index c0c285939..6eea738e7 100644 --- a/docs/dev/plugins.rst +++ b/docs/dev/plugins.rst @@ -459,19 +459,26 @@ Specifying types has several advantages: * User input for flexible fields may be validated and converted. -Log stuff -^^^^^^^^^ +Logging +^^^^^^^ -A plugin has a ``_log`` attribute which is a ``Logger`` instance. A plugin in -``beetsplug/myplugin.py`` will have a logger named ``beets.myplugin``. +Each plugin object has a ``_log`` attribute, which is a ``Logger`` from the +`standard Python logging module`_. The logger is set up to `PEP 3101`_, +str.format-style string formatting. So you can write logging calls like this:: -Logging uses {}-style formatting. Also note that logging should be lazy and not -eager, for example:: + self._log.debug(u'Processing {0.title} by {0.artist}', item) - from beets import logging +.. _PEP 3101: https://www.python.org/dev/peps/pep-3101/ +.. _standard Python logging module: https://docs.python.org/2/library/logging.html - log = logging.getLogger('foo.bar') - log.info("I use the {0} syntax", "new") - log.info("I like the album {0.title} by {0.albumartist}, it is {1}", album, "great") +The per-plugin loggers have two convenient features: -You should use ``beets.logging`` and never ``logging``. +* When beets is in verbose mode, messages are prefixed with the plugin name to + make them easier to see. +* Messages at the ``INFO`` logging level are hidden when the plugin is running + in an importer stage (see above). This addresses a common pattern where + plugins need to use the same code for a command and an import stage, but the + command needs to print more messages than the import stage. (For example, + you'll want to log "found lyrics for this song" when you're run explicitly + as a command, but you don't want to noisily interrupt the importer interface + when running automatically.) From 34c9caae6774874cd90d18b1b363c82fa24b0cb3 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 11 Jan 2015 15:44:38 -0800 Subject: [PATCH 70/86] Changelog for logging changes (#1208) --- docs/changelog.rst | 6 ++++++ docs/dev/plugins.rst | 2 ++ 2 files changed, 8 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 5ca5a7c0d..69aa4da8d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -19,6 +19,12 @@ Fixes: * Fix a crash when the importer deals with Unicode metadata in ``--pretend`` mode. :bug:`1214` +For developers: The logging system in beets has been overhauled. Plugins now +each have their own logger, which helps by automatically adjusting the +verbosity level in import mode and by prefixing the plugin's name. Also, +logging calls can (and should!) use modern ``{}``-style string formatting +lazily. See :ref:`plugin-logging` in the plugin API docs. + 1.3.10 (January 5, 2015) ------------------------ diff --git a/docs/dev/plugins.rst b/docs/dev/plugins.rst index 6eea738e7..d81810bde 100644 --- a/docs/dev/plugins.rst +++ b/docs/dev/plugins.rst @@ -459,6 +459,8 @@ Specifying types has several advantages: * User input for flexible fields may be validated and converted. +.. _plugin-logging: + Logging ^^^^^^^ From 4babc40fd8c48f07542542e266699b05cf0bf776 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Thu, 8 Jan 2015 10:39:18 +0100 Subject: [PATCH 71/86] Delete useless "config['mypluginname']" mentions Replace with self.config where this is painless. More plugins would benefit from this update but that requires turning functions into methods. --- beetsplug/convert.py | 20 ++++++++++---------- beetsplug/echonest.py | 8 ++++---- beetsplug/embedart.py | 14 +++++++------- beetsplug/fetchart.py | 6 +++--- beetsplug/fuzzy.py | 5 ++--- beetsplug/importadded.py | 5 ++--- beetsplug/importfeeds.py | 12 ++++++------ beetsplug/lyrics.py | 4 ++-- beetsplug/mpdstats.py | 6 +++--- beetsplug/scrub.py | 2 +- 10 files changed, 40 insertions(+), 42 deletions(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 899846e88..baf084423 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -166,7 +166,7 @@ class ConvertPlugin(BeetsPlugin): Raises `subprocess.CalledProcessError` if the command exited with a non-zero status code. """ - quiet = config['convert']['quiet'].get() + quiet = self.config['quiet'].get() if not quiet and not pretend: self._log.info(u'Encoding {0}', util.displayable_path(source)) @@ -280,7 +280,7 @@ class ConvertPlugin(BeetsPlugin): item.read() item.store() # Store new path and audio data. - if config['convert']['embed']: + if self.config['embed']: album = item.get_album() if album and album.artpath: EmbedCoverArtPlugin().embed_item(item, album.artpath, @@ -336,24 +336,24 @@ class ConvertPlugin(BeetsPlugin): def convert_func(self, lib, opts, args): if not opts.dest: - opts.dest = config['convert']['dest'].get() + opts.dest = self.config['dest'].get() if not opts.dest: raise ui.UserError('no convert destination set') opts.dest = util.bytestring_path(opts.dest) if not opts.threads: - opts.threads = config['convert']['threads'].get(int) + opts.threads = self.config['threads'].get(int) - if config['convert']['paths']: - path_formats = ui.get_path_formats(config['convert']['paths']) + if self.config['paths']: + path_formats = ui.get_path_formats(self.config['paths']) else: path_formats = ui.get_path_formats() if not opts.format: - opts.format = config['convert']['format'].get(unicode).lower() + opts.format = self.config['format'].get(unicode).lower() pretend = opts.pretend if opts.pretend is not None else \ - config['convert']['pretend'].get(bool) + self.config['pretend'].get(bool) if not pretend: ui.commands.list_items(lib, ui.decargs(args), opts.album, None) @@ -364,7 +364,7 @@ class ConvertPlugin(BeetsPlugin): if opts.album: albums = lib.albums(ui.decargs(args)) items = (i for a in albums for i in a.items()) - if config['convert']['copy_album_art']: + if self.config['copy_album_art']: for album in albums: self.copy_album_art(album, opts.dest, path_formats, pretend) @@ -383,7 +383,7 @@ class ConvertPlugin(BeetsPlugin): """Transcode a file automatically after it is imported into the library. """ - format = config['convert']['format'].get(unicode).lower() + format = self.config['format'].get(unicode).lower() if should_transcode(item, format): command, ext = get_format() fd, dest = tempfile.mkstemp('.' + ext) diff --git a/beetsplug/echonest.py b/beetsplug/echonest.py index 8c60de409..a2b24bf20 100644 --- a/beetsplug/echonest.py +++ b/beetsplug/echonest.py @@ -134,7 +134,7 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): self.config.add(ATTRIBUTES) pyechonest.config.ECHO_NEST_API_KEY = \ - config['echonest']['apikey'].get(unicode) + self.config['apikey'].get(unicode) if self.config['auto']: self.import_stages = [self.imported] @@ -263,13 +263,13 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): source = item.path tmp = None if item.format not in ALLOWED_FORMATS: - if config['echonest']['convert']: + if self.config['convert']: tmp = source = self.convert(source) if not tmp: return if os.stat(source).st_size > UPLOAD_MAX_SIZE: - if config['echonest']['truncate']: + if self.config['truncate']: source = self.truncate(source) if tmp is not None: util.remove(tmp) @@ -394,7 +394,7 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): # There are four different ways to get a song. Each method is a # callable that takes the Item as an argument. methods = [self.profile, self.search] - if config['echonest']['upload']: + if self.config['upload']: methods.append(self.analyze) # Try each method in turn. diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index 1fa6b714b..fbed76c26 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -61,9 +61,9 @@ class EmbedCoverArtPlugin(BeetsPlugin): embed_cmd.parser.add_option( '-f', '--file', metavar='PATH', help='the image file to embed' ) - maxwidth = config['embedart']['maxwidth'].get(int) - compare_threshold = config['embedart']['compare_threshold'].get(int) - ifempty = config['embedart']['ifempty'].get(bool) + maxwidth = self.config['maxwidth'].get(int) + compare_threshold = self.config['compare_threshold'].get(int) + ifempty = self.config['ifempty'].get(bool) def embed_func(lib, opts, args): if opts.file: @@ -102,8 +102,8 @@ class EmbedCoverArtPlugin(BeetsPlugin): def album_imported(self, lib, album): """Automatically embed art into imported albums. """ - if album.artpath and config['embedart']['auto']: - max_width = config['embedart']['maxwidth'].get(int) + if album.artpath and self.config['auto']: + max_width = self.config['maxwidth'].get(int) self.embed_album(album, max_width, True) def embed_item(self, item, imagepath, maxwidth=None, itempath=None, @@ -157,8 +157,8 @@ class EmbedCoverArtPlugin(BeetsPlugin): ) for item in album.items(): - thresh = config['embedart']['compare_threshold'].get(int) - ifempty = config['embedart']['ifempty'].get(bool) + thresh = self.config['compare_threshold'].get(int) + ifempty = self.config['ifempty'].get(bool) self.embed_item(item, imagepath, maxwidth, None, thresh, ifempty, as_album=True) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index e17eb929e..d86d942ee 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -329,9 +329,9 @@ class FetchArtPlugin(plugins.BeetsPlugin): out = None # Local art. - cover_names = config['fetchart']['cover_names'].as_str_seq() + cover_names = self.config['cover_names'].as_str_seq() cover_names = map(util.bytestring_path, cover_names) - cautious = config['fetchart']['cautious'].get(bool) + cautious = self.config['cautious'].get(bool) if paths: for path in paths: # FIXME @@ -340,7 +340,7 @@ class FetchArtPlugin(plugins.BeetsPlugin): break # Web art sources. - remote_priority = config['fetchart']['remote_priority'].get(bool) + remote_priority = self.config['remote_priority'].get(bool) if not local_only and (remote_priority or not out): for url in self._source_urls(album): if self.maxwidth: diff --git a/beetsplug/fuzzy.py b/beetsplug/fuzzy.py index fcc59ef5a..789f862b8 100644 --- a/beetsplug/fuzzy.py +++ b/beetsplug/fuzzy.py @@ -17,7 +17,6 @@ from beets.plugins import BeetsPlugin from beets.dbcore.query import StringFieldQuery -import beets import difflib @@ -28,7 +27,7 @@ class FuzzyQuery(StringFieldQuery): if pattern.islower(): val = val.lower() queryMatcher = difflib.SequenceMatcher(None, pattern, val) - threshold = beets.config['fuzzy']['threshold'].as_number() + threshold = self.config['threshold'].as_number() return queryMatcher.quick_ratio() >= threshold @@ -41,5 +40,5 @@ class FuzzyPlugin(BeetsPlugin): }) def queries(self): - prefix = beets.config['fuzzy']['prefix'].get(basestring) + prefix = self.config['prefix'].get(basestring) return {prefix: FuzzyQuery} diff --git a/beetsplug/importadded.py b/beetsplug/importadded.py index 66639d143..b55d67171 100644 --- a/beetsplug/importadded.py +++ b/beetsplug/importadded.py @@ -8,7 +8,6 @@ from __future__ import unicode_literals, absolute_import, print_function import os -from beets import config from beets import util from beets.plugins import BeetsPlugin @@ -100,7 +99,7 @@ class ImportAddedPlugin(BeetsPlugin): mtime = self.item_mtime.pop(item.path, None) if mtime: album_mtimes.append(mtime) - if config['importadded']['preserve_mtimes'].get(bool): + if self.config['preserve_mtimes'].get(bool): self.write_item_mtime(item, mtime) item.store() album.added = min(album_mtimes) @@ -116,7 +115,7 @@ class ImportAddedPlugin(BeetsPlugin): mtime = self.item_mtime.pop(item.path, None) if mtime: item.added = mtime - if config['importadded']['preserve_mtimes'].get(bool): + if self.config['preserve_mtimes'].get(bool): self.write_item_mtime(item, mtime) self._log.debug(u"Import of item '{0}', selected item.added={1}", util.displayable_path(item.path), item.added) diff --git a/beetsplug/importfeeds.py b/beetsplug/importfeeds.py index 86b7ede74..40d036832 100644 --- a/beetsplug/importfeeds.py +++ b/beetsplug/importfeeds.py @@ -94,15 +94,15 @@ class ImportFeedsPlugin(BeetsPlugin): def _record_items(self, lib, basename, items): """Records relative paths to the given items for each feed format """ - feedsdir = bytestring_path(config['importfeeds']['dir'].as_filename()) - formats = config['importfeeds']['formats'].as_str_seq() - relative_to = config['importfeeds']['relative_to'].get() \ - or config['importfeeds']['dir'].as_filename() + feedsdir = bytestring_path(self.config['dir'].as_filename()) + formats = self.config['formats'].as_str_seq() + relative_to = self.config['relative_to'].get() \ + or self.config['dir'].as_filename() relative_to = bytestring_path(relative_to) paths = [] for item in items: - if config['importfeeds']['absolute_path']: + if self.config['absolute_path']: paths.append(item.path) else: try: @@ -115,7 +115,7 @@ class ImportFeedsPlugin(BeetsPlugin): if 'm3u' in formats: basename = bytestring_path( - config['importfeeds']['m3u_name'].get(unicode) + self.config['m3u_name'].get(unicode) ) m3u_path = os.path.join(feedsdir, basename) _write_m3u(m3u_path, paths) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index a85ce3efa..625e8fff1 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -407,8 +407,8 @@ class Google(Backend): def fetch(self, artist, title): query = u"%s %s" % (artist, title) - api_key = config['lyrics']['google_API_key'].get(unicode) - engine_id = config['lyrics']['google_engine_ID'].get(unicode) + api_key = self.config['google_API_key'].get(unicode) + engine_id = self.config['google_engine_ID'].get(unicode) url = u'https://www.googleapis.com/customsearch/v1?key=%s&cx=%s&q=%s' % \ (api_key, engine_id, urllib.quote(query.encode('utf8'))) diff --git a/beetsplug/mpdstats.py b/beetsplug/mpdstats.py index 86206f9df..0b2c8ba9b 100644 --- a/beetsplug/mpdstats.py +++ b/beetsplug/mpdstats.py @@ -57,7 +57,7 @@ class MPDClientWrapper(object): self._log = log self.music_directory = ( - config['mpdstats']['music_directory'].get(unicode)) + self.config['music_directory'].get(unicode)) self.client = MPDClient() @@ -144,8 +144,8 @@ class MPDStats(object): self.lib = lib self._log = log - self.do_rating = config['mpdstats']['rating'].get(bool) - self.rating_mix = config['mpdstats']['rating_mix'].get(float) + self.do_rating = self.config['rating'].get(bool) + self.rating_mix = self.config['rating_mix'].get(float) self.time_threshold = 10.0 # TODO: maybe add config option? self.now_playing = None diff --git a/beetsplug/scrub.py b/beetsplug/scrub.py index 929b3d9ee..64bbcc9b5 100644 --- a/beetsplug/scrub.py +++ b/beetsplug/scrub.py @@ -134,6 +134,6 @@ class ScrubPlugin(BeetsPlugin): def write_item(self, path): """Automatically embed art into imported albums.""" - if not scrubbing and config['scrub']['auto']: + if not scrubbing and self.config['auto']: self._log.debug(u'auto-scrubbing {0}', util.displayable_path(path)) self._scrub(path) From 8418fb60837463479bfbbf65f6626a98accbf367 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Mon, 12 Jan 2015 21:47:44 +0100 Subject: [PATCH 72/86] Use a standard logger for the import log The import log now relies on a standard logger, named 'beets.importer' and configured upon initialization of the import session. --- beets/importer.py | 27 +++++++++++++++++---------- beets/ui/commands.py | 23 +++++++---------------- test/_common.py | 4 ++-- test/helper.py | 2 +- test/test_importer.py | 9 ++++++--- test/test_ui_importer.py | 2 +- 6 files changed, 34 insertions(+), 33 deletions(-) diff --git a/beets/importer.py b/beets/importer.py index 2ee143e4c..0800f7b96 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -26,6 +26,7 @@ from tempfile import mkdtemp from bisect import insort, bisect_left from contextlib import contextmanager import shutil +import time from beets import logging from beets import autotag @@ -174,14 +175,13 @@ class ImportSession(object): """Controls an import action. Subclasses should implement methods to communicate with the user or otherwise make decisions. """ - def __init__(self, lib, logfile, paths, query): - """Create a session. `lib` is a Library object. `logfile` is a - file-like object open for writing or None if no logging is to be - performed. Either `paths` or `query` is non-null and indicates + def __init__(self, lib, loghandler, paths, query): + """Create a session. `lib` is a Library object. `loghandler` is a + logging.Handler. Either `paths` or `query` is non-null and indicates the source of files to be imported. """ self.lib = lib - self.logfile = logfile + self.logger = self._setup_logging(loghandler) self.paths = paths self.query = query self.seen_idents = set() @@ -191,6 +191,15 @@ class ImportSession(object): if self.paths: self.paths = map(normpath, self.paths) + def _setup_logging(self, loghandler): + logger = logging.getLogger(__name__) + logger.propagate = False + if not loghandler: + log.info(u"Importer progress won't be logged") + loghandler = logging.NullHandler() + logger.handlers = [loghandler] + return logger + def set_config(self, config): """Set `config` property from global import config and make implied changes. @@ -225,13 +234,10 @@ class ImportSession(object): self.want_resume = config['resume'].as_choice([True, False, 'ask']) def tag_log(self, status, paths): - """Log a message about a given album to logfile. The status should + """Log a message about a given album to the log file. The status should reflect the reason the album couldn't be tagged. """ - if self.logfile: - print(u'{0} {1}'.format(status, displayable_path(paths)), - file=self.logfile) - self.logfile.flush() + self.logger.info(u'{0} {1}', status, displayable_path(paths)) def log_choice(self, task, duplicate=False): """Logs the task's current choice if it should be logged. If @@ -269,6 +275,7 @@ class ImportSession(object): def run(self): """Run the import task. """ + self.logger.info(u'import started {0}', time.asctime()) self.set_config(config['import']) # Set up the pipeline. diff --git a/beets/ui/commands.py b/beets/ui/commands.py index da96b1898..2c0863b60 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -18,8 +18,6 @@ interface. from __future__ import print_function import os -import time -import codecs import platform import re import shlex @@ -825,29 +823,22 @@ def import_files(lib, paths, query): # Open the log. if config['import']['log'].get() is not None: - logpath = config['import']['log'].as_filename() + logpath = syspath(config['import']['log'].as_filename()) try: - logfile = codecs.open(syspath(logpath), 'a', 'utf8') + loghandler = logging.FileHandler(logpath) except IOError: - raise ui.UserError(u"could not open log file for writing: %s" % - displayable_path(logpath)) - print(u'import started', time.asctime(), file=logfile) + raise ui.UserError(u"could not open log file for writing: " + u"{0}".format(displayable_path(loghandler))) else: - logfile = None + loghandler = None # Never ask for input in quiet mode. if config['import']['resume'].get() == 'ask' and \ config['import']['quiet']: config['import']['resume'] = False - session = TerminalImportSession(lib, logfile, paths, query) - try: - session.run() - finally: - # If we were logging, close the file. - if logfile: - print(u'', file=logfile) - logfile.close() + session = TerminalImportSession(lib, loghandler, paths, query) + session.run() # Emit event. plugins.send('import', lib=lib, paths=paths) diff --git a/test/_common.py b/test/_common.py index 3852ba2f0..6107601c8 100644 --- a/test/_common.py +++ b/test/_common.py @@ -115,9 +115,9 @@ def album(lib=None): # Dummy import session. -def import_session(lib=None, logfile=None, paths=[], query=[], cli=False): +def import_session(lib=None, loghandler=None, paths=[], query=[], cli=False): cls = commands.TerminalImportSession if cli else importer.ImportSession - return cls(lib, logfile, paths, query) + return cls(lib, loghandler, paths, query) # A test harness for all beets tests. diff --git a/test/helper.py b/test/helper.py index afa5d29d1..af63c18a4 100644 --- a/test/helper.py +++ b/test/helper.py @@ -255,7 +255,7 @@ class TestHelper(object): config['import']['autotag'] = False config['import']['resume'] = False - return TestImportSession(self.lib, logfile=None, query=None, + return TestImportSession(self.lib, loghandler=None, query=None, paths=[import_dir]) # Library fixtures methods diff --git a/test/test_importer.py b/test/test_importer.py index 9a555c665..03b99a560 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -33,6 +33,7 @@ from beets.mediafile import MediaFile from beets import autotag from beets.autotag import AlbumInfo, TrackInfo, AlbumMatch from beets import config +from beets import logging class AutotagStub(object): @@ -209,7 +210,7 @@ class ImportHelper(TestHelper): config['import']['link'] = link self.importer = TestImportSession( - self.lib, logfile=None, query=None, + self.lib, loghandler=None, query=None, paths=[import_dir or self.import_dir] ) @@ -1219,13 +1220,15 @@ class ImportDuplicateSingletonTest(unittest.TestCase, TestHelper): class TagLogTest(_common.TestCase): def test_tag_log_line(self): sio = StringIO.StringIO() - session = _common.import_session(logfile=sio) + handler = logging.StreamHandler(sio) + session = _common.import_session(loghandler=handler) session.tag_log('status', 'path') assert 'status path' in sio.getvalue() def test_tag_log_unicode(self): sio = StringIO.StringIO() - session = _common.import_session(logfile=sio) + handler = logging.StreamHandler(sio) + session = _common.import_session(loghandler=handler) session.tag_log('status', 'caf\xc3\xa9') assert 'status caf' in sio.getvalue() diff --git a/test/test_ui_importer.py b/test/test_ui_importer.py index 8006e4215..0e3599301 100644 --- a/test/test_ui_importer.py +++ b/test/test_ui_importer.py @@ -91,7 +91,7 @@ class TerminalImportSessionSetup(object): self.io = DummyIO() self.io.install() self.importer = TestTerminalImportSession( - self.lib, logfile=None, query=None, io=self.io, + self.lib, loghandler=None, query=None, io=self.io, paths=[import_dir or self.import_dir], ) From 621ea60af4022f180e2ee478d36ad3166dfc9690 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Mon, 12 Jan 2015 22:08:11 +0100 Subject: [PATCH 73/86] Improve importer log unicode-handling test Send unicode instead of utf8-encoded string and check that the non-ASCII char is correctly handled. Bonus: use unittest.TestCase.assertIn(A, B) instead of "assert A in B". --- test/test_importer.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/test_importer.py b/test/test_importer.py index 03b99a560..7eab84e3e 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2015, Adrian Sampson. # @@ -1223,14 +1224,14 @@ class TagLogTest(_common.TestCase): handler = logging.StreamHandler(sio) session = _common.import_session(loghandler=handler) session.tag_log('status', 'path') - assert 'status path' in sio.getvalue() + self.assertIn('status path', sio.getvalue()) def test_tag_log_unicode(self): sio = StringIO.StringIO() handler = logging.StreamHandler(sio) session = _common.import_session(loghandler=handler) - session.tag_log('status', 'caf\xc3\xa9') - assert 'status caf' in sio.getvalue() + session.tag_log('status', u'café') # send unicode + self.assertIn(u'status café', sio.getvalue()) class ResumeImportTest(unittest.TestCase, TestHelper): From cce0a5d81fc15641968323f6cc1fbfc804495103 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 13 Jan 2015 09:12:50 +0100 Subject: [PATCH 74/86] ImporterSession.tag_log(): improve docstring --- beets/importer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beets/importer.py b/beets/importer.py index 0800f7b96..e0957e364 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -234,8 +234,8 @@ class ImportSession(object): self.want_resume = config['resume'].as_choice([True, False, 'ask']) def tag_log(self, status, paths): - """Log a message about a given album to the log file. The status should - reflect the reason the album couldn't be tagged. + """Log a message about a given album to the importer log. The status + should reflect the reason the album couldn't be tagged. """ self.logger.info(u'{0} {1}', status, displayable_path(paths)) From 81754e576062fffeb4a4ecb899fa5b5a53313a72 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 13 Jan 2015 09:20:29 +0100 Subject: [PATCH 75/86] beets.logging exports NullHandler on python 2.6 NullHandler is not available in python 2.6. We backport it so the importer log can use it for it is more convenient than guarding calls to self.logger (see beets/importer.py) --- beets/logging.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/beets/logging.py b/beets/logging.py index dd1a28cf2..933fdb167 100644 --- a/beets/logging.py +++ b/beets/logging.py @@ -92,3 +92,16 @@ if PY26: return logger my_manager.getLogger = new_getLogger + + +# Offer NullHandler in Python 2.6 to reduce the difference with never versions +if PY26: + class NullHandler(Handler): + def handle(self, record): + pass + + def emit(self, record): + pass + + def createLock(self): + self.lock = None From 2cf327e0fd000d9b3426b9ddc66e35ce3237f6da Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 13 Jan 2015 09:23:55 +0100 Subject: [PATCH 76/86] beet.importer: remove unnecessary log.info() call --- beets/importer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/beets/importer.py b/beets/importer.py index e0957e364..deb13fd24 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -195,7 +195,6 @@ class ImportSession(object): logger = logging.getLogger(__name__) logger.propagate = False if not loghandler: - log.info(u"Importer progress won't be logged") loghandler = logging.NullHandler() logger.handlers = [loghandler] return logger From c8309cbe570a50d4706b7b9ce69c3b0e20c658d0 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 13 Jan 2015 11:11:16 +0100 Subject: [PATCH 77/86] Simplify BeetsPlugin.listen() Deduplicate code with BeetsPlugin.register_listener(). --- beets/plugins.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/beets/plugins.py b/beets/plugins.py index ff89c856b..d6cb6469e 100755 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -202,9 +202,7 @@ class BeetsPlugin(object): ... pass """ def helper(func): - if cls.listeners is None: - cls.listeners = defaultdict(list) - cls.listeners[event].append(func) + cls.register_listener(event, func) return func return helper From 2e57d8660e47a5897dc1717cfeec4432090d9f75 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 13 Jan 2015 11:29:42 +0100 Subject: [PATCH 78/86] Ensure there's no duplicate plugin listeners A same function cannot be registered twice on a given event. This will permit improvements of some plugins (e.g. smartplaylist) --- beets/plugins.py | 3 ++- test/test_plugins.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/beets/plugins.py b/beets/plugins.py index d6cb6469e..193a209ab 100755 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -185,7 +185,8 @@ class BeetsPlugin(object): """ if cls.listeners is None: cls.listeners = defaultdict(list) - cls.listeners[event].append(func) + if func not in cls.listeners[event]: + cls.listeners[event].append(func) @classmethod def listen(cls, event): diff --git a/test/test_plugins.py b/test/test_plugins.py index 0880e2d27..256fc2ff4 100644 --- a/test/test_plugins.py +++ b/test/test_plugins.py @@ -161,6 +161,41 @@ class HelpersTest(unittest.TestCase): ('A', 'B', 'C', 'D')), ['D', 'B', 'C', 'A']) +class ListenersTest(unittest.TestCase, TestHelper): + def setUp(self): + self.setup_plugin_loader() + + def tearDown(self): + self.teardown_plugin_loader() + self.teardown_beets() + + def test_register(self): + + class DummyPlugin(plugins.BeetsPlugin): + def __init__(self): + super(DummyPlugin, self).__init__() + self.register_listener('cli_exit', self.dummy) + self.register_listener('cli_exit', self.dummy) + + def dummy(self): + pass + + d = DummyPlugin() + self.assertEqual(DummyPlugin.listeners['cli_exit'], [d.dummy]) + + d2 = DummyPlugin() + DummyPlugin.register_listener('cli_exit', d.dummy) + self.assertEqual(DummyPlugin.listeners['cli_exit'], + [d.dummy, d2.dummy]) + + @DummyPlugin.listen('cli_exit') + def dummy(lib): + pass + + self.assertEqual(DummyPlugin.listeners['cli_exit'], + [d.dummy, d2.dummy, dummy]) + + def suite(): return unittest.TestLoader().loadTestsFromName(__name__) From fdb768c9dbfca33a013b9588e0dadc8c68abc992 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 13 Jan 2015 11:31:42 +0100 Subject: [PATCH 79/86] Simplify smartplaylist flow Suppress the global variable, register listeners if it's needed only. --- beetsplug/smartplaylist.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/beetsplug/smartplaylist.py b/beetsplug/smartplaylist.py index 368d516cc..d299e176c 100644 --- a/beetsplug/smartplaylist.py +++ b/beetsplug/smartplaylist.py @@ -21,10 +21,6 @@ from beets import config, ui, library from beets.util import normpath, syspath import os -# Global variable so that smartplaylist can detect database changes and run -# only once before beets exits. -database_changed = False - def _items_for_query(lib, playlist, album=False): """Get the matching items for a playlist's configured queries. @@ -97,6 +93,9 @@ class SmartPlaylistPlugin(BeetsPlugin): 'playlists': [] }) + if self.config['auto']: + self.register_listener('database_change', self.db_change) + def commands(self): def update(lib, opts, args): update_playlists(lib) @@ -105,15 +104,8 @@ class SmartPlaylistPlugin(BeetsPlugin): spl_update.func = update return [spl_update] + def db_change(self, lib): + self.register_listener('cli_exit', self.update) -@SmartPlaylistPlugin.listen('database_change') -def handle_change(lib): - global database_changed - database_changed = True - - -@SmartPlaylistPlugin.listen('cli_exit') -def update(lib): - auto = config['smartplaylist']['auto'] - if database_changed and auto: + def update(self, lib): update_playlists(lib) From 7c4496c110e7df8901fb756ef53dfc59943f8873 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 13 Jan 2015 11:42:23 +0100 Subject: [PATCH 80/86] Smartplaylist: log messages instead of printing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ui.print_ → self._log.info Also change config['smartplaylist'] into self.config --- beetsplug/smartplaylist.py | 76 +++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/beetsplug/smartplaylist.py b/beetsplug/smartplaylist.py index d299e176c..d0d381090 100644 --- a/beetsplug/smartplaylist.py +++ b/beetsplug/smartplaylist.py @@ -48,41 +48,6 @@ def _items_for_query(lib, playlist, album=False): return results -def update_playlists(lib): - ui.print_("Updating smart playlists...") - playlists = config['smartplaylist']['playlists'].get(list) - playlist_dir = config['smartplaylist']['playlist_dir'].as_filename() - relative_to = config['smartplaylist']['relative_to'].get() - if relative_to: - relative_to = normpath(relative_to) - - for playlist in playlists: - items = [] - items.extend(_items_for_query(lib, playlist, True)) - items.extend(_items_for_query(lib, playlist, False)) - - m3us = {} - basename = playlist['name'].encode('utf8') - # As we allow tags in the m3u names, we'll need to iterate through - # the items and generate the correct m3u file names. - for item in items: - m3u_name = item.evaluate_template(basename, True) - if not (m3u_name in m3us): - m3us[m3u_name] = [] - item_path = item.path - if relative_to: - item_path = os.path.relpath(item.path, relative_to) - if item_path not in m3us[m3u_name]: - m3us[m3u_name].append(item_path) - # Now iterate through the m3us that we need to generate - for m3u in m3us: - m3u_path = normpath(os.path.join(playlist_dir, m3u)) - with open(syspath(m3u_path), 'w') as f: - for path in m3us[m3u]: - f.write(path + '\n') - ui.print_("... Done") - - class SmartPlaylistPlugin(BeetsPlugin): def __init__(self): super(SmartPlaylistPlugin, self).__init__() @@ -98,14 +63,47 @@ class SmartPlaylistPlugin(BeetsPlugin): def commands(self): def update(lib, opts, args): - update_playlists(lib) + self.update_playlists(lib) spl_update = ui.Subcommand('splupdate', help='update the smart playlists') spl_update.func = update return [spl_update] def db_change(self, lib): - self.register_listener('cli_exit', self.update) + self.register_listener('cli_exit', self.update_playlists) + + def update_playlists(self, lib): + self._log.info("Updating smart playlists...") + playlists = self.config['playlists'].get(list) + playlist_dir = self.config['playlist_dir'].as_filename() + relative_to = self.config['relative_to'].get() + if relative_to: + relative_to = normpath(relative_to) + + for playlist in playlists: + items = [] + items.extend(_items_for_query(lib, playlist, True)) + items.extend(_items_for_query(lib, playlist, False)) + + m3us = {} + basename = playlist['name'].encode('utf8') + # As we allow tags in the m3u names, we'll need to iterate through + # the items and generate the correct m3u file names. + for item in items: + m3u_name = item.evaluate_template(basename, True) + if not (m3u_name in m3us): + m3us[m3u_name] = [] + item_path = item.path + if relative_to: + item_path = os.path.relpath(item.path, relative_to) + if item_path not in m3us[m3u_name]: + m3us[m3u_name].append(item_path) + # Now iterate through the m3us that we need to generate + for m3u in m3us: + m3u_path = normpath(os.path.join(playlist_dir, m3u)) + with open(syspath(m3u_path), 'w') as f: + for path in m3us[m3u]: + f.write(path + '\n') + self._log.info("... Done") + - def update(self, lib): - update_playlists(lib) From 82772966c8cb90863345ff9579c07a86bffaec21 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 13 Jan 2015 11:53:39 +0100 Subject: [PATCH 81/86] Smartplaylist: fix incorrect doc With auto mode playlists are regenerated *at the end of the session, if a database update happened*, and not after a database update. --- docs/plugins/smartplaylist.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/plugins/smartplaylist.rst b/docs/plugins/smartplaylist.rst index 270e34def..bc39e581e 100644 --- a/docs/plugins/smartplaylist.rst +++ b/docs/plugins/smartplaylist.rst @@ -53,9 +53,9 @@ to albums that have a ``for_travel`` extensible field set to 1:: album_query: 'for_travel:1' query: 'for_travel:1' -By default, all playlists are automatically regenerated after every beets -command that changes the library database. To force regeneration, you can invoke it manually from the -command line:: +By default, all playlists are automatically regenerated at the end of the +session if the library database was changed. To force regeneration, you can +invoke it manually from the command line:: $ beet splupdate From a7beaa6d6e5b71dc33546381cbcea13d5b629525 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 13 Jan 2015 12:11:52 +0100 Subject: [PATCH 82/86] Clean & shorten smartplaylist code - better log messages - more idiomatic code: "X not in Y" instead of "not (X in Y)" - shorten _items_for_query: - pre-detect whether it's album_query or query, hiding conf. spec to the function. - Let library.{items,album} parse the query string, therefore falling back to beets-level sort spec. if none is given in the query --- beetsplug/smartplaylist.py | 49 ++++++++++++++------------------------ 1 file changed, 18 insertions(+), 31 deletions(-) diff --git a/beetsplug/smartplaylist.py b/beetsplug/smartplaylist.py index d0d381090..a7c83f0b8 100644 --- a/beetsplug/smartplaylist.py +++ b/beetsplug/smartplaylist.py @@ -15,37 +15,22 @@ """Generates smart playlists based on beets queries. """ from __future__ import print_function +from itertools import chain from beets.plugins import BeetsPlugin -from beets import config, ui, library +from beets import ui from beets.util import normpath, syspath import os -def _items_for_query(lib, playlist, album=False): - """Get the matching items for a playlist's configured queries. - `album` indicates whether to process the item-level query or the - album-level query (if any). - """ - key = 'album_query' if album else 'query' - if key not in playlist: - return [] - - # Parse quer(ies). If it's a list, perform the queries and manually - # concatenate the results - query_strings = playlist[key] - if not isinstance(query_strings, (list, tuple)): - query_strings = [query_strings] - model = library.Album if album else library.Item - results = [] - for q in query_strings: - query, sort = library.parse_query_string(q, model) - if album: - new = lib.albums(query, sort) - else: - new = lib.items(query, sort) - results.extend(new) - return results +def _items_for_query(lib, queries, album): + """Get the matching items for a query. + `album` indicates whether the queries are item-level or album-level""" + request = lib.albums if album else lib.items + if isinstance(queries, basestring): + return request(queries) + else: + return chain.from_iterable(map(request, queries)) class SmartPlaylistPlugin(BeetsPlugin): @@ -81,9 +66,13 @@ class SmartPlaylistPlugin(BeetsPlugin): relative_to = normpath(relative_to) for playlist in playlists: + self._log.debug(u"Creating playlist {0.name}", playlist) items = [] - items.extend(_items_for_query(lib, playlist, True)) - items.extend(_items_for_query(lib, playlist, False)) + if 'album_query' in playlist: + items.extend(_items_for_query(lib, playlist['album_query'], + True)) + if 'query' in playlist: + items.extend(_items_for_query(lib, playlist['query'], False)) m3us = {} basename = playlist['name'].encode('utf8') @@ -91,7 +80,7 @@ class SmartPlaylistPlugin(BeetsPlugin): # the items and generate the correct m3u file names. for item in items: m3u_name = item.evaluate_template(basename, True) - if not (m3u_name in m3us): + if m3u_name not in m3us: m3us[m3u_name] = [] item_path = item.path if relative_to: @@ -104,6 +93,4 @@ class SmartPlaylistPlugin(BeetsPlugin): with open(syspath(m3u_path), 'w') as f: for path in m3us[m3u]: f.write(path + '\n') - self._log.info("... Done") - - + self._log.info("{0} playlists updated", len(playlists)) From de86b9b57089f4772d929d065c5280cbee362c0a Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 13 Jan 2015 12:21:45 +0100 Subject: [PATCH 83/86] Clean MPDupdate plugin - no global variable - use logging instead of prints - unicode logging --- beetsplug/mpdupdate.py | 87 ++++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 49 deletions(-) diff --git a/beetsplug/mpdupdate.py b/beetsplug/mpdupdate.py index ff6e6dbe2..f8affed37 100644 --- a/beetsplug/mpdupdate.py +++ b/beetsplug/mpdupdate.py @@ -20,17 +20,11 @@ Put something like the following in your config.yaml to configure: port: 6600 password: seekrit """ -from __future__ import print_function - from beets.plugins import BeetsPlugin import os import socket from beets import config -# Global variable so that mpdupdate can detect database changes and run only -# once before beets exits. -database_changed = False - # No need to introduce a dependency on an MPD library for such a # simple use case. Here's a simple socket abstraction to make things @@ -66,37 +60,6 @@ class BufferedSocket(object): self.sock.close() -def update_mpd(host='localhost', port=6600, password=None): - """Sends the "update" command to the MPD server indicated, - possibly authenticating with a password first. - """ - print('Updating MPD database...') - - s = BufferedSocket(host, port) - resp = s.readline() - if 'OK MPD' not in resp: - print('MPD connection failed:', repr(resp)) - return - - if password: - s.send('password "%s"\n' % password) - resp = s.readline() - if 'OK' not in resp: - print('Authentication failed:', repr(resp)) - s.send('close\n') - s.close() - return - - s.send('update\n') - resp = s.readline() - if 'updating_db' not in resp: - print('Update failed:', repr(resp)) - - s.send('close\n') - s.close() - print('... updated.') - - class MPDUpdatePlugin(BeetsPlugin): def __init__(self): super(MPDUpdatePlugin, self).__init__() @@ -112,18 +75,44 @@ class MPDUpdatePlugin(BeetsPlugin): if self.config[key].exists(): config['mpd'][key] = self.config[key].get() + self.register_listener('database_change', self.db_change) -@MPDUpdatePlugin.listen('database_change') -def handle_change(lib=None): - global database_changed - database_changed = True + def db_change(self, lib): + self.register_listener('cli_exit', self.update) + def update(self, lib): + self.update_mpd( + config['mpd']['host'].get(unicode), + config['mpd']['port'].get(int), + config['mpd']['password'].get(unicode), + ) -@MPDUpdatePlugin.listen('cli_exit') -def update(lib=None): - if database_changed: - update_mpd( - config['mpd']['host'].get(unicode), - config['mpd']['port'].get(int), - config['mpd']['password'].get(unicode), - ) + def update_mpd(self, host='localhost', port=6600, password=None): + """Sends the "update" command to the MPD server indicated, + possibly authenticating with a password first. + """ + self._log.info('Updating MPD database...') + + s = BufferedSocket(host, port) + resp = s.readline() + if 'OK MPD' not in resp: + self._log.warning(u'MPD connection failed: {0!r}', resp) + return + + if password: + s.send('password "%s"\n' % password) + resp = s.readline() + if 'OK' not in resp: + self._log.warning(u'Authentication failed: {0!r}', resp) + s.send('close\n') + s.close() + return + + s.send('update\n') + resp = s.readline() + if 'updating_db' not in resp: + self._log.warning(u'Update failed: {0!r}', resp) + + s.send('close\n') + s.close() + self._log.info('Database updated.') From 753388550e4ea7a8b09ddb22189021be3585a5e5 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 13 Jan 2015 12:35:54 +0100 Subject: [PATCH 84/86] Clean PlexUpdate plugin - no global variable - use logging instead of prints --- beetsplug/plexupdate.py | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/beetsplug/plexupdate.py b/beetsplug/plexupdate.py index c61766c43..f132cd4b3 100644 --- a/beetsplug/plexupdate.py +++ b/beetsplug/plexupdate.py @@ -12,11 +12,6 @@ from beets import config from beets.plugins import BeetsPlugin -# Global variable to detect if database is changed that the update -# is only run once before beets exists. -database_changed = False - - def get_music_section(host, port): """Getting the section key for the music library in Plex. """ @@ -55,30 +50,23 @@ class PlexUpdate(BeetsPlugin): u'host': u'localhost', u'port': 32400}) + self.register_listener('database_change', self.listen_for_db_change) -@PlexUpdate.listen('database_change') -def listen_for_db_change(lib=None): - """Listens for beets db change and set global database_changed - variable to True. - """ - global database_changed - database_changed = True + def listen_for_db_change(self, lib): + """Listens for beets db change and register the update for the end""" + self.register_listener('cli_exit', self.update) - -@PlexUpdate.listen('cli_exit') -def update(lib=None): - """When the client exists and the database_changed variable is True - trying to send refresh request to Plex server. - """ - if database_changed: - print('Updating Plex library...') + def update(self, lib): + """When the client exists try to send refresh request to Plex server. + """ + self._log.info('Updating Plex library...') # Try to send update request. try: update_plex( config['plex']['host'].get(), config['plex']['port'].get()) - print('... started.') + self._log.info('... started.') except requests.exceptions.RequestException: - print('Update failed.') + self._log.warning('Update failed.') From ca91bc8920915c9962fe4d33476eb4fd1f6229ba Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 13 Jan 2015 18:53:30 +0100 Subject: [PATCH 85/86] mpdupdate: fix indent --- beetsplug/mpdupdate.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/beetsplug/mpdupdate.py b/beetsplug/mpdupdate.py index f8affed37..42ef55fa8 100644 --- a/beetsplug/mpdupdate.py +++ b/beetsplug/mpdupdate.py @@ -81,11 +81,11 @@ class MPDUpdatePlugin(BeetsPlugin): self.register_listener('cli_exit', self.update) def update(self, lib): - self.update_mpd( - config['mpd']['host'].get(unicode), - config['mpd']['port'].get(int), - config['mpd']['password'].get(unicode), - ) + self.update_mpd( + config['mpd']['host'].get(unicode), + config['mpd']['port'].get(int), + config['mpd']['password'].get(unicode), + ) def update_mpd(self, host='localhost', port=6600, password=None): """Sends the "update" command to the MPD server indicated, From 9a2a9b0144ebb19108118cce1ecfc8a5391fdba9 Mon Sep 17 00:00:00 2001 From: Bruno Cauet Date: Tue, 13 Jan 2015 18:53:47 +0100 Subject: [PATCH 86/86] smartplaylist: fix docstring --- beetsplug/smartplaylist.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/beetsplug/smartplaylist.py b/beetsplug/smartplaylist.py index a7c83f0b8..15cb46057 100644 --- a/beetsplug/smartplaylist.py +++ b/beetsplug/smartplaylist.py @@ -25,7 +25,8 @@ import os def _items_for_query(lib, queries, album): """Get the matching items for a query. - `album` indicates whether the queries are item-level or album-level""" + `album` indicates whether the queries are item-level or album-level. + """ request = lib.albums if album else lib.items if isinstance(queries, basestring): return request(queries)