Skip to content

Commit b712c49

Browse files
committed
Merge pull request beetbox#1797 from sampsyo/lastimport-fix-1574
Fix lastimport beetbox#1574 (LastFM API change)
2 parents a218da1 + ed3d6b8 commit b712c49

File tree

3 files changed

+107
-40
lines changed

3 files changed

+107
-40
lines changed

beetsplug/lastimport.py

Lines changed: 94 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
from __future__ import (division, absolute_import, print_function,
1717
unicode_literals)
1818

19-
import requests
19+
import pylast
20+
from pylast import TopItem, _extract, _number
2021
from beets import ui
2122
from beets import dbcore
2223
from beets import config
@@ -52,6 +53,63 @@ def func(lib, opts, args):
5253
return [cmd]
5354

5455

56+
class CustomUser(pylast.User):
57+
""" Custom user class derived from pylast.User, and overriding the
58+
_get_things method to return MBID and album. Also introduces new
59+
get_top_tracks_by_page method to allow access to more than one page of top
60+
tracks.
61+
"""
62+
def __init__(self, *args, **kwargs):
63+
super(CustomUser, self).__init__(*args, **kwargs)
64+
65+
def _get_things(self, method, thing, thing_type, params=None,
66+
cacheable=True):
67+
"""Returns a list of the most played thing_types by this thing, in a
68+
tuple with the total number of pages of results. Includes an MBID, if
69+
found.
70+
"""
71+
doc = self._request(
72+
self.ws_prefix + "." + method, cacheable, params)
73+
74+
toptracks_node = doc.getElementsByTagName('toptracks')[0]
75+
total_pages = int(toptracks_node.getAttribute('totalPages'))
76+
77+
seq = []
78+
for node in doc.getElementsByTagName(thing):
79+
title = _extract(node, "name")
80+
artist = _extract(node, "name", 1)
81+
mbid = _extract(node, "mbid")
82+
playcount = _number(_extract(node, "playcount"))
83+
84+
thing = thing_type(artist, title, self.network)
85+
thing.mbid = mbid
86+
seq.append(TopItem(thing, playcount))
87+
88+
return seq, total_pages
89+
90+
def get_top_tracks_by_page(self, period=pylast.PERIOD_OVERALL, limit=None,
91+
page=1, cacheable=True):
92+
"""Returns the top tracks played by a user, in a tuple with the total
93+
number of pages of results.
94+
* period: The period of time. Possible values:
95+
o PERIOD_OVERALL
96+
o PERIOD_7DAYS
97+
o PERIOD_1MONTH
98+
o PERIOD_3MONTHS
99+
o PERIOD_6MONTHS
100+
o PERIOD_12MONTHS
101+
"""
102+
103+
params = self._get_params()
104+
params['period'] = period
105+
params['page'] = page
106+
if limit:
107+
params['limit'] = limit
108+
109+
return self._get_things(
110+
"getTopTracks", "track", pylast.Track, params, cacheable)
111+
112+
55113
def import_lastfm(lib, log):
56114
user = config['lastfm']['user'].get(unicode)
57115
per_page = config['lastimport']['per_page'].get(int)
@@ -73,23 +131,19 @@ def import_lastfm(lib, log):
73131
'/{}'.format(page_total) if page_total > 1 else '')
74132

