LastFM: major code cleanup and refactoring

- 'lastfm 'command is now split into subcommands, wrapping around one shared function (Closes #2).
- Use bolding for prettier formatting in output.
- Update command help/documentation to be more consistent (use "user", not "id" to refer to configured users).
- Rename variables named 'id' -> 'user', since the previous is collides with a built in function.
- Update tests accordingly.
This commit is contained in:
GLolol 2015-02-04 20:26:28 -08:00
parent d6f0b5f4b6
commit fd5ae25adf
2 changed files with 148 additions and 90 deletions

203
plugin.py
View File

@ -49,7 +49,6 @@ except ImportError:
from .LastFMDB import * from .LastFMDB import *
class LastFMParser: class LastFMParser:
def parseRecentTracks(self, stream): def parseRecentTracks(self, stream):
""" """
<stream> <stream>
@ -80,6 +79,7 @@ class LastFMParser:
class LastFM(callbacks.Plugin): class LastFM(callbacks.Plugin):
threaded = True threaded = True
def __init__(self, irc): def __init__(self, irc):
self.__parent = super(LastFM, self) self.__parent = super(LastFM, self)
self.__parent.__init__(irc) self.__parent.__init__(irc)
@ -96,178 +96,235 @@ class LastFM(callbacks.Plugin):
self.db.close() self.db.close()
self.__parent.die() self.__parent.die()
def lastfm(self, irc, msg, args, method, optionalId): def lastfm(self, irc, msg, args, method, user):
"""<method> [<id>] """<method> [<user>]
Lists LastFM info where <method> is in Lists LastFM info where <method> is in
[friends, neighbours, profile, recenttracks, tags, topalbums, [friends, neighbours, profile, recenttracks, tags, topalbums,
topartists, toptracks]. topartists, toptracks].
Set your LastFM ID with the set method (default is your current nick)
or specify <id> to switch for one call.
""" """
if not self.apiKey: if not self.apiKey:
irc.error("The API Key is not set for this plugin. Please set it via" irc.error("The API Key is not set. Please set it via "
"config plugins.lastfm.apikey and reload the plugin. " "'config plugins.lastfm.apikey' and reload the plugin. "
"You can sign up for an API Key using " "You can sign up for an API Key using "
"http://www.last.fm/api/account/create", Raise=True) "http://www.last.fm/api/account/create", Raise=True)
method = method.lower()
knownMethods = {'friends': 'user.getFriends', knownMethods = {'friends': 'user.getFriends',
'neighbours': 'user.getNeighbours', 'neighbours': 'user.getNeighbours',
'profile': 'user.getInfo',
'recenttracks': 'user.getRecentTracks',
'tags': 'user.getTopTags', 'tags': 'user.getTopTags',
'topalbums': 'user.getTopAlbums', 'topalbums': 'user.getTopAlbums',
'topartists': 'user.getTopArtists', 'topartists': 'user.getTopArtists',
'toptracks': 'user.getTopTracks'} 'toptracks': 'user.getTopTracks',
if method not in knownMethods: 'recenttracks': 'user.getRecentTracks'}
irc.error("Unsupported method '%s'" % method, Raise=True) user = (user or self.db.getId(msg.nick) or msg.nick)
id = (optionalId or self.db.getId(msg.nick) or msg.nick)
channel = msg.args[0] channel = msg.args[0]
maxResults = self.registryValue("maxResults", channel) maxResults = self.registryValue("maxResults", channel)
url = "%sapi_key=%s&method=%s&user=%s" % (self.APIURL, url = "%sapi_key=%s&method=%s&user=%s" % (self.APIURL,
self.apiKey, knownMethods[method], id) self.apiKey, knownMethods[method], user)
try: try:
f = utils.web.getUrlFd(url) f = utils.web.getUrlFd(url)
except utils.web.Error: except utils.web.Error:
irc.error("Unknown ID (%s) or unknown method (%s)" irc.error("Unknown user '%s'." % user, Raise=True)
% (msg.nick, method), Raise=True)
xml = minidom.parse(f).getElementsByTagName("lfm")[0] xml = minidom.parse(f).getElementsByTagName("lfm")[0]
# Grab a list of item names
content = xml.childNodes[1].getElementsByTagName("name") content = xml.childNodes[1].getElementsByTagName("name")
results = [res.firstChild.nodeValue.strip() for res in content[0:maxResults*2]] # Fetch their values, strip leading/trailing spaces, and add bolding
if method in ('topalbums', 'toptracks'): results = [ircutils.bold(res.firstChild.nodeValue.strip()) for res in
content[0:maxResults*2]]
if method in ('topalbums', 'toptracks', 'recenttracks'):
# Annoying, hackish way of grouping artist+album/track items # Annoying, hackish way of grouping artist+album/track items
results = ["%s - %s" % (thing, artist) for thing, artist in izip(results[1::2], results[::2])] results = ["%s - %s" % (thing, artist) for thing, artist in
izip(results[1::2], results[::2])]
if len(content) < 1:
irc.error("%s doesn't seem to have any %s on LastFM." % (user,
method), Raise=True)
irc.reply("%s's %s: %s (with a total number of %i entries)" irc.reply("%s's %s: %s (with a total number of %i entries)"
% (id, method, ", ".join(results[0:maxResults]), % (ircutils.bold(user), method,
len(content))) ", ".join(results[0:maxResults]), len(content)))
lastfm = wrap(lastfm, ["something", optional("something")]) @wrap([additional("something")])
def friends(self, irc, msg, args, user):
"""[<user>]
Shows friends for <user>. If <user> is not given, defaults
to the LastFM user configured for your current nick."""
self.lastfm(irc, msg, args, 'friends', user)
@wrap([additional("something")])
def neighbours(self, irc, msg, args, user):
"""[<user>]
Shows friends for <user>. If <user> is not given, defaults
to the LastFM user configured for your current nick."""
self.lastfm(irc, msg, args, 'neighbours', user)
@wrap([additional("something")])
def toptags(self, irc, msg, args, user):
"""[<user>]
Shows the top tags for <user>. If <user> is not given, defaults
to the LastFM user configured for your current nick."""
self.lastfm(irc, msg, args, 'tags', user)
@wrap([additional("something")])
def topalbums(self, irc, msg, args, user):
"""[<user>]
Shows the top albums for <user>. If <user> is not given, defaults
to the LastFM user configured for your current nick."""
self.lastfm(irc, msg, args, 'topalbums', user)
@wrap([additional("something")])
def toptracks(self, irc, msg, args, user):
"""[<user>]
Shows the top tracks for <user>. If <user> is not given, defaults
to the LastFM user configured for your current nick."""
self.lastfm(irc, msg, args, 'toptracks', user)
@wrap([additional("something")])
def topartists(self, irc, msg, args, user):
"""[<user>]
Shows the top artists for <user>. If <user> is not given, defaults
to the LastFM user configured for your current nick."""
self.lastfm(irc, msg, args, 'topartists', user)
@wrap([additional("something")])
def recenttracks(self, irc, msg, args, user):
"""[<user>]
Shows the recent tracks for <user>. If <user> is not given, defaults
to the LastFM user configured for your current nick."""
self.lastfm(irc, msg, args, 'recenttracks', user)
def nowPlaying(self, irc, msg, args, optionalId): def nowPlaying(self, irc, msg, args, optionalId):
"""[<id>] """[<user>]
Announces the now playing track of the specified LastFM ID. Announces the track currently being played by <user>. If <user>
Set your LastFM ID with the set method (default is your current nick) is not given, defaults to the LastFM user configured for your
or specify <id> to switch for one call. current nick.
""" """
if not self.apiKey: if not self.apiKey:
irc.error("The API Key is not set for this plugin. Please set it via" irc.error("The API Key is not set. Please set it via "
"config plugins.lastfm.apikey and reload the plugin. " "'config plugins.lastfm.apikey' and reload the plugin. "
"You can sign up for an API Key using " "You can sign up for an API Key using "
"http://www.last.fm/api/account/create", Raise=True) "http://www.last.fm/api/account/create", Raise=True)
id = (optionalId or self.db.getId(msg.nick) or msg.nick) user = (optionalId or self.db.getId(msg.nick) or msg.nick)
# see http://www.lastfm.de/api/show/user.getrecenttracks # see http://www.lastfm.de/api/show/user.getrecenttracks
url = "%sapi_key=%s&method=user.getrecenttracks&user=%s" % (self.APIURL, self.apiKey, id) url = "%sapi_key=%s&method=user.getrecenttracks&user=%s" % (self.APIURL, self.apiKey, user)
try: try:
f = utils.web.getUrlFd(url) f = utils.web.getUrlFd(url)
except utils.web.Error: except utils.web.Error:
irc.error("Unknown ID (%s)" % id, Raise=True) irc.error("Unknown user '%s'." % user, Raise=True)
parser = LastFMParser() parser = LastFMParser()
(user, isNowPlaying, artist, track, album, time) = parser.parseRecentTracks(f) (user, isNowPlaying, artist, track, album, time) = parser.parseRecentTracks(f)
if track is None: if track is None:
irc.reply("%s doesn't seem to have listened to anything." % id) irc.reply("%s doesn't seem to have listened to anything." % user)
return return
albumStr = ("[%s]" % album) if album else "" albumStr = ("[%s]" % album) if album else ""
track, artist, albumStr = map(ircutils.bold, (track, artist, albumStr))
if isNowPlaying: if isNowPlaying:
irc.reply('%s is listening to "%s" by %s %s' irc.reply('%s is listening to %s by %s %s'
% (user, track, artist, albumStr)) % (user, track, artist, albumStr))
else: else:
irc.reply('%s listened to "%s" by %s %s more than %s' irc.reply('%s listened to %s by %s %s more than %s'
% (user, track, artist, albumStr, % (user, track, artist, albumStr,
self._formatTimeago(time))) self._formatTimeago(time)))
np = wrap(nowPlaying, [optional("something")]) np = wrap(nowPlaying, [optional("something")])
def setUserId(self, irc, msg, args, newId): def setUserId(self, irc, msg, args, newId):
"""<id> """<user>
Sets the LastFM ID for the caller and saves it in a database. Sets the LastFM username for the caller and saves it in a database.
""" """
self.db.set(msg.nick, newId) self.db.set(msg.nick, newId)
irc.reply("LastFM ID changed.") irc.replySuccess()
set = wrap(setUserId, ["something"]) set = wrap(setUserId, ["something"])
def profile(self, irc, msg, args, optionalId): def profile(self, irc, msg, args, optionalId):
"""[<id>] """[<user>]
Prints the profile info for the specified LastFM ID. Prints the profile info for the specified LastFM user. If <user>
Set your LastFM ID with the set method (default is your current nick) is not given, defaults to the LastFM user configured for your
or specify <id> to switch for one call. current nick.
""" """
if not self.apiKey: if not self.apiKey:
irc.error("The API Key is not set for this plugin. Please set it via" irc.error("The API Key is not set. Please set it via "
"config plugins.lastfm.apikey and reload the plugin. " "'config plugins.lastfm.apikey' and reload the plugin. "
"You can sign up for an API Key using " "You can sign up for an API Key using "
"http://www.last.fm/api/account/create", Raise=True) "http://www.last.fm/api/account/create", Raise=True)
id = (optionalId or self.db.getId(msg.nick) or msg.nick) user = (optionalId or self.db.getId(msg.nick) or msg.nick)
url = "%sapi_key=%s&method=user.getInfo&user=%s" % (self.APIURL, self.apiKey, id) url = "%sapi_key=%s&method=user.getInfo&user=%s" % (self.APIURL, self.apiKey, user)
try: try:
f = utils.web.getUrlFd(url) f = utils.web.getUrlFd(url)
except utils.web.Error: except utils.web.Error:
irc.error("Unknown user (%s)" % id, Raise=True) irc.error("Unknown user '%s'." % user, Raise=True)
xml = minidom.parse(f).getElementsByTagName("user")[0] xml = minidom.parse(f).getElementsByTagName("user")[0]
keys = ("realname", "registered", "age", "gender", "country", "playcount") keys = ("realname", "registered", "age", "gender", "country", "playcount")
profile = {"id": id} profile = {"id": ircutils.bold(user)}
for tag in keys: for tag in keys:
try: try:
profile[tag] = xml.getElementsByTagName(tag)[0].firstChild.data.strip() profile[tag] = ircutils.bold(xml.getElementsByTagName(tag)[0].firstChild.data.strip())
except AttributeError: # empty field except AttributeError: # empty field
profile[tag] = 'unknown' profile[tag] = ircutils.bold('unknown')
irc.reply(("%(id)s (realname: %(realname)s) registered on %(registered)s; age: %(age)s / %(gender)s; " irc.reply(("%(id)s (realname: %(realname)s) registered on %(registered)s; age: %(age)s / %(gender)s; "
"Country: %(country)s; Tracks played: %(playcount)s") % profile) "Country: %(country)s; Tracks played: %(playcount)s") % profile)
profile = wrap(profile, [optional("something")]) profile = wrap(profile, [optional("something")])
def compareUsers(self, irc, msg, args, user1, optionalUser2): def compareUsers(self, irc, msg, args, user1, optionalUser2):
"""user1 [<user2>] """<user1> [<user2>]
Compares the taste from two users Compares the music tastes of <user1> and <user2>. If <user2>
If <user2> is ommitted, the taste is compared against the ID of the calling user. is not given, defaults to the LastFM user configured for your
current nick.
""" """
if not self.apiKey: if not self.apiKey:
irc.error("The API Key is not set for this plugin. Please set it via" irc.error("The API Key is not set. Please set it via "
"config plugins.lastfm.apikey and reload the plugin. " "'config plugins.lastfm.apikey' and reload the plugin. "
"You can sign up for an API Key using " "You can sign up for an API Key using "
"http://www.last.fm/api/account/create", Raise=True) "http://www.last.fm/api/account/create", Raise=True)
user2 = (optionalUser2 or self.db.getId(msg.nick) or msg.nick) user2 = (optionalUser2 or self.db.getId(msg.nick) or msg.nick)
channel = msg.args[0] channel = msg.args[0]
maxResults = self.registryValue("maxResults", channel) maxResults = self.registryValue("maxResults", channel)
# see http://www.lastfm.de/api/show/tasteometer.compare url = ("%sapi_key=%s&method=tasteometer.compare&type1=user&type2=user"
url = "%sapi_key=%s&method=tasteometer.compare&type1=user&type2=user&value1=%s&value2=%s&limit=%s" % ( "&value1=%s&value2=%s&limit=%s" % (self.APIURL, self.apiKey,
self.APIURL, self.apiKey, user1, user2, maxResults) user1, user2, maxResults))
try: try:
f = utils.web.getUrlFd(url) f = utils.web.getUrlFd(url)
except utils.web.Error as e: except utils.web.Error as e:
irc.error("Failure: %s" % (e), Raise=True) irc.error(str(e), Raise=True)
xml = minidom.parse(f) xml = minidom.parse(f)
resultNode = xml.getElementsByTagName("result")[0] resultNode = xml.getElementsByTagName("result")[0]
score = float(self._parse(resultNode, "score")) try:
scoreStr = "%s (%s)" % (round(score, 2), self._formatRating(score)) score = resultNode.getElementsByTagName('score')[0].firstChild.data
# Note: XPath would be really cool here... score = round(float(score), 3)
artists = [el for el in resultNode.getElementsByTagName("artist")] except (IndexError, ValueError):
artistNames = [el.getElementsByTagName("name")[0].firstChild.data for el in artists] scoreStr = "unknown"
irc.reply("Result of comparison between %s and %s: score: %s, common artists: %s" \ else:
% (user1, user2, scoreStr, ", ".join(artistNames))) scoreStr = "%s (%s)" % (ircutils.bold(self._formatRating(score)),
score)
artists = resultNode.getElementsByTagName("artist")
artistNames = [ircutils.bold(el.getElementsByTagName("name")[0].firstChild.data)
for el in artists]
s = ("Result of comparison between %s and %s: score: %s, common "
"artists: %s" % (ircutils.bold(user1), ircutils.bold(user2),
scoreStr, ", ".join(artistNames)))
irc.reply(s)
compare = wrap(compareUsers, ["something", optional("something")]) compare = wrap(compareUsers, ["something", optional("something")])
def _parse(self, node, tagName, exceptMsg="not specified"):
try:
return node.getElementsByTagName(tagName)[0].firstChild.data
except IndexError:
return exceptMsg
def _formatTimeago(self, unixtime): def _formatTimeago(self, unixtime):
t = int(time()-unixtime) t = int(time()-unixtime)
if t/86400 >= 1: if t/86400 >= 1:

35
test.py
View File

@ -35,6 +35,7 @@ try:
from StringIO import StringIO from StringIO import StringIO
except ImportError: except ImportError:
from io import StringIO from io import StringIO
import os
class LastFMTestCase(PluginTestCase): class LastFMTestCase(PluginTestCase):
plugins = ('LastFM',) plugins = ('LastFM',)
@ -44,31 +45,31 @@ class LastFMTestCase(PluginTestCase):
apiKey = os.environ.get('lastfm_apikey') apiKey = os.environ.get('lastfm_apikey')
if not apiKey: if not apiKey:
e = ("The LastFM API key has not been set. " e = ("The LastFM API key has not been set. "
"Please set the environment variable 'lastfm_apikey' " "Please set the environment variable 'lastfm_apikey' "
"and try again. ('export lastfm_apikey=<apikey>' for those " "and try again.")
"using bash)")
raise callbacks.Error(e) raise callbacks.Error(e)
conf.supybot.plugins.LastFM.apiKey.setValue(apiKey) conf.supybot.plugins.LastFM.apiKey.setValue(apiKey)
def testLastfm(self): def testRecentTracks(self):
self.assertNotError("lastfm recenttracks") self.assertNotError("recenttracks")
self.assertError("lastfm TESTEXCEPTION") self.assertNotError("recenttracks czshadow")
self.assertNotError("lastfm recenttracks czshadow")
self.assertNotError("lastfm np krf") def testNowPlaying(self):
self.assertNotError("np krf")
def testLastfmDB(self): def testLastfmDB(self):
self.assertNotError("lastfm set nick") # test db self.assertNotError("set nick") # test db
self.assertNotError("lastfm set test") # test db unset self.assertNotError("set test") # test db unset
def testLastfmProfile(self): def testProfile(self):
self.assertNotError("lastfm profile czshadow") self.assertNotError("profile czshadow")
self.assertNotError("lastfm profile test") self.assertNotError("profile test")
def testLastfmCompare(self): def testCompare(self):
self.assertNotError("lastfm compare krf czshadow") self.assertNotError("compare krf czshadow")
self.assertNotError("lastfm compare krf") self.assertNotError("compare krf")
def testLastFMParseRecentTracks(self): def testParseRecentTracks(self):
"""Parser tests""" """Parser tests"""
# noalbum, nowplaying # noalbum, nowplaying