forked from lcharlick/beets-metalarchives
-
Notifications
You must be signed in to change notification settings - Fork 1
/
metalarchives.py
executable file
·221 lines (183 loc) · 7.57 KB
/
metalarchives.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
"""Adds Metal Archives album search support to the beets autotagger.
"""
import logging
import metallum
from beets.autotag.hooks import AlbumInfo, TrackInfo, Distance, string_dist
from beets.plugins import BeetsPlugin
from beets import config, ui
from iso3166 import countries
log = logging.getLogger('beets')
DATA_SOURCE = 'Metal Archives'
ID_PREFIX = 'ma-'
def _add_prefix(id):
"""Add source id prefix to id
"""
return ID_PREFIX + str(id)
def _strip_prefix(id):
"""Strip source id prefix from id
"""
return id[len(ID_PREFIX):]
def _is_source_id(id):
"""Check if an id string contains the source id prefix
"""
return id[:len(ID_PREFIX)] == ID_PREFIX
class MetalArchivesPlugin(BeetsPlugin):
def __init__(self):
super(MetalArchivesPlugin, self).__init__()
self.config.add({
'source_weight': 1.0,
'lyrics': False,
'lyrics_search': False,
'instrumental': '',
})
stages = []
if self.config['lyrics'].get(bool):
stages.append(self.fetch_lyrics)
self.import_stages = stages
def commands(self):
cmd = ui.Subcommand('metalarchives', help='metal archives data source')
cmd.parser.add_option('-l', '--lyrics', dest='lyrics',
action='store_true', default=False,
help='fetch track lyrics from metal archives')
def func(lib, opts, args):
if opts.lyrics:
for item in lib.items(ui.decargs(args)):
self.fetch_item_lyrics(item)
cmd.func = func
return [cmd]
def album_distance(self, items, album_info, mapping):
"""Returns the album distance.
"""
dist = Distance()
if album_info.data_source == DATA_SOURCE:
dist.add('source', self.config['source_weight'].as_number())
return dist
def candidates(self, items, artist, album, va_likely, extra_tags=None):
"""Returns a list of AlbumInfo objects for Metal Archives search results
matching an album and artist (if not various).
"""
return self.get_albums(artist, album)
def fetch_item_lyrics(self, item):
"""Fetch track lyrics from Metal Archives
"""
lyrics = ''
# Skip if lyrics are already present
if item.lyrics:
return
# If this track was matched from metal archives, we can just use
# the track id
if _is_source_id(item.mb_albumid):
self._log.debug(u'fetching lyrics: {0.artist} - {0.title}', item)
track_id = _strip_prefix(item.mb_trackid)
try:
lyrics = metallum.lyrics_for_id(track_id)
except metallum.NetworkError as e:
self._log.debug('network error: {0}', e)
return
# Otherwise perform an album search
elif self.config['lyrics_search'].get(bool):
self._log.debug(u'searching for lyrics: {0.artist} - {0.title}', item)
try:
results = metallum.album_search(item.album, band=item.artist, strict=False,
year_from=item.year, year_to=item.year)
except metallum.NetworkError as e:
self._log.debug('network error: {0}', e)
return
for result in results:
# TODO: use Distance object to calculate actual album distance
# using all data fields (title, year, number of tracks, etc)
album = result.get()
if len(album.tracks) >= item.track:
track = album.tracks[item.track - 1]
dist = string_dist(item.title, track.title)
# TODO: make threshold config key
if dist > 0.1:
continue
try:
lyrics = track.lyrics
except metallum.NetworkError as e:
self._log.debug('network error: {0}', e)
return
else:
return
if lyrics:
lyrics = str(lyrics)
message = ui.colorize('text_success', 'found lyrics')
if lyrics == '(<em>Instrumental</em>)':
lyrics = self.config['instrumental'].get()
item.lyrics = lyrics
if config['import']['write'].get(bool):
item.try_write()
item.store()
else:
message = ui.colorize('text_error', 'no lyrics found')
self._log.info(u'{0.artist} - {0.album} - {0.title}: {1}', item, message)
def fetch_lyrics(self, session, task):
"""Fetch lyrics from Metal Archives for each track
"""
for item in task.imported_items():
self.fetch_item_lyrics(item)
def album_for_id(self, album_id):
"""Fetches an album by its Metal Archives ID and returns an AlbumInfo object
or None if the album is not found.
"""
if not _is_source_id(album_id):
return
try:
result = metallum.album_for_id(_strip_prefix(album_id))
except metallum.NetworkError as e:
self._log.debug('network error: {0}', e)
return
return self.get_album_info(result)
def get_albums(self, artist, album):
"""Returns a list of AlbumInfo objects for a Metal Archives search query.
"""
albums = []
try:
results = metallum.album_search(album, band=artist, strict=False, band_strict=False, formats=['CD'])
except metallum.NetworkError as e:
self._log.debug('network error: {0}', e)
return
for result in results:
try:
album = result.get()
except metallum.NetworkError as e:
self._log.debug('network error: {0}', e)
continue
albums.append(self.get_album_info(album))
return albums
def get_album_info(self, album):
"""Returns an AlbumInfo object for a Metal Archives album object.
"""
artist = album.bands[0]
tracks = self.get_tracks(album.tracks)
album_id = _add_prefix(album.id)
artist_id = _add_prefix(artist.id)
try:
country = countries.get(artist.country).alpha2
except KeyError:
country = ''
band_names = " / ".join([band.name for band in album.bands])
return AlbumInfo(tracks, album.title, album_id, band_names, artist.id,
albumtype=album.type, va=False, year=album.year, month=album.date.month,
day=album.date.day, label=album.label, mediums=album.disc_count,
country=country, data_source=DATA_SOURCE, data_url=metallum.BASE_URL + '/' + album.url)
def get_tracks(self, tracklist):
"""Returns a list of TrackInfo objects for a Metal Archives tracklist.
"""
tracks = []
for track in tracklist:
tracks.append(self.get_track_info(track))
return tracks
def get_track_info(self, track):
"""Returns a TrackInfo object for a Metal Archives track object.
"""
track_id = _add_prefix(track.id)
artist_id = _add_prefix(track.band.id)
return TrackInfo(track.title, track_id,
artist=track.band.name,
artist_id=artist_id,
length=track.duration,
index=track.overall_number,
medium=track.disc_number,
medium_index=track.number)