chroma: fingerprint when task begins

The old "caching"-based approach to fingerprinting was kinda hacky to begin
with. Now, the chroma plugin has an explicit opportunity (in the form of a new
event) to perform its initial fingerprinting and lookup for all tracks. Then,
this information is used explicitly during the autotagging phase rather than
being used transparently through memoization of the lookup function.
This commit is contained in:
Adrian Sampson 2012-04-01 18:55:14 -07:00
parent de6530f4a5
commit 82a4bafc3e
3 changed files with 54 additions and 29 deletions

View file

@ -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):

View file

@ -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

View file

@ -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