mirror of
https://github.com/beetbox/beets.git
synced 2026-01-02 22:12:53 +01:00
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:
parent
de6530f4a5
commit
82a4bafc3e
3 changed files with 54 additions and 29 deletions
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue