Skip to content

Commit

Permalink
Merge pull request #1797 from sampsyo/lastimport-fix-1574
Browse files Browse the repository at this point in the history
Fix lastimport #1574 (LastFM API change)
  • Loading branch information
sampsyo committed Jan 4, 2016
2 parents a218da1 + ed3d6b8 commit b712c49
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 40 deletions.
129 changes: 94 additions & 35 deletions beetsplug/lastimport.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
from __future__ import (division, absolute_import, print_function,
unicode_literals)

import requests
import pylast
from pylast import TopItem, _extract, _number
from beets import ui
from beets import dbcore
from beets import config
Expand Down Expand Up @@ -52,6 +53,63 @@ def func(lib, opts, args):
return [cmd]


class CustomUser(pylast.User):
""" Custom user class derived from pylast.User, and overriding the
_get_things method to return MBID and album. Also introduces new
get_top_tracks_by_page method to allow access to more than one page of top
tracks.
"""
def __init__(self, *args, **kwargs):
super(CustomUser, self).__init__(*args, **kwargs)

def _get_things(self, method, thing, thing_type, params=None,
cacheable=True):
"""Returns a list of the most played thing_types by this thing, in a
tuple with the total number of pages of results. Includes an MBID, if
found.
"""
doc = self._request(
self.ws_prefix + "." + method, cacheable, params)

toptracks_node = doc.getElementsByTagName('toptracks')[0]
total_pages = int(toptracks_node.getAttribute('totalPages'))

seq = []
for node in doc.getElementsByTagName(thing):
title = _extract(node, "name")
artist = _extract(node, "name", 1)
mbid = _extract(node, "mbid")
playcount = _number(_extract(node, "playcount"))

thing = thing_type(artist, title, self.network)
thing.mbid = mbid
seq.append(TopItem(thing, playcount))

return seq, total_pages

def get_top_tracks_by_page(self, period=pylast.PERIOD_OVERALL, limit=None,
page=1, cacheable=True):
"""Returns the top tracks played by a user, in a tuple with the total
number of pages of results.
* period: The period of time. Possible values:
o PERIOD_OVERALL
o PERIOD_7DAYS
o PERIOD_1MONTH
o PERIOD_3MONTHS
o PERIOD_6MONTHS
o PERIOD_12MONTHS
"""

params = self._get_params()
params['period'] = period
params['page'] = page
if limit:
params['limit'] = limit

return self._get_things(
"getTopTracks", "track", pylast.Track, params, cacheable)


def import_lastfm(lib, log):
user = config['lastfm']['user'].get(unicode)
per_page = config['lastimport']['per_page'].get(int)
Expand All @@ -73,23 +131,19 @@ def import_lastfm(lib, log):
'/{}'.format(page_total) if page_total > 1 else '')

for retry in range(0, retry_limit):
page = fetch_tracks(user, page_current + 1, per_page)
if 'tracks' in page:
# Let us the reveal the holy total pages!
page_total = int(page['tracks']['@attr']['totalPages'])
if page_total < 1:
# It means nothing to us!
raise ui.UserError('Last.fm reported no data.')

track = page['tracks']['track']
found, unknown = process_tracks(lib, track, log)
tracks, page_total = fetch_tracks(user, page_current + 1, per_page)
if page_total < 1:
# It means nothing to us!
raise ui.UserError('Last.fm reported no data.')

if tracks:
found, unknown = process_tracks(lib, tracks, log)
found_total += found
unknown_total += unknown
break
else:
log.error('ERROR: unable to read page #{0}',
page_current + 1)
log.debug('API response: {}', page)
if retry < retry_limit:
log.info(
'Retrying page #{0}... ({1}/{2} retry)',
Expand All @@ -107,14 +161,30 @@ def import_lastfm(lib, log):


def fetch_tracks(user, page, limit):
return requests.get(API_URL, params={
'method': 'library.gettracks',
'user': user,
'api_key': plugins.LASTFM_KEY,
'page': bytes(page),
'limit': bytes(limit),
'format': 'json',
}).json()
""" JSON format:
[
{
"mbid": "...",
"artist": "...",
"title": "...",
"playcount": "..."
}
]
"""
network = pylast.LastFMNetwork(api_key=config['lastfm']['api_key'])
user_obj = CustomUser(user, network)
results, total_pages =\
user_obj.get_top_tracks_by_page(limit=limit, page=page)
return [
{
"mbid": track.item.mbid if track.item.mbid else '',
"artist": {
"name": track.item.artist.name
},
"name": track.item.title,
"playcount": track.weight
} for track in results
], total_pages


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

for num in xrange(0, total):
song = ''
song = None
trackid = tracks[num]['mbid'].strip()
artist = tracks[num]['artist'].get('name', '').strip()
title = tracks[num]['name'].strip()
Expand All @@ -140,19 +210,8 @@ def process_tracks(lib, tracks, log):
dbcore.query.MatchQuery('mb_trackid', trackid)
).get()

# Otherwise try artist/title/album
if not song:
log.debug(u'no match for mb_trackid {0}, trying by '
u'artist/title/album', trackid)
query = dbcore.AndQuery([
dbcore.query.SubstringQuery('artist', artist),
dbcore.query.SubstringQuery('title', title),
dbcore.query.SubstringQuery('album', album)
])
song = lib.items(query).get()

# If not, try just artist/title
if not song:
if song is None:
log.debug(u'no album match, trying by artist/title')
query = dbcore.AndQuery([
dbcore.query.SubstringQuery('artist', artist),
Expand All @@ -161,7 +220,7 @@ def process_tracks(lib, tracks, log):
song = lib.items(query).get()

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

if song:
if song is not None:
count = int(song.get('play_count', 0))
new_count = int(tracks[num]['playcount'])
log.debug(u'match: {0} - {1} ({2}) '
Expand Down
4 changes: 4 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ Fixes:
anymore. :bug:`1583`
* :doc:`/plugins/play`: Fix a regression in the last version where there was
no default command. :bug:`1793`
* :doc:`/plugins/lastimport`: Switched API method from library.getTracks to
user.getTopTracks. This fixes :bug:`1574`, which was caused by the former API
method being removed. Also moved from custom HTTP requests to using pylast
library.


1.3.16 (December 28, 2015)
Expand Down
14 changes: 9 additions & 5 deletions docs/plugins/lastimport.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,25 @@ library into beets' database. You can later create :doc:`smart playlists
</plugins/smartplaylist>` by querying ``play_count`` and do other fun stuff
with this field.

.. _Last.fm: http://last.fm

Installation
------------

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

pip install requests
pip install pylast

After you have pylast installed, enable the ``lastimport`` plugin in your
configuration (see :ref:`using-plugins`).

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

lastfm:
user: beetsfanatic

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

Importing Play Counts
---------------------
Expand Down

0 comments on commit b712c49

Please sign in to comment.