diff --git a/beets/importer.py b/beets/importer.py index 3d7f9b1ac..cc81f6ddb 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -296,6 +296,7 @@ class ImportTask(object): self.items = items self.sentinel = False self.remove_duplicates = False + self.is_album = True @classmethod def done_sentinel(cls, toppath): @@ -328,12 +329,12 @@ class ImportTask(object): """Sets the candidates for this album matched by the `autotag.tag_album` method. """ + assert self.is_album assert not self.sentinel self.cur_artist = cur_artist self.cur_album = cur_album self.candidates = candidates self.rec = rec - self.is_album = True def set_null_match(self): """Set the candidates to indicate no album match was found. @@ -393,7 +394,9 @@ class ImportTask(object): if self.sentinel or self.is_album: history_add(self.path) + # Logical decisions. + def should_write_tags(self): """Should new info be written to the files' metadata?""" if self.choice_flag == action.APPLY: @@ -402,16 +405,20 @@ class ImportTask(object): return False else: assert False + def should_fetch_art(self): """Should album art be downloaded for this album?""" return self.should_write_tags() and self.is_album + def should_skip(self): """After a choice has been made, returns True if this is a sentinel or it has been marked for skipping. """ return self.sentinel or self.choice_flag == action.SKIP - # Useful data. + + # Convenient data. + def chosen_ident(self): """Returns identifying metadata about the current choice. For albums, this is an (artist, album) pair. For items, this is @@ -431,6 +438,16 @@ class ImportTask(object): elif self.choice_flag is action.APPLY: return (self.info.artist, self.info.title) + def all_items(self): + """If this is an album task, returns the list of non-None + (non-gap) items. If this is a singleton task, returns a list + containing the item. + """ + if self.is_album: + return [i for i in self.items if i] + else: + return [self.item] + # Full-album pipeline stages. @@ -540,6 +557,8 @@ def initial_lookup(config): if task.sentinel: continue + plugins.send('start_import_task', task=task, config=config) + log.debug('Looking up: %s' % task.path) try: task.set_match(*autotag.tag_album(task.items, config.timid)) @@ -622,7 +641,7 @@ def apply_choices(config): if task.should_skip(): continue - items = [i for i in task.items if i] if task.is_album else [task.item] + items = task.all_items() # Clear IDs in case the items are being re-tagged. for item in items: item.id = None @@ -753,7 +772,7 @@ def finalize(config): task.save_history() continue - items = [i for i in task.items if i] if task.is_album else [task.item] + items = task.all_items() # Announce that we've added an album. if task.is_album: @@ -794,6 +813,8 @@ def item_lookup(config): if task.sentinel: continue + plugins.send('start_import_task', task=task, config=config) + task.set_item_match(*autotag.tag_item(task.item, config.timid)) def item_query(config): diff --git a/beetsplug/chroma.py b/beetsplug/chroma.py index 02cfa37e2..3a71d4727 100644 --- a/beetsplug/chroma.py +++ b/beetsplug/chroma.py @@ -29,22 +29,14 @@ COMMON_REL_THRESH = 0.6 # How many tracks must have an album in common? log = logging.getLogger('beets') -class _cached(object): - """Decorator implementing memoization.""" - def __init__(self, func): - self.func = func - self.cache = {} +# 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_id, release_ids) +# pairs. If a given path is not present in the mapping, then no match +# was found. +_matches = {} - def __call__(self, *args, **kwargs): - cache_key = (args, tuple(sorted(kwargs.iteritems()))) - if cache_key in self.cache: - return self.cache[cache_key] - res = self.func(*args, **kwargs) - self.cache[cache_key] = res - return res - -@_cached -def acoustid_match(path, metadata=None): +def acoustid_match(path): """Gets metadata for a file from Acoustid. Returns a recording ID and a list of release IDs if a match is found; otherwise, returns None. @@ -87,10 +79,10 @@ def _all_releases(items): # Count the number of "hits" for each release. relcounts = defaultdict(int) for item in items: - aidata = acoustid_match(item.path) - if not aidata: + if item.path not in _matches: continue - _, release_ids = aidata + + _, release_ids = _matches[item.path] for release_id in release_ids: relcounts[release_id] += 1 @@ -100,12 +92,11 @@ def _all_releases(items): class AcoustidPlugin(plugins.BeetsPlugin): def track_distance(self, item, info): - aidata = acoustid_match(item.path) - if not aidata: + if item.path not in _matches: # Match failed. return 0.0, 0.0 - recording_id, _ = aidata + recording_id, _ = _matches[item.path] if info.track_id == recording_id: dist = 0.0 else: @@ -123,10 +114,10 @@ class AcoustidPlugin(plugins.BeetsPlugin): return albums def item_candidates(self, item): - aidata = acoustid_match(item.path) - if not aidata: - return [] - recording_id, _ = aidata + if item.path not in _matches: + return 0.0, 0.0 + + recording_id, _ = _matches[item.path] track = hooks._track_for_id(recording_id) if track: log.debug('found acoustid item candidate') @@ -134,3 +125,13 @@ class AcoustidPlugin(plugins.BeetsPlugin): else: log.debug('no acoustid item candidate found') return [] + +@AcoustidPlugin.listen('start_import_task') +def fingerprint_task(config=None, task=None): + """Fingerprint each item in the task for later use during the + autotagging candidate search. + """ + for item in task.all_items(): + match = acoustid_match(item.path) + if match: + _matches[item.path] = match diff --git a/docs/plugins/writing.rst b/docs/plugins/writing.rst index b1790dcf9..327c7ce5e 100644 --- a/docs/plugins/writing.rst +++ b/docs/plugins/writing.rst @@ -134,6 +134,9 @@ currently available are: * *write*: called with an ``Item`` and a ``MediaFile`` object just before a file's metadata is written to disk. +* *start_import_task*: called when before an import task begins processing. + Parameters: ``task`` and ``config``. + The included ``mpdupdate`` plugin provides an example use case for event listeners. Extend the Autotagger