=== modified file 'plugins/contextinfo/__init__.py' --- plugins/contextinfo/__init__.py 2009-12-30 19:50:09 +0000 +++ plugins/contextinfo/__init__.py 2010-01-19 08:01:22 +0000 @@ -2,19 +2,25 @@ from xl.nls import gettext as _ from xlgui import panel, guiutil, playlist, menu import HTMLParser +from StringIO import StringIO import Image import base64 import gobject import gtk import os import pylast +import contextprefs import re import urllib import webkit import xlgui from inspector import Inspector -LFM_API_KEY = '3b954460f7b207e5414ffdf8c5710592' +# Last.fm API Key for Exaile +# if you reuse this code in a different application, please +# register your own key with last.fm +LFM_API_KEY = "3599c79a97fd61ce518b75922688bc38" + PANEL = None CURPATH = os.path.realpath(__file__) BASEDIR = os.path.dirname(CURPATH)+os.path.sep @@ -32,7 +38,7 @@ selected = self.widget.get_selected_tracks() pl = xlgui.controller().main.get_selected_playlist() if pl: - pl.playlist.add_tracks(selected, add_duplicates=False) + pl.playlist.add_tracks(selected) def on_queue(self, selected=None): """ @@ -40,9 +46,9 @@ """ selected = self.widget.get_selected_tracks() pl = xlgui.controller().main.get_selected_playlist() - ex.exaile().queue.add_tracks(selected, add_duplicates=False) + ex.exaile().queue.add_tracks(selected) if pl: - pl.playlist.add_tracks(selected, add_duplicates=False) + pl.playlist.add_tracks(selected) pl.list.queue_draw() class BrowserPage(webkit.WebView, providers.ProviderHandler): @@ -291,7 +297,7 @@ selected = self.get_selected_tracks() pl = xlgui.controller().main.get_selected_playlist() if pl: - pl.playlist.add_tracks(selected, add_duplicates=False) + pl.playlist.add_tracks(selected) def _navigation_requested_cb(self, view, frame, networkRequest): link = self.un_url(networkRequest.get_uri()).split('://', 1) @@ -334,19 +340,6 @@ ex.exaile().gui.panels['collection'].filter.set_text('artist=%s' % self.hover.split('://')[1]) ex.exaile().gui.panel_notebook.set_current_page(0) -class ImageBuffer(object): - def __init__(self): - self.buffer = "" - - def write(self, str): - self.buffer+=str - - def read(self): - return self.buffer - - def get_base64(self): - return base64.b64encode(self.buffer) - class ContextTheme(object): LOCAL_COLORS = None @@ -403,12 +396,23 @@ else: return None +class ImageBuffer(object): + def __init__(self): + self.buffer = "" + + def write(self, str): + self.buffer+=str + + def get_base64(self): + return base64.b64encode(self.buffer) + def get_image_data(data, size): imbuff = ImageBuffer() - imbuff.write(data) - im = Image.open(imbuff) + try: + im = Image.open(StringIO(data)) + except Exception: + im = Image.open(xdg.get_data_path('images/nocover.png')) im = im.resize(size, Image.ANTIALIAS) - imbuff = ImageBuffer() im.save(imbuff, "PNG") return 'data:image/png;base64,%s' % imbuff.get_base64() @@ -588,6 +592,7 @@ track_nbr = len(ex.exaile().collection.search('album=="%s"'% cd, tracks=tracks)) cover = get_track_cover(tr) cover_data = get_image_data(cover, (60, 60)) + html+='''\ \ %s
%s
\ @@ -658,10 +663,14 @@ class DefaultPage(ContextPage): def __init__(self, theme, base='default://', template='default.html', async=[]): + self.user = None try: - self.username = settings.get_option('plugin/ascrobbler/user') + self.username = settings.get_option('plugin/lastfm/user') + self.password_hash = pylast.md5(settings.get_option('plugin/lastfm/password')) except: self.username = None + self.password_hash = None + ContextPage.__init__(self, theme, base, template, async+['last-played-tracks', 'last-played-artists', 'last-added-tracks', 'last-added-artists', 'most-played-tracks', 'most-played-artists', 'lfm-last-played', 'lfm-top-tracks', 'lfm-top-albums', 'lfm-top-artists']) def _last_played_tracks_title(self): @@ -733,39 +742,44 @@ artists = get_top_artists('__playcount', int(limit)) return ', '.join(self.get_artist_anchor(artist) for artist in artists) + def get_lfm_user(self): + if not self.user: + self.user = pylast.User(self.username, self.password_hash, pylast.network) + return self.user + def _lfm_last_played_title(self): return "Last Scrobbled Tracks" def _lfm_last_played(self, limit=10): - if self.username: - tracks = pylast.User(self.username, LFM_API_KEY, None, None).get_recent_tracks(int(limit)) - return '
'.join(self.get_track_anchor_from_artist_title(tr.get_track().get_artist(), tr.get_track().get_title()) for tr in tracks) + if get_lfm_user(): + tracks = self.get_lfm_user().get_recent_tracks(int(limit)) + return '
'.join(self.get_track_anchor_from_artist_title(tr.track.get_artist(), tr.track.get_title()) for tr in tracks) return "Enter your username in the settings" def _lfm_top_tracks_title(self): return "Your Top Tracks on Last.fm" def _lfm_top_tracks(self, period='overall', limit=15): - if self.username: - tracks = pylast.User(self.username, LFM_API_KEY, None, None).get_top_tracks(period)[:int(limit)] - return '
'.join(self.get_track_anchor_from_artist_title(tr.get_item().get_artist(), tr.get_item().get_title()) for tr in tracks) + if self.get_lfm_user(): + tracks = self.get_lfm_user().get_top_tracks(period, int(limit)) + return '
'.join(self.get_track_anchor_from_artist_title(tr.track.get_artist(), tr.track.get_title()) for tr in tracks) return "Enter your username in the settings" def _lfm_top_artists_title(self): return "Your Top Artists on Last.fm" def _lfm_top_artists(self, period='overall', limit=20): - if self.username: - artists = pylast.User(self.username, LFM_API_KEY, None, None).get_top_artists(period)[:int(limit)] - return ', '.join(self.get_artist_anchor(artist.get_item().get_name()) for artist in artists) + if self.get_lfm_user(): + artists = self.get_lfm_user().get_top_artists(period,int(limit)) + return ', '.join(self.get_artist_anchor(artist.item.get_name()) for artist in artists) return "Enter your username in the settings" def _lfm_top_albums_title(self): return "Your Top Albums on Last.fm" def _lfm_top_albums(self, period='overall', limit=10): - if self.username: - cds = [album.get_item().get_title() for album in pylast.User(self.username, LFM_API_KEY, None, None).get_top_albums(period)[:int(limit)]] + if self.get_lfm_user(): + cds = [album.get_item().get_title() for album in self.get_lfm_user().get_top_albums(period,int(limit))] tracks = [] if len(cds)>0: for cd in cds: @@ -777,9 +791,15 @@ class ArtistPage(DefaultPage): def __init__(self, theme, artist, base = 'artist://', template ='artist.html', async=[]): self.artist = artist - self.artist_tracks = get_artist_tracks(artist) + self.lfm_artist = None + DefaultPage.__init__(self, theme, base, template, async+['compils', 'albums', 'artist-info', 'artist-img', 'artist-tags', 'similar-artists', 'top-tracks']) + def get_lfm_artist(self): + if not self.lfm_artist: + self.lfm_artist = pylast.Artist(self.artist, pylast.network) + return self.lfm_artist + def get_template_fields(self): fields = ContextPage.get_template_fields(self) for field in fields: @@ -810,30 +830,36 @@ def _artist(self): return self.artist - def _artist_img(self, size=pylast.IMAGE_LARGE): + def _artist_img(self, size=pylast.COVER_LARGE): try: - url = pylast.search_for_artist(self.artist, LFM_API_KEY, None, None).get_next_page()[0].get_image_url(size) + url = self.get_lfm_artist().get_cover_image(size) return '' % url except: return '' def _artist_info(self): - bio = pylast.search_for_artist(self.artist, LFM_API_KEY, None, None).get_next_page()[0].get_bio_summary() - return self.LFMInfoParser(self, str(bio), self['artist']).data + if self.get_lfm_artist(): + bio = self.get_lfm_artist().get_bio_summary() + return self.LFMInfoParser(self, str(bio), self['artist']).data + return '' def _top_tracks_title(self): return 'Top tracks by %s' % self['artist'] def _top_tracks(self, limit=10): - doc = pylast.search_for_artist(self['artist'],LFM_API_KEY, None, None).get_next_page()[0].get_top_tracks()[:int(limit)] - return '
'.join(self.get_track_anchor_from_artist_title(self['artist'], tr.get_item().get_title(), img=True) for tr in doc) + if self.get_lfm_artist(): + doc = self.get_lfm_artist().get_top_tracks(int(limit)) + return '
'.join(self.get_track_anchor_from_artist_title(self['artist'], tr.get_item().get_title(), img=True) for tr in doc) + return '' def _artist_tags_title(self): return "Tags for %s" % self['artist'] def _artist_tags(self, limit=10): - doc = pylast.search_for_artist(self['artist'],LFM_API_KEY, None, None).get_next_page()[0].get_top_tags()[:int(limit)] - return ', '.join(self.get_tag_anchor(tag) for tag in doc) + if self.get_lfm_artist(): + doc = self.get_lfm_artist().get_top_tags(int(limit)) + return ', '.join(self.get_tag_anchor(tag.item) for tag in doc) + return '' def _compils_title(self): return "Compilations with %s"%self['artist'] @@ -847,17 +873,11 @@ def _similar_artists_title(self): return "Artists related to %s" % self['artist'] - def _similar_artists_images(self): - doc = pylast.search_for_artist(self['artist'], LFM_API_KEY, None, None) - temp = doc.get_next_page()[0].get_similar(10) - data = [] - for art in temp: - data.append('' % (art, pylast.Artist.get_square_image(art))) - return ''.join(data) - def _similar_artists(self, limit=10): - sim_artists = pylast.search_for_artist(self['artist'], LFM_API_KEY, None, None).get_next_page()[0].get_similar(int(limit)) - return ', '.join(self.get_artist_anchor(sim_artist) for sim_artist in sim_artists) + if self.get_lfm_artist(): + sim_artists = self.get_lfm_artist().get_similar(int(limit)) + return ', '.join(self.get_artist_anchor(sim_artist.item) for sim_artist in sim_artists) + return '' def _albums_title(self): return "Albums by %s" % self['artist'] @@ -913,7 +933,14 @@ def __init__(self, theme, tag, base='tag://', template='tag.html', async=[]): self.tag = tag + self.lfm_tag = None + DefaultPage.__init__(self, theme, base, template, async+['similar-tags', 'top-artists', 'tag-top-tracks', 'tag-top-albums']) + + def get_lfm_tag(self): + if not self.lfm_tag: + self.lfm_tag = pylast.Tag(self.tag, pylast.network) + return self.lfm_tag def _tag(self): return self.tag @@ -922,37 +949,51 @@ return "Tags similar to %s" % self['tag'] def _similar_tags(self): - tags = pylast.search_for_tag(self.tag, LFM_API_KEY, None, None).get_next_page()[0].get_similar() - return ', '.join(self.get_tag_anchor(tag) for tag in tags[:20]) + if self.get_lfm_tag(): + tags = self.get_lfm_tag().get_similar() + return ', '.join(self.get_tag_anchor(tag) for tag in tags[:20]) + return '' def _top_artists_title(self): return "Top %s artists" % self.tag def _top_artists(self): - artists = pylast.search_for_tag(self.tag, LFM_API_KEY, None, None).get_next_page()[0].get_top_artists() - return ', '.join(self.get_artist_anchor(artist.get_item().get_name()) for artist in artists[:20]) + if self.get_lfm_tag(): + artists = self.get_lfm_tag().get_top_artists() + return ', '.join(self.get_artist_anchor(artist.item) for artist in artists[:20]) + return '' def _tag_top_tracks_title(self): return "Top %s tracks" % self.tag def _tag_top_tracks(self): - tracks = pylast.search_for_tag(self.tag, LFM_API_KEY, None, None).get_next_page()[0].get_top_tracks() - return '
'.join(self.get_track_anchor_from_artist_title(track.get_item().get_artist(), track.get_item().get_title()) for track in tracks[:15]) + if self.get_lfm_tag(): + tracks = self.get_lfm_tag().get_top_tracks() + return '
'.join(self.get_track_anchor_from_artist_title(track.item.get_artist(), track.item.get_title()) for track in tracks[:15]) + return '' def _tag_top_albums_title(self): return "Top %s albums" % self.tag def _tag_top_albums(self): - albums = pylast.search_for_tag(self.tag, LFM_API_KEY, None, None).get_next_page()[0].get_top_albums() - return '
'.join("%s %s"%(self['album-ico'],self.get_album_anchor_from_artist_title(album.get_item().get_artist(), album.get_item().get_title())) for album in albums[:15]) + if self.get_lfm_tag(): + albums = self.get_lfm_tag().get_top_albums() + return '
'.join("%s %s"%(self['album-ico'],self.get_album_anchor_from_artist_title(album.item.get_artist(), album.item.get_name())) for album in albums[:15]) + return '' class PlayingPage(ArtistPage): def __init__(self, theme, track, base='playing://', template='playing.html', async=[]): self.track = track + self.lfm_track = None ArtistPage.__init__(self, theme, get_track_tag(self.track, 'artist', 'unknown'),base, template, async+['track-tags', 'suggested-tracks', 'similar-tracks', 'lyrics']) event.add_callback(self.refresh_rating, 'rating_changed') + def get_lfm_track(self): + if not self.lfm_track: + self.lfm_track = pylast.Track(get_track_tag(self.track, 'artist', 'unknown'), get_track_tag(self.track, 'title', 'unknown'), pylast.network) + return self.lfm_track + def link_clicked(self, link): if link[0] == 'rate': self.track.set_rating(int(link[1])) @@ -969,9 +1010,9 @@ return get_track_tag(self.track, 'album', 'unknown') def _track_cover(self): - coverpath = get_track_cover(self.track) - cover = get_image_data(coverpath, (100, 100)) - return "" % (self.get_album_href(self['album']), cover) + cover = get_track_cover(self.track) + cover_data = get_image_data(cover, (100, 100)) + return "" % (self.get_album_href(self['album']), cover_data) def _playcount(self): try: @@ -1008,9 +1049,11 @@ def _track_tags_title(self): return "Tags for %s" % self['title'] - def _track_tags(self): - tags = pylast.search_for_track(self['artist'], self['title'], LFM_API_KEY, None, None).get_next_page()[0].get_top_tags() - return ', '.join(self.get_tag_anchor(tag) for tag in tags) + def _track_tags(self, limit=10): + if self.get_lfm_track(): + tags = self.get_lfm_track().get_top_tags(limit) + return ', '.join(self.get_tag_anchor(tag.item) for tag in tags) + return '' def _suggested_tracks_title(self): return "Suggested tracks for %s" % self['title'] @@ -1023,8 +1066,10 @@ return 'Tracks similar to %s' % self['title'] def _similar_tracks(self, limit=10): - doc = pylast.search_for_track(self['artist'], self['title'],LFM_API_KEY, None, None).get_next_page()[0].get_similar()[:int(limit)] - return '
'.join(self.get_track_anchor_from_artist_title(str(tr.get_artist()), str(tr.get_title()), img=True) for tr in doc) + if self.get_lfm_track(): + doc = self.get_lfm_track().get_similar(int(limit)) + return '
'.join(self.get_track_anchor_from_artist_title(str(tr.get_artist()), str(tr.get_title()), img=True) for tr in doc) + return '' def _lyrics_title(self): return "Lyrics of %s by %s" % (self['title'],self['artist']) @@ -1056,7 +1101,8 @@ os.mkdir(cachedir) #TODO last.fm class - pylast.enable_caching(os.path.join(cachedir, 'lastfm.cache')) + pylast.network = pylast.get_lastfm_network(LFM_API_KEY) + pylast.network.enable_caching(os.path.join(cachedir, 'lastfm.cache')) self.controller = parent @@ -1099,6 +1145,9 @@ window.destroy() return (self._child, self.name) + +def get_prefs_pane(): + return contextprefs def exaile_ready(object=None, a=None, b=None): global PANEL === added file 'plugins/contextinfo/context_pane.ui' --- plugins/contextinfo/context_pane.ui 1970-01-01 00:00:00 +0000 +++ plugins/contextinfo/context_pane.ui 2010-01-14 16:33:34 +0000 @@ -0,0 +1,117 @@ + + + + + + + + True + cursor + 3 + vertical + + + True + 0.4699999988079071 + Please enter your Last.fm authentication: + + + False + False + 0 + + + + + True + 2 + 2 + + + True + True + False + + + + 1 + 2 + 1 + 2 + + + + + True + True + + + + 1 + 2 + + + + + True + 0 + 6 + Password: + + + 1 + 2 + GTK_FILL + + + + + True + 0 + 6 + Username: + + + GTK_FILL + + + + + False + False + 4 + 1 + + + + + True + + + Sign up for Last.fm + True + True + True + none + https://www.last.fm/join + + + False + False + 0 + + + + + + + + False + False + 2 + + + + + + === modified file 'plugins/contextinfo/pylast.py' --- plugins/contextinfo/pylast.py 2009-10-27 02:30:20 +0000 +++ plugins/contextinfo/pylast.py 2010-01-18 21:44:24 +0000 @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# pylast - A Python interface to Last.fm +# pylast - A Python interface to Last.fm (and other API compatible social networks) # Copyright (C) 2008-2009 Amr Hassan # # This program is free software; you can redistribute it and/or modify @@ -19,40 +19,29 @@ # USA # # http://code.google.com/p/pylast/ - -__name__ = 'pylast' -__version__ = '0.3' -__revision__ = "$Revision$" -__doc__ = 'A Python interface to Last.fm' + +__version__ = '0.4' __author__ = 'Amr Hassan' __copyright__ = "Copyright (C) 2008-2009 Amr Hassan" __license__ = "gpl" __email__ = 'amr.hassan@gmail.com' -# Parse revision and add it to __version__ -r = __revision__ -__version__ = __version__ + "." + r[r.find(" ")+1:r.rfind("$")-1] - -# Default values for Last.fm. -WS_SERVER = ('ws.audioscrobbler.com', '/2.0/') -SUBMISSION_SERVER = "http://post.audioscrobbler.com:80/" - -__proxy = None -__proxy_enabled = False -__cache_shelf = None -__cache_filename = None -__last_call_time = 0 - import hashlib import httplib import urllib import threading from xml.dom import minidom -import os +import xml.dom import time -from logging import info, warn, debug import shelve import tempfile +import sys +import htmlentitydefs + +try: + import collections +except ImportError: + pass STATUS_INVALID_SERVICE = 2 STATUS_INVALID_METHOD = 3 @@ -78,23 +67,28 @@ PERIOD_6MONTHS = '6month' PERIOD_12MONTHS = '12month' -IMAGE_SMALL = 0 -IMAGE_MEDIUM = 1 -IMAGE_LARGE = 2 -IMAGE_EXTRA_LARGE = 3 - -DOMAIN_ENGLISH = 'www.last.fm' -DOMAIN_GERMAN = 'www.lastfm.de' -DOMAIN_SPANISH = 'www.lastfm.es' -DOMAIN_FRENCH = 'www.lastfm.fr' -DOMAIN_ITALIAN = 'www.lastfm.it' -DOMAIN_POLISH = 'www.lastfm.pl' -DOMAIN_PORTUGUESE = 'www.lastfm.com.br' -DOMAIN_SWEDISH = 'www.lastfm.se' -DOMAIN_TURKISH = 'www.lastfm.com.tr' -DOMAIN_RUSSIAN = 'www.lastfm.ru' -DOMAIN_JAPANESE = 'www.lastfm.jp' -DOMAIN_CHINESE = 'cn.last.fm' +DOMAIN_ENGLISH = 0 +DOMAIN_GERMAN = 1 +DOMAIN_SPANISH = 2 +DOMAIN_FRENCH = 3 +DOMAIN_ITALIAN = 4 +DOMAIN_POLISH = 5 +DOMAIN_PORTUGUESE = 6 +DOMAIN_SWEDISH = 7 +DOMAIN_TURKISH = 8 +DOMAIN_RUSSIAN = 9 +DOMAIN_JAPANESE = 10 +DOMAIN_CHINESE = 11 + +COVER_SMALL = 0 +COVER_MEDIUM = 1 +COVER_LARGE = 2 +COVER_EXTRA_LARGE = 3 +COVER_MEGA = 4 + +IMAGES_ORDER_POPULARITY = "popularity" +IMAGES_ORDER_DATE = "dateadded" + USER_MALE = 'Male' USER_FEMALE = 'Female' @@ -105,3151 +99,3524 @@ SCROBBLE_SOURCE_LASTFM = "L" SCROBBLE_SOURCE_UNKNOWN = "U" -SCROBBLE_MODE_PLAYED = "L" +SCROBBLE_MODE_PLAYED = "" +SCROBBLE_MODE_LOVED = "L" SCROBBLE_MODE_BANNED = "B" SCROBBLE_MODE_SKIPPED = "S" +""" +A list of the implemented webservices (from http://www.last.fm/api/intro) +===================================== +# Album + + * album.addTags DONE + * album.getInfo DONE + * album.getTags DONE + * album.removeTag DONE + * album.search DONE + +# Artist + + * artist.addTags DONE + * artist.getEvents DONE + * artist.getImages DONE + * artist.getInfo DONE + * artist.getPodcast TODO + * artist.getShouts DONE + * artist.getSimilar DONE + * artist.getTags DONE + * artist.getTopAlbums DONE + * artist.getTopFans DONE + * artist.getTopTags DONE + * artist.getTopTracks DONE + * artist.removeTag DONE + * artist.search DONE + * artist.share DONE + * artist.shout DONE + +# Auth + + * auth.getMobileSession DONE + * auth.getSession DONE + * auth.getToken DONE + +# Event + + * event.attend DONE + * event.getAttendees DONE + * event.getInfo DONE + * event.getShouts DONE + * event.share DONE + * event.shout DONE + +# Geo + + * geo.getEvents + * geo.getTopArtists + * geo.getTopTracks + +# Group + + * group.getMembers DONE + * group.getWeeklyAlbumChart DONE + * group.getWeeklyArtistChart DONE + * group.getWeeklyChartList DONE + * group.getWeeklyTrackChart DONE + +# Library + + * library.addAlbum DONE + * library.addArtist DONE + * library.addTrack DONE + * library.getAlbums DONE + * library.getArtists DONE + * library.getTracks DONE + +# Playlist + + * playlist.addTrack DONE + * playlist.create DONE + * playlist.fetch DONE + +# Radio + + * radio.getPlaylist + * radio.tune + +# Tag + + * tag.getSimilar DONE + * tag.getTopAlbums DONE + * tag.getTopArtists DONE + * tag.getTopTags DONE + * tag.getTopTracks DONE + * tag.getWeeklyArtistChart DONE + * tag.getWeeklyChartList DONE + * tag.search DONE + +# Tasteometer + + * tasteometer.compare DONE + +# Track + + * track.addTags DONE + * track.ban DONE + * track.getInfo DONE + * track.getSimilar DONE + * track.getTags DONE + * track.getTopFans DONE + * track.getTopTags DONE + * track.love DONE + * track.removeTag DONE + * track.search DONE + * track.share DONE + +# User + + * user.getEvents DONE + * user.getFriends DONE + * user.getInfo DONE + * user.getLovedTracks DONE + * user.getNeighbours DONE + * user.getPastEvents DONE + * user.getPlaylists DONE + * user.getRecentStations TODO + * user.getRecentTracks DONE + * user.getRecommendedArtists DONE + * user.getRecommendedEvents DONE + * user.getShouts DONE + * user.getTopAlbums DONE + * user.getTopArtists DONE + * user.getTopTags DONE + * user.getTopTracks DONE + * user.getWeeklyAlbumChart DONE + * user.getWeeklyArtistChart DONE + * user.getWeeklyChartList DONE + * user.getWeeklyTrackChart DONE + * user.shout DONE + +# Venue + + * venue.getEvents DONE + * venue.getPastEvents DONE + * venue.search DONE +""" + +class Network(object): + """ + A music social network website that is Last.fm or one exposing a Last.fm compatible API + """ + + def __init__(self, name, homepage, ws_server, api_key, api_secret, session_key, submission_server, username, password_hash, + domain_names, urls): + """ + name: the name of the network + homepage: the homepage url + ws_server: the url of the webservices server + api_key: a provided API_KEY + api_secret: a provided API_SECRET + session_key: a generated session_key or None + submission_server: the url of the server to which tracks are submitted (scrobbled) + username: a username of a valid user + password_hash: the output of pylast.md5(password) where password is the user's password thingy + domain_names: a dict mapping each DOMAIN_* value to a string domain name + urls: a dict mapping types to urls + + if username and password_hash were provided and not session_key, session_key will be + generated automatically when needed. + + Either a valid session_key or a combination of username and password_hash must be present for scrobbling. + + You should use a preconfigured network object through a get_*_network(...) method instead of creating an object + of this class, unless you know what you're doing. + """ + + self.ws_server = ws_server + self.submission_server = submission_server + self.name = name + self.homepage = homepage + self.api_key = api_key + self.api_secret = api_secret + self.session_key = session_key + self.username = username + self.password_hash = password_hash + self.domain_names = domain_names + self.urls = urls + + self.cache_backend = None + self.proxy_enabled = False + self.proxy = None + self.last_call_time = 0 + + #generate a session_key if necessary + if (self.api_key and self.api_secret) and not self.session_key and (self.username and self.password_hash): + sk_gen = SessionKeyGenerator(self) + self.session_key = sk_gen.get_session_key(self.username, self.password_hash) + + def get_artist(self, artist_name): + """ + Return an Artist object + """ + + return Artist(artist_name, self) + + def get_track(self, artist, title): + """ + Return a Track object + """ + + return Track(artist, title, self) + + def get_album(self, artist, title): + """ + Return an Album object + """ + + return Album(artist, title, self) + + def get_authenticated_user(self): + """ + Returns the authenticated user + """ + + return AuthenticatedUser(self) + + def get_country(self, country_name): + """ + Returns a country object + """ + + return Country(country_name, self) + + def get_group(self, name): + """ + Returns a Group object + """ + + return Group(name, self) + + def get_user(self, username): + """ + Returns a user object + """ + + return User(username, self) + + def get_tag(self, name): + """ + Returns a tag object + """ + + return Tag(name, self) + + def get_scrobbler(self, client_id, client_version): + """ + Returns a Scrobbler object used for submitting tracks to the server + + Quote from http://www.last.fm/api/submissions: + ======== + Client identifiers are used to provide a centrally managed database of + the client versions, allowing clients to be banned if they are found to + be behaving undesirably. The client ID is associated with a version + number on the server, however these are only incremented if a client is + banned and do not have to reflect the version of the actual client application. + + During development, clients which have not been allocated an identifier should + use the identifier tst, with a version number of 1.0. Do not distribute code or + client implementations which use this test identifier. Do not use the identifiers + used by other clients. + ========= + + To obtain a new client identifier please contact: + * Last.fm: submissions@last.fm + * # TODO: list others + + ...and provide us with the name of your client and its homepage address. + """ + + return Scrobbler(self, client_id, client_version) + + def _get_language_domain(self, domain_language): + """ + Returns the mapped domain name of the network to a DOMAIN_* value + """ + + if domain_language in self.domain_names: + return self.domain_names[domain_language] + + def _get_url(self, domain, type): + return "http://%s/%s" %(self._get_language_domain(domain), self.urls[type]) + + def _get_ws_auth(self): + """ + Returns a (API_KEY, API_SECRET, SESSION_KEY) tuple. + """ + return (self.api_key, self.api_secret, self.session_key) + + def _delay_call(self): + """ + Makes sure that web service calls are at least a second apart + """ + + # delay time in seconds + DELAY_TIME = 1.0 + now = time.time() + + if (now - self.last_call_time) < DELAY_TIME: + time.sleep(1) + + self.last_call_time = now + + def create_new_playlist(self, title, description): + """ + Creates a playlist for the authenticated user and returns it + title: The title of the new playlist. + description: The description of the new playlist. + """ + + params = {} + params['title'] = _unicode(title) + params['description'] = _unicode(description) + + doc = _Request(self, 'playlist.create', params).execute(False) + + e_id = doc.getElementsByTagName("id")[0].firstChild.data + user = doc.getElementsByTagName('playlists')[0].getAttribute('user') + + return Playlist(user, e_id, self) + + def get_top_tags(self, limit=None): + """Returns a sequence of the most used tags as a sequence of TopItem objects.""" + + doc = _Request(self, "tag.getTopTags").execute(True) + seq = [] + for node in doc.getElementsByTagName("tag"): + tag = Tag(_extract(node, "name"), self) + weight = _number(_extract(node, "count")) + + if len(seq) < limit: + seq.append(TopItem(tag, weight)) + + return seq + + def enable_proxy(self, host, port): + """Enable a default web proxy""" + + self.proxy = [host, _number(port)] + self.proxy_enabled = True + + def disable_proxy(self): + """Disable using the web proxy""" + + self.proxy_enabled = False + + def is_proxy_enabled(self): + """Returns True if a web proxy is enabled.""" + + return self.proxy_enabled + + def _get_proxy(self): + """Returns proxy details.""" + + return self.proxy + + def enable_caching(self, file_path = None): + """Enables caching request-wide for all cachable calls. + In choosing the backend used for caching, it will try _SqliteCacheBackend first if + the module sqlite3 is present. If not, it will fallback to _ShelfCacheBackend which uses shelve.Shelf objects. + + * file_path: A file path for the backend storage file. If + None set, a temp file would probably be created, according the backend. + """ + + if not file_path: + file_path = tempfile.mktemp(prefix="pylast_tmp_") + + self.cache_backend = _ShelfCacheBackend(file_path) + + def disable_caching(self): + """Disables all caching features.""" + + self.cache_backend = None + + def is_caching_enabled(self): + """Returns True if caching is enabled.""" + + return not (self.cache_backend == None) + + def _get_cache_backend(self): + + return self.cache_backend + + def search_for_album(self, album_name): + """Searches for an album by its name. Returns a AlbumSearch object. + Use get_next_page() to retreive sequences of results.""" + + return AlbumSearch(album_name, self) + + def search_for_artist(self, artist_name): + """Searches of an artist by its name. Returns a ArtistSearch object. + Use get_next_page() to retreive sequences of results.""" + + return ArtistSearch(artist_name, self) + + def search_for_tag(self, tag_name): + """Searches of a tag by its name. Returns a TagSearch object. + Use get_next_page() to retreive sequences of results.""" + + return TagSearch(tag_name, self) + + def search_for_track(self, artist_name, track_name): + """Searches of a track by its name and its artist. Set artist to an empty string if not available. + Returns a TrackSearch object. + Use get_next_page() to retreive sequences of results.""" + + return TrackSearch(artist_name, track_name, self) + + def search_for_venue(self, venue_name, country_name): + """Searches of a venue by its name and its country. Set country_name to an empty string if not available. + Returns a VenueSearch object. + Use get_next_page() to retreive sequences of results.""" + + return VenueSearch(venue_name, country_name, self) + + def get_track_by_mbid(self, mbid): + """Looks up a track by its MusicBrainz ID""" + + params = {"mbid": _unicode(mbid)} + + doc = _Request(self, "track.getInfo", params).execute(True) + + return Track(_extract(doc, "name", 1), _extract(doc, "name"), self) + + def get_artist_by_mbid(self, mbid): + """Loooks up an artist by its MusicBrainz ID""" + + params = {"mbid": _unicode(mbid)} + + doc = _Request(self, "artist.getInfo", params).execute(True) + + return Artist(_extract(doc, "name"), self) + + def get_album_by_mbid(self, mbid): + """Looks up an album by its MusicBrainz ID""" + + params = {"mbid": _unicode(mbid)} + + doc = _Request(self, "album.getInfo", params).execute(True) + + return Album(_extract(doc, "artist"), _extract(doc, "name"), self) + +def get_lastfm_network(api_key="", api_secret="", session_key = "", username = "", password_hash = ""): + """ + Returns a preconfigured Network object for Last.fm + + api_key: a provided API_KEY + api_secret: a provided API_SECRET + session_key: a generated session_key or None + username: a username of a valid user + password_hash: the output of pylast.md5(password) where password is the user's password + + if username and password_hash were provided and not session_key, session_key will be + generated automatically when needed. + + Either a valid session_key or a combination of username and password_hash must be present for scrobbling. + + Most read-only webservices only require an api_key and an api_secret, see about obtaining them from: + http://www.last.fm/api/account + """ + + return Network ( + name = "Last.fm", + homepage = "http://last.fm", + ws_server = ("ws.audioscrobbler.com", "/2.0/"), + api_key = api_key, + api_secret = api_secret, + session_key = session_key, + submission_server = "http://post.audioscrobbler.com:80/", + username = username, + password_hash = password_hash, + domain_names = { + DOMAIN_ENGLISH: 'www.last.fm', + DOMAIN_GERMAN: 'www.lastfm.de', + DOMAIN_SPANISH: 'www.lastfm.es', + DOMAIN_FRENCH: 'www.lastfm.fr', + DOMAIN_ITALIAN: 'www.lastfm.it', + DOMAIN_POLISH: 'www.lastfm.pl', + DOMAIN_PORTUGUESE: 'www.lastfm.com.br', + DOMAIN_SWEDISH: 'www.lastfm.se', + DOMAIN_TURKISH: 'www.lastfm.com.tr', + DOMAIN_RUSSIAN: 'www.lastfm.ru', + DOMAIN_JAPANESE: 'www.lastfm.jp', + DOMAIN_CHINESE: 'cn.last.fm', + }, + urls = { + "album": "music/%(artist)s/%(album)s", + "artist": "music/%(artist)s", + "event": "event/%(id)s", + "country": "place/%(country_name)s", + "playlist": "user/%(user)s/library/playlists/%(appendix)s", + "tag": "tag/%(name)s", + "track": "music/%(artist)s/_/%(title)s", + "group": "group/%(name)s", + "user": "user/%(name)s", + } + ) + +def get_librefm_network(api_key="", api_secret="", session_key = "", username = "", password_hash = ""): + """ + Returns a preconfigured Network object for Libre.fm + + api_key: a provided API_KEY + api_secret: a provided API_SECRET + session_key: a generated session_key or None + username: a username of a valid user + password_hash: the output of pylast.md5(password) where password is the user's password + + if username and password_hash were provided and not session_key, session_key will be + generated automatically when needed. + """ + + return Network ( + name = "Libre.fm", + homepage = "http://alpha.dev.libre.fm", + ws_server = ("alpha.dev.libre.fm", "/2.0/"), + api_key = api_key, + api_secret = api_secret, + session_key = session_key, + submission_server = "http://turtle.libre.fm:80/", + username = username, + password_hash = password_hash, + domain_names = { + DOMAIN_ENGLISH: "alpha.dev.libre.fm", + DOMAIN_GERMAN: "alpha.dev.libre.fm", + DOMAIN_SPANISH: "alpha.dev.libre.fm", + DOMAIN_FRENCH: "alpha.dev.libre.fm", + DOMAIN_ITALIAN: "alpha.dev.libre.fm", + DOMAIN_POLISH: "alpha.dev.libre.fm", + DOMAIN_PORTUGUESE: "alpha.dev.libre.fm", + DOMAIN_SWEDISH: "alpha.dev.libre.fm", + DOMAIN_TURKISH: "alpha.dev.libre.fm", + DOMAIN_RUSSIAN: "alpha.dev.libre.fm", + DOMAIN_JAPANESE: "alpha.dev.libre.fm", + DOMAIN_CHINESE: "alpha.dev.libre.fm", + }, + urls = { + "album": "artist/%(artist)s/album/%(album)s", + "artist": "artist/%(artist)s", + "event": "event/%(id)s", + "country": "place/%(country_name)s", + "playlist": "user/%(user)s/library/playlists/%(appendix)s", + "tag": "tag/%(name)s", + "track": "music/%(artist)s/_/%(title)s", + "group": "group/%(name)s", + "user": "user/%(name)s", + } + ) + +class _ShelfCacheBackend(object): + """Used as a backend for caching cacheable requests.""" + def __init__(self, file_path = None): + self.shelf = shelve.open(file_path) + + def get_xml(self, key): + return self.shelf[key] + + def set_xml(self, key, xml_string): + self.shelf[key] = xml_string + + def has_key(self, key): + return key in self.shelf.keys() + class _ThreadedCall(threading.Thread): - """Facilitates calling a function on another thread.""" - - def __init__(self, sender, funct, funct_args, callback, callback_args): - - threading.Thread.__init__(self) - - self.funct = funct - self.funct_args = funct_args - self.callback = callback - self.callback_args = callback_args - - self.sender = sender - - def run(self): - - output = [] - - if self.funct: - if self.funct_args: - output = self.funct(*self.funct_args) - else: - output = self.funct() - - if self.callback: - if self.callback_args: - self.callback(self.sender, output, *self.callback_args) - else: - self.callback(self.sender, output) - + """Facilitates calling a function on another thread.""" + + def __init__(self, sender, funct, funct_args, callback, callback_args): + + threading.Thread.__init__(self) + + self.funct = funct + self.funct_args = funct_args + self.callback = callback + self.callback_args = callback_args + + self.sender = sender + + def run(self): + + output = [] + + if self.funct: + if self.funct_args: + output = self.funct(*self.funct_args) + else: + output = self.funct() + + if self.callback: + if self.callback_args: + self.callback(self.sender, output, *self.callback_args) + else: + self.callback(self.sender, output) + class _Request(object): - """Representing an abstract web service operation.""" - - global WS_SERVER - (HOST_NAME, HOST_SUBDIR) = WS_SERVER - - def __init__(self, method_name, params, api_key, api_secret, session_key = None): - - self.params = params - self.api_secret = api_secret - - self.params["api_key"] = api_key - self.params["method"] = method_name - - if is_caching_enabled(): - self.shelf = get_cache_shelf() - - if session_key: - self.params["sk"] = session_key - self.sign_it() - - def sign_it(self): - """Sign this request.""" - - if not "api_sig" in self.params.keys(): - self.params['api_sig'] = self._get_signature() - - def _get_signature(self): - """Returns a 32-character hexadecimal md5 hash of the signature string.""" - - keys = self.params.keys()[:] - - keys.sort() - - string = "" - - for name in keys: - string += name - string += self.params[name] - - string += self.api_secret - - return md5(string) - - def _get_cache_key(self): - """The cache key is a string of concatenated sorted names and values.""" - - keys = self.params.keys() - keys.sort() - - cache_key = str() - - for key in keys: - if key != "api_sig" and key != "api_key" and key != "sk": - cache_key += key + self.params[key].encode("utf-8") - - return hashlib.sha1(cache_key).hexdigest() - - def _get_cached_response(self): - """Returns a file object of the cached response.""" - - if not self._is_cached(): - response = self._download_response() - self.shelf[self._get_cache_key()] = response - - return self.shelf[self._get_cache_key()] - - def _is_cached(self): - """Returns True if the request is already in cache.""" - - return self.shelf.has_key(self._get_cache_key()) - - def _download_response(self): - """Returns a response body string from the server.""" - - # Delay the call if necessary - _delay_call() - - data = [] - for name in self.params.keys(): - data.append('='.join((name, urllib.quote_plus(self.params[name].encode("utf-8"))))) - data = '&'.join(data) - - headers = { - "Content-type": "application/x-www-form-urlencoded", - 'Accept-Charset': 'utf-8', - 'User-Agent': __name__ + '/' + __version__ - } - - if is_proxy_enabled(): - conn = httplib.HTTPConnection(host = _get_proxy()[0], port = _get_proxy()[1]) - conn.request(method='POST', url="http://" + self.HOST_NAME + self.HOST_SUBDIR, - body=data, headers=headers) - else: - conn = httplib.HTTPConnection(host=self.HOST_NAME) - conn.request(method='POST', url=self.HOST_SUBDIR, body=data, headers=headers) - - response = conn.getresponse().read() - self._check_response_for_errors(response) - return response - - def execute(self, cacheable = False): - """Returns the XML DOM response of the POST Request from the server""" - - if is_caching_enabled() and cacheable: - response = self._get_cached_response() - else: - response = self._download_response() - - return minidom.parseString(response) - - def _check_response_for_errors(self, response): - """Checks the response for errors and raises one if any exists.""" - - doc = minidom.parseString(response) - e = doc.getElementsByTagName('lfm')[0] - - if e.getAttribute('status') != "ok": - e = doc.getElementsByTagName('error')[0] - status = e.getAttribute('code') - details = e.firstChild.data.strip() - raise ServiceException(status, details) + """Representing an abstract web service operation.""" + + def __init__(self, network, method_name, params = {}): + + self.params = params + self.network = network + + (self.api_key, self.api_secret, self.session_key) = network._get_ws_auth() + + self.params["api_key"] = self.api_key + self.params["method"] = method_name + + if network.is_caching_enabled(): + self.cache = network._get_cache_backend() + + if self.session_key: + self.params["sk"] = self.session_key + self.sign_it() + + def sign_it(self): + """Sign this request.""" + + if not "api_sig" in self.params.keys(): + self.params['api_sig'] = self._get_signature() + + def _get_signature(self): + """Returns a 32-character hexadecimal md5 hash of the signature string.""" + + keys = self.params.keys()[:] + + keys.sort() + + string = "" + + for name in keys: + string += name + string += self.params[name] + + string += self.api_secret + + return md5(string) + + def _get_cache_key(self): + """The cache key is a string of concatenated sorted names and values.""" + + keys = self.params.keys() + keys.sort() + + cache_key = str() + + for key in keys: + if key != "api_sig" and key != "api_key" and key != "sk": + cache_key += key + _string(self.params[key]) + + return hashlib.sha1(cache_key).hexdigest() + + def _get_cached_response(self): + """Returns a file object of the cached response.""" + + if not self._is_cached(): + response = self._download_response() + self.cache.set_xml(self._get_cache_key(), response) + + return self.cache.get_xml(self._get_cache_key()) + + def _is_cached(self): + """Returns True if the request is already in cache.""" + + return self.cache.has_key(self._get_cache_key()) + + def _download_response(self): + """Returns a response body string from the server.""" + + # Delay the call if necessary + #self.network._delay_call() # enable it if you want. + + data = [] + for name in self.params.keys(): + data.append('='.join((name, urllib.quote_plus(_string(self.params[name]))))) + data = '&'.join(data) + + headers = { + "Content-type": "application/x-www-form-urlencoded", + 'Accept-Charset': 'utf-8', + 'User-Agent': "pylast" + '/' + __version__ + } + + (HOST_NAME, HOST_SUBDIR) = self.network.ws_server + + if self.network.is_proxy_enabled(): + conn = httplib.HTTPConnection(host = self._get_proxy()[0], port = self._get_proxy()[1]) + conn.request(method='POST', url="http://" + HOST_NAME + HOST_SUBDIR, + body=data, headers=headers) + else: + conn = httplib.HTTPConnection(host=HOST_NAME) + conn.request(method='POST', url=HOST_SUBDIR, body=data, headers=headers) + + response = conn.getresponse() + response_text = _unicode(response.read()) + self._check_response_for_errors(response_text) + return response_text + + def execute(self, cacheable = False): + """Returns the XML DOM response of the POST Request from the server""" + + if self.network.is_caching_enabled() and cacheable: + response = self._get_cached_response() + else: + response = self._download_response() + + return minidom.parseString(_string(response)) + + def _check_response_for_errors(self, response): + """Checks the response for errors and raises one if any exists.""" + + doc = minidom.parseString(_string(response)) + e = doc.getElementsByTagName('lfm')[0] + + if e.getAttribute('status') != "ok": + e = doc.getElementsByTagName('error')[0] + status = e.getAttribute('code') + details = e.firstChild.data.strip() + raise WSError(self.network, status, details) class SessionKeyGenerator(object): - """Methods of generating a session key: - 1) Web Authentication: - a. sg = SessionKeyGenerator(API_KEY, API_SECRET) - b. url = sg.get_web_auth_url() - c. Ask the user to open the url and authorize you, and wait for it. - d. session_key = sg.get_web_auth_session_key(url) - 2) Username and Password Authentication: - a. username = raw_input("Please enter your username: ") - b. md5_password = pylast.md5(raw_input("Please enter your password: ") - c. session_key = SessionKeyGenerator(API_KEY, API_SECRET).get_session_key(username, md5_password) - - A session key's lifetime is infinie, unless the user provokes the rights of the given API Key. - """ - - def __init__(self, api_key, api_secret): - self.api_key = api_key - self.api_secret = api_secret - self.web_auth_tokens = {} - - def _get_web_auth_token(self): - """Retrieves a token from Last.fm for web authentication. - The token then has to be authorized from getAuthURL before creating session. - """ - - request = _Request('auth.getToken', dict(), self.api_key, self.api_secret) - - # default action is that a request is signed only when - # a session key is provided. - request.sign_it() - - doc = request.execute() - - e = doc.getElementsByTagName('token')[0] - return e.firstChild.data - - def get_web_auth_url(self): - """The user must open this page, and you first, then call get_web_auth_session_key(url) after that.""" - - token = self._get_web_auth_token() - - url = 'http://www.last.fm/api/auth/?api_key=%(api)s&token=%(token)s' % \ - {'api': self.api_key, 'token': token} - - self.web_auth_tokens[url] = token - - return url - - def get_web_auth_session_key(self, url): - """Retrieves the session key of a web authorization process by its url.""" - - if url in self.web_auth_tokens.keys(): - token = self.web_auth_tokens[url] - else: - token = "" #that's gonna raise a ServiceException of an unauthorized token when the request is executed. - - request = _Request('auth.getSession', {'token': token}, self.api_key, self.api_secret) - - # default action is that a request is signed only when - # a session key is provided. - request.sign_it() - - doc = request.execute() - - return doc.getElementsByTagName('key')[0].firstChild.data - - def get_session_key(self, username, md5_password): - """Retrieve a session key with a username and a md5 hash of the user's password.""" - - params = {"username": username, "authToken": md5(username + md5_password)} - request = _Request("auth.getMobileSession", params, self.api_key, self.api_secret) - - # default action is that a request is signed only when - # a session key is provided. - request.sign_it() - - doc = request.execute() - - return _extract(doc, "key") + """Methods of generating a session key: + 1) Web Authentication: + a. network = get_*_network(API_KEY, API_SECRET) + b. sg = SessionKeyGenerator(network) + c. url = sg.get_web_auth_url() + d. Ask the user to open the url and authorize you, and wait for it. + e. session_key = sg.get_web_auth_session_key(url) + 2) Username and Password Authentication: + a. network = get_*_network(API_KEY, API_SECRET) + b. username = raw_input("Please enter your username: ") + c. password_hash = pylast.md5(raw_input("Please enter your password: ") + d. session_key = SessionKeyGenerator(network).get_session_key(username, password_hash) + + A session key's lifetime is infinie, unless the user provokes the rights of the given API Key. + + If you create a Network object with just a API_KEY and API_SECRET and a username and a password_hash, a + SESSION_KEY will be automatically generated for that network and stored in it so you don't have to do this + manually, unless you want to. + """ + + def __init__(self, network): + self.network = network + self.web_auth_tokens = {} + + def _get_web_auth_token(self): + """Retrieves a token from the network for web authentication. + The token then has to be authorized from getAuthURL before creating session. + """ + + request = _Request(self.network, 'auth.getToken') + + # default action is that a request is signed only when + # a session key is provided. + request.sign_it() + + doc = request.execute() + + e = doc.getElementsByTagName('token')[0] + return e.firstChild.data + + def get_web_auth_url(self): + """The user must open this page, and you first, then call get_web_auth_session_key(url) after that.""" + + token = self._get_web_auth_token() + + url = '%(homepage)s/api/auth/?api_key=%(api)s&token=%(token)s' % \ + {"homepage": self.network.homepage, "api": self.network.api_key, "token": token} + + self.web_auth_tokens[url] = token + + return url + + def get_web_auth_session_key(self, url): + """Retrieves the session key of a web authorization process by its url.""" + + if url in self.web_auth_tokens.keys(): + token = self.web_auth_tokens[url] + else: + token = "" #that's gonna raise a WSError of an unauthorized token when the request is executed. + + request = _Request(self.network, 'auth.getSession', {'token': token}) + + # default action is that a request is signed only when + # a session key is provided. + request.sign_it() + + doc = request.execute() + + return doc.getElementsByTagName('key')[0].firstChild.data + + def get_session_key(self, username, password_hash): + """Retrieve a session key with a username and a md5 hash of the user's password.""" + + params = {"username": username, "authToken": md5(username + password_hash)} + request = _Request(self.network, "auth.getMobileSession", params) + + # default action is that a request is signed only when + # a session key is provided. + request.sign_it() + + doc = request.execute() + + return _extract(doc, "key") + +def _namedtuple(name, children): + """ + collections.namedtuple is available in (python >= 2.6) + """ + + v = sys.version_info + if v[1] >= 6 and v[0] < 3: + return collections.namedtuple(name, children) + else: + def fancydict(*args): + d = {} + i = 0 + for child in children: + d[child.strip()] = args[i] + i += 1 + return d + + return fancydict + +TopItem = _namedtuple("TopItem", ["item", "weight"]) +SimilarItem = _namedtuple("SimilarItem", ["item", "match"]) +LibraryItem = _namedtuple("LibraryItem", ["item", "playcount", "tagcount"]) +PlayedTrack = _namedtuple("PlayedTrack", ["track", "playback_date", "timestamp"]) +ImageSizes = _namedtuple("ImageSizes", ["original", "large", "largesquare", "medium", "small", "extralarge"]) +Image = _namedtuple("Image", ["title", "url", "dateadded", "format", "owner", "sizes", "votes"]) +Shout = _namedtuple("Shout", ["body", "author", "date"]) + +def _string_output(funct): + def r(*args): + return _string(funct(*args)) + + return r class _BaseObject(object): - """An abstract webservices object.""" - - def __init__(self, api_key, api_secret, session_key): - - self.api_key = api_key - self.api_secret = api_secret - self.session_key = session_key - - self.auth_data = (self.api_key, self.api_secret, self.session_key) - - def _request(self, method_name, cacheable = False, params = None): - if not params: - params = self._get_params() - - return _Request(method_name, params, *self.auth_data).execute(cacheable) - - def _get_params(): - """Returns the most common set of parameters between all objects.""" - - return dict() + """An abstract webservices object.""" + + network = None + + def __init__(self, network): + self.network = network + + def _request(self, method_name, cacheable = False, params = None): + if not params: + params = self._get_params() + + return _Request(self.network, method_name, params).execute(cacheable) + + def _get_params(self): + """Returns the most common set of parameters between all objects.""" + + return {} + + def __hash__(self): + return hash(self.network) + \ + hash(str(type(self)) + "".join(self._get_params().keys() + self._get_params().values()).lower()) class _Taggable(object): - """Common functions for classes with tags.""" - - def __init__(self, ws_prefix): - self.ws_prefix = ws_prefix - - def add_tags(self, *tags): - """Adds one or several tags. - * *tags: Any number of tag names or Tag objects. - """ - - for tag in tags: - self._add_tag(tag) - - def _add_tag(self, tag): - """Adds one or several tags. - * tag: one tag name or a Tag object. - """ - - if isinstance(tag, Tag): - tag = tag.get_name() - - params = self._get_params() - params['tags'] = unicode(tag) - - self._request(self.ws_prefix + '.addTags', False, params) - info("Tagged " + repr(self) + " as (" + repr(tag) + ")") - - def _remove_tag(self, single_tag): - """Remove a user's tag from this object.""" - - if isinstance(single_tag, Tag): - single_tag = single_tag.get_name() - - params = self._get_params() - params['tag'] = unicode(single_tag) - - self._request(self.ws_prefix + '.removeTag', False, params) - info("Removed tag (" + repr(tag) + ") from " + repr(self)) - - def get_tags(self): - """Returns a list of the tags set by the user to this object.""" - - # Uncacheable because it can be dynamically changed by the user. - params = self._get_params() - doc = _Request(self.ws_prefix + '.getTags', params, *self.auth_data).execute(cacheable = False) - - tag_names = _extract_all(doc, 'name') - tags = [] - for tag in tag_names: - tags.append(Tag(tag, *self.auth_data)) - - return tags - - def remove_tags(self, *tags): - """Removes one or several tags from this object. - * *tags: Any number of tag names or Tag objects. - """ - - for tag in tags: - self._remove_tag(tag) - - def clear_tags(self): - """Clears all the user-set tags. """ - - self.remove_tags(*(self.get_tags())) - - def set_tags(self, *tags): - """Sets this object's tags to only those tags. - * *tags: any number of tag names. - """ - - c_old_tags = [] - old_tags = [] - c_new_tags = [] - new_tags = [] - - to_remove = [] - to_add = [] - - tags_on_server = self.get_tags() - - for tag in tags_on_server: - c_old_tags.append(tag.get_name().lower()) - old_tags.append(tag.get_name()) - - for tag in tags: - c_new_tags.append(tag.lower()) - new_tags.append(tag) - - for i in range(0, len(old_tags)): - if not c_old_tags[i] in c_new_tags: - to_remove.append(old_tags[i]) - - for i in range(0, len(new_tags)): - if not c_new_tags[i] in c_old_tags: - to_add.append(new_tags[i]) - - self.remove_tags(*to_remove) - self.add_tags(*to_add) - - def get_top_tags(self, limit = None): - """Returns a list of the most frequently used Tags on this object.""" - - doc = self._request(self.ws_prefix + '.getTopTags', True) - - elements = doc.getElementsByTagName('tag') - list = [] - - for element in elements: - if limit and len(list) >= limit: - break - tag_name = _extract(element, 'name') - tagcount = _extract(element, 'count') - - list.append(TopItem(Tag(tag_name, *self.auth_data), tagcount)) - - return list - -class ServiceException(Exception): - """Exception related to the Last.fm web service""" - - def __init__(self, lastfm_status, details): - self._lastfm_status = lastfm_status - self._details = details - - def __str__(self): - return self._details - - def get_id(self): - """Returns the exception ID, from one of the following: - STATUS_INVALID_SERVICE = 2 - STATUS_INVALID_METHOD = 3 - STATUS_AUTH_FAILED = 4 - STATUS_INVALID_FORMAT = 5 - STATUS_INVALID_PARAMS = 6 - STATUS_INVALID_RESOURCE = 7 - STATUS_TOKEN_ERROR = 8 - STATUS_INVALID_SK = 9 - STATUS_INVALID_API_KEY = 10 - STATUS_OFFLINE = 11 - STATUS_SUBSCRIBERS_ONLY = 12 - STATUS_TOKEN_UNAUTHORIZED = 14 - STATUS_TOKEN_EXPIRED = 15 - """ - - return self._lastfm_status - -class TopItem (object): - """A top item in a list that has a weight. Returned from functions like get_top_tracks() and get_top_artists().""" - - def __init__(self, item, weight): - object.__init__(self) - - self.item = item - self.weight = _number(weight) - - def __repr__(self): - return "Item: " + self.get_item().__repr__() + ", Weight: " + str(self.get_weight()) - - def get_item(self): - """Returns the item.""" - - return self.item - - def get_weight(self): - """Returns the weight of the itme in the list.""" - - return self.weight - - -class LibraryItem (object): - """An item in a User's Library. It could be an artist, an album or a track.""" - - def __init__(self, item, playcount, tagcount): - object.__init__(self) - - self.item = item - self.playcount = _number(playcount) - self.tagcount = _number(tagcount) - - def __repr__(self): - return "Item: " + self.get_item().__repr__() + ", Playcount: " + str(self.get_playcount()) + ", Tagcount: " + str(self.get_tagcount()) - - def get_item(self): - """Returns the itme.""" - - return self.item - - def get_playcount(self): - """Returns the item's playcount in the Library.""" - - return self.playcount - - def get_tagcount(self): - """Returns the item's tagcount in the Library.""" - - return self.tagcount - -class PlayedTrack (object): - """A track with a playback date.""" - - def __init__(self, track, date, timestamp): - object.__init__(self) - - self.track = track - self.date = date - self.timestamp = timestamp - - def __repr__(self): - return repr(self.track) + " played at " + self.date - - def get_track(self): - """Return the track.""" - - return self.track - - def get_item(self): - """Returns the played track. An alias to get_track().""" - - return self.get_track(); - - def get_date(self): - """Returns the playback date.""" - - return self.date - - def get_timestamp(self): - """Returns the unix timestamp of the playback date.""" - - return self.timestamp - + """Common functions for classes with tags.""" + + def __init__(self, ws_prefix): + self.ws_prefix = ws_prefix + + def add_tags(self, *tags): + """Adds one or several tags. + * *tags: Any number of tag names or Tag objects. + """ + + for tag in tags: + self._add_tag(tag) + + def _add_tag(self, tag): + """Adds one or several tags. + * tag: one tag name or a Tag object. + """ + + if isinstance(tag, Tag): + tag = tag.get_name() + + params = self._get_params() + params['tags'] = _unicode(tag) + + self._request(self.ws_prefix + '.addTags', False, params) + + def _remove_tag(self, single_tag): + """Remove a user's tag from this object.""" + + if isinstance(single_tag, Tag): + single_tag = single_tag.get_name() + + params = self._get_params() + params['tag'] = _unicode(single_tag) + + self._request(self.ws_prefix + '.removeTag', False, params) + + def get_tags(self): + """Returns a list of the tags set by the user to this object.""" + + # Uncacheable because it can be dynamically changed by the user. + params = self._get_params() + + doc = self._request(self.ws_prefix + '.getTags', False, params) + tag_names = _extract_all(doc, 'name') + tags = [] + for tag in tag_names: + tags.append(Tag(tag, self.network)) + + return tags + + def remove_tags(self, *tags): + """Removes one or several tags from this object. + * *tags: Any number of tag names or Tag objects. + """ + + for tag in tags: + self._remove_tag(tag) + + def clear_tags(self): + """Clears all the user-set tags. """ + + self.remove_tags(*(self.get_tags())) + + def set_tags(self, *tags): + """Sets this object's tags to only those tags. + * *tags: any number of tag names. + """ + + c_old_tags = [] + old_tags = [] + c_new_tags = [] + new_tags = [] + + to_remove = [] + to_add = [] + + tags_on_server = self.get_tags() + + for tag in tags_on_server: + c_old_tags.append(tag.get_name().lower()) + old_tags.append(tag.get_name()) + + for tag in tags: + c_new_tags.append(tag.lower()) + new_tags.append(tag) + + for i in range(0, len(old_tags)): + if not c_old_tags[i] in c_new_tags: + to_remove.append(old_tags[i]) + + for i in range(0, len(new_tags)): + if not c_new_tags[i] in c_old_tags: + to_add.append(new_tags[i]) + + self.remove_tags(*to_remove) + self.add_tags(*to_add) + + def get_top_tags(self, limit = None): + """Returns a list of the most frequently used Tags on this object.""" + + doc = self._request(self.ws_prefix + '.getTopTags', True) + + elements = doc.getElementsByTagName('tag') + seq = [] + + for element in elements: + if limit and len(seq) >= limit: + break + tag_name = _extract(element, 'name') + tagcount = _extract(element, 'count') + + seq.append(TopItem(Tag(tag_name, self.network), tagcount)) + + return seq + +class WSError(Exception): + """Exception related to the Network web service""" + + def __init__(self, network, status, details): + self.status = status + self.details = details + self.network = network + + @_string_output + def __str__(self): + return self.details + + def get_id(self): + """Returns the exception ID, from one of the following: + STATUS_INVALID_SERVICE = 2 + STATUS_INVALID_METHOD = 3 + STATUS_AUTH_FAILED = 4 + STATUS_INVALID_FORMAT = 5 + STATUS_INVALID_PARAMS = 6 + STATUS_INVALID_RESOURCE = 7 + STATUS_TOKEN_ERROR = 8 + STATUS_INVALID_SK = 9 + STATUS_INVALID_API_KEY = 10 + STATUS_OFFLINE = 11 + STATUS_SUBSCRIBERS_ONLY = 12 + STATUS_TOKEN_UNAUTHORIZED = 14 + STATUS_TOKEN_EXPIRED = 15 + """ + + return self.status class Album(_BaseObject, _Taggable): - """A Last.fm album.""" - - def __init__(self, artist, title, api_key, api_secret, session_key): - """ - Create an album instance. - # Parameters: - * artist str|Artist: An artist name or an Artist object. - * title str: The album title. - """ - - _BaseObject.__init__(self, api_key, api_secret, session_key) - _Taggable.__init__(self, 'album') - - if isinstance(artist, Artist): - self.artist = artist - else: - self.artist = Artist(artist, *self.auth_data) - - self.title = title - - def __repr__(self): - return self.get_artist().get_name() + ' - ' + self.get_title() - - def __eq__(self, other): - return (self.get_title().lower() == other.get_title().lower()) and (self.get_artist().get_name().lower() == other.get_artist().get_name().lower()) - - def __ne__(self, other): - return (self.get_title().lower() != other.get_title().lower()) or (self.get_artist().get_name().lower() != other.get_artist().get_name().lower()) - - def _get_params(self): - return {'artist': self.get_artist().get_name(), 'album': self.get_title(), } - - def get_artist(self): - """Returns the associated Artist object.""" - - return self.artist - - def get_title(self): - """Returns the album title.""" - - return self.title - - def get_name(self): - """Returns the album title (alias to Album.get_title).""" - - return self.get_title() - - def get_release_date(self): - """Retruns the release date of the album.""" - - return _extract(self._request("album.getInfo", cacheable = True), "releasedate") - - def get_image_url(self, size = IMAGE_EXTRA_LARGE): - """Returns the associated image URL. - # Parameters: - * size int: The image size. Possible values: - o IMAGE_EXTRA_LARGE - o IMAGE_LARGE - o IMAGE_MEDIUM - o IMAGE_SMALL - """ - - return _extract_all(self._request("album.getInfo", cacheable = True), 'image')[size] - - def get_id(self): - """Returns the Last.fm ID.""" - - return _extract(self._request("album.getInfo", cacheable = True), "id") - - def get_playcount(self): - """Returns the number of plays on Last.fm.""" - - return _number(_extract(self._request("album.getInfo", cacheable = True), "playcount")) - - def get_listener_count(self): - """Returns the number of liteners on Last.fm.""" - - return _number(_extract(self._request("album.getInfo", cacheable = True), "listeners")) - - def get_top_tags(self, limit = None): - """Returns a list of the most-applied tags to this album.""" - - # BROKEN: Web service is currently broken. - - return None - - def get_tracks(self): - """Returns the list of Tracks on this album.""" - - uri = 'lastfm://playlist/album/%s' %self.get_id() - - return XSPF(uri, *self.auth_data).get_tracks() - - def get_mbid(self): - """Returns the MusicBrainz id of the album.""" - - return _extract(self._request("album.getInfo", cacheable = True), "mbid") - - def get_url(self, domain_name = DOMAIN_ENGLISH): - """Returns the url of the album page on Last.fm. - # Parameters: - * domain_name str: Last.fm's language domain. Possible values: - o DOMAIN_ENGLISH - o DOMAIN_GERMAN - o DOMAIN_SPANISH - o DOMAIN_FRENCH - o DOMAIN_ITALIAN - o DOMAIN_POLISH - o DOMAIN_PORTUGUESE - o DOMAIN_SWEDISH - o DOMAIN_TURKISH - o DOMAIN_RUSSIAN - o DOMAIN_JAPANESE - o DOMAIN_CHINESE - """ - - url = 'http://%(domain)s/music/%(artist)s/%(album)s' - - artist = _get_url_safe(self.get_artist().get_name()) - album = _get_url_safe(self.get_title()) - - return url %{'domain': domain_name, 'artist': artist, 'album': album} + """An album.""" + + title = None + artist = None + + def __init__(self, artist, title, network): + """ + Create an album instance. + # Parameters: + * artist: An artist name or an Artist object. + * title: The album title. + """ + + _BaseObject.__init__(self, network) + _Taggable.__init__(self, 'album') + + if isinstance(artist, Artist): + self.artist = artist + else: + self.artist = Artist(artist, self.network) + + self.title = title + + @_string_output + def __repr__(self): + return u"%s - %s" %(self.get_artist().get_name(), self.get_title()) + + def __eq__(self, other): + return (self.get_title().lower() == other.get_title().lower()) and (self.get_artist().get_name().lower() == other.get_artist().get_name().lower()) + + def __ne__(self, other): + return (self.get_title().lower() != other.get_title().lower()) or (self.get_artist().get_name().lower() != other.get_artist().get_name().lower()) + + def _get_params(self): + return {'artist': self.get_artist().get_name(), 'album': self.get_title(), } + + def get_artist(self): + """Returns the associated Artist object.""" + + return self.artist + + def get_title(self): + """Returns the album title.""" + + return self.title + + def get_name(self): + """Returns the album title (alias to Album.get_title).""" + + return self.get_title() + + def get_release_date(self): + """Retruns the release date of the album.""" + + return _extract(self._request("album.getInfo", cacheable = True), "releasedate") + + def get_cover_image(self, size = COVER_EXTRA_LARGE): + """ + Returns a uri to the cover image + size can be one of: + COVER_MEGA + COVER_EXTRA_LARGE + COVER_LARGE + COVER_MEDIUM + COVER_SMALL + """ + + return _extract_all(self._request("album.getInfo", cacheable = True), 'image')[size] + + def get_id(self): + """Returns the ID""" + + return _extract(self._request("album.getInfo", cacheable = True), "id") + + def get_playcount(self): + """Returns the number of plays on the network""" + + return _number(_extract(self._request("album.getInfo", cacheable = True), "playcount")) + + def get_listener_count(self): + """Returns the number of liteners on the network""" + + return _number(_extract(self._request("album.getInfo", cacheable = True), "listeners")) + + def get_top_tags(self, limit=None): + """Returns a list of the most-applied tags to this album.""" + + doc = self._request("album.getInfo", True) + e = doc.getElementsByTagName("toptags")[0] + + seq = [] + for name in _extract_all(e, "name"): + if len(seq) < limit: + seq.append(Tag(name, self.network)) + + return seq + + def get_tracks(self): + """Returns the list of Tracks on this album.""" + + uri = 'lastfm://playlist/album/%s' %self.get_id() + + return XSPF(uri, self.network).get_tracks() + + def get_mbid(self): + """Returns the MusicBrainz id of the album.""" + + return _extract(self._request("album.getInfo", cacheable = True), "mbid") + + def get_url(self, domain_name = DOMAIN_ENGLISH): + """Returns the url of the album page on the network. + # Parameters: + * domain_name str: The network's language domain. Possible values: + o DOMAIN_ENGLISH + o DOMAIN_GERMAN + o DOMAIN_SPANISH + o DOMAIN_FRENCH + o DOMAIN_ITALIAN + o DOMAIN_POLISH + o DOMAIN_PORTUGUESE + o DOMAIN_SWEDISH + o DOMAIN_TURKISH + o DOMAIN_RUSSIAN + o DOMAIN_JAPANESE + o DOMAIN_CHINESE + """ + + artist = _url_safe(self.get_artist().get_name()) + album = _url_safe(self.get_title()) + + return self.network._get_url(domain_name, "album") %{'artist': artist, 'album': album} + + def get_wiki_published_date(self): + """Returns the date of publishing this version of the wiki.""" + + doc = self._request("album.getInfo", True) + + if len(doc.getElementsByTagName("wiki")) == 0: + return + + node = doc.getElementsByTagName("wiki")[0] + + return _extract(node, "published") + + def get_wiki_summary(self): + """Returns the summary of the wiki.""" + + doc = self._request("album.getInfo", True) + + if len(doc.getElementsByTagName("wiki")) == 0: + return + + node = doc.getElementsByTagName("wiki")[0] + + return _extract(node, "summary") + + def get_wiki_content(self): + """Returns the content of the wiki.""" + + doc = self._request("album.getInfo", True) + + if len(doc.getElementsByTagName("wiki")) == 0: + return + + node = doc.getElementsByTagName("wiki")[0] + + return _extract(node, "content") class Artist(_BaseObject, _Taggable): - """A Last.fm artist.""" - - def __init__(self, name, api_key, api_secret, session_key): - """Create an artist object. - # Parameters: - * name str: The artist's name. - """ - - _BaseObject.__init__(self, api_key, api_secret, session_key) - _Taggable.__init__(self, 'artist') - - self.name = name - - def __repr__(self): - return unicode(self.get_name()) - - def __eq__(self, other): - return self.get_name().lower() == other.get_name().lower() - - def __ne__(self, other): - return self.get_name().lower() != other.get_name().lower() - - def _get_params(self): - return {'artist': self.get_name()} - - def get_name(self): - """Returns the name of the artist.""" - - return self.name - - def get_image_url(self, size = IMAGE_LARGE): - """Returns the associated image URL. - # Parameters: - * size int: The image size. Possible values: - o IMAGE_LARGE - o IMAGE_MEDIUM - o IMAGE_SMALL - """ - - return _extract_all(self._request("artist.getInfo", True), "image")[size] - - def get_square_image(self): - return _extract_all(self._request("artist.getImages", True), "size")[2] - - def get_playcount(self): - """Returns the number of plays on Last.fm.""" - - return _number(_extract(self._request("artist.getInfo", True), "playcount")) - - def get_mbid(self): - """Returns the MusicBrainz ID of this artist.""" - - doc = self._request("artist.getInfo", True) - - return _extract(doc, "mbid") - - def get_listener_count(self): - """Returns the number of liteners on Last.fm.""" - - return _number(_extract(self._request("artist.getInfo", True), "listeners")) - - def is_streamable(self): - """Returns True if the artist is streamable.""" - - return bool(_number(_extract(self._request("artist.getInfo", True), "streamable"))) - - def get_bio_published_date(self): - """Returns the date on which the artist's biography was published.""" - - return _extract(self._request("artist.getInfo", True), "published") - - def get_bio_summary(self): - """Returns the summary of the artist's biography.""" - - return _extract(self._request("artist.getInfo", True), "summary") - - def get_bio_content(self): - """Returns the content of the artist's biography.""" - - return _extract(self._request("artist.getInfo", True), "content") - - def get_upcoming_events(self): - """Returns a list of the upcoming Events for this artist.""" - - doc = self._request('artist.getEvents', True) - - ids = _extract_all(doc, 'id') - - events = [] - for id in ids: - events.append(Event(id, *self.auth_data)) - - return events - - def get_similar(self, limit = None): - """Returns the similar artists on Last.fm.""" - - params = self._get_params() - if limit: - params['limit'] = unicode(limit) - - doc = self._request('artist.getSimilar', True, params) - - names = _extract_all(doc, 'name') - - artists = [] - for name in names: - artists.append(Artist(name, *self.auth_data)) - - return artists - -# def get_top_tags(self): -# """Returns the top tags.""" -# -# doc = self._request("artist.getInfo", True) -# -# list = [] -# -# elements = doc.getElementsByTagName('tag') -# -# for element in elements: -# -# name = _extract(element, 'name') -# -# list.append(name) -# -# return list - - def get_top_tags(self): - """Returns the top tags.""" - - doc = self._request("artist.getTopTags", True) - - list = [] - - elements = doc.getElementsByTagName('tag') - - for element in elements: - - name = _extract(element, 'name') - - list.append(name) - - return list - - def get_top_albums(self): - """Retuns a list of the top albums.""" - - doc = self._request('artist.getTopAlbums', True) - - list = [] - - for node in doc.getElementsByTagName("album"): - name = _extract(node, "name") - artist = _extract(node, "name", 1) - playcount = _extract(node, "playcount") - - list.append(TopItem(Album(artist, name, *self.auth_data), playcount)) - - return list - - def get_top_tracks(self): - """Returns a list of the most played Tracks by this artist.""" - - doc = self._request("artist.getTopTracks", True) - - list = [] - for track in doc.getElementsByTagName('track'): - - title = _extract(track, "name") - artist = _extract(track, "name", 1) - playcount = _number(_extract(track, "playcount")) - - list.append( TopItem(Track(artist, title, *self.auth_data), playcount) ) - - return list - - def get_top_fans(self, limit = None): - """Returns a list of the Users who played this artist the most. - # Parameters: - * limit int: Max elements. - """ - - params = self._get_params() - doc = self._request('artist.getTopFans', True) - - list = [] - - elements = doc.getElementsByTagName('user') - - for element in elements: - if limit and len(list) >= limit: - break - - name = _extract(element, 'name') - weight = _number(_extract(element, 'weight')) - - list.append(TopItem(User(name, *self.auth_data), weight)) - - return list - - def share(self, users, message = None): - """Shares this artist (sends out recommendations). - # Parameters: - * users [User|str,]: A list that can contain usernames, emails, User objects, or all of them. - * message str: A message to include in the recommendation message. - """ - - #last.fm currently accepts a max of 10 recipient at a time - while(len(users) > 10): - section = users[0:9] - users = users[9:] - self.share(section, message) - - nusers = [] - for user in users: - if isinstance(user, User): - nusers.append(user.get_name()) - else: - nusers.append(user) - - params = self._get_params() - recipients = ','.join(nusers) - params['recipient'] = recipients - if message: params['message'] = unicode(message) - - self._request('artist.share', False, params) - info(repr(self) + " was shared with " + repr(users)) - - def get_url(self, domain_name = DOMAIN_ENGLISH): - """Returns the url of the artist page on Last.fm. - # Parameters: - * domain_name: Last.fm's language domain. Possible values: - o DOMAIN_ENGLISH - o DOMAIN_GERMAN - o DOMAIN_SPANISH - o DOMAIN_FRENCH - o DOMAIN_ITALIAN - o DOMAIN_POLISH - o DOMAIN_PORTUGUESE - o DOMAIN_SWEDISH - o DOMAIN_TURKISH - o DOMAIN_RUSSIAN - o DOMAIN_JAPANESE - o DOMAIN_CHINESE - """ - - url = 'http://%(domain)s/music/%(artist)s' - - artist = _get_url_safe(self.get_name()) - - return url %{'domain': domain_name, 'artist': artist} - + """An artist.""" + + name = None + + def __init__(self, name, network): + """Create an artist object. + # Parameters: + * name str: The artist's name. + """ + + _BaseObject.__init__(self, network) + _Taggable.__init__(self, 'artist') + + self.name = name + + @_string_output + def __repr__(self): + return self.get_name() + + def __eq__(self, other): + return self.get_name().lower() == other.get_name().lower() + + def __ne__(self, other): + return self.get_name().lower() != other.get_name().lower() + + def _get_params(self): + return {'artist': self.get_name()} + + def get_name(self): + """Returns the name of the artist.""" + + return self.name + + def get_cover_image(self, size = COVER_LARGE): + """ + Returns a uri to the cover image + size can be one of: + COVER_MEGA + COVER_EXTRA_LARGE + COVER_LARGE + COVER_MEDIUM + COVER_SMALL + """ + + return _extract_all(self._request("artist.getInfo", True), "image")[size] + + def get_playcount(self): + """Returns the number of plays on the network.""" + + return _number(_extract(self._request("artist.getInfo", True), "playcount")) + + def get_mbid(self): + """Returns the MusicBrainz ID of this artist.""" + + doc = self._request("artist.getInfo", True) + + return _extract(doc, "mbid") + + def get_listener_count(self): + """Returns the number of liteners on the network.""" + + return _number(_extract(self._request("artist.getInfo", True), "listeners")) + + def is_streamable(self): + """Returns True if the artist is streamable.""" + + return bool(_number(_extract(self._request("artist.getInfo", True), "streamable"))) + + def get_bio_published_date(self): + """Returns the date on which the artist's biography was published.""" + + return _extract(self._request("artist.getInfo", True), "published") + + def get_bio_summary(self): + """Returns the summary of the artist's biography.""" + + return _extract(self._request("artist.getInfo", True), "summary") + + def get_bio_content(self): + """Returns the content of the artist's biography.""" + + return _extract(self._request("artist.getInfo", True), "content") + + def get_upcoming_events(self): + """Returns a list of the upcoming Events for this artist.""" + + doc = self._request('artist.getEvents', True) + + ids = _extract_all(doc, 'id') + + events = [] + for e_id in ids: + events.append(Event(e_id, self.network)) + + return events + + def get_similar(self, limit = None): + """Returns the similar artists on the network.""" + + params = self._get_params() + if limit: + params['limit'] = _unicode(limit) + + doc = self._request('artist.getSimilar', True, params) + + names = _extract_all(doc, "name") + matches = _extract_all(doc, "match") + + artists = [] + for i in range(0, len(names)): + artists.append(SimilarItem(Artist(names[i], self.network), _number(matches[i]))) + + return artists + + def get_top_albums(self): + """Retuns a list of the top albums.""" + + doc = self._request('artist.getTopAlbums', True) + + seq = [] + + for node in doc.getElementsByTagName("album"): + name = _extract(node, "name") + artist = _extract(node, "name", 1) + playcount = _extract(node, "playcount") + + seq.append(TopItem(Album(artist, name, self.network), playcount)) + + return seq + + def get_top_tracks(self): + """Returns a list of the most played Tracks by this artist.""" + + doc = self._request("artist.getTopTracks", True) + + seq = [] + for track in doc.getElementsByTagName('track'): + + title = _extract(track, "name") + artist = _extract(track, "name", 1) + playcount = _number(_extract(track, "playcount")) + + seq.append( TopItem(Track(artist, title, self.network), playcount) ) + + return seq + + def get_top_fans(self, limit = None): + """Returns a list of the Users who played this artist the most. + # Parameters: + * limit int: Max elements. + """ + + doc = self._request('artist.getTopFans', True) + + seq = [] + + elements = doc.getElementsByTagName('user') + + for element in elements: + if limit and len(seq) >= limit: + break + + name = _extract(element, 'name') + weight = _number(_extract(element, 'weight')) + + seq.append(TopItem(User(name, self.network), weight)) + + return seq + + def share(self, users, message = None): + """Shares this artist (sends out recommendations). + # Parameters: + * users [User|str,]: A list that can contain usernames, emails, User objects, or all of them. + * message str: A message to include in the recommendation message. + """ + + #last.fm currently accepts a max of 10 recipient at a time + while(len(users) > 10): + section = users[0:9] + users = users[9:] + self.share(section, message) + + nusers = [] + for user in users: + if isinstance(user, User): + nusers.append(user.get_name()) + else: + nusers.append(user) + + params = self._get_params() + recipients = ','.join(nusers) + params['recipient'] = recipients + if message: params['message'] = _unicode(message) + + self._request('artist.share', False, params) + + def get_url(self, domain_name = DOMAIN_ENGLISH): + """Returns the url of the artist page on the network. + # Parameters: + * domain_name: The network's language domain. Possible values: + o DOMAIN_ENGLISH + o DOMAIN_GERMAN + o DOMAIN_SPANISH + o DOMAIN_FRENCH + o DOMAIN_ITALIAN + o DOMAIN_POLISH + o DOMAIN_PORTUGUESE + o DOMAIN_SWEDISH + o DOMAIN_TURKISH + o DOMAIN_RUSSIAN + o DOMAIN_JAPANESE + o DOMAIN_CHINESE + """ + + artist = _url_safe(self.get_name()) + + return self.network._get_url(domain_name, "artist") %{'artist': artist} + + def get_images(self, order=IMAGES_ORDER_POPULARITY, limit=None): + """ + Returns a sequence of Image objects + if limit is None it will return all + order can be IMAGES_ORDER_POPULARITY or IMAGES_ORDER_DATE + """ + + images = [] + + params = self._get_params() + params["order"] = order + nodes = _collect_nodes(limit, self, "artist.getImages", True, params) + for e in nodes: + if _extract(e, "name"): + user = User(_extract(e, "name"), self.network) + else: + user = None + + images.append(Image( + _extract(e, "title"), + _extract(e, "url"), + _extract(e, "dateadded"), + _extract(e, "format"), + user, + ImageSizes(*_extract_all(e, "size")), + (_extract(e, "thumbsup"), _extract(e, "thumbsdown")) + ) + ) + return images + + def get_shouts(self, limit=50): + """ + Returns a sequqence of Shout objects + """ + + shouts = [] + for node in _collect_nodes(limit, self, "artist.getShouts", False): + shouts.append(Shout( + _extract(node, "body"), + User(_extract(node, "author"), self.network), + _extract(node, "date") + ) + ) + return shouts + + def shout(self, message): + """ + Post a shout + """ + + params = self._get_params() + params["message"] = message + + self._request("artist.Shout", False, params) + class Event(_BaseObject): - """A Last.fm event.""" - - def __init__(self, event_id, api_key, api_secret, session_key): - _BaseObject.__init__(self, api_key, api_secret, session_key) - - self.id = unicode(event_id) - - def __repr__(self): - return "Event #" + self.get_id() - - def __eq__(self, other): - return self.get_id() == other.get_id() - - def __ne__(self, other): - return self.get_id() != other.get_id() - - def _get_params(self): - return {'event': self.get_id()} - - def attend(self, attending_status): - """Sets the attending status. - * attending_status: The attending status. Possible values: - o EVENT_ATTENDING - o EVENT_MAYBE_ATTENDING - o EVENT_NOT_ATTENDING - """ - - params = self._get_params() - params['status'] = unicode(attending_status) - - doc = self._request('event.attend', False, params) - info("Attendance to " + repr(self) + " was set to " + repr(attending_status)) - - def get_id(self): - """Returns the id of the event on Last.fm. """ - return self.id - - def get_title(self): - """Returns the title of the event. """ - - doc = self._request("event.getInfo", True) - - return _extract(doc, "title") - - def get_headliner(self): - """Returns the headliner of the event. """ - - doc = self._request("event.getInfo", True) - - return Artist(_extract(doc, "headliner"), *self.auth_data) - - def get_artists(self): - """Returns a list of the participating Artists. """ - - doc = self._request("event.getInfo", True) - names = _extract_all(doc, "artist") - - artists = [] - for name in names: - artists.append(Artist(name, *self.auth_data)) - - return artists - - def get_venue(self): - """Returns the venue where the event is held.""" - - doc = self._request("event.getInfo", True) - - venue_url = _extract(doc, "url") - venue_id = _number(venue_url[venue_url.rfind("/") + 1:]) - - return Venue(venue_id, *self.auth_data) - - def get_start_date(self): - """Returns the date when the event starts.""" - - doc = self._request("event.getInfo", True) - - return _extract(doc, "startDate") - - def get_description(self): - """Returns the description of the event. """ - - doc = self._request("event.getInfo", True) - - return _extract(doc, "description") - - def get_image_url(self, size = IMAGE_LARGE): - """Returns the associated image URL. - * size: The image size. Possible values: - o IMAGE_LARGE - o IMAGE_MEDIUM - o IMAGE_SMALL - """ - - doc = self._request("event.getInfo", True) - - return _extract_all(doc, "image")[size] - - def get_attendance_count(self): - """Returns the number of attending people. """ - - doc = self._request("event.getInfo", True) - - return _number(_extract(doc, "attendance")) - - def get_review_count(self): - """Returns the number of available reviews for this event. """ - - doc = self._request("event.getInfo", True) - - return _number(_extract(doc, "reviews")) - - def get_url(self, domain_name = DOMAIN_ENGLISH): - """Returns the url of the event page on Last.fm. - * domain_name: Last.fm's language domain. Possible values: - o DOMAIN_ENGLISH - o DOMAIN_GERMAN - o DOMAIN_SPANISH - o DOMAIN_FRENCH - o DOMAIN_ITALIAN - o DOMAIN_POLISH - o DOMAIN_PORTUGUESE - o DOMAIN_SWEDISH - o DOMAIN_TURKISH - o DOMAIN_RUSSIAN - o DOMAIN_JAPANESE - o DOMAIN_CHINESE - """ - - url = 'http://%(domain)s/event/%(id)s' - - return url %{'domain': domain_name, 'id': self.get_id()} - - def share(self, users, message = None): - """Shares this event (sends out recommendations). - * users: A list that can contain usernames, emails, User objects, or all of them. - * message: A message to include in the recommendation message. - """ - - #last.fm currently accepts a max of 10 recipient at a time - while(len(users) > 10): - section = users[0:9] - users = users[9:] - self.share(section, message) - - nusers = [] - for user in users: - if isinstance(user, User): - nusers.append(user.get_name()) - else: - nusers.append(user) - - params = self._get_params() - recipients = ','.join(nusers) - params['recipient'] = recipients - if message: params['message'] = unicode(message) - - self._request('event.share', False, params) - info(repr(self) + " was shared with " + repr(users)) - + """An event.""" + + id = None + + def __init__(self, event_id, network): + _BaseObject.__init__(self, network) + + self.id = _unicode(event_id) + + @_string_output + def __repr__(self): + return "Event #" + self.get_id() + + def __eq__(self, other): + return self.get_id() == other.get_id() + + def __ne__(self, other): + return self.get_id() != other.get_id() + + def _get_params(self): + return {'event': self.get_id()} + + def attend(self, attending_status): + """Sets the attending status. + * attending_status: The attending status. Possible values: + o EVENT_ATTENDING + o EVENT_MAYBE_ATTENDING + o EVENT_NOT_ATTENDING + """ + + params = self._get_params() + params['status'] = _unicode(attending_status) + + self._request('event.attend', False, params) + + def get_attendees(self): + """ + Get a list of attendees for an event + """ + + doc = self._request("event.getAttendees", False) + + users = [] + for name in _extract_all(doc, "name"): + users.append(User(name, self.network)) + + return users + + def get_id(self): + """Returns the id of the event on the network. """ + + return self.id + + def get_title(self): + """Returns the title of the event. """ + + doc = self._request("event.getInfo", True) + + return _extract(doc, "title") + + def get_headliner(self): + """Returns the headliner of the event. """ + + doc = self._request("event.getInfo", True) + + return Artist(_extract(doc, "headliner"), self.network) + + def get_artists(self): + """Returns a list of the participating Artists. """ + + doc = self._request("event.getInfo", True) + names = _extract_all(doc, "artist") + + artists = [] + for name in names: + artists.append(Artist(name, self.network)) + + return artists + + def get_venue(self): + """Returns the venue where the event is held.""" + + doc = self._request("event.getInfo", True) + + v = doc.getElementsByTagName("venue")[0] + venue_id = _number(_extract(v, "id")) + + return Venue(venue_id, self.network) + + def get_start_date(self): + """Returns the date when the event starts.""" + + doc = self._request("event.getInfo", True) + + return _extract(doc, "startDate") + + def get_description(self): + """Returns the description of the event. """ + + doc = self._request("event.getInfo", True) + + return _extract(doc, "description") + + def get_cover_image(self, size = COVER_LARGE): + """ + Returns a uri to the cover image + size can be one of: + COVER_MEGA + COVER_EXTRA_LARGE + COVER_LARGE + COVER_MEDIUM + COVER_SMALL + """ + + doc = self._request("event.getInfo", True) + + return _extract_all(doc, "image")[size] + + def get_attendance_count(self): + """Returns the number of attending people. """ + + doc = self._request("event.getInfo", True) + + return _number(_extract(doc, "attendance")) + + def get_review_count(self): + """Returns the number of available reviews for this event. """ + + doc = self._request("event.getInfo", True) + + return _number(_extract(doc, "reviews")) + + def get_url(self, domain_name = DOMAIN_ENGLISH): + """Returns the url of the event page on the network. + * domain_name: The network's language domain. Possible values: + o DOMAIN_ENGLISH + o DOMAIN_GERMAN + o DOMAIN_SPANISH + o DOMAIN_FRENCH + o DOMAIN_ITALIAN + o DOMAIN_POLISH + o DOMAIN_PORTUGUESE + o DOMAIN_SWEDISH + o DOMAIN_TURKISH + o DOMAIN_RUSSIAN + o DOMAIN_JAPANESE + o DOMAIN_CHINESE + """ + + return self.network._get_url(domain_name, "event") %{'id': self.get_id()} + + def share(self, users, message = None): + """Shares this event (sends out recommendations). + * users: A list that can contain usernames, emails, User objects, or all of them. + * message: A message to include in the recommendation message. + """ + + #last.fm currently accepts a max of 10 recipient at a time + while(len(users) > 10): + section = users[0:9] + users = users[9:] + self.share(section, message) + + nusers = [] + for user in users: + if isinstance(user, User): + nusers.append(user.get_name()) + else: + nusers.append(user) + + params = self._get_params() + recipients = ','.join(nusers) + params['recipient'] = recipients + if message: params['message'] = _unicode(message) + + self._request('event.share', False, params) + + def get_shouts(self, limit=50): + """ + Returns a sequqence of Shout objects + """ + + shouts = [] + for node in _collect_nodes(limit, self, "event.getShouts", False): + shouts.append(Shout( + _extract(node, "body"), + User(_extract(node, "author"), self.network), + _extract(node, "date") + ) + ) + return shouts + + def shout(self, message): + """ + Post a shout + """ + + params = self._get_params() + params["message"] = message + + self._request("event.Shout", False, params) + class Country(_BaseObject): - """A country at Last.fm.""" - - def __init__(self, name, api_key, api_secret, session_key): - _BaseObject.__init__(self, api_key, api_secret, session_key) - - self.name = name - - def __repr__(self): - return self.get_name() - - def __eq__(self, other): - self.get_name().lower() == other.get_name().lower() - - def __ne__(self, other): - self.get_name() != other.get_name() - - def _get_params(self): - return {'country': self.get_name()} - - def _get_name_from_code(self, alpha2code): - # TODO: Have this function lookup the alpha-2 code and return the country name. - - return alpha2code - - def get_name(self): - """Returns the country name. """ - - return self.name - - def get_top_artists(self): - """Returns a sequence of the most played artists.""" - - doc = self._request('geo.getTopArtists', True) - - list = [] - for node in doc.getElementsByTagName("artist"): - name = _extract(node, 'name') - playcount = _extract(node, "playcount") - - list.append(TopItem(Artist(name, *self.auth_data), playcount)) - - return list - - def get_top_tracks(self): - """Returns a sequence of the most played tracks""" - - doc = self._request("geo.getTopTracks", True) - - list = [] - - for n in doc.getElementsByTagName('track'): - - title = _extract(n, 'name') - artist = _extract(n, 'name', 1) - playcount = _number(_extract(n, "playcount")) - - list.append( TopItem(Track(artist, title, *self.auth_data), playcount)) - - return list - - def get_url(self, domain_name = DOMAIN_ENGLISH): - """Returns the url of the event page on Last.fm. - * domain_name: Last.fm's language domain. Possible values: - o DOMAIN_ENGLISH - o DOMAIN_GERMAN - o DOMAIN_SPANISH - o DOMAIN_FRENCH - o DOMAIN_ITALIAN - o DOMAIN_POLISH - o DOMAIN_PORTUGUESE - o DOMAIN_SWEDISH - o DOMAIN_TURKISH - o DOMAIN_RUSSIAN - o DOMAIN_JAPANESE - o DOMAIN_CHINESE - """ - - url = 'http://%(domain)s/place/%(country_name)s' - - country_name = _get_url_safe(self.get_name()) - - return url %{'domain': domain_name, 'country_name': country_name} + """A country at Last.fm.""" + + name = None + + def __init__(self, name, network): + _BaseObject.__init__(self, network) + + self.name = name + + @_string_output + def __repr__(self): + return self.get_name() + + def __eq__(self, other): + return self.get_name().lower() == other.get_name().lower() + + def __ne__(self, other): + return self.get_name() != other.get_name() + + def _get_params(self): + return {'country': self.get_name()} + + def _get_name_from_code(self, alpha2code): + # TODO: Have this function lookup the alpha-2 code and return the country name. + + return alpha2code + + def get_name(self): + """Returns the country name. """ + + return self.name + + def get_top_artists(self): + """Returns a sequence of the most played artists.""" + + doc = self._request('geo.getTopArtists', True) + + seq = [] + for node in doc.getElementsByTagName("artist"): + name = _extract(node, 'name') + playcount = _extract(node, "playcount") + + seq.append(TopItem(Artist(name, self.network), playcount)) + + return seq + + def get_top_tracks(self): + """Returns a sequence of the most played tracks""" + + doc = self._request("geo.getTopTracks", True) + + seq = [] + + for n in doc.getElementsByTagName('track'): + + title = _extract(n, 'name') + artist = _extract(n, 'name', 1) + playcount = _number(_extract(n, "playcount")) + + seq.append( TopItem(Track(artist, title, self.network), playcount)) + + return seq + + def get_url(self, domain_name = DOMAIN_ENGLISH): + """Returns the url of the event page on the network. + * domain_name: The network's language domain. Possible values: + o DOMAIN_ENGLISH + o DOMAIN_GERMAN + o DOMAIN_SPANISH + o DOMAIN_FRENCH + o DOMAIN_ITALIAN + o DOMAIN_POLISH + o DOMAIN_PORTUGUESE + o DOMAIN_SWEDISH + o DOMAIN_TURKISH + o DOMAIN_RUSSIAN + o DOMAIN_JAPANESE + o DOMAIN_CHINESE + """ + + country_name = _url_safe(self.get_name()) + + return self.network._get_url(domain_name, "country") %{'country_name': country_name} class Library(_BaseObject): - """A user's Last.fm library.""" - - def __init__(self, user, api_key, api_secret, session_key): - _BaseObject.__init__(self, api_key, api_secret, session_key) - - if isinstance(user, User): - self.user = user - else: - self.user = User(user, *self.auth_data) - - self._albums_index = 0 - self._artists_index = 0 - self._tracks_index = 0 - - def __repr__(self): - return self.get_user().__repr__() + "'s Library" - - def _get_params(self): - return {'user': self.user.get_name()} - - def get_user(self): - """Returns the user who owns this library.""" - - return self.user - - def add_album(self, album): - """Add an album to this library.""" - - params = self._get_params() - params["artist"] = album.get_artist.get_name() - params["album"] = album.get_name() - - self._request("library.addAlbum", False, params) - info(repr(album) + " was added to " + repr(self)) - - def add_artist(self, artist): - """Add an artist to this library.""" - - params = self._get_params() - params["artist"] = artist.get_name() - - self._request("library.addArtist", False, params) - info(repr(artist) + " was added to " + repr(self)) - - def add_track(self, track): - """Add a track to this library.""" - - params = self._get_prams() - params["track"] = track.get_title() - - self._request("library.addTrack", False, params) - info(repr(track) + " was added to " + repr(self)) - - def _get_albums_pagecount(self): - """Returns the number of album pages in this library.""" - - doc = self._request("library.getAlbums", True) - - return _number(doc.getElementsByTagName("albums")[0].getAttribute("totalPages")) - - def is_end_of_albums(self): - """Returns True when the last page of albums has ben retrieved.""" - - if self._albums_index >= self._get_albums_pagecount(): - return True - else: - return False - - def _get_artists_pagecount(self): - """Returns the number of artist pages in this library.""" - - doc = self._request("library.getArtists", True) - - return _number(doc.getElementsByTagName("artists")[0].getAttribute("totalPages")) - - def is_end_of_artists(self): - """Returns True when the last page of artists has ben retrieved.""" - - if self._artists_index >= self._get_artists_pagecount(): - return True - else: - return False - - def _get_tracks_pagecount(self): - """Returns the number of track pages in this library.""" - - doc = self._request("library.getTracks", True) - - return _number(doc.getElementsByTagName("tracks")[0].getAttribute("totalPages")) - - def is_end_of_tracks(self): - """Returns True when the last page of tracks has ben retrieved.""" - - if self._tracks_index >= self._get_tracks_pagecount(): - return True - else: - return False - - def get_albums_page(self): - """Retreives the next page of albums in the Library. Returns a sequence of TopItem objects. - Use the function extract_items like extract_items(Library.get_albums_page()) to return only a sequence of - Album objects with no extra data. - - Example: - ------- - library = Library("rj", API_KEY, API_SECRET, SESSION_KEY) - - while not library.is_end_of_albums(): - print library.get_albums_page() - """ - - self._albums_index += 1 - - params = self._get_params() - params["page"] = str(self._albums_index) - - list = [] - doc = self._request("library.getAlbums", True, params) - for node in doc.getElementsByTagName("album"): - name = _extract(node, "name") - artist = _extract(node, "name", 1) - playcount = _number(_extract(node, "playcount")) - tagcount = _number(_extract(node, "tagcount")) - - list.append(LibraryItem(Album(artist, name, *self.auth_data), playcount, tagcount)) - - return list - - def get_artists_page(self): - """Retreives the next page of artists in the Library. Returns a sequence of TopItem objects. - Use the function extract_items like extract_items(Library.get_artists_page()) to return only a sequence of - Artist objects with no extra data. - - Example: - ------- - library = Library("rj", API_KEY, API_SECRET, SESSION_KEY) - - while not library.is_end_of_artists(): - print library.get_artists_page() - """ - - self._artists_index += 1 - - params = self._get_params() - params["page"] = str(self._artists_index) - - list = [] - doc = self._request("library.getArtists", True, params) - for node in doc.getElementsByTagName("artist"): - name = _extract(node, "name") - - playcount = _number(_extract(node, "playcount")) - tagcount = _number(_extract(node, "tagcount")) - - list.append(LibraryItem(Artist(name, *self.auth_data), playcount, tagcount)) - - return list - - def get_tracks_page(self): - """Retreives the next page of tracks in the Library. Returns a sequence of TopItem objects. - Use the function extract_items like extract_items(Library.get_tracks_page()) to return only a sequence of - Track objects with no extra data. - - Example: - ------- - library = Library("rj", API_KEY, API_SECRET, SESSION_KEY) - - while not library.is_end_of_tracks(): - print library.get_tracks_page() - """ - - self._tracks_index += 1 - - params = self._get_params() - params["page"] = str(self._tracks_index) - - list = [] - doc = self._request("library.getTracks", True, params) - for node in doc.getElementsByTagName("track"): - name = _extract(node, "name") - artist = _extract(node, "name", 1) - playcount = _number(_extract(node, "playcount")) - tagcount = _number(_extract(node, "tagcount")) - - list.append(LibraryItem(Track(artist, name, *self.auth_data), playcount, tagcount)) - - return list - + """A user's Last.fm library.""" + + user = None + + def __init__(self, user, network): + _BaseObject.__init__(self, network) + + if isinstance(user, User): + self.user = user + else: + self.user = User(user, self.network) + + self._albums_index = 0 + self._artists_index = 0 + self._tracks_index = 0 + + @_string_output + def __repr__(self): + return repr(self.get_user()) + "'s Library" + + def _get_params(self): + return {'user': self.user.get_name()} + + def get_user(self): + """Returns the user who owns this library.""" + + return self.user + + def add_album(self, album): + """Add an album to this library.""" + + params = self._get_params() + params["artist"] = album.get_artist.get_name() + params["album"] = album.get_name() + + self._request("library.addAlbum", False, params) + + def add_artist(self, artist): + """Add an artist to this library.""" + + params = self._get_params() + params["artist"] = artist.get_name() + + self._request("library.addArtist", False, params) + + def add_track(self, track): + """Add a track to this library.""" + + params = self._get_params() + params["track"] = track.get_title() + + self._request("library.addTrack", False, params) + + def get_albums(self, limit=50): + """ + Returns a sequence of Album objects + if limit==None it will return all (may take a while) + """ + + seq = [] + for node in _collect_nodes(limit, self, "library.getAlbums", True): + name = _extract(node, "name") + artist = _extract(node, "name", 1) + playcount = _number(_extract(node, "playcount")) + tagcount = _number(_extract(node, "tagcount")) + + seq.append(LibraryItem(Album(artist, name, self.network), playcount, tagcount)) + + return seq + + def get_artists(self, limit=50): + """ + Returns a sequence of Album objects + if limit==None it will return all (may take a while) + """ + + seq = [] + for node in _collect_nodes(limit, self, "library.getArtists", True): + name = _extract(node, "name") + + playcount = _number(_extract(node, "playcount")) + tagcount = _number(_extract(node, "tagcount")) + + seq.append(LibraryItem(Artist(name, self.network), playcount, tagcount)) + + return seq + + def get_tracks(self, limit=50): + """ + Returns a sequence of Album objects + if limit==None it will return all (may take a while) + """ + + seq = [] + for node in _collect_nodes(limit, self, "library.getTracks", True): + name = _extract(node, "name") + artist = _extract(node, "name", 1) + playcount = _number(_extract(node, "playcount")) + tagcount = _number(_extract(node, "tagcount")) + + seq.append(LibraryItem(Track(artist, name, self.network), playcount, tagcount)) + + return seq + class Playlist(_BaseObject): - """A Last.fm user playlist.""" - - def __init__(self, user, id, api_key, api_secret, session_key): - _BaseObject.__init__(self, api_key, api_secret, session_key) - - if isinstance(user, User): - self.user = user - else: - self.user = User(user, *self.auth_data) - - self.id = unicode(id) - - def __repr__(self): - return repr(self.user) + "'s playlist # " + repr(self.id) - - def _get_info_node(self): - """Returns the node from user.getPlaylists where this playlist's info is.""" - - doc = self._request("user.getPlaylists", True) - - for node in doc.getElementsByTagName("playlist"): - if _extract(node, "id") == str(self.get_id()): - return node - - def _get_params(self): - return {'user': self.user.get_name(), 'playlistID': self.get_id()} - - def get_id(self): - """Returns the playlist id.""" - - return self.id - - def get_user(self): - """Returns the owner user of this playlist.""" - - return self.user - - def get_tracks(self): - """Returns a list of the tracks on this user playlist.""" - - uri = u'lastfm://playlist/%s' %self.get_id() - - return XSPF(uri, *self.auth_data).get_tracks() - - def add_track(self, track): - """Adds a Track to this Playlist.""" - - params = self._get_params() - params['artist'] = track.get_artist().get_name() - params['track'] = track.get_title() - - self._request('playlist.addTrack', False, params) - info(repr(track) + " was added to " + repr(self)) - - def get_title(self): - """Returns the title of this playlist.""" - - return _extract(self._get_info_node(), "title") - - def get_creation_date(self): - """Returns the creation date of this playlist.""" - - return _extract(self._get_info_node(), "date") - - def get_size(self): - """Returns the number of tracks in this playlist.""" - - return _number(_extract(self._get_info_node(), "size")) - - def get_description(self): - """Returns the description of this playlist.""" - - return _extract(self._get_info_node(), "description") - - def get_duration(self): - """Returns the duration of this playlist in milliseconds.""" - - return _number(_extract(self._get_info_node(), "duration")) - - def is_streamable(self): - """Returns True if the playlist is streamable. - For a playlist to be streamable, it needs at least 45 tracks by 15 different artists.""" - - if _extract(self._get_info_node(), "streamable") == '1': - return True - else: - return False - - def has_track(self, track): - """Checks to see if track is already in the playlist. - * track: Any Track object. - """ - - return track in self.get_tracks() - - def get_image_url(self, size = IMAGE_LARGE): - """Returns the associated image URL. - * size: The image size. Possible values: - o IMAGE_LARGE - o IMAGE_MEDIUM - o IMAGE_SMALL - """ - - return _extract(self._get_info_node(), "image")[size] - - def get_url(self, domain_name = DOMAIN_ENGLISH): - """Returns the url of the playlist on Last.fm. - * domain_name: Last.fm's language domain. Possible values: - o DOMAIN_ENGLISH - o DOMAIN_GERMAN - o DOMAIN_SPANISH - o DOMAIN_FRENCH - o DOMAIN_ITALIAN - o DOMAIN_POLISH - o DOMAIN_PORTUGUESE - o DOMAIN_SWEDISH - o DOMAIN_TURKISH - o DOMAIN_RUSSIAN - o DOMAIN_JAPANESE - o DOMAIN_CHINESE - """ - url = "http://%(domain)s/user/%(user)s/library/playlists/%(appendix)s" - - english_url = _extract(self._get_info_node(), "url") - appendix = english_url[english_url.rfind("/") + 1:] - - return url %{'domain': domain_name, 'appendix': appendix, "user": self.get_user().get_name()} - + """A Last.fm user playlist.""" + + id = None + user = None + + def __init__(self, user, id, network): + _BaseObject.__init__(self, network) + + if isinstance(user, User): + self.user = user + else: + self.user = User(user, self.network) + + self.id = _unicode(id) + + @_string_output + def __repr__(self): + return repr(self.user) + "'s playlist # " + repr(self.id) + + def _get_info_node(self): + """Returns the node from user.getPlaylists where this playlist's info is.""" + + doc = self._request("user.getPlaylists", True) + + for node in doc.getElementsByTagName("playlist"): + if _extract(node, "id") == str(self.get_id()): + return node + + def _get_params(self): + return {'user': self.user.get_name(), 'playlistID': self.get_id()} + + def get_id(self): + """Returns the playlist id.""" + + return self.id + + def get_user(self): + """Returns the owner user of this playlist.""" + + return self.user + + def get_tracks(self): + """Returns a list of the tracks on this user playlist.""" + + uri = u'lastfm://playlist/%s' %self.get_id() + + return XSPF(uri, self.network).get_tracks() + + def add_track(self, track): + """Adds a Track to this Playlist.""" + + params = self._get_params() + params['artist'] = track.get_artist().get_name() + params['track'] = track.get_title() + + self._request('playlist.addTrack', False, params) + + def get_title(self): + """Returns the title of this playlist.""" + + return _extract(self._get_info_node(), "title") + + def get_creation_date(self): + """Returns the creation date of this playlist.""" + + return _extract(self._get_info_node(), "date") + + def get_size(self): + """Returns the number of tracks in this playlist.""" + + return _number(_extract(self._get_info_node(), "size")) + + def get_description(self): + """Returns the description of this playlist.""" + + return _extract(self._get_info_node(), "description") + + def get_duration(self): + """Returns the duration of this playlist in milliseconds.""" + + return _number(_extract(self._get_info_node(), "duration")) + + def is_streamable(self): + """Returns True if the playlist is streamable. + For a playlist to be streamable, it needs at least 45 tracks by 15 different artists.""" + + if _extract(self._get_info_node(), "streamable") == '1': + return True + else: + return False + + def has_track(self, track): + """Checks to see if track is already in the playlist. + * track: Any Track object. + """ + + return track in self.get_tracks() + + def get_cover_image(self, size = COVER_LARGE): + """ + Returns a uri to the cover image + size can be one of: + COVER_MEGA + COVER_EXTRA_LARGE + COVER_LARGE + COVER_MEDIUM + COVER_SMALL + """ + + return _extract(self._get_info_node(), "image")[size] + + def get_url(self, domain_name = DOMAIN_ENGLISH): + """Returns the url of the playlist on the network. + * domain_name: The network's language domain. Possible values: + o DOMAIN_ENGLISH + o DOMAIN_GERMAN + o DOMAIN_SPANISH + o DOMAIN_FRENCH + o DOMAIN_ITALIAN + o DOMAIN_POLISH + o DOMAIN_PORTUGUESE + o DOMAIN_SWEDISH + o DOMAIN_TURKISH + o DOMAIN_RUSSIAN + o DOMAIN_JAPANESE + o DOMAIN_CHINESE + """ + + english_url = _extract(self._get_info_node(), "url") + appendix = english_url[english_url.rfind("/") + 1:] + + return self.network._get_url(domain_name, "playlist") %{'appendix': appendix, "user": self.get_user().get_name()} + class Tag(_BaseObject): - """A Last.fm object tag.""" - - # TODO: getWeeklyArtistChart (too lazy, i'll wait for when someone requests it) - - def __init__(self, name, api_key, api_secret, session_key): - _BaseObject.__init__(self, api_key, api_secret, session_key) - - self.name = name - - def _get_params(self): - return {'tag': self.get_name()} - - def __repr__(self): - return self.get_name() - - def __eq__(self): - return self.get_name().lower() == other.get_name().lower() - - def __ne__(self): - return self.get_name().lower() != other.get_name().lower() - - def get_name(self): - """Returns the name of the tag. """ - - return self.name - - def get_similar(self): - """Returns the tags similar to this one, ordered by similarity. """ - - doc = self._request('tag.getSimilar', True) - - list = [] - names = _extract_all(doc, 'name') - for name in names: - list.append(Tag(name, *self.auth_data)) - - return list - - def get_top_albums(self): - """Retuns a list of the top albums.""" - - doc = self._request('tag.getTopAlbums', True) - - list = [] - - for node in doc.getElementsByTagName("album"): - name = _extract(node, "name") - artist = _extract(node, "name", 1) - playcount = _extract(node, "playcount") - - list.append(TopItem(Album(artist, name, *self.auth_data), playcount)) - - return list - - def get_top_tracks(self): - """Returns a list of the most played Tracks by this artist.""" - - doc = self._request("tag.getTopTracks", True) - - list = [] - for track in doc.getElementsByTagName('track'): - - title = _extract(track, "name") - artist = _extract(track, "name", 1) - playcount = _number(_extract(track, "playcount")) - - list.append( TopItem(Track(artist, title, *self.auth_data), playcount) ) - - return list - - def get_top_artists(self): - """Returns a sequence of the most played artists.""" - - doc = self._request('tag.getTopArtists', True) - - list = [] - for node in doc.getElementsByTagName("artist"): - name = _extract(node, 'name') - playcount = _extract(node, "playcount") - - list.append(TopItem(Artist(name, *self.auth_data), playcount)) - - return list - - def get_weekly_chart_dates(self): - """Returns a list of From and To tuples for the available charts.""" - - doc = self._request("tag.getWeeklyChartList", True) - - list = [] - for node in doc.getElementsByTagName("chart"): - list.append( (node.getAttribute("from"), node.getAttribute("to")) ) - - return list - - def get_weekly_artist_charts(self, from_date = None, to_date = None): - """Returns the weekly artist charts for the week starting from the from_date value to the to_date value.""" - - params = self._get_params() - if from_date and to_date: - params["from"] = from_date - params["to"] = to_date - - doc = self._request("tag.getWeeklyArtistChart", True, params) - - list = [] - for node in doc.getElementsByTagName("artist"): - item = Artist(_extract(node, "name"), *self.auth_data) - weight = _extract(node, "weight") - list.append(TopItem(item, weight)) - - return list - - def get_url(self, domain_name = DOMAIN_ENGLISH): - """Returns the url of the tag page on Last.fm. - * domain_name: Last.fm's language domain. Possible values: - o DOMAIN_ENGLISH - o DOMAIN_GERMAN - o DOMAIN_SPANISH - o DOMAIN_FRENCH - o DOMAIN_ITALIAN - o DOMAIN_POLISH - o DOMAIN_PORTUGUESE - o DOMAIN_SWEDISH - o DOMAIN_TURKISH - o DOMAIN_RUSSIAN - o DOMAIN_JAPANESE - o DOMAIN_CHINESE - """ - - url = 'http://%(domain)s/tag/%(name)s' - - name = _get_url_safe(self.get_name()) - - return url %{'domain': domain_name, 'name': name} + """A Last.fm object tag.""" + + # TODO: getWeeklyArtistChart (too lazy, i'll wait for when someone requests it) + + name = None + + def __init__(self, name, network): + _BaseObject.__init__(self, network) + + self.name = name + + def _get_params(self): + return {'tag': self.get_name()} + + @_string_output + def __repr__(self): + return self.get_name() + + def __eq__(self, other): + return self.get_name().lower() == other.get_name().lower() + + def __ne__(self, other): + return self.get_name().lower() != other.get_name().lower() + + def get_name(self): + """Returns the name of the tag. """ + + return self.name + + def get_similar(self): + """Returns the tags similar to this one, ordered by similarity. """ + + doc = self._request('tag.getSimilar', True) + + seq = [] + names = _extract_all(doc, 'name') + for name in names: + seq.append(Tag(name, self.network)) + + return seq + + def get_top_albums(self): + """Retuns a list of the top albums.""" + + doc = self._request('tag.getTopAlbums', True) + + seq = [] + + for node in doc.getElementsByTagName("album"): + name = _extract(node, "name") + artist = _extract(node, "name", 1) + playcount = _extract(node, "playcount") + + seq.append(TopItem(Album(artist, name, self.network), playcount)) + + return seq + + def get_top_tracks(self): + """Returns a list of the most played Tracks by this artist.""" + + doc = self._request("tag.getTopTracks", True) + + seq = [] + for track in doc.getElementsByTagName('track'): + + title = _extract(track, "name") + artist = _extract(track, "name", 1) + playcount = _number(_extract(track, "playcount")) + + seq.append( TopItem(Track(artist, title, self.network), playcount) ) + + return seq + + def get_top_artists(self): + """Returns a sequence of the most played artists.""" + + doc = self._request('tag.getTopArtists', True) + + seq = [] + for node in doc.getElementsByTagName("artist"): + name = _extract(node, 'name') + playcount = _extract(node, "playcount") + + seq.append(TopItem(Artist(name, self.network), playcount)) + + return seq + + def get_weekly_chart_dates(self): + """Returns a list of From and To tuples for the available charts.""" + + doc = self._request("tag.getWeeklyChartList", True) + + seq = [] + for node in doc.getElementsByTagName("chart"): + seq.append( (node.getAttribute("from"), node.getAttribute("to")) ) + + return seq + + def get_weekly_artist_charts(self, from_date = None, to_date = None): + """Returns the weekly artist charts for the week starting from the from_date value to the to_date value.""" + + params = self._get_params() + if from_date and to_date: + params["from"] = from_date + params["to"] = to_date + + doc = self._request("tag.getWeeklyArtistChart", True, params) + + seq = [] + for node in doc.getElementsByTagName("artist"): + item = Artist(_extract(node, "name"), self.network) + weight = _number(_extract(node, "weight")) + seq.append(TopItem(item, weight)) + + return seq + + def get_url(self, domain_name = DOMAIN_ENGLISH): + """Returns the url of the tag page on the network. + * domain_name: The network's language domain. Possible values: + o DOMAIN_ENGLISH + o DOMAIN_GERMAN + o DOMAIN_SPANISH + o DOMAIN_FRENCH + o DOMAIN_ITALIAN + o DOMAIN_POLISH + o DOMAIN_PORTUGUESE + o DOMAIN_SWEDISH + o DOMAIN_TURKISH + o DOMAIN_RUSSIAN + o DOMAIN_JAPANESE + o DOMAIN_CHINESE + """ + + name = _url_safe(self.get_name()) + + return self.network._get_url(domain_name, "tag") %{'name': name} class Track(_BaseObject, _Taggable): - """A Last.fm track.""" - - def __init__(self, artist, title, api_key, api_secret, session_key): - _BaseObject.__init__(self, api_key, api_secret, session_key) - _Taggable.__init__(self, 'track') - - if isinstance(artist, Artist): - self.artist = artist - else: - self.artist = Artist(artist, *self.auth_data) - - self.title = title - - def __repr__(self): - return self.get_artist().get_name() + ' - ' + self.get_title() - - def __eq__(self, other): - return (self.get_title().lower() == other.get_title().lower()) and (self.get_artist().get_name().lower() == other.get_artist().get_name().lower()) - - def __ne__(self, other): - return (self.get_title().lower() != other.get_title().lower()) or (self.get_artist().get_name().lower() != other.get_artist().get_name().lower()) - - def _get_params(self): - return {'artist': self.get_artist().get_name(), 'track': self.get_title()} - - def get_artist(self): - """Returns the associated Artist object.""" - - return self.artist - - def get_title(self): - """Returns the track title.""" - - return self.title - - def get_name(self): - """Returns the track title (alias to Track.get_title).""" - - return self.get_title() - - def get_id(self): - """Returns the track id on Last.fm.""" - - doc = self._request("track.getInfo", True) - - return _extract(doc, "id") - - def get_duration(self): - """Returns the track duration.""" - - doc = self._request("track.getInfo", True) - - return _number(_extract(doc, "duration")) - - def get_mbid(self): - """Returns the MusicBrainz ID of this track.""" - - doc = self._request("track.getInfo", True) - - return _extract(doc, "mbid") - - def get_listener_count(self): - """Returns the listener count.""" - - doc = self._request("track.getInfo", True) - - return _number(_extract(doc, "listeners")) - - def get_playcount(self): - """Returns the play count.""" - - doc = self._request("track.getInfo", True) - return _number(_extract(doc, "playcount")) - - def get_top_tags(self): - """Returns the play count.""" - - doc = self._request("track.getInfo", True) - - list = [] - - elements = doc.getElementsByTagName('tag') - - for element in elements: - - name = _extract(element, 'name') - - list.append(name) - - return list - - def is_streamable(self): - """Returns True if the track is available at Last.fm.""" - - doc = self._request("track.getInfo", True) - return _extract(doc, "streamable") == "1" - - def is_fulltrack_available(self): - """Returns True if the fulltrack is available for streaming.""" - - doc = self._request("track.getInfo", True) - return doc.getElementsByTagName("streamable")[0].getAttribute("fulltrack") == "1" - - def get_album(self): - """Returns the album object of this track.""" - - doc = self._request("track.getInfo", True) - - albums = doc.getElementsByTagName("album") - - if len(albums) == 0: - return - - node = doc.getElementsByTagName("album")[0] - return Album(_extract(node, "artist"), _extract(node, "title"), *self.auth_data) - - def get_wiki_published_date(self): - """Returns the date of publishing this version of the wiki.""" - - doc = self._request("track.getInfo", True) - - if len(doc.getElementsByTagName("wiki")) == 0: - return - - node = doc.getElementsByTagName("wiki")[0] - - return _extract(node, "published") - - def get_wiki_summary(self): - """Returns the summary of the wiki.""" - - doc = self._request("track.getInfo", True) - - if len(doc.getElementsByTagName("wiki")) == 0: - return - - node = doc.getElementsByTagName("wiki")[0] - - return _extract(node, "summary") - - def get_wiki_content(self): - """Returns the content of the wiki.""" - - doc = self._request("track.getInfo", True) - - if len(doc.getElementsByTagName("wiki")) == 0: - return - - node = doc.getElementsByTagName("wiki")[0] - - return _extract(node, "content") - - def love(self): - """Adds the track to the user's loved tracks. """ - - self._request('track.love') - - def ban(self): - """Ban this track from ever playing on the radio. """ - - self._request('track.ban') - - def get_similar(self): - """Returns similar tracks for this track on Last.fm, based on listening data. """ - - doc = self._request('track.getSimilar', True) - - list = [] - for node in doc.getElementsByTagName("track"): - title = _extract(node, 'name') - artist = _extract(node, 'name', 1) - - list.append(Track(artist, title, *self.auth_data)) - - return list - - def get_top_fans(self, limit = None): - """Returns a list of the Users who played this track.""" - - doc = self._request('track.getTopFans', True) - - list = [] - - elements = doc.getElementsByTagName('user') - - for element in elements: - if limit and len(list) >= limit: - break - - name = _extract(element, 'name') - weight = _number(_extract(element, 'weight')) - - list.append(TopItem(User(name, *self.auth_data), weight)) - - return list - - def share(self, users, message = None): - """Shares this track (sends out recommendations). - * users: A list that can contain usernames, emails, User objects, or all of them. - * message: A message to include in the recommendation message. - """ - - #last.fm currently accepts a max of 10 recipient at a time - while(len(users) > 10): - section = users[0:9] - users = users[9:] - self.share(section, message) - - nusers = [] - for user in users: - if isinstance(user, User): - nusers.append(user.get_name()) - else: - nusers.append(user) - - params = self._get_params() - recipients = ','.join(nusers) - params['recipient'] = recipients - if message: params['message'] = unicode(message) - - self._request('track.share', False, params) - - def get_url(self, domain_name = DOMAIN_ENGLISH): - """Returns the url of the track page on Last.fm. - * domain_name: Last.fm's language domain. Possible values: - o DOMAIN_ENGLISH - o DOMAIN_GERMAN - o DOMAIN_SPANISH - o DOMAIN_FRENCH - o DOMAIN_ITALIAN - o DOMAIN_POLISH - o DOMAIN_PORTUGUESE - o DOMAIN_SWEDISH - o DOMAIN_TURKISH - o DOMAIN_RUSSIAN - o DOMAIN_JAPANESE - o DOMAIN_CHINESE - """ - url = 'http://%(domain)s/music/%(artist)s/_/%(title)s' - - artist = _get_url_safe(self.get_artist().get_name()) - title = _get_url_safe(self.get_title()) - - return url %{'domain': domain_name, 'artist': artist, 'title': title} - + """A Last.fm track.""" + + artist = None + title = None + + def __init__(self, artist, title, network): + _BaseObject.__init__(self, network) + _Taggable.__init__(self, 'track') + + if isinstance(artist, Artist): + self.artist = artist + else: + self.artist = Artist(artist, self.network) + + self.title = title + + @_string_output + def __repr__(self): + return self.get_artist().get_name() + ' - ' + self.get_title() + + def __eq__(self, other): + return (self.get_title().lower() == other.get_title().lower()) and (self.get_artist().get_name().lower() == other.get_artist().get_name().lower()) + + def __ne__(self, other): + return (self.get_title().lower() != other.get_title().lower()) or (self.get_artist().get_name().lower() != other.get_artist().get_name().lower()) + + def _get_params(self): + return {'artist': self.get_artist().get_name(), 'track': self.get_title()} + + def get_artist(self): + """Returns the associated Artist object.""" + + return self.artist + + def get_title(self): + """Returns the track title.""" + + return self.title + + def get_name(self): + """Returns the track title (alias to Track.get_title).""" + + return self.get_title() + + def get_id(self): + """Returns the track id on the network.""" + + doc = self._request("track.getInfo", True) + + return _extract(doc, "id") + + def get_duration(self): + """Returns the track duration.""" + + doc = self._request("track.getInfo", True) + + return _number(_extract(doc, "duration")) + + def get_mbid(self): + """Returns the MusicBrainz ID of this track.""" + + doc = self._request("track.getInfo", True) + + return _extract(doc, "mbid") + + def get_listener_count(self): + """Returns the listener count.""" + + doc = self._request("track.getInfo", True) + + return _number(_extract(doc, "listeners")) + + def get_playcount(self): + """Returns the play count.""" + + doc = self._request("track.getInfo", True) + return _number(_extract(doc, "playcount")) + + def is_streamable(self): + """Returns True if the track is available at Last.fm.""" + + doc = self._request("track.getInfo", True) + return _extract(doc, "streamable") == "1" + + def is_fulltrack_available(self): + """Returns True if the fulltrack is available for streaming.""" + + doc = self._request("track.getInfo", True) + return doc.getElementsByTagName("streamable")[0].getAttribute("fulltrack") == "1" + + def get_album(self): + """Returns the album object of this track.""" + + doc = self._request("track.getInfo", True) + + albums = doc.getElementsByTagName("album") + + if len(albums) == 0: + return + + node = doc.getElementsByTagName("album")[0] + return Album(_extract(node, "artist"), _extract(node, "title"), self.network) + + def get_wiki_published_date(self): + """Returns the date of publishing this version of the wiki.""" + + doc = self._request("track.getInfo", True) + + if len(doc.getElementsByTagName("wiki")) == 0: + return + + node = doc.getElementsByTagName("wiki")[0] + + return _extract(node, "published") + + def get_wiki_summary(self): + """Returns the summary of the wiki.""" + + doc = self._request("track.getInfo", True) + + if len(doc.getElementsByTagName("wiki")) == 0: + return + + node = doc.getElementsByTagName("wiki")[0] + + return _extract(node, "summary") + + def get_wiki_content(self): + """Returns the content of the wiki.""" + + doc = self._request("track.getInfo", True) + + if len(doc.getElementsByTagName("wiki")) == 0: + return + + node = doc.getElementsByTagName("wiki")[0] + + return _extract(node, "content") + + def love(self): + """Adds the track to the user's loved tracks. """ + + self._request('track.love') + + def ban(self): + """Ban this track from ever playing on the radio. """ + + self._request('track.ban') + + def get_similar(self): + """Returns similar tracks for this track on the network, based on listening data. """ + + doc = self._request('track.getSimilar', True) + + seq = [] + for node in doc.getElementsByTagName("track"): + title = _extract(node, 'name') + artist = _extract(node, 'name', 1) + match = _number(_extract(node, "match")) + + seq.append(SimilarItem(Track(artist, title, self.network), match)) + + return seq + + def get_top_fans(self, limit = None): + """Returns a list of the Users who played this track.""" + + doc = self._request('track.getTopFans', True) + + seq = [] + + elements = doc.getElementsByTagName('user') + + for element in elements: + if limit and len(seq) >= limit: + break + + name = _extract(element, 'name') + weight = _number(_extract(element, 'weight')) + + seq.append(TopItem(User(name, self.network), weight)) + + return seq + + def share(self, users, message = None): + """Shares this track (sends out recommendations). + * users: A list that can contain usernames, emails, User objects, or all of them. + * message: A message to include in the recommendation message. + """ + + #last.fm currently accepts a max of 10 recipient at a time + while(len(users) > 10): + section = users[0:9] + users = users[9:] + self.share(section, message) + + nusers = [] + for user in users: + if isinstance(user, User): + nusers.append(user.get_name()) + else: + nusers.append(user) + + params = self._get_params() + recipients = ','.join(nusers) + params['recipient'] = recipients + if message: params['message'] = _unicode(message) + + self._request('track.share', False, params) + + def get_url(self, domain_name = DOMAIN_ENGLISH): + """Returns the url of the track page on the network. + * domain_name: The network's language domain. Possible values: + o DOMAIN_ENGLISH + o DOMAIN_GERMAN + o DOMAIN_SPANISH + o DOMAIN_FRENCH + o DOMAIN_ITALIAN + o DOMAIN_POLISH + o DOMAIN_PORTUGUESE + o DOMAIN_SWEDISH + o DOMAIN_TURKISH + o DOMAIN_RUSSIAN + o DOMAIN_JAPANESE + o DOMAIN_CHINESE + """ + + artist = _url_safe(self.get_artist().get_name()) + title = _url_safe(self.get_title()) + + return self.network._get_url(domain_name, "track") %{'domain': self.network._get_language_domain(domain_name), 'artist': artist, 'title': title} + + def get_shouts(self, limit=50): + """ + Returns a sequqence of Shout objects + """ + + shouts = [] + for node in _collect_nodes(limit, self, "track.getShouts", False): + shouts.append(Shout( + _extract(node, "body"), + User(_extract(node, "author"), self.network), + _extract(node, "date") + ) + ) + return shouts + + def shout(self, message): + """ + Post a shout + """ + + params = self._get_params() + params["message"] = message + + self._request("track.Shout", False, params) + class Group(_BaseObject): - """A Last.fm group.""" - - def __init__(self, group_name, api_key, api_secret, session_key): - _BaseObject.__init__(self, api_key, api_secret, session_key) - - self.name = group_name - - def __repr__(self): - return self.get_name() - - def __eq__(self, other): - return self.get_name().lower() == other.get_name().lower() - - def __ne__(self, other): - return self.get_name() != other.get_name() - - def _get_params(self): - return {'group': self.get_name()} - - def get_name(self): - """Returns the group name. """ - return self.name - - def get_weekly_chart_dates(self): - """Returns a list of From and To tuples for the available charts.""" - - doc = self._request("group.getWeeklyChartList", True) - - list = [] - for node in doc.getElementsByTagName("chart"): - list.append( (node.getAttribute("from"), node.getAttribute("to")) ) - - return list - - def get_weekly_artist_charts(self, from_date = None, to_date = None): - """Returns the weekly artist charts for the week starting from the from_date value to the to_date value.""" - - params = self._get_params() - if from_date and to_date: - params["from"] = from_date - params["to"] = to_date - - doc = self._request("group.getWeeklyArtistChart", True, params) - - list = [] - for node in doc.getElementsByTagName("artist"): - item = Artist(_extract(node, "name"), *self.auth_data) - weight = _extract(node, "playcount") - list.append(TopItem(item, weight)) - - return list - - def get_weekly_album_charts(self, from_date = None, to_date = None): - """Returns the weekly album charts for the week starting from the from_date value to the to_date value.""" - - params = self._get_params() - if from_date and to_date: - params["from"] = from_date - params["to"] = to_date - - doc = self._request("group.getWeeklyAlbumChart", True, params) - - list = [] - for node in doc.getElementsByTagName("album"): - item = Album(_extract(node, "artist"), _extract(node, "name"), *self.auth_data) - weight = _extract(node, "playcount") - list.append(TopItem(item, weight)) - - return list - - def get_weekly_track_charts(self, from_date = None, to_date = None): - """Returns the weekly track charts for the week starting from the from_date value to the to_date value.""" - - params = self._get_params() - if from_date and to_date: - params["from"] = from_date - params["to"] = to_date - - doc = self._request("group.getWeeklyTrackChart", True, params) - - list = [] - for node in doc.getElementsByTagName("track"): - item = Track(_extract(node, "artist"), _extract(node, "name"), *self.auth_data) - weight = _extract(node, "playcount") - list.append(TopItem(item, weight)) - - return list - - def get_url(self, domain_name = DOMAIN_ENGLISH): - """Returns the url of the group page on Last.fm. - * domain_name: Last.fm's language domain. Possible values: - o DOMAIN_ENGLISH - o DOMAIN_GERMAN - o DOMAIN_SPANISH - o DOMAIN_FRENCH - o DOMAIN_ITALIAN - o DOMAIN_POLISH - o DOMAIN_PORTUGUESE - o DOMAIN_SWEDISH - o DOMAIN_TURKISH - o DOMAIN_RUSSIAN - o DOMAIN_JAPANESE - o DOMAIN_CHINESE - """ - - url = 'http://%(domain)s/group/%(name)s' - - name = _get_url_safe(self.get_name()) - - return url %{'domain': domain_name, 'name': name} + """A Last.fm group.""" + + name = None + + def __init__(self, group_name, network): + _BaseObject.__init__(self, network) + + self.name = group_name + + @_string_output + def __repr__(self): + return self.get_name() + + def __eq__(self, other): + return self.get_name().lower() == other.get_name().lower() + + def __ne__(self, other): + return self.get_name() != other.get_name() + + def _get_params(self): + return {'group': self.get_name()} + + def get_name(self): + """Returns the group name. """ + return self.name + + def get_weekly_chart_dates(self): + """Returns a list of From and To tuples for the available charts.""" + + doc = self._request("group.getWeeklyChartList", True) + + seq = [] + for node in doc.getElementsByTagName("chart"): + seq.append( (node.getAttribute("from"), node.getAttribute("to")) ) + + return seq + + def get_weekly_artist_charts(self, from_date = None, to_date = None): + """Returns the weekly artist charts for the week starting from the from_date value to the to_date value.""" + + params = self._get_params() + if from_date and to_date: + params["from"] = from_date + params["to"] = to_date + + doc = self._request("group.getWeeklyArtistChart", True, params) + + seq = [] + for node in doc.getElementsByTagName("artist"): + item = Artist(_extract(node, "name"), self.network) + weight = _number(_extract(node, "playcount")) + seq.append(TopItem(item, weight)) + + return seq + + def get_weekly_album_charts(self, from_date = None, to_date = None): + """Returns the weekly album charts for the week starting from the from_date value to the to_date value.""" + + params = self._get_params() + if from_date and to_date: + params["from"] = from_date + params["to"] = to_date + + doc = self._request("group.getWeeklyAlbumChart", True, params) + + seq = [] + for node in doc.getElementsByTagName("album"): + item = Album(_extract(node, "artist"), _extract(node, "name"), self.network) + weight = _number(_extract(node, "playcount")) + seq.append(TopItem(item, weight)) + + return seq + + def get_weekly_track_charts(self, from_date = None, to_date = None): + """Returns the weekly track charts for the week starting from the from_date value to the to_date value.""" + + params = self._get_params() + if from_date and to_date: + params["from"] = from_date + params["to"] = to_date + + doc = self._request("group.getWeeklyTrackChart", True, params) + + seq = [] + for node in doc.getElementsByTagName("track"): + item = Track(_extract(node, "artist"), _extract(node, "name"), self.network) + weight = _number(_extract(node, "playcount")) + seq.append(TopItem(item, weight)) + + return seq + + def get_url(self, domain_name = DOMAIN_ENGLISH): + """Returns the url of the group page on the network. + * domain_name: The network's language domain. Possible values: + o DOMAIN_ENGLISH + o DOMAIN_GERMAN + o DOMAIN_SPANISH + o DOMAIN_FRENCH + o DOMAIN_ITALIAN + o DOMAIN_POLISH + o DOMAIN_PORTUGUESE + o DOMAIN_SWEDISH + o DOMAIN_TURKISH + o DOMAIN_RUSSIAN + o DOMAIN_JAPANESE + o DOMAIN_CHINESE + """ + + name = _url_safe(self.get_name()) + + return self.network._get_url(domain_name, "group") %{'name': name} + + def get_members(self, limit=50): + """ + Returns a sequence of User objects + if limit==None it will return all + """ + + nodes = _collect_nodes(limit, self, "group.getMembers", False) + + users = [] + + for node in nodes: + users.append(User(_extract(node, "name"), self.network)) + + return users class XSPF(_BaseObject): - "A Last.fm XSPF playlist.""" - - def __init__(self, uri, api_key, api_secret, session_key): - _BaseObject.__init__(self, api_key, api_secret, session_key) - - self.uri = uri - - def _get_params(self): - return {'playlistURL': self.get_uri()} - - def __repr__(self): - return self.get_uri() - - def __eq__(self, other): - return self.get_uri() == other.get_uri() - - def __ne__(self, other): - return self.get_uri() != other.get_uri() - - def get_uri(self): - """Returns the Last.fm playlist URI. """ - - return self.uri - - def get_tracks(self): - """Returns the tracks on this playlist.""" - - doc = self._request('playlist.fetch', True) - - list = [] - for n in doc.getElementsByTagName('track'): - title = _extract(n, 'title') - artist = _extract(n, 'creator') - - list.append(Track(artist, title, *self.auth_data)) - - return list + "A Last.fm XSPF playlist.""" + + uri = None + + def __init__(self, uri, network): + _BaseObject.__init__(self, network) + + self.uri = uri + + def _get_params(self): + return {'playlistURL': self.get_uri()} + + @_string_output + def __repr__(self): + return self.get_uri() + + def __eq__(self, other): + return self.get_uri() == other.get_uri() + + def __ne__(self, other): + return self.get_uri() != other.get_uri() + + def get_uri(self): + """Returns the Last.fm playlist URI. """ + + return self.uri + + def get_tracks(self): + """Returns the tracks on this playlist.""" + + doc = self._request('playlist.fetch', True) + + seq = [] + for n in doc.getElementsByTagName('track'): + title = _extract(n, 'title') + artist = _extract(n, 'creator') + + seq.append(Track(artist, title, self.network)) + + return seq class User(_BaseObject): - """A Last.fm user.""" - - def __init__(self, user_name, api_key, api_secret, session_key): - _BaseObject.__init__(self, api_key, api_secret, session_key) - - self.name = user_name - - self._past_events_index = 0 - self._recommended_events_index = 0 - self._recommended_artists_index = 0 - - def __repr__(self): - return self.get_name() - - def __eq__(self, another): - return self.get_name() == another.get_name() - - def __ne__(self, another): - return self.get_name() != another.get_name() - - def _get_params(self): - return {"user": self.get_name()} - - def get_name(self): - """Returns the nuser name.""" - - return self.name - - def get_upcoming_events(self): - """Returns all the upcoming events for this user. """ - - doc = self._request('user.getEvents', True) - - ids = _extract_all(doc, 'id') - events = [] - - for id in ids: - events.append(Event(id, *self.auth_data)) - - return events - - def get_friends(self, limit = None): - """Returns a list of the user's friends. """ - - params = self._get_params() - if limit: - params['limit'] = unicode(limit) - - doc = self._request('user.getFriends', True, params) - - names = _extract_all(doc, 'name') - - list = [] - for name in names: - list.append(User(name, *self.auth_data)) - - return list - - def get_loved_tracks(self): - """Returns the last 50 tracks loved by this user. """ - - doc = self._request('user.getLovedTracks', True) - - list = [] - for track in doc.getElementsByTagName('track'): - title = _extract(track, 'name', 0) - artist = _extract(track, 'name', 1) - - list.append(Track(artist, title, *self.auth_data)) - - return list - - def get_neighbours(self, limit = None): - """Returns a list of the user's friends.""" - - params = self._get_params() - if limit: - params['limit'] = unicode(limit) - - doc = self._request('user.getNeighbours', True, params) - - list = [] - names = _extract_all(doc, 'name') - - for name in names: - list.append(User(name, *self.auth_data)) - - return list - - def _get_past_events_pagecount(self): - """Returns the number of pages in the past events.""" - - params = self._get_params() - params["page"] = str(self._past_events_index) - doc = self._request("user.getPastEvents", True, params) - - return _number(doc.getElementsByTagName("events")[0].getAttribute("totalPages")) - - def is_end_of_past_events(self): - """Returns True if the end of Past Events was reached.""" - - return self._past_events_index >= self._get_past_events_pagecount() - - def get_past_events_page(self, ): - """Retruns a paginated list of all events a user has attended in the past. - - Example: - -------- - - while not user.is_end_of_past_events(): - print user.get_past_events_page() - - """ - - self._past_events_index += 1 - params = self._get_params() - params["page"] = str(self._past_events_index) - - doc = self._request('user.getPastEvents', True, params) - - list = [] - for id in _extract_all(doc, 'id'): - list.append(Event(id, *self.auth_data)) - - return list - - def get_playlists(self): - """Returns a list of Playlists that this user owns.""" - - doc = self._request("user.getPlaylists", True) - - playlists = [] - for id in _extract_all(doc, "id"): - playlists.append(Playlist(self.get_name(), id, *self.auth_data)) - - return playlists - - def get_now_playing(self): - """Returns the currently playing track, or None if nothing is playing. """ - - params = self._get_params() - params['limit'] = '1' - - list = [] - - doc = self._request('user.getRecentTracks', False, params) - - e = doc.getElementsByTagName('track')[0] - - if not e.hasAttribute('nowplaying'): - return None - - artist = _extract(e, 'artist') - title = _extract(e, 'name') - - return Track(artist, title, *self.auth_data) - - - def get_recent_tracks(self, limit = None): - """Returns this user's recent listened-to tracks as - a sequence of PlayedTrack objects. - Use extract_items() with the return of this function to - get only a sequence of Track objects with no playback dates. """ - - params = self._get_params() - if limit: - params['limit'] = unicode(limit) - - doc = self._request('user.getRecentTracks', False, params) - - list = [] - for track in doc.getElementsByTagName('track'): - title = _extract(track, "name") - artist = _extract(track, "artist") - date = _extract(track, "date") - timestamp = track.getElementsByTagName("date")[0].getAttribute("uts") - - if track.hasAttribute('nowplaying'): - continue #to prevent the now playing track from sneaking in here - - list.append(PlayedTrack(Track(artist, title, *self.auth_data), date, timestamp)) - - return list - - def get_top_albums(self, period = PERIOD_OVERALL): - """Returns the top albums played by a user. - * period: The period of time. Possible values: - o PERIOD_OVERALL - o PERIOD_3MONTHS - o PERIOD_6MONTHS - o PERIOD_12MONTHS - """ - - params = self._get_params() - params['period'] = period - - doc = self._request('user.getTopAlbums', True, params) - - list = [] - for album in doc.getElementsByTagName('album'): - name = _extract(album, 'name') - artist = _extract(album, 'name', 1) - playcount = _extract(album, "playcount") - - list.append(TopItem(Album(artist, name, *self.auth_data), playcount)) - - return list - - def get_top_artists(self, period = PERIOD_OVERALL): - """Returns the top artists played by a user. - * period: The period of time. Possible values: - o PERIOD_OVERALL - o PERIOD_3MONTHS - o PERIOD_6MONTHS - o PERIOD_12MONTHS - """ - - params = self._get_params() - params['period'] = period - - doc = self._request('user.getTopArtists', True, params) - - list = [] - for node in doc.getElementsByTagName('artist'): - name = _extract(node, 'name') - playcount = _extract(node, "playcount") - - list.append(TopItem(Artist(name, *self.auth_data), playcount)) - - return list - - def get_top_tags(self, limit = None): - """Returns a sequence of the top tags used by this user with their counts as (Tag, tagcount). - * limit: The limit of how many tags to return. - """ - - doc = self._request("user.getTopTags", True) - - list = [] - for node in doc.getElementsByTagName("tag"): - list.append(TopItem(Tag(_extract(node, "name"), *self.auth_data), _extract(node, "count"))) - - return list - - def get_top_tracks(self, period = PERIOD_OVERALL): - """Returns the top tracks played by a user. - * period: The period of time. Possible values: - o PERIOD_OVERALL - o PERIOD_3MONTHS - o PERIOD_6MONTHS - o PERIOD_12MONTHS - """ - - params = self._get_params() - params['period'] = period - - doc = self._request('user.getTopTracks', True, params) - - list = [] - for track in doc.getElementsByTagName('track'): - name = _extract(track, 'name') - artist = _extract(track, 'name', 1) - playcount = _extract(track, "playcount") - - list.append(TopItem(Track(artist, name, *self.auth_data), playcount)) - - return list - - def get_weekly_chart_dates(self): - """Returns a list of From and To tuples for the available charts.""" - - doc = self._request("user.getWeeklyChartList", True) - - list = [] - for node in doc.getElementsByTagName("chart"): - list.append( (node.getAttribute("from"), node.getAttribute("to")) ) - - return list - - def get_weekly_artist_charts(self, from_date = None, to_date = None): - """Returns the weekly artist charts for the week starting from the from_date value to the to_date value.""" - - params = self._get_params() - if from_date and to_date: - params["from"] = from_date - params["to"] = to_date - - doc = self._request("user.getWeeklyArtistChart", True, params) - - list = [] - for node in doc.getElementsByTagName("artist"): - item = Artist(_extract(node, "name"), *self.auth_data) - weight = _extract(node, "playcount") - list.append(TopItem(item, weight)) - - return list - - def get_weekly_album_charts(self, from_date = None, to_date = None): - """Returns the weekly album charts for the week starting from the from_date value to the to_date value.""" - - params = self._get_params() - if from_date and to_date: - params["from"] = from_date - params["to"] = to_date - - doc = self._request("user.getWeeklyAlbumChart", True, params) - - list = [] - for node in doc.getElementsByTagName("album"): - item = Album(_extract(node, "artist"), _extract(node, "name"), *self.auth_data) - weight = _extract(node, "playcount") - list.append(TopItem(item, weight)) - - return list - - def get_weekly_track_charts(self, from_date = None, to_date = None): - """Returns the weekly track charts for the week starting from the from_date value to the to_date value.""" - - params = self._get_params() - if from_date and to_date: - params["from"] = from_date - params["to"] = to_date - - doc = self._request("user.getWeeklyTrackChart", True, params) - - list = [] - for node in doc.getElementsByTagName("track"): - item = Track(_extract(node, "artist"), _extract(node, "name"), *self.auth_data) - weight = _extract(node, "playcount") - list.append(TopItem(item, weight)) - - return list - - def compare_with_user(self, user, shared_artists_limit = None): - """Compare this user with another Last.fm user. - Returns a sequence (tasteometer_score, (shared_artist1, shared_artist2, ...)) - user: A User object or a username string/unicode object. - """ - - if isinstance(user, User): - user = user.get_name() - - params = self._get_params() - if shared_artists_limit: - params['limit'] = unicode(shared_artists_limit) - params['type1'] = 'user' - params['type2'] = 'user' - params['value1'] = self.get_name() - params['value2'] = user - - doc = _Request('tasteometer.compare', params, *self.auth_data).execute() - - score = _extract(doc, 'score') - - artists = doc.getElementsByTagName('artists')[0] - shared_artists_names = _extract_all(artists, 'name') - - shared_artists_list = [] - - for name in shared_artists_names: - shared_artists_list.append(Artist(name, *self.auth_data)) - - return (score, shared_artists_list) - - def getRecommendedEvents(self, page = None, limit = None): - """Returns a paginated list of all events recommended to a user by Last.fm, based on their listening profile. - * page: The page number of results to return. - * limit: The limit of events to return. - """ - - params = self._get_params() - if page: - params['page'] = unicode(page) - if limit: - params['limit'] = unicode(limit) - - doc = _Request('user.getRecommendedEvents', params, *self.auth_data).execute() - - ids = _extract_all(doc, 'id') - list = [] - for id in ids: - list.append(Event(id, *self.auth_data)) - - return list - - def get_url(self, domain_name = DOMAIN_ENGLISH): - """Returns the url of the user page on Last.fm. - * domain_name: Last.fm's language domain. Possible values: - o DOMAIN_ENGLISH - o DOMAIN_GERMAN - o DOMAIN_SPANISH - o DOMAIN_FRENCH - o DOMAIN_ITALIAN - o DOMAIN_POLISH - o DOMAIN_PORTUGUESE - o DOMAIN_SWEDISH - o DOMAIN_TURKISH - o DOMAIN_RUSSIAN - o DOMAIN_JAPANESE - o DOMAIN_CHINESE - """ - url = 'http://%(domain)s/user/%(name)s' - - name = _get_url_safe(self.get_name()) - - return url %{'domain': domain_name, 'name': name} - - def get_library(self): - """Returns the associated Library object. """ - - return Library(self, *self.auth_data) + """A Last.fm user.""" + + name = None + + def __init__(self, user_name, network): + _BaseObject.__init__(self, network) + + self.name = user_name + + self._past_events_index = 0 + self._recommended_events_index = 0 + self._recommended_artists_index = 0 + + @_string_output + def __repr__(self): + return self.get_name() + + def __eq__(self, another): + return self.get_name() == another.get_name() + + def __ne__(self, another): + return self.get_name() != another.get_name() + + def _get_params(self): + return {"user": self.get_name()} + + def get_name(self): + """Returns the nuser name.""" + + return self.name + + def get_upcoming_events(self): + """Returns all the upcoming events for this user. """ + + doc = self._request('user.getEvents', True) + + ids = _extract_all(doc, 'id') + events = [] + + for e_id in ids: + events.append(Event(e_id, self.network)) + + return events + + def get_friends(self, limit = 50): + """Returns a list of the user's friends. """ + + seq = [] + for node in _collect_nodes(limit, self, "user.getFriends", False): + seq.append(User(_extract(node, "name"), self.network)) + + return seq + + def get_loved_tracks(self, limit=50): + """Returns the loved tracks by this user + if limit is None, it will return all of them + """ + + tracks = [] + for track in _collect_nodes(limit, self, "user.getLovedTracks", False): + title = _extract(track, 'name', 0) + artist = _extract(track, 'name', 1) + + tracks.append(Track(artist, title, self.network)) + + return tracks + + def get_neighbours(self, limit = 50): + """Returns a list of the user's friends.""" + + params = self._get_params() + if limit: + params['limit'] = _unicode(limit) + + doc = self._request('user.getNeighbours', True, params) + + seq = [] + names = _extract_all(doc, 'name') + + for name in names: + seq.append(User(name, self.network)) + + return seq + + def get_past_events(self, limit=50): + """ + Returns a sequence of Event objects + if limit==None it will return all + """ + + seq = [] + for n in _collect_nodes(limit, self, "user.getPastEvents", False): + seq.append(Event(_extract(n, "id"), self.network)) + + return seq + + def get_playlists(self): + """Returns a list of Playlists that this user owns.""" + + doc = self._request("user.getPlaylists", True) + + playlists = [] + for playlist_id in _extract_all(doc, "id"): + playlists.append(Playlist(self.get_name(), playlist_id, self.network)) + + return playlists + + def get_now_playing(self): + """Returns the currently playing track, or None if nothing is playing. """ + + params = self._get_params() + params['limit'] = '1' + + doc = self._request('user.getRecentTracks', False, params) + + e = doc.getElementsByTagName('track')[0] + + if not e.hasAttribute('nowplaying'): + return None + + artist = _extract(e, 'artist') + title = _extract(e, 'name') + + return Track(artist, title, self.network) + + + def get_recent_tracks(self, limit = None): + """Returns this user's recent listened-to tracks as + a sequence of PlayedTrack objects. + Use extract_items() with the return of this function to + get only a sequence of Track objects with no playback dates. """ + + params = self._get_params() + if limit: + params['limit'] = _unicode(limit) + + doc = self._request('user.getRecentTracks', False, params) + + seq = [] + for track in doc.getElementsByTagName('track'): + title = _extract(track, "name") + artist = _extract(track, "artist") + date = _extract(track, "date") + timestamp = track.getElementsByTagName("date")[0].getAttribute("uts") + + if track.hasAttribute('nowplaying'): + continue #to prevent the now playing track from sneaking in here + + seq.append(PlayedTrack(Track(artist, title, self.network), date, timestamp)) + + return seq + + def get_top_albums(self, period = PERIOD_OVERALL): + """Returns the top albums played by a user. + * period: The period of time. Possible values: + o PERIOD_OVERALL + o PERIOD_3MONTHS + o PERIOD_6MONTHS + o PERIOD_12MONTHS + """ + + params = self._get_params() + params['period'] = period + + doc = self._request('user.getTopAlbums', True, params) + + seq = [] + for album in doc.getElementsByTagName('album'): + name = _extract(album, 'name') + artist = _extract(album, 'name', 1) + playcount = _extract(album, "playcount") + + seq.append(TopItem(Album(artist, name, self.network), playcount)) + + return seq + + def get_top_artists(self, period = PERIOD_OVERALL): + """Returns the top artists played by a user. + * period: The period of time. Possible values: + o PERIOD_OVERALL + o PERIOD_3MONTHS + o PERIOD_6MONTHS + o PERIOD_12MONTHS + """ + + params = self._get_params() + params['period'] = period + + doc = self._request('user.getTopArtists', True, params) + + seq = [] + for node in doc.getElementsByTagName('artist'): + name = _extract(node, 'name') + playcount = _extract(node, "playcount") + + seq.append(TopItem(Artist(name, self.network), playcount)) + + return seq + + def get_top_tags(self, limit = None): + """Returns a sequence of the top tags used by this user with their counts as (Tag, tagcount). + * limit: The limit of how many tags to return. + """ + + doc = self._request("user.getTopTags", True) + + seq = [] + for node in doc.getElementsByTagName("tag"): + if len(seq) < limit: + seq.append(TopItem(Tag(_extract(node, "name"), self.network), _extract(node, "count"))) + + return seq + + def get_top_tracks(self, period = PERIOD_OVERALL): + """Returns the top tracks played by a user. + * period: The period of time. Possible values: + o PERIOD_OVERALL + o PERIOD_3MONTHS + o PERIOD_6MONTHS + o PERIOD_12MONTHS + """ + + params = self._get_params() + params['period'] = period + + doc = self._request('user.getTopTracks', True, params) + + seq = [] + for track in doc.getElementsByTagName('track'): + name = _extract(track, 'name') + artist = _extract(track, 'name', 1) + playcount = _extract(track, "playcount") + + seq.append(TopItem(Track(artist, name, self.network), playcount)) + + return seq + + def get_weekly_chart_dates(self): + """Returns a list of From and To tuples for the available charts.""" + + doc = self._request("user.getWeeklyChartList", True) + + seq = [] + for node in doc.getElementsByTagName("chart"): + seq.append( (node.getAttribute("from"), node.getAttribute("to")) ) + + return seq + + def get_weekly_artist_charts(self, from_date = None, to_date = None): + """Returns the weekly artist charts for the week starting from the from_date value to the to_date value.""" + + params = self._get_params() + if from_date and to_date: + params["from"] = from_date + params["to"] = to_date + + doc = self._request("user.getWeeklyArtistChart", True, params) + + seq = [] + for node in doc.getElementsByTagName("artist"): + item = Artist(_extract(node, "name"), self.network) + weight = _number(_extract(node, "playcount")) + seq.append(TopItem(item, weight)) + + return seq + + def get_weekly_album_charts(self, from_date = None, to_date = None): + """Returns the weekly album charts for the week starting from the from_date value to the to_date value.""" + + params = self._get_params() + if from_date and to_date: + params["from"] = from_date + params["to"] = to_date + + doc = self._request("user.getWeeklyAlbumChart", True, params) + + seq = [] + for node in doc.getElementsByTagName("album"): + item = Album(_extract(node, "artist"), _extract(node, "name"), self.network) + weight = _number(_extract(node, "playcount")) + seq.append(TopItem(item, weight)) + + return seq + + def get_weekly_track_charts(self, from_date = None, to_date = None): + """Returns the weekly track charts for the week starting from the from_date value to the to_date value.""" + + params = self._get_params() + if from_date and to_date: + params["from"] = from_date + params["to"] = to_date + + doc = self._request("user.getWeeklyTrackChart", True, params) + + seq = [] + for node in doc.getElementsByTagName("track"): + item = Track(_extract(node, "artist"), _extract(node, "name"), self.network) + weight = _number(_extract(node, "playcount")) + seq.append(TopItem(item, weight)) + + return seq + + def compare_with_user(self, user, shared_artists_limit = None): + """Compare this user with another Last.fm user. + Returns a sequence (tasteometer_score, (shared_artist1, shared_artist2, ...)) + user: A User object or a username string/unicode object. + """ + + if isinstance(user, User): + user = user.get_name() + + params = self._get_params() + if shared_artists_limit: + params['limit'] = _unicode(shared_artists_limit) + params['type1'] = 'user' + params['type2'] = 'user' + params['value1'] = self.get_name() + params['value2'] = user + + doc = self._request('tasteometer.compare', False, params) + + score = _extract(doc, 'score') + + artists = doc.getElementsByTagName('artists')[0] + shared_artists_names = _extract_all(artists, 'name') + + shared_artists_seq = [] + + for name in shared_artists_names: + shared_artists_seq.append(Artist(name, self.network)) + + return (score, shared_artists_seq) + + def get_url(self, domain_name = DOMAIN_ENGLISH): + """Returns the url of the user page on the network. + * domain_name: The network's language domain. Possible values: + o DOMAIN_ENGLISH + o DOMAIN_GERMAN + o DOMAIN_SPANISH + o DOMAIN_FRENCH + o DOMAIN_ITALIAN + o DOMAIN_POLISH + o DOMAIN_PORTUGUESE + o DOMAIN_SWEDISH + o DOMAIN_TURKISH + o DOMAIN_RUSSIAN + o DOMAIN_JAPANESE + o DOMAIN_CHINESE + """ + + name = _url_safe(self.get_name()) + + return self.network._get_url(domain_name, "user") %{'name': name} + + def get_library(self): + """Returns the associated Library object. """ + + return Library(self, self.network) + + def get_shouts(self, limit=50): + """ + Returns a sequqence of Shout objects + """ + + shouts = [] + for node in _collect_nodes(limit, self, "user.getShouts", False): + shouts.append(Shout( + _extract(node, "body"), + User(_extract(node, "author"), self.network), + _extract(node, "date") + ) + ) + return shouts + + def shout(self, message): + """ + Post a shout + """ + + params = self._get_params() + params["message"] = message + + self._request("user.Shout", False, params) class AuthenticatedUser(User): - def __init__(self, api_key, api_secret, session_key): - User.__init__(self, "", api_key, api_secret, session_key); - - def _get_params(self): - return {} - - def get_name(self): - """Returns the name of the authenticated user.""" - - doc = self._request("user.getInfo", True) - - self.name = _extract(doc, "name") - return self.name - - def get_id(self): - """Returns the user id.""" - - doc = self._request("user.getInfo", True) - - return _extract(doc, "id") - - def get_image_url(self): - """Returns the user's avatar.""" - - doc = self._request("user.getInfo", True) - - return _extract(doc, "image") - - def get_language(self): - """Returns the language code of the language used by the user.""" - - doc = self._request("user.getInfo", True) - - return _extract(doc, "lang") - - def get_country(self): - """Returns the name of the country of the user.""" - - doc = self._request("user.getInfo", True) - - return Country(_extract(doc, "country"), *self.auth_data) - - def get_age(self): - """Returns the user's age.""" - - doc = self._request("user.getInfo", True) - - return _number(_extract(doc, "age")) - - def get_gender(self): - """Returns the user's gender. Either USER_MALE or USER_FEMALE.""" - - doc = self._request("user.getInfo", True) - - return _extract(doc, "gender") - - if value == 'm': - return USER_MALE - elif value == 'f': - return USER_FEMALE - - return None - - def is_subscriber(self): - """Returns whether the user is a subscriber or not. True or False.""" - - doc = self._request("user.getInfo", True) - - return _extract(doc, "subscriber") == "1" - - def get_playcount(self): - """Returns the user's playcount so far.""" - - doc = self._request("user.getInfo", True) - - return _number(_extract(doc, "playcount")) - - def _get_recommended_events_pagecount(self): - """Returns the number of pages in the past events.""" - - params = self._get_params() - params["page"] = str(self._recommended_events_index) - doc = self._request("user.getRecommendedEvents", True, params) - - return _number(doc.getElementsByTagName("events")[0].getAttribute("totalPages")) - - def is_end_of_recommended_events(self): - """Returns True if the end of Past Events was reached.""" - - return self._recommended_events_index >= self._get_recommended_events_pagecount() - - def get_recommended_events_page(self, ): - """Retruns a paginated list of all events a user has attended in the past. - - Example: - -------- - - while not user.is_end_of_recommended_events(): - print user.get_recommended_events_page() - - """ - - self._recommended_events_index += 1 - params = self._get_params() - params["page"] = str(self._recommended_events_index) - - doc = self._request('user.getRecommendedEvents', True, params) - - list = [] - for id in _extract_all(doc, 'id'): - list.append(Event(id, *self.auth_data)) - - return list - - def _get_recommended_artists_pagecount(self): - """Returns the number of pages in the past artists.""" - - params = self._get_params() - params["page"] = str(self._recommended_artists_index) - doc = self._request("user.getRecommendedArtists", True, params) - - return _number(doc.getElementsByTagName("recommendations")[0].getAttribute("totalPages")) - - def is_end_of_recommended_artists(self): - """Returns True if the end of Past Artists was reached.""" - - return self._recommended_artists_index >= self._get_recommended_artists_pagecount() - - def get_recommended_artists_page(self, ): - """Retruns a paginated list of all artists a user has attended in the past. - - Example: - -------- - - while not user.is_end_of_recommended_artists(): - print user.get_recommended_artists_page() - - """ - - self._recommended_artists_index += 1 - params = self._get_params() - params["page"] = str(self._recommended_artists_index) - - doc = self._request('user.getRecommendedArtists', True, params) - - list = [] - for name in _extract_all(doc, 'name'): - list.append(Artist(name, *self.auth_data)) - - return list - + def __init__(self, network): + User.__init__(self, "", network); + + def _get_params(self): + return {"user": self.get_name()} + + def get_name(self): + """Returns the name of the authenticated user.""" + + doc = self._request("user.getInfo", True, {"user": ""}) # hack + + self.name = _extract(doc, "name") + return self.name + + def get_id(self): + """Returns the user id.""" + + doc = self._request("user.getInfo", True) + + return _extract(doc, "id") + + def get_cover_image(self): + """Returns the user's avatar.""" + + doc = self._request("user.getInfo", True) + + return _extract(doc, "image") + + def get_language(self): + """Returns the language code of the language used by the user.""" + + doc = self._request("user.getInfo", True) + + return _extract(doc, "lang") + + def get_country(self): + """Returns the name of the country of the user.""" + + doc = self._request("user.getInfo", True) + + return Country(_extract(doc, "country"), self.network) + + def get_age(self): + """Returns the user's age.""" + + doc = self._request("user.getInfo", True) + + return _number(_extract(doc, "age")) + + def get_gender(self): + """Returns the user's gender. Either USER_MALE or USER_FEMALE.""" + + doc = self._request("user.getInfo", True) + + value = _extract(doc, "gender") + + if value == 'm': + return USER_MALE + elif value == 'f': + return USER_FEMALE + + return None + + def is_subscriber(self): + """Returns whether the user is a subscriber or not. True or False.""" + + doc = self._request("user.getInfo", True) + + return _extract(doc, "subscriber") == "1" + + def get_playcount(self): + """Returns the user's playcount so far.""" + + doc = self._request("user.getInfo", True) + + return _number(_extract(doc, "playcount")) + + def get_recommended_events(self, limit=50): + """ + Returns a sequence of Event objects + if limit==None it will return all + """ + + seq = [] + for node in _collect_nodes(limit, self, "user.getRecommendedEvents", False): + seq.append(Event(_extract(node, "id"), self.network)) + + return seq + + def get_recommended_artists(self, limit=50): + """ + Returns a sequence of Event objects + if limit==None it will return all + """ + + seq = [] + for node in _collect_nodes(limit, self, "user.getRecommendedArtists", False): + seq.append(Artist(_extract(node, "name"), self.network)) + + return seq + class _Search(_BaseObject): - """An abstract class. Use one of its derivatives.""" - - def __init__(self, ws_prefix, search_terms, api_key, api_secret, session_key): - _BaseObject.__init__(self, api_key, api_secret, session_key) - - self._ws_prefix = ws_prefix - self.search_terms = search_terms - - self._last_page_index = 0 - - def _get_params(self): - params = {} - - for key in self.search_terms.keys(): - params[key] = self.search_terms[key] - - return params - - def get_total_result_count(self): - """Returns the total count of all the results.""" - - doc = self._request(self._ws_prefix + ".search", True) - - return _extract(doc, "opensearch:totalResults") - - def _retreive_page(self, page_index): - """Returns the node of matches to be processed""" - - params = self._get_params() - params["page"] = str(page_index) - doc = self._request(self._ws_prefix + ".search", True, params) - - return doc.getElementsByTagName(self._ws_prefix + "matches")[0] - - def _retrieve_next_page(self): - self._last_page_index += 1 - return self._retreive_page(self._last_page_index) + """An abstract class. Use one of its derivatives.""" + + def __init__(self, ws_prefix, search_terms, network): + _BaseObject.__init__(self, network) + + self._ws_prefix = ws_prefix + self.search_terms = search_terms + + self._last_page_index = 0 + + def _get_params(self): + params = {} + + for key in self.search_terms.keys(): + params[key] = self.search_terms[key] + + return params + + def get_total_result_count(self): + """Returns the total count of all the results.""" + + doc = self._request(self._ws_prefix + ".search", True) + + return _extract(doc, "opensearch:totalResults") + + def _retreive_page(self, page_index): + """Returns the node of matches to be processed""" + + params = self._get_params() + params["page"] = str(page_index) + doc = self._request(self._ws_prefix + ".search", True, params) + + return doc.getElementsByTagName(self._ws_prefix + "matches")[0] + + def _retrieve_next_page(self): + self._last_page_index += 1 + return self._retreive_page(self._last_page_index) class AlbumSearch(_Search): - """Search for an album by name.""" - - def __init__(self, album_name, api_key, api_secret, session_key): - - _Search.__init__(self, "album", {"album": album_name}, api_key, api_secret, session_key) - - def get_next_page(self): - """Returns the next page of results as a sequence of Album objects.""" - - master_node = self._retrieve_next_page() - - list = [] - for node in master_node.getElementsByTagName("album"): - list.append(Album(_extract(node, "artist"), _extract(node, "name"), *self.auth_data)) - - return list + """Search for an album by name.""" + + def __init__(self, album_name, network): + + _Search.__init__(self, "album", {"album": album_name}, network) + + def get_next_page(self): + """Returns the next page of results as a sequence of Album objects.""" + + master_node = self._retrieve_next_page() + + seq = [] + for node in master_node.getElementsByTagName("album"): + seq.append(Album(_extract(node, "artist"), _extract(node, "name"), self.network)) + + return seq class ArtistSearch(_Search): - """Search for an artist by artist name.""" - - def __init__(self, artist_name, api_key, api_secret, session_key): - _Search.__init__(self, "artist", {"artist": artist_name}, api_key, api_secret, session_key) - - def get_next_page(self): - """Returns the next page of results as a sequence of Artist objects.""" - - master_node = self._retrieve_next_page() - - list = [] - for node in master_node.getElementsByTagName("artist"): - list.append(Artist(_extract(node, "name"), *self.auth_data)) - - return list + """Search for an artist by artist name.""" + + def __init__(self, artist_name, network): + _Search.__init__(self, "artist", {"artist": artist_name}, network) + + def get_next_page(self): + """Returns the next page of results as a sequence of Artist objects.""" + + master_node = self._retrieve_next_page() + + seq = [] + for node in master_node.getElementsByTagName("artist"): + seq.append(Artist(_extract(node, "name"), self.network)) + + return seq class TagSearch(_Search): - """Search for a tag by tag name.""" - - def __init__(self, tag_name, api_key, api_secret, session_key): - - _Search.__init__(self, "tag", {"tag": tag_name}, api_key, api_secret, session_key) - - def get_next_page(self): - """Returns the next page of results as a sequence of Tag objects.""" - - master_node = self._retrieve_next_page() - - list = [] - for node in master_node.getElementsByTagName("tag"): - list.append(Tag(_extract(node, "name"), *self.auth_data)) - - return list + """Search for a tag by tag name.""" + + def __init__(self, tag_name, network): + + _Search.__init__(self, "tag", {"tag": tag_name}, network) + + def get_next_page(self): + """Returns the next page of results as a sequence of Tag objects.""" + + master_node = self._retrieve_next_page() + + seq = [] + for node in master_node.getElementsByTagName("tag"): + seq.append(Tag(_extract(node, "name"), self.network)) + + return seq class TrackSearch(_Search): - """Search for a track by track title. If you don't wanna narrow the results down - by specifying the artist name, set it to empty string.""" - - def __init__(self, artist_name, track_title, api_key, api_secret, session_key): - - _Search.__init__(self, "track", {"track": track_title, "artist": artist_name}, api_key, api_secret, session_key) - - def get_next_page(self): - """Returns the next page of results as a sequence of Track objects.""" - - master_node = self._retrieve_next_page() - - list = [] - for node in master_node.getElementsByTagName("track"): - list.append(Track(_extract(node, "artist"), _extract(node, "name"), *self.auth_data)) - - return list + """Search for a track by track title. If you don't wanna narrow the results down + by specifying the artist name, set it to empty string.""" + + def __init__(self, artist_name, track_title, network): + + _Search.__init__(self, "track", {"track": track_title, "artist": artist_name}, network) + + def get_next_page(self): + """Returns the next page of results as a sequence of Track objects.""" + + master_node = self._retrieve_next_page() + + seq = [] + for node in master_node.getElementsByTagName("track"): + seq.append(Track(_extract(node, "artist"), _extract(node, "name"), self.network)) + + return seq class VenueSearch(_Search): - """Search for a venue by its name. If you don't wanna narrow the results down - by specifying a country, set it to empty string.""" - - def __init__(self, venue_name, country_name, api_key, api_secret, session_key): - - _Search.__init__(self, "venue", {"venue": venue_name, "country": country_name}, api_key, api_secret, session_key) - - def get_next_page(self): - """Returns the next page of results as a sequence of Track objects.""" - - master_node = self._retrieve_next_page() - - list = [] - for node in master_node.getElementsByTagName("venue"): - list.append(Venue(_extract(node, "id"), *self.auth_data)) - - return list + """Search for a venue by its name. If you don't wanna narrow the results down + by specifying a country, set it to empty string.""" + + def __init__(self, venue_name, country_name, network): + + _Search.__init__(self, "venue", {"venue": venue_name, "country": country_name}, network) + + def get_next_page(self): + """Returns the next page of results as a sequence of Track objects.""" + + master_node = self._retrieve_next_page() + + seq = [] + for node in master_node.getElementsByTagName("venue"): + seq.append(Venue(_extract(node, "id"), self.network)) + + return seq class Venue(_BaseObject): - """A venue where events are held.""" - - # TODO: waiting for a venue.getInfo web service to use. - - def __init__(self, id, api_key, api_secret, session_key): - _BaseObject.__init__(self, api_key, api_secret, session_key) - - self.id = _number(id) - - def __repr__(self): - return "Venue #" + str(self.id) - - def __eq__(self, other): - return self.get_id() == other.get_id() - - def _get_params(self): - return {"venue": self.get_id()} - - def get_id(self): - """Returns the id of the venue.""" - - return self.id - - def get_upcoming_events(self): - """Returns the upcoming events in this venue.""" - - doc = self._request("venue.getEvents", True) - - list = [] - for node in doc.getElementsByTagName("event"): - list.append(Event(_extract(node, "id"), *self.auth_data)) - - return list - - def get_past_events(self): - """Returns the past events held in this venue.""" - - doc = self._request("venue.getEvents", True) - - list = [] - for node in doc.getElementsByTagName("event"): - list.append(Event(_extract(node, "id"), *self.auth_data)) - - return list - -def create_new_playlist(title, description, api_key, api_secret, session_key): - """Creates a playlist for the authenticated user and returns it. - * title: The title of the new playlist. - * description: The description of the new playlist. - """ - - params = dict() - params['title'] = unicode(title) - params['description'] = unicode(description) - - doc = _Request('playlist.create', params, api_key, api_secret, session_key).execute() - - id = doc.getElementsByTagName("id")[0].firstChild.data - user = doc.getElementsByTagName('playlists')[0].getAttribute('user') - - return Playlist(user, id, api_key, api_secret, session_key) - -def get_authenticated_user(api_key, api_secret, session_key): - """Returns the authenticated user.""" - - return AuthenticatedUser(api_key, api_secret, session_key) + """A venue where events are held.""" + + # TODO: waiting for a venue.getInfo web service to use. + + id = None + + def __init__(self, id, network): + _BaseObject.__init__(self, network) + + self.id = _number(id) + + @_string_output + def __repr__(self): + return "Venue #" + str(self.id) + + def __eq__(self, other): + return self.get_id() == other.get_id() + + def _get_params(self): + return {"venue": self.get_id()} + + def get_id(self): + """Returns the id of the venue.""" + + return self.id + + def get_upcoming_events(self): + """Returns the upcoming events in this venue.""" + + doc = self._request("venue.getEvents", True) + + seq = [] + for node in doc.getElementsByTagName("event"): + seq.append(Event(_extract(node, "id"), self.network)) + + return seq + + def get_past_events(self): + """Returns the past events held in this venue.""" + + doc = self._request("venue.getEvents", True) + + seq = [] + for node in doc.getElementsByTagName("event"): + seq.append(Event(_extract(node, "id"), self.network)) + + return seq def md5(text): - """Returns the md5 hash of a string.""" - - hash = hashlib.md5() - hash.update(text.encode("utf-8")) - - return hash.hexdigest() - -def enable_proxy(host, port): - """Enable a default web proxy.""" - - global __proxy - global __proxy_enabled - - __proxy = [host, _number(port)] - __proxy_enabled = True - -def disable_proxy(): - """Disable using the web proxy.""" - - global __proxy_enabled - - __proxy_enabled = False - -def is_proxy_enabled(): - """Returns True if a web proxy is enabled.""" - - global __proxy_enabled - - return __proxy_enabled - -def _get_proxy(): - """Returns proxy details.""" - - global __proxy - - return __proxy + """Returns the md5 hash of a string.""" + + h = hashlib.md5() + h.update(_string(text)) + + return h.hexdigest() def async_call(sender, call, callback = None, call_args = None, callback_args = None): - """This is the function for setting up an asynchronous operation. - * call: The function to call asynchronously. - * callback: The function to call after the operation is complete, Its prototype has to be like: - callback(sender, output[, param1, param3, ... ]) - * call_args: A sequence of args to be passed to call. - * callback_args: A sequence of args to be passed to callback. - """ - - thread = _ThreadedCall(sender, call, call_args, callback, callback_args) - thread.start() - -def enable_caching(cache_filename = None): - """Enables caching request-wide for all cachable calls. Uses a shelve.DbfilenameShelf object. - * cache_filename: A filename for the db. Defaults to a temporary filename in the tmp directory. - """ - - global __cache_shelf - global __cache_filename - - if not cache_filename: - cache_filename = tempfile.mktemp(prefix="pylast_tmp_") - - __cache_filename = cache_filename - __cache_shelf = shelve.open(__cache_filename) - -def disable_caching(): - """Disables all caching features.""" - - global __cache_shelf - __cache_shelf = None - -def close_shelf(): - global __cache_shelf - if __cache_shelf: - __cache_shelf.close() - __cache_shelf = None - -def is_caching_enabled(): - """Returns True if caching is enabled.""" - - global __cache_shelf - return not (__cache_shelf == None) - -def get_cache_filename(): - """Returns filename of the cache db in use.""" - - global __cache_filename - return __cache_filename - -def get_cache_shelf(): - """Returns the Shelf object used for caching.""" - - global __cache_shelf - return __cache_shelf - + """This is the function for setting up an asynchronous operation. + * call: The function to call asynchronously. + * callback: The function to call after the operation is complete, Its prototype has to be like: + callback(sender, output[, param1, param3, ... ]) + * call_args: A sequence of args to be passed to call. + * callback_args: A sequence of args to be passed to callback. + """ + + thread = _ThreadedCall(sender, call, call_args, callback, callback_args) + thread.start() + +def _unicode(text): + if type(text) == unicode: + return text + + if type(text) == int: + return unicode(text) + + return unicode(text, "utf-8") + +def _string(text): + if type(text) == str: + return text + + if type(text) == int: + return str(text) + + return text.encode("utf-8") + +def _collect_nodes(limit, sender, method_name, cacheable, params=None): + """ + Returns a sequqnce of dom.Node objects about as close to + limit as possible + """ + + if not limit: limit = sys.maxint + if not params: params = sender._get_params() + + nodes = [] + page = 1 + end_of_pages = False + + while len(nodes) < limit and not end_of_pages: + params["page"] = str(page) + doc = sender._request(method_name, cacheable, params) + + main = doc.documentElement.childNodes[1] + + if main.hasAttribute("totalPages"): + total_pages = _number(main.getAttribute("totalPages")) + elif main.hasAttribute("totalpages"): + total_pages = _number(main.getAttribute("totalpages")) + else: + raise Exception("No total pages attribute") + + for node in main.childNodes: + if not node.nodeType == xml.dom.Node.TEXT_NODE and len(nodes) < limit: + nodes.append(node) + + if page >= total_pages: + end_of_pages = True + + page += 1 + + return nodes + def _extract(node, name, index = 0): - """Extracts a value from the xml string""" - - nodes = node.getElementsByTagName(name) - - if len(nodes): - if nodes[index].firstChild: - return nodes[index].firstChild.data.strip() - else: - return None + """Extracts a value from the xml string""" + + nodes = node.getElementsByTagName(name) + + if len(nodes): + if nodes[index].firstChild: + return _unescape_htmlentity(nodes[index].firstChild.data.strip()) + else: + return None def _extract_all(node, name, limit_count = None): - """Extracts all the values from the xml string. returning a list.""" - - list = [] - - for i in range(0, len(node.getElementsByTagName(name))): - if len(list) == limit_count: - break - - list.append(_extract(node, name, i)) - - return list - -def _get_url_safe(text): - """Does all kinds of tricks on a text to make it safe to use in a url.""" - - if type(text) == type(unicode()): - text = text - - return urllib.quote_plus(urllib.quote_plus(text)).lower() + """Extracts all the values from the xml string. returning a list.""" + + seq = [] + + for i in range(0, len(node.getElementsByTagName(name))): + if len(seq) == limit_count: + break + + seq.append(_extract(node, name, i)) + + return seq + +def _url_safe(text): + """Does all kinds of tricks on a text to make it safe to use in a url.""" + + if type(text) == unicode: + text = text.encode('utf-8') + + return urllib.quote_plus(urllib.quote_plus(text)).lower() def _number(string): - """Extracts an int from a string. Returns a 0 if None or an empty string was passed.""" - - if not string: - return 0 - elif string == "": - return 0 - else: - return int(string) - -def search_for_album(album_name, api_key, api_secret, session_key): - """Searches for an album by its name. Returns a AlbumSearch object. - Use get_next_page() to retreive sequences of results.""" - - return AlbumSearch(album_name, api_key, api_secret, session_key) - -def search_for_artist(artist_name, api_key, api_secret, session_key): - """Searches of an artist by its name. Returns a ArtistSearch object. - Use get_next_page() to retreive sequences of results.""" - - return ArtistSearch(artist_name, api_key, api_secret, session_key) - -def search_for_tag(tag_name, api_key, api_secret, session_key): - """Searches of a tag by its name. Returns a TagSearch object. - Use get_next_page() to retreive sequences of results.""" - - return TagSearch(tag_name, api_key, api_secret, session_key) - -def search_for_track(artist_name, track_name, api_key, api_secret, session_key): - """Searches of a track by its name and its artist. Set artist to an empty string if not available. - Returns a TrackSearch object. - Use get_next_page() to retreive sequences of results.""" - - return TrackSearch(artist_name, track_name, api_key, api_secret, session_key) - -def search_for_venue(venue_name, country_name, api_key, api_secret, session_key): - """Searches of a venue by its name and its country. Set country_name to an empty string if not available. - Returns a VenueSearch object. - Use get_next_page() to retreive sequences of results.""" - - return VenueSearch(venue_name, country_name, api_key, api_secret, session_key) - + """ + Extracts an int from a string. Returns a 0 if None or an empty string was passed + """ + + if not string: + return 0 + elif string == "": + return 0 + else: + try: + return int(string) + except ValueError: + return float(string) + +def _unescape_htmlentity(string): + + string = _unicode(string) + + mapping = htmlentitydefs.name2codepoint + for key in mapping: + string = string.replace("&%s;" %key, unichr(mapping[key])) + + return string + def extract_items(topitems_or_libraryitems): - """Extracts a sequence of items from a sequence of TopItem or LibraryItem objects.""" - - list = [] - for i in topitems_or_libraryitems: - list.append(i.get_item()) - - return list - -def get_top_tags(api_key, api_secret, session_key): - """Returns a sequence of the most used Last.fm tags as a sequence of TopItem objects.""" - - doc = _Request("tag.getTopTags", dict(), api_key, api_secret, session_key).execute(True) - list = [] - for node in doc.getElementsByTagName("tag"): - tag = Tag(_extract(node, "name"), api_key, api_secret, session_key) - weight = _extract(node, "count") - - list.append(TopItem(tag, weight)) - - return list - -def get_track_by_mbid(mbid, api_key, api_secret, session_key): - """Looks up a track by its MusicBrainz ID.""" - - params = {"mbid": unicode(mbid)} - - doc = _Request("track.getInfo", params, api_key, api_secret, session_key).execute(True) - - return Track(_extract(doc, "name", 1), _extract(doc, "name"), api_key, api_secret, session_key) - -def get_artist_by_mbid(mbid, api_key, api_secret, session_key): - """Loooks up an artist by its MusicBrainz ID.""" - - params = {"mbid": unicode(mbid)} - - doc = _Request("artist.getInfo", params, api_key, api_secret, session_key).execute(True) - - return Artist(_extract(doc, "name"), api_key, api_secret, session_key) - -def get_album_by_mbid(mbid, api_key, api_secret, session_key): - """Looks up an album by its MusicBrainz ID.""" - - params = {"mbid": unicode(mbid)} - - doc = _Request("album.getInfo", params, api_key, api_secret, session_key).execute(True) - - return Album(_extract(doc, "artist"), _extract(doc, "name"), api_key, api_secret, session_key) - -def _delay_call(): - """Makes sure that web service calls are at least a second apart.""" - - global __last_call_time - - # delay time in seconds - DELAY_TIME = 1.0 - now = time.time() - - if (now - __last_call_time) < DELAY_TIME: - time.sleep(1) - - __last_call_time = now - -def clear_cache(): - """Clears the cache data and starts fresh.""" - - global __cache_dir - - - if not os.path.exists(__cache_dir): - return - - for file in os.listdir(__cache_dir): - os.remove(os.path.join(__cache_dir, file)) - - - -# ------------------------------------------------------------ - -class ScrobblingException(Exception): - def __inint__(self, message): - Exception.__init__(self) - self.message = message - - def __repr__(self): - return self.message - -class BannedClient(ScrobblingException): - def __init__(self): - ScrobblingException.__init__(self, "This version of the client has been banned") - -class BadAuthentication(ScrobblingException): - def __init__(self): - ScrobblingException.__init__(self, "Bad authentication token") - -class BadTime(ScrobblingException): - def __init__(self): - ScrobblingException.__init__(self, "Time provided is not close enough to current time") - -class BadSession(ScrobblingException): - def __init__(self): - ScrobblingException.__init__(self, "Bad session id, consider re-handshaking") + """Extracts a sequence of items from a sequence of TopItem or LibraryItem objects.""" + + seq = [] + for i in topitems_or_libraryitems: + seq.append(i.get_item()) + + return seq + +class ScrobblingError(Exception): + def __init__(self, message): + Exception.__init__(self) + self.message = message + + @_string_output + def __str__(self): + return self.message + +class BannedClientError(ScrobblingError): + def __init__(self): + ScrobblingError.__init__(self, "This version of the client has been banned") + +class BadAuthenticationError(ScrobblingError): + def __init__(self): + ScrobblingError.__init__(self, "Bad authentication token") + +class BadTimeError(ScrobblingError): + def __init__(self): + ScrobblingError.__init__(self, "Time provided is not close enough to current time") + +class BadSessionError(ScrobblingError): + def __init__(self): + ScrobblingError.__init__(self, "Bad session id, consider re-handshaking") class _ScrobblerRequest(object): - - def __init__(self, url, params): - self.params = params - self.hostname = url[url.find("//") + 2:url.rfind("/")] - self.subdir = url[url.rfind("/"):] - - def execute(self): - """Returns a string response of this request.""" - - connection = httplib.HTTPConnection(self.hostname) - - data = [] - for name in self.params.keys(): - value = urllib.quote_plus(self.params[name]) - data.append('='.join((name, value))) - data = "&".join(data) - - headers = { - "Content-type": "application/x-www-form-urlencoded", - "Accept-Charset": "utf-8", - "User-Agent": __name__ + "/" + __version__, - "HOST": self.hostname - } - - debug("Scrobbler Request:\n\tHOST :" + self.hostname + "\n\tSUBDIR: " + self.subdir + - "\n\tDATA:" + repr(data) + "\n\tHEADERS: " + repr(headers)) - connection.request("POST", self.subdir, data, headers) - response = connection.getresponse().read() - - self._check_response_for_errors(response) - debug("Scrobbler Response:\n\t" + response) - - return response - - def _check_response_for_errors(self, response): - """When passed a string response it checks for erros, raising - any exceptions as necessary.""" - - lines = response.split("\n") - status_line = lines[0] - - if status_line == "OK": - return - elif status_line == "BANNED": - raise BannedClient() - elif status_line == "BADAUTH": - raise BadAuthenticationException() - elif status_line == "BADTIME": - raise BadTime() - elif status_line == "BADSESSION": - raise BadSession() - elif status_line.startswith("FAILED "): - reason = status_line[status_line.find("FAILED ")+len("FAILED "):] - raise ScrobblingException(reason) - + + def __init__(self, url, params, network, type="POST"): + self.params = params + self.type = type + (self.hostname, self.subdir) = urllib.splithost(url[len("http:"):]) + self.network = network + + def execute(self): + """Returns a string response of this request.""" + + connection = httplib.HTTPConnection(self.hostname) + + data = [] + for name in self.params.keys(): + value = urllib.quote_plus(self.params[name]) + data.append('='.join((name, value))) + data = "&".join(data) + + headers = { + "Content-type": "application/x-www-form-urlencoded", + "Accept-Charset": "utf-8", + "User-Agent": "pylast" + "/" + __version__, + "HOST": self.hostname + } + + if self.type == "GET": + connection.request("GET", self.subdir + "?" + data, headers = headers) + else: + connection.request("POST", self.subdir, data, headers) + response = connection.getresponse().read() + + self._check_response_for_errors(response) + + return response + + def _check_response_for_errors(self, response): + """When passed a string response it checks for erros, raising + any exceptions as necessary.""" + + lines = response.split("\n") + status_line = lines[0] + + if status_line == "OK": + return + elif status_line == "BANNED": + raise BannedClientError() + elif status_line == "BADAUTH": + raise BadAuthenticationError() + elif status_line == "BadTimeError": + raise BadTimeError() + elif status_line == "BadSessionError": + raise BadSessionError() + elif status_line.startswith("FAILED "): + reason = status_line[status_line.find("FAILED ")+len("FAILED "):] + raise ScrobblingError(reason) + class Scrobbler(object): - """A class for scrobbling tracks to Last.fm""" - - session_id = None - nowplaying_url = None - submissions_url = None - - def __init__(self, client_id, client_version, username, md5_password): - self.client_id = client_id - self.client_version = client_version - self.username = username - self.password = md5_password - - def _do_handshake(self): - """Handshakes with the server""" - - timestamp = str(int(time.time())) - token = md5(self.password + timestamp) - - params = {"hs": "true", "p": "1.2.1", "c": self.client_id, - "v": self.client_version, "u": self.username, "t": timestamp, - "a": token} - - global SUBMISSION_SERVER - response = _ScrobblerRequest(SUBMISSION_SERVER, params).execute().split("\n") - - self.session_id = response[1] - self.nowplaying_url = response[2] - self.submissions_url = response[3] - - def _get_session_id(self, new = False): - """Returns a handshake. If new is true, then it will be requested from the server - even if one was cached.""" - - if not self.session_id or new: - debug("Doing a scrobbling handshake") - self._do_handshake() - - return self.session_id - - def report_now_playing(self, artist, title, album = "", duration = "", track_number = "", mbid = ""): - - params = {"s": self._get_session_id(), "a": artist, "t": title, - "b": album, "l": duration, "n": track_number, "m": mbid} - - response = _ScrobblerRequest(self.nowplaying_url, params).execute() - - try: - _ScrobblerRequest(self.nowplaying_url, params).execute() - except BadSession: - self._do_handshake() - self.report_now_playing(artist, title, album, duration, track_number, mbid) - - info(artist + " - " + title + " was reported as now-playing") - - def scrobble(self, artist, title, time_started, source, mode, duration, album="", track_number="", mbid=""): - """Scrobble a track. parameters: - artist: Artist name. - title: Track title. - time_started: UTC timestamp of when the track started playing. - source: The source of the track - SCROBBLE_SOURCE_USER: Chosen by the user (the most common value, unless you have a reason for choosing otherwise, use this). - SCROBBLE_SOURCE_NON_PERSONALIZED_BROADCAST: Non-personalised broadcast (e.g. Shoutcast, BBC Radio 1). - SCROBBLE_SOURCE_PERSONALIZED_BROADCAST: Personalised recommendation except Last.fm (e.g. Pandora, Launchcast). - SCROBBLE_SOURCE_LASTFM: ast.fm (any mode). In this case, the 5-digit recommendation_key value must be set. - SCROBBLE_SOURCE_UNKNOWN: Source unknown. - mode: The submission mode - SCROBBLE_MODE_PLAYED: The track was played. - SCROBBLE_MODE_SKIPPED: The track was skipped (Only if source was Last.fm) - SCROBBLE_MODE_BANNED: The track was banned (Only if source was Last.fm) - duration: Track duration in seconds. - album: The album name. - track_number: The track number on the album. - mbid: MusicBrainz ID. - """ - - params = {"s": self._get_session_id(), "a[0]": artist, "t[0]": title, - "i[0]": str(time_started), "o[0]": source, "r[0]": mode, "l[0]": str(duration), - "b[0]": album, "n[0]": track_number, "m[0]": mbid} - - response = _ScrobblerRequest(self.submissions_url, params).execute() - info(artist + " - " + title + " was scrobbled") - - -#LFM_API_KEY = '3b954460f7b207e5414ffdf8c5710592' -#print search_for_track('Air', 'Universal Traveller', LFM_API_KEY, None, None).get_next_page()[0].get_top_tags() \ No newline at end of file + """A class for scrobbling tracks to Last.fm""" + + session_id = None + nowplaying_url = None + submissions_url = None + + def __init__(self, network, client_id, client_version): + self.client_id = client_id + self.client_version = client_version + self.username = network.username + self.password = network.password_hash + self.network = network + + def _do_handshake(self): + """Handshakes with the server""" + + timestamp = str(int(time.time())) + token = md5(self.password + timestamp) + + params = {"hs": "true", "p": "1.2.1", "c": self.client_id, + "v": self.client_version, "u": self.username, "t": timestamp, + "a": token} + + server = self.network.submission_server + response = _ScrobblerRequest(server, params, self.network, "GET").execute().split("\n") + + self.session_id = response[1] + self.nowplaying_url = response[2] + self.submissions_url = response[3] + + def _get_session_id(self, new = False): + """Returns a handshake. If new is true, then it will be requested from the server + even if one was cached.""" + + if not self.session_id or new: + self._do_handshake() + + return self.session_id + + def report_now_playing(self, artist, title, album = "", duration = "", track_number = "", mbid = ""): + + params = {"s": self._get_session_id(), "a": artist, "t": title, + "b": album, "l": duration, "n": track_number, "m": mbid} + + try: + _ScrobblerRequest(self.nowplaying_url, params, self.network).execute() + except BadSessionError: + self._do_handshake() + self.report_now_playing(artist, title, album, duration, track_number, mbid) + + def scrobble(self, artist, title, time_started, source, mode, duration, album="", track_number="", mbid=""): + """Scrobble a track. parameters: + artist: Artist name. + title: Track title. + time_started: UTC timestamp of when the track started playing. + source: The source of the track + SCROBBLE_SOURCE_USER: Chosen by the user (the most common value, unless you have a reason for choosing otherwise, use this). + SCROBBLE_SOURCE_NON_PERSONALIZED_BROADCAST: Non-personalised broadcast (e.g. Shoutcast, BBC Radio 1). + SCROBBLE_SOURCE_PERSONALIZED_BROADCAST: Personalised recommendation except Last.fm (e.g. Pandora, Launchcast). + SCROBBLE_SOURCE_LASTFM: ast.fm (any mode). In this case, the 5-digit recommendation_key value must be set. + SCROBBLE_SOURCE_UNKNOWN: Source unknown. + mode: The submission mode + SCROBBLE_MODE_PLAYED: The track was played. + SCROBBLE_MODE_LOVED: The user manually loved the track (implies a listen) + SCROBBLE_MODE_SKIPPED: The track was skipped (Only if source was Last.fm) + SCROBBLE_MODE_BANNED: The track was banned (Only if source was Last.fm) + duration: Track duration in seconds. + album: The album name. + track_number: The track number on the album. + mbid: MusicBrainz ID. + """ + + params = {"s": self._get_session_id(), "a[0]": _string(artist), "t[0]": _string(title), + "i[0]": str(time_started), "o[0]": source, "r[0]": mode, "l[0]": str(duration), + "b[0]": _string(album), "n[0]": track_number, "m[0]": mbid} + + _ScrobblerRequest(self.submissions_url, params, self.network).execute()