75133
for retry in range(0, retry_limit):
76-
page = fetch_tracks(user, page_current + 1, per_page)
77-
if 'tracks' in page:
78-
# Let us the reveal the holy total pages!
79-
page_total = int(page['tracks']['@attr']['totalPages'])
80-
if page_total < 1:
81-
# It means nothing to us!
82-
raise ui.UserError('Last.fm reported no data.')
83-
84-
track = page['tracks']['track']
85-
found, unknown = process_tracks(lib, track, log)
134+
tracks, page_total = fetch_tracks(user, page_current + 1, per_page)
135+
if page_total < 1:
136+
# It means nothing to us!
137+
raise ui.UserError('Last.fm reported no data.')
138+
139+
if tracks:
140+
found, unknown = process_tracks(lib, tracks, log)
86141
found_total += found
87142
unknown_total += unknown
88143
break
89144
else:
90145
log.error('ERROR: unable to read page #{0}',
91146
page_current + 1)
92-
log.debug('API response: {}', page)
93147
if retry < retry_limit:
94148
log.info(
95149
'Retrying page #{0}... ({1}/{2} retry)',
@@ -107,14 +161,30 @@ def import_lastfm(lib, log):
107161

108162

109163
def fetch_tracks(user, page, limit):
110-
return requests.get(API_URL, params={
111-
'method': 'library.gettracks',
112-
'user': user,
113-
'api_key': plugins.LASTFM_KEY,
114-
'page': bytes(page),
115-
'limit': bytes(limit),
116-
'format': 'json',
117-
}).json()
164+
""" JSON format:
165+
[
166+
{
167+
"mbid": "...",
168+
"artist": "...",
169+
"title": "...",
170+
"playcount": "..."
171+
}
172+
]
173+
"""
174+
network = pylast.LastFMNetwork(api_key=config['lastfm']['api_key'])
175+
user_obj = CustomUser(user, network)
176+
results, total_pages =\
177+
user_obj.get_top_tracks_by_page(limit=limit, page=page)
178+
return [
179+
{
180+
"mbid": track.item.mbid if track.item.mbid else '',
181+
"artist": {
182+
"name": track.item.artist.name
183+
},
184+
"name": track.item.title,
185+
"playcount": track.weight
186+
} for track in results
187+
], total_pages
118188

119189

120190
def process_tracks(lib, tracks, log):
@@ -124,7 +194,7 @@ def process_tracks(lib, tracks, log):
124194
log.info('Received {0} tracks in this page, processing...', total)
125195

126196
for num in xrange(0, total):
127-
song = ''
197+
song = None
128198
trackid = tracks[num]['mbid'].strip()
129199
artist = tracks[num]['artist'].get('name', '').strip()
130200
title = tracks[num]['name'].strip()
@@ -140,19 +210,8 @@ def process_tracks(lib, tracks, log):
140210
dbcore.query.MatchQuery('mb_trackid', trackid)
141211
).get()
142212

143-
# Otherwise try artist/title/album
144-
if not song:
145-
log.debug(u'no match for mb_trackid {0}, trying by '
146-
u'artist/title/album', trackid)
147-
query = dbcore.AndQuery([
148-
dbcore.query.SubstringQuery('artist', artist),
149-
dbcore.query.SubstringQuery('title', title),
150-
dbcore.query.SubstringQuery('album', album)
151-
])
152-
song = lib.items(query).get()
153-
154213
# If not, try just artist/title
155-
if not song:
214+
if song is None:
156215
log.debug(u'no album match, trying by artist/title')
157216
query = dbcore.AndQuery([
158217
dbcore.query.SubstringQuery('artist', artist),
@@ -161,7 +220,7 @@ def process_tracks(lib, tracks, log):
161220
song = lib.items(query).get()
162221

163222
# Last resort, try just replacing to utf-8 quote
164-
if not song:
223+
if song is None:
165224
title = title.replace("'", u'\u2019')
166225
log.debug(u'no title match, trying utf-8 single quote')
167226
query = dbcore.AndQuery([
@@ -170,7 +229,7 @@ def process_tracks(lib, tracks, log):
170229
])
171230
song = lib.items(query).get()
172231

173-
if song:
232+
if song is not None:
174233
count = int(song.get('play_count', 0))
175234
new_count = int(tracks[num]['playcount'])
176235
log.debug(u'match: {0} - {1} ({2}) '

docs/changelog.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ Fixes:
2929
anymore. :bug:`1583`
3030
* :doc:`/plugins/play`: Fix a regression in the last version where there was
3131
no default command. :bug:`1793`
32+
* :doc:`/plugins/lastimport`: Switched API method from library.getTracks to
33+
user.getTopTracks. This fixes :bug:`1574`, which was caused by the former API
34+
method being removed. Also moved from custom HTTP requests to using pylast
35+
library.
3236

3337

3438
1.3.16 (December 28, 2015)

docs/plugins/lastimport.rst

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,25 @@ library into beets' database. You can later create :doc:`smart playlists
66
</plugins/smartplaylist>` by querying ``play_count`` and do other fun stuff
77
with this field.
88

9+
.. _Last.fm: http://last.fm
10+
911
Installation
1012
------------
1113

12-
To use the ``lastimport`` plugin, first enable it in your configuration (see
13-
:ref:`using-plugins`). Then install the `requests`_ library by typing::
14+
The plugin requires `pylast`_, which you can install using `pip`_ by typing::
1415

15-
pip install requests
16+
pip install pylast
17+
18+
After you have pylast installed, enable the ``lastimport`` plugin in your
19+
configuration (see :ref:`using-plugins`).
1620

1721
Next, add your Last.fm username to your beets configuration file::
1822

1923
lastfm:
2024
user: beetsfanatic
2125

22-
.. _requests: http://docs.python-requests.org/en/latest/
23-
.. _Last.fm: http://last.fm
26+
.. _pip: http://www.pip-installer.org/
27+
.. _pylast: http://code.google.com/p/pylast/
2428

2529
Importing Play Counts
2630
---------------------

0 commit comments

Comments
 (0)