PageRenderTime 55ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/pylast.py

http://radioappz.googlecode.com/
Python | 2040 lines | 1933 code | 56 blank | 51 comment | 4 complexity | c08f326bfb837c1d91ab13df70e62b8d MD5 | raw file
  1. # -*- coding: utf-8 -*-
  2. #
  3. # pylast - A Python interface to Last.fm (and other API compatible social networks)
  4. # Copyright (C) 2008-2009 Amr Hassan
  5. #
  6. # This program is free software; you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation; either version 2 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program; if not, write to the Free Software
  18. # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  19. # USA
  20. #
  21. # http://code.google.com/p/pylast/
  22. __version__ = '0.4'
  23. __author__ = 'Amr Hassan'
  24. __copyright__ = "Copyright (C) 2008-2009 Amr Hassan"
  25. __license__ = "gpl"
  26. __email__ = 'amr.hassan@gmail.com'
  27. import hashlib
  28. import httplib
  29. import urllib
  30. import threading
  31. from xml.dom import minidom
  32. import xml.dom
  33. import time
  34. import shelve
  35. import tempfile
  36. import sys
  37. import htmlentitydefs
  38. try:
  39. import collections
  40. except ImportError:
  41. pass
  42. STATUS_INVALID_SERVICE = 2
  43. STATUS_INVALID_METHOD = 3
  44. STATUS_AUTH_FAILED = 4
  45. STATUS_INVALID_FORMAT = 5
  46. STATUS_INVALID_PARAMS = 6
  47. STATUS_INVALID_RESOURCE = 7
  48. STATUS_TOKEN_ERROR = 8
  49. STATUS_INVALID_SK = 9
  50. STATUS_INVALID_API_KEY = 10
  51. STATUS_OFFLINE = 11
  52. STATUS_SUBSCRIBERS_ONLY = 12
  53. STATUS_INVALID_SIGNATURE = 13
  54. STATUS_TOKEN_UNAUTHORIZED = 14
  55. STATUS_TOKEN_EXPIRED = 15
  56. EVENT_ATTENDING = '0'
  57. EVENT_MAYBE_ATTENDING = '1'
  58. EVENT_NOT_ATTENDING = '2'
  59. PERIOD_OVERALL = 'overall'
  60. PERIOD_3MONTHS = '3month'
  61. PERIOD_6MONTHS = '6month'
  62. PERIOD_12MONTHS = '12month'
  63. DOMAIN_ENGLISH = 0
  64. DOMAIN_GERMAN = 1
  65. DOMAIN_SPANISH = 2
  66. DOMAIN_FRENCH = 3
  67. DOMAIN_ITALIAN = 4
  68. DOMAIN_POLISH = 5
  69. DOMAIN_PORTUGUESE = 6
  70. DOMAIN_SWEDISH = 7
  71. DOMAIN_TURKISH = 8
  72. DOMAIN_RUSSIAN = 9
  73. DOMAIN_JAPANESE = 10
  74. DOMAIN_CHINESE = 11
  75. COVER_SMALL = 0
  76. COVER_MEDIUM = 1
  77. COVER_LARGE = 2
  78. COVER_EXTRA_LARGE = 3
  79. COVER_MEGA = 4
  80. IMAGES_ORDER_POPULARITY = "popularity"
  81. IMAGES_ORDER_DATE = "dateadded"
  82. USER_MALE = 'Male'
  83. USER_FEMALE = 'Female'
  84. SCROBBLE_SOURCE_USER = "P"
  85. SCROBBLE_SOURCE_NON_PERSONALIZED_BROADCAST = "R"
  86. SCROBBLE_SOURCE_PERSONALIZED_BROADCAST = "E"
  87. SCROBBLE_SOURCE_LASTFM = "L"
  88. SCROBBLE_SOURCE_UNKNOWN = "U"
  89. SCROBBLE_MODE_PLAYED = ""
  90. SCROBBLE_MODE_LOVED = "L"
  91. SCROBBLE_MODE_BANNED = "B"
  92. SCROBBLE_MODE_SKIPPED = "S"
  93. """
  94. A list of the implemented webservices (from http://www.last.fm/api/intro)
  95. =====================================
  96. # Album
  97. * album.addTags DONE
  98. * album.getInfo DONE
  99. * album.getTags DONE
  100. * album.removeTag DONE
  101. * album.search DONE
  102. # Artist
  103. * artist.addTags DONE
  104. * artist.getEvents DONE
  105. * artist.getImages DONE
  106. * artist.getInfo DONE
  107. * artist.getPodcast TODO
  108. * artist.getShouts DONE
  109. * artist.getSimilar DONE
  110. * artist.getTags DONE
  111. * artist.getTopAlbums DONE
  112. * artist.getTopFans DONE
  113. * artist.getTopTags DONE
  114. * artist.getTopTracks DONE
  115. * artist.removeTag DONE
  116. * artist.search DONE
  117. * artist.share DONE
  118. * artist.shout DONE
  119. # Auth
  120. * auth.getMobileSession DONE
  121. * auth.getSession DONE
  122. * auth.getToken DONE
  123. # Event
  124. * event.attend DONE
  125. * event.getAttendees DONE
  126. * event.getInfo DONE
  127. * event.getShouts DONE
  128. * event.share DONE
  129. * event.shout DONE
  130. # Geo
  131. * geo.getEvents
  132. * geo.getTopArtists
  133. * geo.getTopTracks
  134. # Group
  135. * group.getMembers DONE
  136. * group.getWeeklyAlbumChart DONE
  137. * group.getWeeklyArtistChart DONE
  138. * group.getWeeklyChartList DONE
  139. * group.getWeeklyTrackChart DONE
  140. # Library
  141. * library.addAlbum DONE
  142. * library.addArtist DONE
  143. * library.addTrack DONE
  144. * library.getAlbums DONE
  145. * library.getArtists DONE
  146. * library.getTracks DONE
  147. # Playlist
  148. * playlist.addTrack DONE
  149. * playlist.create DONE
  150. * playlist.fetch DONE
  151. # Radio
  152. * radio.getPlaylist
  153. * radio.tune
  154. # Tag
  155. * tag.getSimilar DONE
  156. * tag.getTopAlbums DONE
  157. * tag.getTopArtists DONE
  158. * tag.getTopTags DONE
  159. * tag.getTopTracks DONE
  160. * tag.getWeeklyArtistChart DONE
  161. * tag.getWeeklyChartList DONE
  162. * tag.search DONE
  163. # Tasteometer
  164. * tasteometer.compare DONE
  165. # Track
  166. * track.addTags DONE
  167. * track.ban DONE
  168. * track.getInfo DONE
  169. * track.getSimilar DONE
  170. * track.getTags DONE
  171. * track.getTopFans DONE
  172. * track.getTopTags DONE
  173. * track.love DONE
  174. * track.removeTag DONE
  175. * track.search DONE
  176. * track.share DONE
  177. # User
  178. * user.getEvents DONE
  179. * user.getFriends DONE
  180. * user.getInfo DONE
  181. * user.getLovedTracks DONE
  182. * user.getNeighbours DONE
  183. * user.getPastEvents DONE
  184. * user.getPlaylists DONE
  185. * user.getRecentStations TODO
  186. * user.getRecentTracks DONE
  187. * user.getRecommendedArtists DONE
  188. * user.getRecommendedEvents DONE
  189. * user.getShouts DONE
  190. * user.getTopAlbums DONE
  191. * user.getTopArtists DONE
  192. * user.getTopTags DONE
  193. * user.getTopTracks DONE
  194. * user.getWeeklyAlbumChart DONE
  195. * user.getWeeklyArtistChart DONE
  196. * user.getWeeklyChartList DONE
  197. * user.getWeeklyTrackChart DONE
  198. * user.shout DONE
  199. # Venue
  200. * venue.getEvents DONE
  201. * venue.getPastEvents DONE
  202. * venue.search DONE
  203. """
  204. class Network(object):
  205. """
  206. A music social network website that is Last.fm or one exposing a Last.fm compatible API
  207. """
  208. def __init__(self, name, homepage, ws_server, api_key, api_secret, session_key, submission_server, username, password_hash,
  209. domain_names, urls):
  210. """
  211. name: the name of the network
  212. homepage: the homepage url
  213. ws_server: the url of the webservices server
  214. api_key: a provided API_KEY
  215. api_secret: a provided API_SECRET
  216. session_key: a generated session_key or None
  217. submission_server: the url of the server to which tracks are submitted (scrobbled)
  218. username: a username of a valid user
  219. password_hash: the output of pylast.md5(password) where password is the user's password thingy
  220. domain_names: a dict mapping each DOMAIN_* value to a string domain name
  221. urls: a dict mapping types to urls
  222. if username and password_hash were provided and not session_key, session_key will be
  223. generated automatically when needed.
  224. Either a valid session_key or a combination of username and password_hash must be present for scrobbling.
  225. You should use a preconfigured network object through a get_*_network(...) method instead of creating an object
  226. of this class, unless you know what you're doing.
  227. """
  228. self.ws_server = ws_server
  229. self.submission_server = submission_server
  230. self.name = name
  231. self.homepage = homepage
  232. self.api_key = api_key
  233. self.api_secret = api_secret
  234. self.session_key = session_key
  235. self.username = username
  236. self.password_hash = password_hash
  237. self.domain_names = domain_names
  238. self.urls = urls
  239. self.cache_backend = None
  240. self.proxy_enabled = False
  241. self.proxy = None
  242. self.last_call_time = 0
  243. #generate a session_key if necessary
  244. if (self.api_key and self.api_secret) and not self.session_key and (self.username and self.password_hash):
  245. sk_gen = SessionKeyGenerator(self)
  246. self.session_key = sk_gen.get_session_key(self.username, self.password_hash)
  247. def get_artist(self, artist_name):
  248. """
  249. Return an Artist object
  250. """
  251. return Artist(artist_name, self)
  252. def get_track(self, artist, title):
  253. """
  254. Return a Track object
  255. """
  256. return Track(artist, title, self)
  257. def get_album(self, artist, title):
  258. """
  259. Return an Album object
  260. """
  261. return Album(artist, title, self)
  262. def get_authenticated_user(self):
  263. """
  264. Returns the authenticated user
  265. """
  266. return AuthenticatedUser(self)
  267. def get_country(self, country_name):
  268. """
  269. Returns a country object
  270. """
  271. return Country(country_name, self)
  272. def get_group(self, name):
  273. """
  274. Returns a Group object
  275. """
  276. return Group(name, self)
  277. def get_user(self, username):
  278. """
  279. Returns a user object
  280. """
  281. return User(username, self)
  282. def get_tag(self, name):
  283. """
  284. Returns a tag object
  285. """
  286. return Tag(name, self)
  287. def get_scrobbler(self, client_id, client_version):
  288. """
  289. Returns a Scrobbler object used for submitting tracks to the server
  290. Quote from http://www.last.fm/api/submissions:
  291. ========
  292. Client identifiers are used to provide a centrally managed database of
  293. the client versions, allowing clients to be banned if they are found to
  294. be behaving undesirably. The client ID is associated with a version
  295. number on the server, however these are only incremented if a client is
  296. banned and do not have to reflect the version of the actual client application.
  297. During development, clients which have not been allocated an identifier should
  298. use the identifier tst, with a version number of 1.0. Do not distribute code or
  299. client implementations which use this test identifier. Do not use the identifiers
  300. used by other clients.
  301. =========
  302. To obtain a new client identifier please contact:
  303. * Last.fm: submissions@last.fm
  304. * # TODO: list others
  305. ...and provide us with the name of your client and its homepage address.
  306. """
  307. return Scrobbler(self, client_id, client_version)
  308. def _get_language_domain(self, domain_language):
  309. """
  310. Returns the mapped domain name of the network to a DOMAIN_* value
  311. """
  312. if domain_language in self.domain_names:
  313. return self.domain_names[domain_language]
  314. def _get_url(self, domain, type):
  315. return "http://%s/%s" %(self._get_language_domain(domain), self.urls[type])
  316. def _get_ws_auth(self):
  317. """
  318. Returns a (API_KEY, API_SECRET, SESSION_KEY) tuple.
  319. """
  320. return (self.api_key, self.api_secret, self.session_key)
  321. def _delay_call(self):
  322. """
  323. Makes sure that web service calls are at least a second apart
  324. """
  325. # delay time in seconds
  326. DELAY_TIME = 1.0
  327. now = time.time()
  328. if (now - self.last_call_time) < DELAY_TIME:
  329. time.sleep(1)
  330. self.last_call_time = now
  331. def create_new_playlist(self, title, description):
  332. """
  333. Creates a playlist for the authenticated user and returns it
  334. title: The title of the new playlist.
  335. description: The description of the new playlist.
  336. """
  337. params = {}
  338. params['title'] = _unicode(title)
  339. params['description'] = _unicode(description)
  340. doc = _Request(self, 'playlist.create', params).execute(False)
  341. e_id = doc.getElementsByTagName("id")[0].firstChild.data
  342. user = doc.getElementsByTagName('playlists')[0].getAttribute('user')
  343. return Playlist(user, e_id, self)
  344. def get_top_tags(self, limit=None):
  345. """Returns a sequence of the most used tags as a sequence of TopItem objects."""
  346. doc = _Request(self, "tag.getTopTags").execute(True)
  347. seq = []
  348. for node in doc.getElementsByTagName("tag"):
  349. tag = Tag(_extract(node, "name"), self)
  350. weight = _number(_extract(node, "count"))
  351. if len(seq) < limit:
  352. seq.append(TopItem(tag, weight))
  353. return seq
  354. def enable_proxy(self, host, port):
  355. """Enable a default web proxy"""
  356. self.proxy = [host, _number(port)]
  357. self.proxy_enabled = True
  358. def disable_proxy(self):
  359. """Disable using the web proxy"""
  360. self.proxy_enabled = False
  361. def is_proxy_enabled(self):
  362. """Returns True if a web proxy is enabled."""
  363. return self.proxy_enabled
  364. def _get_proxy(self):
  365. """Returns proxy details."""
  366. return self.proxy
  367. def enable_caching(self, file_path = None):
  368. """Enables caching request-wide for all cachable calls.
  369. In choosing the backend used for caching, it will try _SqliteCacheBackend first if
  370. the module sqlite3 is present. If not, it will fallback to _ShelfCacheBackend which uses shelve.Shelf objects.
  371. * file_path: A file path for the backend storage file. If
  372. None set, a temp file would probably be created, according the backend.
  373. """
  374. if not file_path:
  375. file_path = tempfile.mktemp(prefix="pylast_tmp_")
  376. self.cache_backend = _ShelfCacheBackend(file_path)
  377. def disable_caching(self):
  378. """Disables all caching features."""
  379. self.cache_backend = None
  380. def is_caching_enabled(self):
  381. """Returns True if caching is enabled."""
  382. return not (self.cache_backend == None)
  383. def _get_cache_backend(self):
  384. return self.cache_backend
  385. def search_for_album(self, album_name):
  386. """Searches for an album by its name. Returns a AlbumSearch object.
  387. Use get_next_page() to retreive sequences of results."""
  388. return AlbumSearch(album_name, self)
  389. def search_for_artist(self, artist_name):
  390. """Searches of an artist by its name. Returns a ArtistSearch object.
  391. Use get_next_page() to retreive sequences of results."""
  392. return ArtistSearch(artist_name, self)
  393. def search_for_tag(self, tag_name):
  394. """Searches of a tag by its name. Returns a TagSearch object.
  395. Use get_next_page() to retreive sequences of results."""
  396. return TagSearch(tag_name, self)
  397. def search_for_track(self, artist_name, track_name):
  398. """Searches of a track by its name and its artist. Set artist to an empty string if not available.
  399. Returns a TrackSearch object.
  400. Use get_next_page() to retreive sequences of results."""
  401. return TrackSearch(artist_name, track_name, self)
  402. def search_for_venue(self, venue_name, country_name):
  403. """Searches of a venue by its name and its country. Set country_name to an empty string if not available.
  404. Returns a VenueSearch object.
  405. Use get_next_page() to retreive sequences of results."""
  406. return VenueSearch(venue_name, country_name, self)
  407. def get_track_by_mbid(self, mbid):
  408. """Looks up a track by its MusicBrainz ID"""
  409. params = {"mbid": _unicode(mbid)}
  410. doc = _Request(self, "track.getInfo", params).execute(True)
  411. return Track(_extract(doc, "name", 1), _extract(doc, "name"), self)
  412. def get_artist_by_mbid(self, mbid):
  413. """Loooks up an artist by its MusicBrainz ID"""
  414. params = {"mbid": _unicode(mbid)}
  415. doc = _Request(self, "artist.getInfo", params).execute(True)
  416. return Artist(_extract(doc, "name"), self)
  417. def get_album_by_mbid(self, mbid):
  418. """Looks up an album by its MusicBrainz ID"""
  419. params = {"mbid": _unicode(mbid)}
  420. doc = _Request(self, "album.getInfo", params).execute(True)
  421. return Album(_extract(doc, "artist"), _extract(doc, "name"), self)
  422. def get_lastfm_network(api_key="", api_secret="", session_key = "", username = "", password_hash = ""):
  423. """
  424. Returns a preconfigured Network object for Last.fm
  425. api_key: a provided API_KEY
  426. api_secret: a provided API_SECRET
  427. session_key: a generated session_key or None
  428. username: a username of a valid user
  429. password_hash: the output of pylast.md5(password) where password is the user's password
  430. if username and password_hash were provided and not session_key, session_key will be
  431. generated automatically when needed.
  432. Either a valid session_key or a combination of username and password_hash must be present for scrobbling.
  433. Most read-only webservices only require an api_key and an api_secret, see about obtaining them from:
  434. http://www.last.fm/api/account
  435. """
  436. return Network (
  437. name = "Last.fm",
  438. homepage = "http://last.fm",
  439. ws_server = ("ws.audioscrobbler.com", "/2.0/"),
  440. api_key = api_key,
  441. api_secret = api_secret,
  442. session_key = session_key,
  443. submission_server = "http://post.audioscrobbler.com:80/",
  444. username = username,
  445. password_hash = password_hash,
  446. domain_names = {
  447. DOMAIN_ENGLISH: 'www.last.fm',
  448. DOMAIN_GERMAN: 'www.lastfm.de',
  449. DOMAIN_SPANISH: 'www.lastfm.es',
  450. DOMAIN_FRENCH: 'www.lastfm.fr',
  451. DOMAIN_ITALIAN: 'www.lastfm.it',
  452. DOMAIN_POLISH: 'www.lastfm.pl',
  453. DOMAIN_PORTUGUESE: 'www.lastfm.com.br',
  454. DOMAIN_SWEDISH: 'www.lastfm.se',
  455. DOMAIN_TURKISH: 'www.lastfm.com.tr',
  456. DOMAIN_RUSSIAN: 'www.lastfm.ru',
  457. DOMAIN_JAPANESE: 'www.lastfm.jp',
  458. DOMAIN_CHINESE: 'cn.last.fm',
  459. },
  460. urls = {
  461. "album": "music/%(artist)s/%(album)s",
  462. "artist": "music/%(artist)s",
  463. "event": "event/%(id)s",
  464. "country": "place/%(country_name)s",
  465. "playlist": "user/%(user)s/library/playlists/%(appendix)s",
  466. "tag": "tag/%(name)s",
  467. "track": "music/%(artist)s/_/%(title)s",
  468. "group": "group/%(name)s",
  469. "user": "user/%(name)s",
  470. }
  471. )
  472. def get_librefm_network(api_key="", api_secret="", session_key = "", username = "", password_hash = ""):
  473. """
  474. Returns a preconfigured Network object for Libre.fm
  475. api_key: a provided API_KEY
  476. api_secret: a provided API_SECRET
  477. session_key: a generated session_key or None
  478. username: a username of a valid user
  479. password_hash: the output of pylast.md5(password) where password is the user's password
  480. if username and password_hash were provided and not session_key, session_key will be
  481. generated automatically when needed.
  482. """
  483. return Network (
  484. name = "Libre.fm",
  485. homepage = "http://alpha.dev.libre.fm",
  486. ws_server = ("alpha.dev.libre.fm", "/2.0/"),
  487. api_key = api_key,
  488. api_secret = api_secret,
  489. session_key = session_key,
  490. submission_server = "http://turtle.libre.fm:80/",
  491. username = username,
  492. password_hash = password_hash,
  493. domain_names = {
  494. DOMAIN_ENGLISH: "alpha.dev.libre.fm",
  495. DOMAIN_GERMAN: "alpha.dev.libre.fm",
  496. DOMAIN_SPANISH: "alpha.dev.libre.fm",
  497. DOMAIN_FRENCH: "alpha.dev.libre.fm",
  498. DOMAIN_ITALIAN: "alpha.dev.libre.fm",
  499. DOMAIN_POLISH: "alpha.dev.libre.fm",
  500. DOMAIN_PORTUGUESE: "alpha.dev.libre.fm",
  501. DOMAIN_SWEDISH: "alpha.dev.libre.fm",
  502. DOMAIN_TURKISH: "alpha.dev.libre.fm",
  503. DOMAIN_RUSSIAN: "alpha.dev.libre.fm",
  504. DOMAIN_JAPANESE: "alpha.dev.libre.fm",
  505. DOMAIN_CHINESE: "alpha.dev.libre.fm",
  506. },
  507. urls = {
  508. "album": "artist/%(artist)s/album/%(album)s",
  509. "artist": "artist/%(artist)s",
  510. "event": "event/%(id)s",
  511. "country": "place/%(country_name)s",
  512. "playlist": "user/%(user)s/library/playlists/%(appendix)s",
  513. "tag": "tag/%(name)s",
  514. "track": "music/%(artist)s/_/%(title)s",
  515. "group": "group/%(name)s",
  516. "user": "user/%(name)s",
  517. }
  518. )
  519. class _ShelfCacheBackend(object):
  520. """Used as a backend for caching cacheable requests."""
  521. def __init__(self, file_path = None):
  522. self.shelf = shelve.open(file_path)
  523. def get_xml(self, key):
  524. return self.shelf[key]
  525. def set_xml(self, key, xml_string):
  526. self.shelf[key] = xml_string
  527. def has_key(self, key):
  528. return key in self.shelf.keys()
  529. class _ThreadedCall(threading.Thread):
  530. """Facilitates calling a function on another thread."""
  531. def __init__(self, sender, funct, funct_args, callback, callback_args):
  532. threading.Thread.__init__(self)
  533. self.funct = funct
  534. self.funct_args = funct_args
  535. self.callback = callback
  536. self.callback_args = callback_args
  537. self.sender = sender
  538. def run(self):
  539. output = []
  540. if self.funct:
  541. if self.funct_args:
  542. output = self.funct(*self.funct_args)
  543. else:
  544. output = self.funct()
  545. if self.callback:
  546. if self.callback_args:
  547. self.callback(self.sender, output, *self.callback_args)
  548. else:
  549. self.callback(self.sender, output)
  550. class _Request(object):
  551. """Representing an abstract web service operation."""
  552. def __init__(self, network, method_name, params = {}):
  553. self.params = params
  554. self.network = network
  555. (self.api_key, self.api_secret, self.session_key) = network._get_ws_auth()
  556. self.params["api_key"] = self.api_key
  557. self.params["method"] = method_name
  558. if network.is_caching_enabled():
  559. self.cache = network._get_cache_backend()
  560. if self.session_key:
  561. self.params["sk"] = self.session_key
  562. self.sign_it()
  563. def sign_it(self):
  564. """Sign this request."""
  565. if not "api_sig" in self.params.keys():
  566. self.params['api_sig'] = self._get_signature()
  567. def _get_signature(self):
  568. """Returns a 32-character hexadecimal md5 hash of the signature string."""
  569. keys = self.params.keys()[:]
  570. keys.sort()
  571. string = ""
  572. for name in keys:
  573. string += _unicode(name)
  574. string += _unicode(self.params[name])
  575. string += _unicode(self.api_secret)
  576. return md5(string)
  577. def _get_cache_key(self):
  578. """The cache key is a string of concatenated sorted names and values."""
  579. keys = self.params.keys()
  580. keys.sort()
  581. cache_key = str()
  582. for key in keys:
  583. if key != "api_sig" and key != "api_key" and key != "sk":
  584. cache_key += key + _string(self.params[key])
  585. return hashlib.sha1(cache_key).hexdigest()
  586. def _get_cached_response(self):
  587. """Returns a file object of the cached response."""
  588. if not self._is_cached():
  589. response = self._download_response()
  590. self.cache.set_xml(self._get_cache_key(), response)
  591. return self.cache.get_xml(self._get_cache_key())
  592. def _is_cached(self):
  593. """Returns True if the request is already in cache."""
  594. return self.cache.has_key(self._get_cache_key())
  595. def _download_response(self):
  596. """Returns a response body string from the server."""
  597. # Delay the call if necessary
  598. #self.network._delay_call() # enable it if you want.
  599. data = []
  600. for name in self.params.keys():
  601. data.append('='.join((name, urllib.quote_plus(_string(self.params[name])))))
  602. data = '&'.join(data)
  603. headers = {
  604. "Content-type": "application/x-www-form-urlencoded",
  605. 'Accept-Charset': 'utf-8',
  606. 'User-Agent': "pylast" + '/' + __version__
  607. }
  608. (HOST_NAME, HOST_SUBDIR) = self.network.ws_server
  609. if self.network.is_proxy_enabled():
  610. conn = httplib.HTTPConnection(host = self._get_proxy()[0], port = self._get_proxy()[1])
  611. conn.request(method='POST', url="http://" + HOST_NAME + HOST_SUBDIR,
  612. body=data, headers=headers)
  613. else:
  614. conn = httplib.HTTPConnection(host=HOST_NAME)
  615. conn.request(method='POST', url=HOST_SUBDIR, body=data, headers=headers)
  616. response = conn.getresponse()
  617. response_text = _unicode(response.read())
  618. self._check_response_for_errors(response_text)
  619. return response_text
  620. def execute(self, cacheable = False):
  621. """Returns the XML DOM response of the POST Request from the server"""
  622. if self.network.is_caching_enabled() and cacheable:
  623. response = self._get_cached_response()
  624. else:
  625. response = self._download_response()
  626. return minidom.parseString(_string(response))
  627. def _check_response_for_errors(self, response):
  628. """Checks the response for errors and raises one if any exists."""
  629. doc = minidom.parseString(_string(response))
  630. e = doc.getElementsByTagName('lfm')[0]
  631. if e.getAttribute('status') != "ok":
  632. e = doc.getElementsByTagName('error')[0]
  633. status = e.getAttribute('code')
  634. details = e.firstChild.data.strip()
  635. raise WSError(self.network, status, details)
  636. class SessionKeyGenerator(object):
  637. """Methods of generating a session key:
  638. 1) Web Authentication:
  639. a. network = get_*_network(API_KEY, API_SECRET)
  640. b. sg = SessionKeyGenerator(network)
  641. c. url = sg.get_web_auth_url()
  642. d. Ask the user to open the url and authorize you, and wait for it.
  643. e. session_key = sg.get_web_auth_session_key(url)
  644. 2) Username and Password Authentication:
  645. a. network = get_*_network(API_KEY, API_SECRET)
  646. b. username = raw_input("Please enter your username: ")
  647. c. password_hash = pylast.md5(raw_input("Please enter your password: ")
  648. d. session_key = SessionKeyGenerator(network).get_session_key(username, password_hash)
  649. A session key's lifetime is infinie, unless the user provokes the rights of the given API Key.
  650. If you create a Network object with just a API_KEY and API_SECRET and a username and a password_hash, a
  651. SESSION_KEY will be automatically generated for that network and stored in it so you don't have to do this
  652. manually, unless you want to.
  653. """
  654. def __init__(self, network):
  655. self.network = network
  656. self.web_auth_tokens = {}
  657. def _get_web_auth_token(self):
  658. """Retrieves a token from the network for web authentication.
  659. The token then has to be authorized from getAuthURL before creating session.
  660. """
  661. request = _Request(self.network, 'auth.getToken')
  662. # default action is that a request is signed only when
  663. # a session key is provided.
  664. request.sign_it()
  665. doc = request.execute()
  666. e = doc.getElementsByTagName('token')[0]
  667. return e.firstChild.data
  668. def get_web_auth_url(self):
  669. """The user must open this page, and you first, then call get_web_auth_session_key(url) after that."""
  670. token = self._get_web_auth_token()
  671. url = '%(homepage)s/api/auth/?api_key=%(api)s&token=%(token)s' % \
  672. {"homepage": self.network.homepage, "api": self.network.api_key, "token": token}
  673. self.web_auth_tokens[url] = token
  674. return url
  675. def get_web_auth_session_key(self, url):
  676. """Retrieves the session key of a web authorization process by its url."""
  677. if url in self.web_auth_tokens.keys():
  678. token = self.web_auth_tokens[url]
  679. else:
  680. token = "" #that's gonna raise a WSError of an unauthorized token when the request is executed.
  681. request = _Request(self.network, 'auth.getSession', {'token': token})
  682. # default action is that a request is signed only when
  683. # a session key is provided.
  684. request.sign_it()
  685. doc = request.execute()
  686. return doc.getElementsByTagName('key')[0].firstChild.data
  687. def get_session_key(self, username, password_hash):
  688. """Retrieve a session key with a username and a md5 hash of the user's password."""
  689. params = {"username": username, "authToken": md5(username + password_hash)}
  690. request = _Request(self.network, "auth.getMobileSession", params)
  691. # default action is that a request is signed only when
  692. # a session key is provided.
  693. request.sign_it()
  694. doc = request.execute()
  695. return _extract(doc, "key")
  696. def _namedtuple(name, children):
  697. """
  698. collections.namedtuple is available in (python >= 2.6)
  699. """
  700. v = sys.version_info
  701. if v[1] >= 6 and v[0] < 3:
  702. return collections.namedtuple(name, children)
  703. else:
  704. def fancydict(*args):
  705. d = {}
  706. i = 0
  707. for child in children:
  708. d[child.strip()] = args[i]
  709. i += 1
  710. return d
  711. return fancydict
  712. TopItem = _namedtuple("TopItem", ["item", "weight"])
  713. SimilarItem = _namedtuple("SimilarItem", ["item", "match"])
  714. LibraryItem = _namedtuple("LibraryItem", ["item", "playcount", "tagcount"])
  715. PlayedTrack = _namedtuple("PlayedTrack", ["track", "playback_date", "timestamp"])
  716. LovedTrack = _namedtuple("LovedTrack", ["track", "date", "timestamp"])
  717. ImageSizes = _namedtuple("ImageSizes", ["original", "large", "largesquare", "medium", "small", "extralarge"])
  718. Image = _namedtuple("Image", ["title", "url", "dateadded", "format", "owner", "sizes", "votes"])
  719. Shout = _namedtuple("Shout", ["body", "author", "date"])
  720. def _string_output(funct):
  721. def r(*args):
  722. return _string(funct(*args))
  723. return r
  724. class _BaseObject(object):
  725. """An abstract webservices object."""
  726. network = None
  727. def __init__(self, network):
  728. self.network = network
  729. def _request(self, method_name, cacheable = False, params = None):
  730. if not params:
  731. params = self._get_params()
  732. return _Request(self.network, method_name, params).execute(cacheable)
  733. def _get_params(self):
  734. """Returns the most common set of parameters between all objects."""
  735. return {}
  736. def __hash__(self):
  737. return hash(self.network) + \
  738. hash(str(type(self)) + "".join(self._get_params().keys() + self._get_params().values()).lower())
  739. class _Taggable(object):
  740. """Common functions for classes with tags."""
  741. def __init__(self, ws_prefix):
  742. self.ws_prefix = ws_prefix
  743. def add_tags(self, *tags):
  744. """Adds one or several tags.
  745. * *tags: Any number of tag names or Tag objects.
  746. """
  747. for tag in tags:
  748. self._add_tag(tag)
  749. def _add_tag(self, tag):
  750. """Adds one or several tags.
  751. * tag: one tag name or a Tag object.
  752. """
  753. if isinstance(tag, Tag):
  754. tag = tag.get_name()
  755. params = self._get_params()
  756. params['tags'] = _unicode(tag)
  757. self._request(self.ws_prefix + '.addTags', False, params)
  758. def _remove_tag(self, single_tag):
  759. """Remove a user's tag from this object."""
  760. if isinstance(single_tag, Tag):
  761. single_tag = single_tag.get_name()
  762. params = self._get_params()
  763. params['tag'] = _unicode(single_tag)
  764. self._request(self.ws_prefix + '.removeTag', False, params)
  765. def get_tags(self):
  766. """Returns a list of the tags set by the user to this object."""
  767. # Uncacheable because it can be dynamically changed by the user.
  768. params = self._get_params()
  769. doc = self._request(self.ws_prefix + '.getTags', False, params)
  770. tag_names = _extract_all(doc, 'name')
  771. tags = []
  772. for tag in tag_names:
  773. tags.append(Tag(tag, self.network))
  774. return tags
  775. def remove_tags(self, *tags):
  776. """Removes one or several tags from this object.
  777. * *tags: Any number of tag names or Tag objects.
  778. """
  779. for tag in tags:
  780. self._remove_tag(tag)
  781. def clear_tags(self):
  782. """Clears all the user-set tags. """
  783. self.remove_tags(*(self.get_tags()))
  784. def set_tags(self, *tags):
  785. """Sets this object's tags to only those tags.
  786. * *tags: any number of tag names.
  787. """
  788. c_old_tags = []
  789. old_tags = []
  790. c_new_tags = []
  791. new_tags = []
  792. to_remove = []
  793. to_add = []
  794. tags_on_server = self.get_tags()
  795. for tag in tags_on_server:
  796. c_old_tags.append(tag.get_name().lower())
  797. old_tags.append(tag.get_name())
  798. for tag in tags:
  799. c_new_tags.append(tag.lower())
  800. new_tags.append(tag)
  801. for i in range(0, len(old_tags)):
  802. if not c_old_tags[i] in c_new_tags:
  803. to_remove.append(old_tags[i])
  804. for i in range(0, len(new_tags)):
  805. if not c_new_tags[i] in c_old_tags:
  806. to_add.append(new_tags[i])
  807. self.remove_tags(*to_remove)
  808. self.add_tags(*to_add)
  809. def get_top_tags(self, limit = None):
  810. """Returns a list of the most frequently used Tags on this object."""
  811. doc = self._request(self.ws_prefix + '.getTopTags', True)
  812. elements = doc.getElementsByTagName('tag')
  813. seq = []
  814. for element in elements:
  815. if limit and len(seq) >= limit:
  816. break
  817. tag_name = _extract(element, 'name')
  818. tagcount = _extract(element, 'count')
  819. seq.append(TopItem(Tag(tag_name, self.network), tagcount))
  820. return seq
  821. class WSError(Exception):
  822. """Exception related to the Network web service"""
  823. def __init__(self, network, status, details):
  824. self.status = status
  825. self.details = details
  826. self.network = network
  827. @_string_output
  828. def __str__(self):
  829. return self.details
  830. def get_id(self):
  831. """Returns the exception ID, from one of the following:
  832. STATUS_INVALID_SERVICE = 2
  833. STATUS_INVALID_METHOD = 3
  834. STATUS_AUTH_FAILED = 4
  835. STATUS_INVALID_FORMAT = 5
  836. STATUS_INVALID_PARAMS = 6
  837. STATUS_INVALID_RESOURCE = 7
  838. STATUS_TOKEN_ERROR = 8
  839. STATUS_INVALID_SK = 9
  840. STATUS_INVALID_API_KEY = 10
  841. STATUS_OFFLINE = 11
  842. STATUS_SUBSCRIBERS_ONLY = 12
  843. STATUS_TOKEN_UNAUTHORIZED = 14
  844. STATUS_TOKEN_EXPIRED = 15
  845. """
  846. return self.status
  847. class Album(_BaseObject, _Taggable):
  848. """An album."""
  849. title = None
  850. artist = None
  851. def __init__(self, artist, title, network):
  852. """
  853. Create an album instance.
  854. # Parameters:
  855. * artist: An artist name or an Artist object.
  856. * title: The album title.
  857. """
  858. _BaseObject.__init__(self, network)
  859. _Taggable.__init__(self, 'album')
  860. if isinstance(artist, Artist):
  861. self.artist = artist
  862. else:
  863. self.artist = Artist(artist, self.network)
  864. self.title = title
  865. @_string_output
  866. def __repr__(self):
  867. return u"%s - %s" %(self.get_artist().get_name(), self.get_title())
  868. def __eq__(self, other):
  869. return (self.get_title().lower() == other.get_title().lower()) and (self.get_artist().get_name().lower() == other.get_artist().get_name().lower())
  870. def __ne__(self, other):
  871. return (self.get_title().lower() != other.get_title().lower()) or (self.get_artist().get_name().lower() != other.get_artist().get_name().lower())
  872. def _get_params(self):
  873. return {'artist': self.get_artist().get_name(), 'album': self.get_title(), }
  874. def get_artist(self):
  875. """Returns the associated Artist object."""
  876. return self.artist
  877. def get_title(self):
  878. """Returns the album title."""
  879. return self.title
  880. def get_name(self):
  881. """Returns the album title (alias to Album.get_title)."""
  882. return self.get_title()
  883. def get_release_date(self):
  884. """Retruns the release date of the album."""
  885. return _extract(self._request("album.getInfo", cacheable = True), "releasedate")
  886. def get_cover_image(self, size = COVER_EXTRA_LARGE):
  887. """
  888. Returns a uri to the cover image
  889. size can be one of:
  890. COVER_MEGA
  891. COVER_EXTRA_LARGE
  892. COVER_LARGE
  893. COVER_MEDIUM
  894. COVER_SMALL
  895. """
  896. return _extract_all(self._request("album.getInfo", cacheable = True), 'image')[size]
  897. def get_id(self):
  898. """Returns the ID"""
  899. return _extract(self._request("album.getInfo", cacheable = True), "id")
  900. def get_playcount(self):
  901. """Returns the number of plays on the network"""
  902. return _number(_extract(self._request("album.getInfo", cacheable = True), "playcount"))
  903. def get_listener_count(self):
  904. """Returns the number of liteners on the network"""
  905. return _number(_extract(self._request("album.getInfo", cacheable = True), "listeners"))
  906. def get_top_tags(self, limit=None):
  907. """Returns a list of the most-applied tags to this album."""
  908. doc = self._request("album.getInfo", True)
  909. e = doc.getElementsByTagName("toptags")[0]
  910. seq = []
  911. for name in _extract_all(e, "name"):
  912. if len(seq) < limit:
  913. seq.append(Tag(name, self.network))
  914. return seq
  915. def get_tracks(self):
  916. """Returns the list of Tracks on this album."""
  917. uri = 'lastfm://playlist/album/%s' %self.get_id()
  918. return XSPF(uri, self.network).get_tracks()
  919. def get_mbid(self):
  920. """Returns the MusicBrainz id of the album."""
  921. return _extract(self._request("album.getInfo", cacheable = True), "mbid")
  922. def get_url(self, domain_name = DOMAIN_ENGLISH):
  923. """Returns the url of the album page on the network.
  924. # Parameters:
  925. * domain_name str: The network's language domain. Possible values:
  926. o DOMAIN_ENGLISH
  927. o DOMAIN_GERMAN
  928. o DOMAIN_SPANISH
  929. o DOMAIN_FRENCH
  930. o DOMAIN_ITALIAN
  931. o DOMAIN_POLISH
  932. o DOMAIN_PORTUGUESE
  933. o DOMAIN_SWEDISH
  934. o DOMAIN_TURKISH
  935. o DOMAIN_RUSSIAN
  936. o DOMAIN_JAPANESE
  937. o DOMAIN_CHINESE
  938. """
  939. artist = _url_safe(self.get_artist().get_name())
  940. album = _url_safe(self.get_title())
  941. return self.network._get_url(domain_name, "album") %{'artist': artist, 'album': album}
  942. def get_wiki_published_date(self):
  943. """Returns the date of publishing this version of the wiki."""
  944. doc = self._request("album.getInfo", True)
  945. if len(doc.getElementsByTagName("wiki")) == 0:
  946. return
  947. node = doc.getElementsByTagName("wiki")[0]
  948. return _extract(node, "published")
  949. def get_wiki_summary(self):
  950. """Returns the summary of the wiki."""
  951. doc = self._request("album.getInfo", True)
  952. if len(doc.getElementsByTagName("wiki")) == 0:
  953. return
  954. node = doc.getElementsByTagName("wiki")[0]
  955. return _extract(node, "summary")
  956. def get_wiki_content(self):
  957. """Returns the content of the wiki."""
  958. doc = self._request("album.getInfo", True)
  959. if len(doc.getElementsByTagName("wiki")) == 0:
  960. return
  961. node = doc.getElementsByTagName("wiki")[0]
  962. return _extract(node, "content")
  963. class Artist(_BaseObject, _Taggable):
  964. """An artist."""
  965. name = None
  966. def __init__(self, name, network):
  967. """Create an artist object.
  968. # Parameters:
  969. * name str: The artist's name.
  970. """
  971. _BaseObject.__init__(self, network)
  972. _Taggable.__init__(self, 'artist')
  973. self.name = name
  974. @_string_output
  975. def __repr__(self):
  976. return self.get_name()
  977. def __eq__(self, other):
  978. return self.get_name().lower() == other.get_name().lower()
  979. def __ne__(self, other):
  980. return self.get_name().lower() != other.get_name().lower()
  981. def _get_params(self):
  982. return {'artist': self.get_name()}
  983. def get_name(self):
  984. """Returns the name of the artist."""
  985. return self.name
  986. def get_cover_image(self, size = COVER_LARGE):
  987. """
  988. Returns a uri to the cover image
  989. size can be one of:
  990. COVER_MEGA
  991. COVER_EXTRA_LARGE
  992. COVER_LARGE
  993. COVER_MEDIUM
  994. COVER_SMALL
  995. """
  996. return _extract_all(self._request("artist.getInfo", True), "image")[size]
  997. def get_playcount(self):
  998. """Returns the number of plays on the network."""
  999. return _number(_extract(self._request("artist.getInfo", True), "playcount"))
  1000. def get_mbid(self):
  1001. """Returns the MusicBrainz ID of this artist."""
  1002. doc = self._request("artist.getInfo", True)
  1003. return _extract(doc, "mbid")
  1004. def get_listener_count(self):
  1005. """Returns the number of liteners on the network."""
  1006. return _number(_extract(self._request("artist.getInfo", True), "listeners"))
  1007. def is_streamable(self):
  1008. """Returns True if the artist is streamable."""
  1009. return bool(_number(_extract(self._request("artist.getInfo", True), "streamable")))
  1010. def get_bio_published_date(self):
  1011. """Returns the date on which the artist's biography was published."""
  1012. return _extract(self._request("artist.getInfo", True), "published")
  1013. def get_bio_summary(self):
  1014. """Returns the summary of the artist's biography."""
  1015. return _extract(self._request("artist.getInfo", True), "summary")
  1016. def get_bio_content(self):
  1017. """Returns the content of the artist's biography."""
  1018. return _extract(self._request("artist.getInfo", True), "content")
  1019. def get_upcoming_events(self):
  1020. """Returns a list of the upcoming Events for this artist."""
  1021. doc = self._request('artist.getEvents', True)
  1022. ids = _extract_all(doc, 'id')
  1023. events = []
  1024. for e_id in ids:
  1025. events.append(Event(e_id, self.network))
  1026. return events
  1027. def get_similar(self, limit = None):
  1028. """Returns the similar artists on the network."""
  1029. params = self._get_params()
  1030. if limit:
  1031. params['limit'] = _unicode(limit)
  1032. doc = self._request('artist.getSimilar', True, params)
  1033. names = _extract_all(doc, "name")
  1034. matches = _extract_all(doc, "match")
  1035. artists = []
  1036. for i in range(0, len(names)):
  1037. artists.append(SimilarItem(Artist(names[i], self.network), _number(matches[i])))
  1038. return artists
  1039. def get_top_albums(self):
  1040. """Retuns a list of the top albums."""
  1041. doc = self._request('artist.getTopAlbums', True)
  1042. seq = []
  1043. for node in doc.getElementsByTagName("album"):
  1044. name = _extract(node, "name")
  1045. artist = _extract(node, "name", 1)
  1046. playcount = _extract(node, "playcount")
  1047. seq.append(TopItem(Album(artist, name, self.network), playcount))
  1048. return seq
  1049. def get_top_tracks(self):
  1050. """Returns a list of the most played Tracks by this artist."""
  1051. doc = self._request("artist.getTopTracks", True)
  1052. seq = []
  1053. for track in doc.getElementsByTagName('track'):
  1054. title = _extract(track, "name")
  1055. artist = _extract(track, "name", 1)
  1056. playcount = _number(_extract(track, "playcount"))
  1057. seq.append( TopItem(Track(artist, title, self.network), playcount) )
  1058. return seq
  1059. def get_top_fans(self, limit = None):
  1060. """Returns a list of the Users who played this artist the most.
  1061. # Parameters:
  1062. * limit int: Max elements.
  1063. """
  1064. doc = self._request('artist.getTopFans', True)
  1065. seq = []
  1066. elements = doc.getElementsByTagName('user')
  1067. for element in elements:
  1068. if limit and len(seq) >= limit:
  1069. break
  1070. name = _extract(element, 'name')
  1071. weight = _number(_extract(element, 'weight'))
  1072. seq.append(TopItem(User(name, self.network), weight))
  1073. return seq
  1074. def share(self, users, message = None):
  1075. """Shares this artist (sends out recommendations).
  1076. # Parameters:
  1077. * users [User|str,]: A list that can contain usernames, emails, User objects, or all of them.
  1078. * message str: A message to include in the recommendation message.
  1079. """
  1080. #last.fm currently accepts a max of 10 recipient at a time
  1081. while(len(users) > 10):
  1082. section = users[0:9]
  1083. users = users[9:]
  1084. self.share(section, message)
  1085. nusers = []
  1086. for user in users:
  1087. if isinstance(user, User):
  1088. nusers.append(user.get_name())
  1089. else:
  1090. nusers.append(user)
  1091. params = self._get_params()
  1092. recipients = ','.join(nusers)
  1093. params['recipient'] = recipients
  1094. if message: params['message'] = _unicode(message)
  1095. self._request('artist.share', False, params)
  1096. def get_url(self, domain_name = DOMAIN_ENGLISH):
  1097. """Returns the url of the artist page on the network.
  1098. # Parameters:
  1099. * domain_name: The network's language domain. Possible values:
  1100. o DOMAIN_ENGLISH
  1101. o DOMAIN_GERMAN
  1102. o DOMAIN_SPANISH
  1103. o DOMAIN_FRENCH
  1104. o DOMAIN_ITALIAN
  1105. o DOMAIN_POLISH
  1106. o DOMAIN_PORTUGUESE
  1107. o DOMAIN_SWEDISH
  1108. o DOMAIN_TURKISH
  1109. o DOMAIN_RUSSIAN
  1110. o DOMAIN_JAPANESE
  1111. o DOMAIN_CHINESE
  1112. """
  1113. artist = _url_safe(self.get_name())
  1114. return self.network._get_url(domain_name, "artist") %{'artist': artist}
  1115. def get_images(self, order=IMAGES_ORDER_POPULARITY, limit=None):
  1116. """
  1117. Returns a sequence of Image objects
  1118. if limit is None it will return all
  1119. order can be IMAGES_ORDER_POPULARITY or IMAGES_ORDER_DATE
  1120. """
  1121. images = []
  1122. params = self._get_params()
  1123. params["order"] = order
  1124. nodes = _collect_nodes(limit, self, "artist.getImages", True, params)
  1125. for e in nodes:
  1126. if _extract(e, "name"):
  1127. user = User(_extract(e, "name"), self.network)
  1128. else:
  1129. user = None
  1130. images.append(Image(
  1131. _extract(e, "title"),
  1132. _extract(e, "url"),
  1133. _extract(e, "dateadded"),
  1134. _extract(e, "format"),
  1135. user,
  1136. ImageSizes(*_extract_all(e, "size")),
  1137. (_extract(e, "thumbsup"), _extract(e, "thumbsdown"))
  1138. )
  1139. )
  1140. return images
  1141. def get_shouts(self, limit=50):
  1142. """
  1143. Returns a sequqence of Shout objects
  1144. """
  1145. shouts = []
  1146. for node in _collect_nodes(limit, self, "artist.getShouts", False):
  1147. shouts.append(Shout(
  1148. _extract(node, "body"),
  1149. User(_extract(node, "author"), self.network),
  1150. _extract(node, "date")
  1151. )
  1152. )
  1153. return shouts
  1154. def shout(self, message):
  1155. """
  1156. Post a shout
  1157. """
  1158. params = self._get_params()
  1159. params["message"] = message
  1160. self._request("artist.Shout", False, params)
  1161. class Event(_BaseObject):
  1162. """An event."""
  1163. id = None
  1164. def __init__(self, event_id, network):
  1165. _BaseObject.__init__(self, network)
  1166. self.id = _unicode(event_id)
  1167. @_string_output
  1168. def __repr__(self):
  1169. return "Event #" + self.get_id()
  1170. def __eq__(self, other):
  1171. return self.get_id() == other.get_id()
  1172. def __ne__(self, other):
  1173. return self.get_id() != other.get_id()
  1174. def _get_params(self):
  1175. return {'event': self.get_id()}
  1176. def attend(self, attending_status):
  1177. """Sets the attending status.
  1178. * attending_status: The attending status. Possible values:
  1179. o EVENT_ATTENDING
  1180. o EVENT_MAYBE_ATTENDING
  1181. o EVENT_NOT_ATTENDING
  1182. """
  1183. params = self._get_params()
  1184. params['status'] = _unicode(attending_status)
  1185. self._request('event.attend', False, params)
  1186. def get_attendees(self):
  1187. """
  1188. Get a list of attendees for an event
  1189. """
  1190. doc = self._request("event.getAttendees", False)
  1191. users = []
  1192. for name in _extract_all(doc, "name"):
  1193. users.append(User(name, self.network))
  1194. return users
  1195. def get_id(self):
  1196. """Returns the id of the event on the network. """
  1197. return self.id
  1198. def get_title(self):
  1199. """Returns the title of the event. """
  1200. doc = self._request("event.getInfo", True)
  1201. return _extract(doc, "title")
  1202. def get_headliner(self):
  1203. """Returns the headliner of the event. """
  1204. doc = self._request("event.getInfo", True)
  1205. return Artist(_extract(doc, "headliner"), self.network)
  1206. def get_artists(self):
  1207. """Returns a list of the participating Artists. """
  1208. doc = self._request("event.getInfo", True)
  1209. names = _extract_all(doc, "artist")
  1210. artists = []
  1211. for name in names:
  1212. artists.append(Artist(name, self.network))
  1213. return artists
  1214. def get_venue(self):
  1215. """Returns the venue where the event is held."""
  1216. doc = self._request("event.getInfo", True)
  1217. v = doc.getElementsByTagName("venue")[0]
  1218. venue_id = _number(_extract(v, "id"))
  1219. return Venue(venue_id, self.network)
  1220. def get_start_date(self):
  1221. """Returns the date when the event starts."""
  1222. doc = self._request("event.getInfo", True)
  1223. return _extract(doc, "startDate")
  1224. def get_description(self):
  1225. """Returns the description of the event. """
  1226. doc = self._request("event.getInfo", True)
  1227. return _extract(doc, "description")
  1228. def get_cover_image(self, size = COVER_LARGE):
  1229. """
  1230. Returns a uri to the cover image
  1231. size can be one of:
  1232. COVER_MEGA
  1233. COVER_EXTRA_LARGE
  1234. COVER_LARGE
  1235. COVER_MEDIUM
  1236. COVER_SMALL
  1237. """
  1238. doc = self._request("event.getInfo", True)
  1239. return _extract_all(doc, "image")[size]
  1240. def get_attendance_count(self):
  1241. """Returns the number of attending people. """
  1242. doc = self._request("event.getInfo", True)
  1243. return _number(_extract(doc, "attendance"))
  1244. def get_review_count(self):
  1245. """Returns the number of available reviews for this event. """
  1246. doc = self._request("event.getInfo", True)
  1247. return _number(_extract(doc, "reviews"))
  1248. def get_url(self, domain_name = DOMAIN_ENGLISH):
  1249. """Returns the url of the event page on the network.
  1250. * domain_name: The network's language domain. Possible values:
  1251. o DOMAIN_ENGLISH
  1252. o DOMAIN_GERMAN
  1253. o DOMAIN_SPANISH
  1254. o DOMAIN_FRENCH
  1255. o DOMAIN_ITALIAN
  1256. o DOMAIN_POLISH
  1257. o DOMAIN_PORTUGUESE
  1258. o DOMAIN_SWEDISH
  1259. o DOMAIN_TURKISH
  1260. o DOMAIN_RUSSIAN
  1261. o DOMAIN_JAPANESE
  1262. o DOMAIN_CHINESE
  1263. """
  1264. return self.network._get_url(domain_name, "event") %{'id': self.get_id()}
  1265. def share(self, users, message = None):
  1266. """Shares this event (sends out recommendations).
  1267. * users: A list that can contain usernames, emails, User objects, or all of them.
  1268. * message: A message to include in the recommendation message.
  1269. """
  1270. #last.fm currently accepts a max of 10 recipient at a time
  1271. while(len(users) > 10):
  1272. section = users[0:9]
  1273. users = users[9:]
  1274. self.share(section, message)
  1275. nusers = []
  1276. for user in users:
  1277. if isinstance(user, User):
  1278. nusers.append(user.get_name())
  1279. else:
  1280. nusers.append(user)
  1281. params = self._get_params()
  1282. recipients = ','.join(nusers)
  1283. params['recipient'] = recipients
  1284. if message: params['message'] = _unicode(message)
  1285. self._request('event.share', False, params)
  1286. def get_shouts(self, limit=50):
  1287. """
  1288. Returns a sequqence of Shout objects
  1289. """
  1290. shouts = []
  1291. for node in _collect_nodes(limit, self, "event.getShouts", False):
  1292. shouts.append(Shout(
  1293. _extract(node, "body"),
  1294. User(_extract(node, "author"), self.network),
  1295. _extract(node, "date")
  1296. )
  1297. )
  1298. return shouts
  1299. def shout(self, message):
  1300. """
  1301. Post a shout
  1302. """
  1303. params = self._get_params()
  1304. params["message"] = message
  1305. self._request("event.Shout", False, params)
  1306. class Country(_BaseObject):
  1307. """A country at Last.fm."""
  1308. name = None
  1309. def __init__(self, name, network):
  1310. _BaseObject.__init__(self, network)
  1311. self.name = name
  1312. @_string_output
  1313. def __repr__(self):
  1314. return self.get_name()
  1315. def __eq__(self, other):
  1316. return self.get_name().lower() == other.get_name().lower()
  1317. def __ne__(self, other):
  1318. return self.get_name() != other.get_name()
  1319. def _get_params(self):
  1320. return {'country': self.get_name()}
  1321. def _get_name_from_code(self, alpha2code):
  1322. # TODO: Have this function lookup the alpha-2 code and return the country name.
  1323. return alpha2code
  1324. def get_name(self):
  1325. """Returns the country name. """
  1326. return self.name
  1327. def get_top_artists(self):
  1328. """Returns a sequence of the most played artists."""
  1329. doc = self._request('geo.getTopArtists', True)
  1330. seq = []
  1331. for node in doc.getElementsByTagName("artist"):
  1332. name = _extract(node, 'name')
  1333. playcount = _extract(node, "playcount")
  1334. seq.append(TopItem(Artist(name, self.network), playcount))
  1335. return seq
  1336. def get_top_tracks(self):
  1337. """Returns a sequence of the most played tracks"""
  1338. doc = self._request("geo.getTopTracks", True)
  1339. seq = []
  1340. for n in doc.getElementsByTagName('track'):
  1341. title = _extract(n, 'name')
  1342. artist = _extract(n, 'name', 1)
  1343. playcount = _number(_extract(n, "playcount"))
  1344. seq.append( TopItem(Track(artist, title, self.network), playcount))
  1345. return seq
  1346. def get_url(self, domain_name = DOMAIN_ENGLISH):
  1347. """Returns the url of the event page on the network.
  1348. * domain_name: The network's language domain. Possible values:
  1349. o DOMAIN_ENGLISH
  1350. o DOMAIN_GERMAN
  1351. o DOMAIN_SPANISH
  1352. o DOMAIN_FRENCH
  1353. o DOMAIN_ITALIAN
  1354. o DOMAIN_POLISH
  1355. o DOMAIN_PORTUGUESE
  1356. o DOMAIN_SWEDISH
  1357. o DOMAIN_TURKISH
  1358. o DOMAIN_RUSSIAN
  1359. o DOMAIN_JAPANESE
  1360. o DOMAIN_CHINESE
  1361. """
  1362. country_name = _url_safe(self.get_name())
  1363. return self.network._get_url(domain_name, "country") %{'country_name': country_name}
  1364. class Library(_BaseObject):
  1365. """A user's Last.fm library."""
  1366. user = None
  1367. def __init__(self, user, network):
  1368. _BaseObject.__init__(self, network)
  1369. if isinstance(user, User):
  1370. self.user = user
  1371. else:
  1372. self.user = User(user, self.network)
  1373. self._albums_index = 0
  1374. self._artists_index = 0
  1375. self._tracks_index = 0
  1376. @_string_output
  1377. def __repr__(self):
  1378. return repr(self.get_user()) + "'s Library"
  1379. def _get_params(self):
  1380. return {'user': self.user.get_name()}
  1381. def get_user(self):
  1382. """Returns the user who owns this library."""
  1383. return self.user
  1384. def add_album(self, album):
  1385. """Add an album to this library."""
  1386. params = self._get_params()
  1387. params["artist"] = album.get_artist.get_name()
  1388. params["album"] = album.get_name()
  1389. self._request("library.addAlbum", False, params)
  1390. def add_artist(self, artist):
  1391. """Add an artist to this library."""
  1392. params = self._get_params()
  1393. params["artist"] = artist.get_name()
  1394. self._request("library.addArtist", False, params)
  1395. def add_track(self, track):
  1396. """Add a track to this library."""
  1397. params = self._get_params()
  1398. params["track"] = track.get_title()
  1399. self._request("library.addTrack", False, params)
  1400. def get_albums(self, limit=50):
  1401. """
  1402. Returns a sequence of Album objects
  1403. if limit==None it will return all (may take a while)
  1404. """
  1405. seq = []
  1406. for node in _collect_nodes(limit, self, "library.getAlbums", True):
  1407. name = _extract(node, "name")
  1408. artist = _extract(node, "name", 1)
  1409. playcount = _number(_extract(node, "playcount"))
  1410. tagcount = _number(_extract(node, "tagcount"))
  1411. seq.append(LibraryItem(Album(artist, name, self.network), playcount, tagcount))
  1412. return seq
  1413. def get_artists(self, limit=50):
  1414. """
  1415. Returns a sequence of Album objects
  1416. if limit==None it will return all (may take a while)
  1417. """
  1418. seq = []
  1419. for node in _collect_nodes(limit, self, "library.getArtists", True):
  1420. name = _extract(node, "name")
  1421. playcount = _number(_extract(node, "playcount"))
  1422. tagcount = _number(_extract(node, "tagcount"))
  1423. seq.append(LibraryItem(Artist(name, self.network), playcount, tagcount))
  1424. return seq
  1425. def get_tracks(self, limit=50):
  1426. """
  1427. Returns a sequence of Album objects
  1428. if limit==None it will return all (may take a while)
  1429. """
  1430. seq = []
  1431. for node in _collect_nodes(limit, self, "library.getTracks", True):
  1432. name = _extract(node, "name")
  1433. artist = _extract(node, "name", 1)
  1434. playcount = _number(_extract(node, "playcount"))
  1435. tagcount = _number(_extract(node, "tagcount"))
  1436. seq.append(LibraryItem(Track(artist, name, self.network), playcount, tagcount))
  1437. return seq
  1438. class Playlist(_BaseObject):
  1439. """A Last.fm user playlist."""
  1440. id = None
  1441. user = None
  1442. def __init__(self, user, id, network):
  1443. _BaseObject.__init__(self, network)
  1444. if isinstance(user, User):
  1445. self.user = user
  1446. else:
  1447. self.user = User(user, self.network)
  1448. self.id = _unicode(id)
  1449. @_string_output
  1450. def __repr__(self):
  1451. return repr(self.user) + "'s playlist # " + repr(self.id)
  1452. def _get_info_node(self):
  1453. """Returns the node from user.getPlaylists where this playlist's info is."""
  1454. doc = self._request("user.getPlaylists", True)
  1455. for node in doc.getElementsByTagName("playlist"):
  1456. if _extract(node, "id") == str(self.get_id()):
  1457. return node
  1458. def _get_params(self):
  1459. return {'user': self.user.get_name(), 'playlistID': self.get_id()}
  1460. def get_id(self):
  1461. """Returns the playlist id."""
  1462. return self.id
  1463. def get_user(self):
  1464. """Returns the owner user of this playlist."""
  1465. return self.user
  1466. def get_tracks(self):
  1467. """Returns a list of the tracks on this user playlist."""
  1468. uri = u'lastfm://playlist/%s' %self.get_id()
  1469. return XSPF(uri, self.network).get_tracks()
  1470. def add_track(self, track):
  1471. """Adds a Track to this Playlist."""
  1472. params = self._get_params()
  1473. params['artist'] = track.get_artist().get_name()
  1474. params['track'] = track.get_title()
  1475. self._request('playlist.addTrack', False, params)
  1476. def get_title(self):
  1477. """Returns the title of this playlist."""
  1478. return _extract(self._get_info_node(), "title")
  1479. def get_creation_date(self):
  1480. """Returns the creation date of this playlist."""
  1481. return _extract(self._get_info_node(), "date")
  1482. def get_size(self):
  1483. """Returns the number of tracks in this playlist."""
  1484. return _number(_extract(self._get_info_node(), "size"))
  1485. def get_description(self):
  1486. """Returns the description of this playlist."""
  1487. return _extract(self._get_info_node(), "description")
  1488. def get_duration(self):
  1489. """Returns the duration of this playlist in milliseconds."""
  1490. return _number(_extract(self._get_info_node(), "duration"))
  1491. def is_streamable(self):
  1492. """Returns True