Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 129 lines (113 sloc) 4.246 kB
f2bb220 @sampsyo chroma plugin (for acoustid fingerprinting) (#152)
sampsyo authored
1 # This file is part of beets.
2 # Copyright 2011, Adrian Sampson.
3 #
4 # Permission is hereby granted, free of charge, to any person obtaining
5 # a copy of this software and associated documentation files (the
6 # "Software"), to deal in the Software without restriction, including
7 # without limitation the rights to use, copy, modify, merge, publish,
8 # distribute, sublicense, and/or sell copies of the Software, and to
9 # permit persons to whom the Software is furnished to do so, subject to
10 # the following conditions:
11 #
12 # The above copyright notice and this permission notice shall be
13 # included in all copies or substantial portions of the Software.
14
15 """Adds Chromaprint/Acoustid acoustic fingerprinting support to the
16 autotagger. Requires the pyacoustid library.
17 """
18 from __future__ import with_statement
19 from beets import plugins
20 from beets.autotag import hooks
21 import acoustid
22 import logging
23 from collections import defaultdict
24
25 API_KEY = '1vOwZtEn'
26 SCORE_THRESH = 0.5
27 TRACK_ID_WEIGHT = 10.0
28 COMMON_REL_THRESH = 0.6 # How many tracks must have an album in common?
29
30 log = logging.getLogger('beets')
31
32 class _cached(object):
33 """Decorator implementing memoization."""
34 def __init__(self, func):
35 self.func = func
36 self.cache = {}
37
38 def __call__(self, *args, **kwargs):
39 cache_key = (args, tuple(sorted(kwargs.iteritems())))
40 if cache_key in self.cache:
41 return self.cache[cache_key]
42 res = self.func(*args, **kwargs)
43 self.cache[cache_key] = res
44 return res
45
46 @_cached
47 def acoustid_match(path, metadata=None):
48 """Gets metadata for a file from Acoustid. Returns a recording ID
49 and a list of release IDs if a match is found; otherwise, returns
50 None.
51 """
52 try:
53 res = acoustid.match(API_KEY, path, meta='recordings releases',
54 parse=False)
55 except acoustid.AcoustidError, exc:
56 log.debug('fingerprint matching %s failed: %s' %
57 (repr(path), str(exc)))
58 return None
59 log.debug('fingerprinted: %s' % repr(path))
60
61 # Ensure the response is usable and parse it.
62 if res['status'] != 'ok' or not res.get('results'):
63 return None
64 result = res['results'][0]
65 if result['score'] < SCORE_THRESH or not result.get('recordings'):
66 return None
67 recording = result['recordings'][0]
68 recording_id = recording['id']
99bb4df @sampsyo check for releases in acoustid result (fixes #252)
sampsyo authored
69 if 'releases' in recording:
70 release_ids = [rel['id'] for rel in recording['releases']]
71 else:
72 release_ids = []
f2bb220 @sampsyo chroma plugin (for acoustid fingerprinting) (#152)
sampsyo authored
73
74 return recording_id, release_ids
75
76 def _all_releases(items):
77 """Given an iterable of Items, determines (according to Acoustid)
78 which releases the items have in common. Generates release IDs.
79 """
80 # Count the number of "hits" for each release.
81 relcounts = defaultdict(int)
82 for item in items:
83 aidata = acoustid_match(item.path)
84 if not aidata:
85 continue
86 _, release_ids = aidata
87 for release_id in release_ids:
88 relcounts[release_id] += 1
89
90 for release_id, count in relcounts.iteritems():
91 if float(count) / len(items) > COMMON_REL_THRESH:
92 yield release_id
93
94 class AcoustidPlugin(plugins.BeetsPlugin):
95 def track_distance(self, item, info):
96 aidata = acoustid_match(item.path)
97 if not aidata:
98 # Match failed.
99 return 0.0, 0.0
100
101 recording_id, _ = aidata
102 if info.track_id == recording_id:
103 dist = 0.0
104 else:
105 dist = TRACK_ID_WEIGHT
106 return dist, TRACK_ID_WEIGHT
107
108 def candidates(self, items):
109 albums = []
110 for relid in _all_releases(items):
111 album = hooks._album_for_id(relid)
112 if album:
113 albums.append(album)
114
115 log.debug('acoustid album candidates: %i' % len(albums))
116 return albums
117
118 def item_candidates(self, item):
119 aidata = acoustid_match(item.path)
120 if not aidata:
121 return []
122 recording_id, _ = aidata
123 track = hooks._track_for_id(recording_id)
124 if track:
125 log.debug('found acoustid item candidate')
126 return [track]
127 else:
128 log.debug('no acoustid item candidate found')
Something went wrong with that request. Please try again.