16
16
from __future__ import (division , absolute_import , print_function ,
17
17
unicode_literals )
18
18
19
- import requests
19
+ import pylast
20
+ from pylast import TopItem , _extract , _number
20
21
from beets import ui
21
22
from beets import dbcore
22
23
from beets import config
@@ -52,6 +53,63 @@ def func(lib, opts, args):
52
53
return [cmd ]
53
54
54
55
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
+
55
113
def import_lastfm (lib , log ):
56
114
user = config ['lastfm' ]['user' ].get (unicode )
57
115
per_page = config ['lastimport' ]['per_page' ].get (int )
@@ -73,23 +131,19 @@ def import_lastfm(lib, log):
73
131
'/{}' .format (page_total ) if page_total > 1 else '' )
74
132
75
133
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 )
86
141
found_total += found
87
142
unknown_total += unknown
88
143
break
89
144
else :
90
145
log .error ('ERROR: unable to read page #{0}' ,
91
146
page_current + 1 )
92
- log .debug ('API response: {}' , page )
93
147
if retry < retry_limit :
94
148
log .info (
95
149
'Retrying page #{0}... ({1}/{2} retry)' ,
@@ -107,14 +161,30 @@ def import_lastfm(lib, log):
107
161
108
162
109
163
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
118
188
119
189
120
190
def process_tracks (lib , tracks , log ):
@@ -124,7 +194,7 @@ def process_tracks(lib, tracks, log):
124
194
log .info ('Received {0} tracks in this page, processing...' , total )
125
195
126
196
for num in xrange (0 , total ):
127
- song = ''
197
+ song = None
128
198
trackid = tracks [num ]['mbid' ].strip ()
129
199
artist = tracks [num ]['artist' ].get ('name' , '' ).strip ()
130
200
title = tracks [num ]['name' ].strip ()
@@ -140,19 +210,8 @@ def process_tracks(lib, tracks, log):
140
210
dbcore .query .MatchQuery ('mb_trackid' , trackid )
141
211
).get ()
142
212
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
-
154
213
# If not, try just artist/title
155
- if not song :
214
+ if song is None :
156
215
log .debug (u'no album match, trying by artist/title' )
157
216
query = dbcore .AndQuery ([
158
217
dbcore .query .SubstringQuery ('artist' , artist ),
@@ -161,7 +220,7 @@ def process_tracks(lib, tracks, log):
161
220
song = lib .items (query ).get ()
162
221
163
222
# Last resort, try just replacing to utf-8 quote
164
- if not song :
223
+ if song is None :
165
224
title = title .replace ("'" , u'\u2019 ' )
166
225
log .debug (u'no title match, trying utf-8 single quote' )
167
226
query = dbcore .AndQuery ([
@@ -170,7 +229,7 @@ def process_tracks(lib, tracks, log):
170
229
])
171
230
song = lib .items (query ).get ()
172
231
173
- if song :
232
+ if song is not None :
174
233
count = int (song .get ('play_count' , 0 ))
175
234
new_count = int (tracks [num ]['playcount' ])
176
235
log .debug (u'match: {0} - {1} ({2}) '
0 commit comments