From ec5c62f60e8ce274edb36fd4756a008c05eeb558 Mon Sep 17 00:00:00 2001 From: Daniel Cellucci Date: Wed, 20 Jun 2012 11:06:39 -0400 Subject: [PATCH 1/4] Changed Plugins to include Chrome, my location, lastfm uname, etc. --- hu-notes.txt | 16 ++++++++++++++++ hu_currentlyplaying/__init__.pyc | Bin 201 -> 196 bytes hu_currentlyplaying/hu_currentlyplaying.pyc | Bin 604 -> 594 bytes hu_googleweather/__init__.pyc | Bin 195 -> 190 bytes hu_googleweather/hu_googleweather.py | 2 +- hu_googleweather/hu_googleweather.pyc | Bin 757 -> 749 bytes hu_googleweather/pywapi.pyc | Bin 11618 -> 11571 bytes hu_lastfm/__init__.pyc | Bin 181 -> 176 bytes hu_lastfm/hu_lastfm.py | 2 +- hu_lastfm/hu_lastfm.pyc | Bin 2357 -> 2332 bytes hu_lastfm/pylast.pyc | Bin 135138 -> 133343 bytes hu_openapps/__init__.pyc | Bin 185 -> 180 bytes hu_openapps/hu_openapps.pyc | Bin 633 -> 623 bytes hu_openbrowsertabs/__init__.pyc | Bin 199 -> 194 bytes hu_openbrowsertabs/hu_openbrowsertabs.pyc | Bin 655 -> 645 bytes hu_openbrowsertabs/urls.applescript | 2 +- hu_ssidname/__init__.pyc | Bin 185 -> 180 bytes hu_ssidname/hu_ssidname.pyc | Bin 663 -> 653 bytes 18 files changed, 19 insertions(+), 3 deletions(-) diff --git a/hu-notes.txt b/hu-notes.txt index a777a08..45f5f9e 100644 --- a/hu-notes.txt +++ b/hu-notes.txt @@ -69,4 +69,20 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/hu_currentlyplaying/__init__.pyc b/hu_currentlyplaying/__init__.pyc index f753a05a57bd69d3b33794d93e5c46b7eb54a09b..c7a7683bdb1a19e0762d2af969bc152ebcfb2729 100644 GIT binary patch delta 57 zcmX@fc!ZJT;wN6NyN@6GPvnSGbzxv&&<`z6Eh^SeNlwklDNRn!)OX2GF3nBND=F4b N&n(F(O`15t5&(A=6~_Po delta 62 zcmX@Yc#@Ih;wN6NwVxz?CUV4Sc``6C=!X`k78UCkC+C-xWF;nN7w6|y=)2@6m*%GC Sl@#lzq?YNYXO>Ltwgdpe;uiG) diff --git a/hu_currentlyplaying/hu_currentlyplaying.pyc b/hu_currentlyplaying/hu_currentlyplaying.pyc index 86e305cc551e41957a26d090958bab9392aa4dab..3d15c648406d51fee2548062a545de453b28415a 100644 GIT binary patch delta 105 zcmcb^a*2iG;wN6NyN@6GZ{!GIRP|?IV9*aOPAw|dPf1SA$tg`v&eV6wPcF?(%_}L^ WPtPpLC{3Cy#-u-aBcnCZs@(wH?j;-m delta 115 zcmcb_a)*WE;wN6NomNsl8#w|PwL%ye81zGnQ;UlAi<9$9O0p7@vy1cdD)e3QlS^|` b^Gb^KQ&P+H(=$sZ*D_|2rd)rrHIq013n3^P diff --git a/hu_googleweather/__init__.pyc b/hu_googleweather/__init__.pyc index 2fa9f07ed81c912aaed8138cc86689c776d4b278..3b3041eada90d47e56b4fd601c37a4e6dfea4879 100644 GIT binary patch delta 57 zcmX@ixQ~(J;wN6NyN@6GPvnSJb!1>*&<`z6Eh^SeNlwklDNRn!)OX2GF3nBND=F4b N&n(F(O`6zi1^{z+6|VpQ delta 62 zcmdnTc$ksn;wN4%o0pP46FH)_+!z=b^h1kNi;DG&lk-bTvJ#WCi}Uj;^j-3kOLJ56 SN{aPUQp@zyGfO6Rm;nH#uohzg diff --git a/hu_googleweather/hu_googleweather.py b/hu_googleweather/hu_googleweather.py index fdbf7dc..38bd3a3 100644 --- a/hu_googleweather/hu_googleweather.py +++ b/hu_googleweather/hu_googleweather.py @@ -1,4 +1,4 @@ -location = "brisbane,australia" +location = "athens, georgia, USA" import pywapi diff --git a/hu_googleweather/hu_googleweather.pyc b/hu_googleweather/hu_googleweather.pyc index 84979ac7630778e89cec2f547f9173c0f9314615..a4b7f867094584ef4a4e89582891919796c09f77 100644 GIT binary patch delta 133 zcmey$`j(aB;wN4%zb6m5EoT4# delta 141 zcmaFM`jwUA;wN6N7gka}6FIEKgcukYl8Q2mlM?e%brMU9ONtV6G7~pOPiNHf14`+K z7N-^!>lY{Imy~2BCTADt=T+#t!+la>8EFwOkT~TIoW~9jWjJz0E`nc Ar2qf` diff --git a/hu_googleweather/pywapi.pyc b/hu_googleweather/pywapi.pyc index abb900da9143d9b9284610357ede7f175f784caa..81e0631fbb74b75b9ec5473dcaaeebe9023a861e 100755 GIT binary patch delta 407 zcmaD9wK@EhEv&5Ra&~ckUWL9(esXDUYFPAyMT^-LDkSxTBUApl*Zp~nCK diff --git a/hu_lastfm/__init__.pyc b/hu_lastfm/__init__.pyc index 4bc156ea730fc4d66e56e52e90570ac3b22f0f52..251f20b63a2f7be70d786e853028118d4fe8d65a 100644 GIT binary patch delta 57 zcmdnWxPg)5;wN6NyN@6GPvi(uwPIjk&<`z6Eh^SeNlwklDNRn!)OX2GF3nBND=F4b N&n(F(O`6!I4*+Wf6?Xst delta 62 zcmdnMxRsIP;wN6Nj7O3_6FEY(>=_ss^h1kNi;DG&lk-bTvJ#WCi}Uj;^j-3kOLJ56 SN{aPUQp@zyGfO5m>H`3z8x~Li diff --git a/hu_lastfm/hu_lastfm.py b/hu_lastfm/hu_lastfm.py index 3ecf64a..5638fdc 100644 --- a/hu_lastfm/hu_lastfm.py +++ b/hu_lastfm/hu_lastfm.py @@ -1,4 +1,4 @@ -lastfm_username="scottjacksonx" +lastfm_username="gendanvs" import pylast as lastfm import time diff --git a/hu_lastfm/hu_lastfm.pyc b/hu_lastfm/hu_lastfm.pyc index ddecf78e055a61a8df3a5600497cf4f03f24583f..6af6fa932fe4c1e2ca2dd37c93e9a59250f17b4b 100644 GIT binary patch delta 215 zcmdlgG)IWz;wN6N^rsK~Cvs%+a4;}1q^IVkB<7VBZ*1Daq-qTm&<`z6Eh^SeNlwkl zDNRn!)OX2GF3nBND=F4b&n(F(P1>Bm{EJAXlM|SwHqT?LVI!+la>8EFwOn$&2y}6M2K1q5EfO;mdRWp)ej{!$FDas#k*Z=?w C)Kgdh diff --git a/hu_lastfm/pylast.pyc b/hu_lastfm/pylast.pyc index 511d4aa784e64377d5494671747c3e73620a224e..599667330cce0e841992a16059dd721dc86eaa00 100644 GIT binary patch delta 17149 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 diff --git a/hu_openbrowsertabs/hu_openbrowsertabs.pyc b/hu_openbrowsertabs/hu_openbrowsertabs.pyc index e99b8de0271980c169f4057b063176b8ff0570cb..f92006f9ef56d51c3246fbbc95e36d6fc9feb1a6 100644 GIT binary patch delta 119 zcmeBYZDr-S_=%V6?&F948#&q;HGCNu81zGnQ;UlAQ<772a!QkvGxc5alS^|`^Gb^K d(=$slN|W?6N+%~W2~R%H7(}WD@yQiT;sE&-DenLP delta 115 zcmZo=?Puk<_=%V6jkT1|Mvitytsn*l2K~_D)S_bj;^h32lB~q!?Be{q3VoOS Date: Wed, 20 Jun 2012 15:49:35 -0400 Subject: [PATCH 2/4] Added a plugin (battery power), tried to fix another, moved plugins to their own folder. Need to commit more often --- .gitignore | 14 + hu-notes.txt => hu-notes copy.xml | 59 + hu.py | 12 +- plugins/__init__.py | 0 plugins/__init__.pyc | Bin 0 -> 143 bytes plugins/hu_batterypower/__init__.py | 1 + plugins/hu_batterypower/__init__.pyc | Bin 0 -> 188 bytes plugins/hu_batterypower/hu_batterypower.py | 56 + plugins/hu_batterypower/hu_batterypower.pyc | Bin 0 -> 1345 bytes plugins/hu_currentlyplaying/__init__.py | 1 + plugins/hu_currentlyplaying/__init__.pyc | Bin 0 -> 196 bytes .../currentlyPlaying.applescript | 3 + .../hu_currentlyplaying.py | 10 + .../hu_currentlyplaying.pyc | Bin 0 -> 594 bytes plugins/hu_googleweather/__init__.py | 1 + plugins/hu_googleweather/__init__.pyc | Bin 0 -> 190 bytes plugins/hu_googleweather/hu_googleweather.py | 16 + plugins/hu_googleweather/hu_googleweather.pyc | Bin 0 -> 749 bytes plugins/hu_googleweather/pywapi.py | 334 ++ plugins/hu_googleweather/pywapi.pyc | Bin 0 -> 11571 bytes plugins/hu_lastfm/__init__.py | 1 + plugins/hu_lastfm/__init__.pyc | Bin 0 -> 176 bytes plugins/hu_lastfm/hu_lastfm.py | 67 + plugins/hu_lastfm/hu_lastfm.pyc | Bin 0 -> 2332 bytes plugins/hu_lastfm/pylast.py | 3702 +++++++++++++++++ plugins/hu_lastfm/pylast.pyc | Bin 0 -> 133343 bytes plugins/hu_openapps/__init__.py | 1 + plugins/hu_openapps/__init__.pyc | Bin 0 -> 180 bytes plugins/hu_openapps/hu_openapps.py | 10 + plugins/hu_openapps/hu_openapps.pyc | Bin 0 -> 623 bytes plugins/hu_openapps/openapps.applescript | 9 + plugins/hu_openbrowsertabs/__init__.py | 1 + plugins/hu_openbrowsertabs/__init__.pyc | Bin 0 -> 194 bytes .../hu_openbrowsertabs/hu_openbrowsertabs.py | 10 + .../hu_openbrowsertabs/hu_openbrowsertabs.pyc | Bin 0 -> 661 bytes plugins/hu_openbrowsertabs/urls.applescript | 16 + plugins/hu_ssidname/__init__.py | 1 + plugins/hu_ssidname/__init__.pyc | Bin 0 -> 180 bytes plugins/hu_ssidname/hu_ssidname.py | 10 + plugins/hu_ssidname/hu_ssidname.pyc | Bin 0 -> 653 bytes 40 files changed, 4330 insertions(+), 5 deletions(-) create mode 100644 .gitignore rename hu-notes.txt => hu-notes copy.xml (55%) create mode 100644 plugins/__init__.py create mode 100644 plugins/__init__.pyc create mode 100644 plugins/hu_batterypower/__init__.py create mode 100644 plugins/hu_batterypower/__init__.pyc create mode 100644 plugins/hu_batterypower/hu_batterypower.py create mode 100644 plugins/hu_batterypower/hu_batterypower.pyc create mode 100644 plugins/hu_currentlyplaying/__init__.py create mode 100644 plugins/hu_currentlyplaying/__init__.pyc create mode 100644 plugins/hu_currentlyplaying/currentlyPlaying.applescript create mode 100644 plugins/hu_currentlyplaying/hu_currentlyplaying.py create mode 100644 plugins/hu_currentlyplaying/hu_currentlyplaying.pyc create mode 100644 plugins/hu_googleweather/__init__.py create mode 100644 plugins/hu_googleweather/__init__.pyc create mode 100644 plugins/hu_googleweather/hu_googleweather.py create mode 100644 plugins/hu_googleweather/hu_googleweather.pyc create mode 100755 plugins/hu_googleweather/pywapi.py create mode 100755 plugins/hu_googleweather/pywapi.pyc create mode 100644 plugins/hu_lastfm/__init__.py create mode 100644 plugins/hu_lastfm/__init__.pyc create mode 100644 plugins/hu_lastfm/hu_lastfm.py create mode 100644 plugins/hu_lastfm/hu_lastfm.pyc create mode 100644 plugins/hu_lastfm/pylast.py create mode 100644 plugins/hu_lastfm/pylast.pyc create mode 100644 plugins/hu_openapps/__init__.py create mode 100644 plugins/hu_openapps/__init__.pyc create mode 100644 plugins/hu_openapps/hu_openapps.py create mode 100644 plugins/hu_openapps/hu_openapps.pyc create mode 100644 plugins/hu_openapps/openapps.applescript create mode 100644 plugins/hu_openbrowsertabs/__init__.py create mode 100644 plugins/hu_openbrowsertabs/__init__.pyc create mode 100644 plugins/hu_openbrowsertabs/hu_openbrowsertabs.py create mode 100644 plugins/hu_openbrowsertabs/hu_openbrowsertabs.pyc create mode 100644 plugins/hu_openbrowsertabs/urls.applescript create mode 100644 plugins/hu_ssidname/__init__.py create mode 100644 plugins/hu_ssidname/__init__.pyc create mode 100644 plugins/hu_ssidname/hu_ssidname.py create mode 100644 plugins/hu_ssidname/hu_ssidname.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..26b2b58 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Privacy # +################## +hu-notes.txt + +# Basic OS Stuff # +################## +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +Icon? +ehthumbs.db +Thumbs.db \ No newline at end of file diff --git a/hu-notes.txt b/hu-notes copy.xml similarity index 55% rename from hu-notes.txt rename to hu-notes copy.xml index 45f5f9e..ec91ff3 100644 --- a/hu-notes.txt +++ b/hu-notes copy.xml @@ -85,4 +85,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/hu.py b/hu.py index f422a94..26346fa 100644 --- a/hu.py +++ b/hu.py @@ -1,6 +1,6 @@ import datetime import time -import os +import os, sys, inspect class hu: @@ -17,12 +17,14 @@ def __init__(self): this is the part where i find out all of the things you want me to record. """ - + #cmd_folder = os.path.realpath(os.path.abspath(os.path.split(inspect.getfile( inspect.currentframe() ))[0])) + #if cmd_folder not in sys.path: + # sys.path.insert(0, cmd_folder) # get names of all folders in the current directory that start with "hu_" - for file in os.listdir("."): + for file in os.listdir("./plugins/"): if file[0:3] == "hu_": - exec "import " + file + "." + file - exec "self.plugin_modules.append(" + file + "." + file + ")" + exec "import plugins." + file + "." + file + exec "self.plugin_modules.append(plugins." + file + "." + file + ")" def takeSnapshot(self): """ diff --git a/plugins/__init__.py b/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/__init__.pyc b/plugins/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7a8be4e2ba2a1b22195d9280b909baeadf795bfd GIT binary patch literal 143 zcmcckiI*!!{gHn%0~9a= 0: + time = powerInfo[timeOffset-2:timeOffset+3] + if time[0] == " ": + time = time[1:] + if powerState != "": + outputString = "" + return outputString + + + + + + + + + + diff --git a/plugins/hu_batterypower/hu_batterypower.pyc b/plugins/hu_batterypower/hu_batterypower.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aff43a0d08ad261fecfeeafeb159b7f7e85356ff GIT binary patch literal 1345 zcmb_b&2G~`5T14Z-6o|#oAv^3q)NF!P9Q*~qNEaVp{NxXF37c=#G$rR?^>u*d;*?< z7vOymufQYl0`Se)v@U0o?9Tr;Gdq(XzqFWr6ix%DEUeXxkAV*#1#tIvPu3bZGr6M zby}jXE^Yfgnp>3g5CFv*-GwPe{xbZAgtutfV()o_l1E}=nXy3DslSt>33PZJ4+_R> z(D?(U&L1l!xR*74LD@IC=DvT6`Z*etSL5Yzq!&&Y zPm(lpO_;=XN3S5X@V_^878>gTyf&_bvGXSR7^H?5?_g|NAkPa3;k}ugP6J=~amh27 zA8Ij$@zk(hb$6h-X2*mPd$4M>FaHn2b(GJe4FgRBlVoO*8B>SE&oyMV*d+NY zJestVWbisMDR;n0#d^Y(iMbG^s={29" +end tell \ No newline at end of file diff --git a/plugins/hu_currentlyplaying/hu_currentlyplaying.py b/plugins/hu_currentlyplaying/hu_currentlyplaying.py new file mode 100644 index 0000000..5855e6a --- /dev/null +++ b/plugins/hu_currentlyplaying/hu_currentlyplaying.py @@ -0,0 +1,10 @@ +import commands + +def getData(): + """ + gets the currently-playing track from iTunes. + """ + track = commands.getoutput("osascript hu_currentlyplaying/currentlyPlaying.applescript") + if track[0:12] == "hu_currently": + return "" + return track \ No newline at end of file diff --git a/plugins/hu_currentlyplaying/hu_currentlyplaying.pyc b/plugins/hu_currentlyplaying/hu_currentlyplaying.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3d15c648406d51fee2548062a545de453b28415a GIT binary patch literal 594 zcmcIgu};G<5Iv_2(5i@qg)M9upb;xV2#L-VwXm=tW1Ge_iKF;DRDub9kPl!$`~g1z zccCgsuwtE`&v$;lclZ3h*?)O^9V=L_h|Uw*euqGl377+1krTlaIf5h8v_o0@LF=q# z3c7OWl(yd^KuWhy9|b%B+_!w)fnD8&y9itiLdP%_l$fS#%sL?G_htrtg1JdlXc|*t zvDmK*xvY5P?%$$gXijl=o8J8oPYoT literal 0 HcmV?d00001 diff --git a/plugins/hu_googleweather/__init__.py b/plugins/hu_googleweather/__init__.py new file mode 100644 index 0000000..73b61b5 --- /dev/null +++ b/plugins/hu_googleweather/__init__.py @@ -0,0 +1 @@ +import hu_googleweather \ No newline at end of file diff --git a/plugins/hu_googleweather/__init__.pyc b/plugins/hu_googleweather/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3b3041eada90d47e56b4fd601c37a4e6dfea4879 GIT binary patch literal 190 zcmcckiI?l{Qb`YFk&IXR`t$(i~t`N^fZsd**E`stY^ s8Kp`38KppkYLI?>d}dx|NqoFsL1hUC&=i~8{FKt1R6CH>#X!se0FT@" + except: + return "" \ No newline at end of file diff --git a/plugins/hu_googleweather/hu_googleweather.pyc b/plugins/hu_googleweather/hu_googleweather.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a4b7f867094584ef4a4e89582891919796c09f77 GIT binary patch literal 749 zcmcIh!A{#i5S_J2NZdlDf)sJ874?tZm0 z{e(*W0)C)<<0NqCxohw2zS)_1Z)bl#?M2hyzpgZGMu7DK)4zi;M7OYtOhDfh-C`5c z4>$wy%qKe%cnFj6!j}Fx&TZA?E<28|=O5f3jA>UmL`Vr9UhyufLFHw}nLM!HhoN4m-l% + +#Permission is hereby granted, free of charge, to any person +#obtaining a copy of this software and associated documentation +#files (the "Software"), to deal in the Software without +#restriction, including without limitation the rights to use, +#copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the +#Software is furnished to do so, subject to the following +#conditions: + +#The above copyright notice and this permission notice shall be +#included in all copies or substantial portions of the Software. + +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +#EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +#OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +#NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +#HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +#WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +#FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +#OTHER DEALINGS IN THE SOFTWARE. + +""" +Fetches weather reports from Google Weather, Yahoo Wheather and NOAA +""" + +import urllib2, re +from xml.dom import minidom +from urllib import quote + +GOOGLE_WEATHER_URL = 'http://www.google.com/ig/api?weather=%s&hl=%s' +GOOGLE_COUNTRIES_URL = 'http://www.google.com/ig/countries?output=xml&hl=%s' +GOOGLE_CITIES_URL = 'http://www.google.com/ig/cities?output=xml&country=%s&hl=%s' + +YAHOO_WEATHER_URL = 'http://xml.weather.yahoo.com/forecastrss?p=%s&u=%s' +YAHOO_WEATHER_NS = 'http://xml.weather.yahoo.com/ns/rss/1.0' + +NOAA_WEATHER_URL = 'http://www.weather.gov/xml/current_obs/%s.xml' + +def get_weather_from_google(location_id, hl = ''): + """ + Fetches weather report from Google + + Parameters + location_id: a zip code (10001); city name, state (weather=woodland,PA); city name, country (weather=london, england); + latitude/longitude(weather=,,,30670000,104019996) or possibly other. + hl: the language parameter (language code). Default value is empty string, in this case Google will use English. + + Returns: + weather_data: a dictionary of weather data that exists in XML feed. + """ + location_id, hl = map(quote, (location_id, hl)) + url = GOOGLE_WEATHER_URL % (location_id, hl) + handler = urllib2.urlopen(url) + content_type = handler.info().dict['content-type'] + charset = re.search('charset\=(.*)',content_type).group(1) + if not charset: + charset = 'utf-8' + if charset.lower() != 'utf-8': + xml_response = handler.read().decode(charset).encode('utf-8') + else: + xml_response = handler.read() + dom = minidom.parseString(xml_response) + handler.close() + + weather_data = {} + weather_dom = dom.getElementsByTagName('weather')[0] + + data_structure = { + 'forecast_information': ('city', 'postal_code', 'latitude_e6', 'longitude_e6', 'forecast_date', 'current_date_time', 'unit_system'), + 'current_conditions': ('condition','temp_f', 'temp_c', 'humidity', 'wind_condition', 'icon') + } + for (tag, list_of_tags2) in data_structure.iteritems(): + tmp_conditions = {} + for tag2 in list_of_tags2: + try: + tmp_conditions[tag2] = weather_dom.getElementsByTagName(tag)[0].getElementsByTagName(tag2)[0].getAttribute('data') + except IndexError: + pass + weather_data[tag] = tmp_conditions + + forecast_conditions = ('day_of_week', 'low', 'high', 'icon', 'condition') + forecasts = [] + + for forecast in dom.getElementsByTagName('forecast_conditions'): + tmp_forecast = {} + for tag in forecast_conditions: + tmp_forecast[tag] = forecast.getElementsByTagName(tag)[0].getAttribute('data') + forecasts.append(tmp_forecast) + + weather_data['forecasts'] = forecasts + dom.unlink() + + return weather_data + +def get_countries_from_google(hl = ''): + """ + Get list of countries in specified language from Google + + Parameters + hl: the language parameter (language code). Default value is empty string, in this case Google will use English. + Returns: + countries: a list of elements(all countries that exists in XML feed). Each element is a dictionary with 'name' and 'iso_code' keys. + For example: [{'iso_code': 'US', 'name': 'USA'}, {'iso_code': 'FR', 'name': 'France'}] + """ + url = GOOGLE_COUNTRIES_URL % hl + + handler = urllib2.urlopen(url) + content_type = handler.info().dict['content-type'] + charset = re.search('charset\=(.*)',content_type).group(1) + if not charset: + charset = 'utf-8' + if charset.lower() != 'utf-8': + xml_response = handler.read().decode(charset).encode('utf-8') + else: + xml_response = handler.read() + dom = minidom.parseString(xml_response) + handler.close() + + countries = [] + countries_dom = dom.getElementsByTagName('country') + + for country_dom in countries_dom: + country = {} + country['name'] = country_dom.getElementsByTagName('name')[0].getAttribute('data') + country['iso_code'] = country_dom.getElementsByTagName('iso_code')[0].getAttribute('data') + countries.append(country) + + dom.unlink() + return countries + +def get_cities_from_google(country_code, hl = ''): + """ + Get list of cities of necessary country in specified language from Google + + Parameters + country_code: code of the necessary country. For example 'de' or 'fr'. + hl: the language parameter (language code). Default value is empty string, in this case Google will use English. + Returns: + cities: a list of elements(all cities that exists in XML feed). Each element is a dictionary with 'name', 'latitude_e6' and 'longitude_e6' keys. For example: [{'longitude_e6': '1750000', 'name': 'Bourges', 'latitude_e6': '47979999'}] + """ + url = GOOGLE_CITIES_URL % (country_code.lower(), hl) + + handler = urllib2.urlopen(url) + content_type = handler.info().dict['content-type'] + charset = re.search('charset\=(.*)',content_type).group(1) + if not charset: + charset = 'utf-8' + if charset.lower() != 'utf-8': + xml_response = handler.read().decode(charset).encode('utf-8') + else: + xml_response = handler.read() + dom = minidom.parseString(xml_response) + handler.close() + + cities = [] + cities_dom = dom.getElementsByTagName('city') + + for city_dom in cities_dom: + city = {} + city['name'] = city_dom.getElementsByTagName('name')[0].getAttribute('data') + city['latitude_e6'] = city_dom.getElementsByTagName('latitude_e6')[0].getAttribute('data') + city['longitude_e6'] = city_dom.getElementsByTagName('longitude_e6')[0].getAttribute('data') + cities.append(city) + + dom.unlink() + + return cities + +def get_weather_from_yahoo(location_id, units = 'metric'): + """ + Fetches weather report from Yahoo! + + Parameters + location_id: A five digit US zip code or location ID. To find your location ID, + browse or search for your city from the Weather home page(http://weather.yahoo.com/) + The weather ID is in the URL for the forecast page for that city. You can also get the location ID by entering your zip code on the home page. For example, if you search for Los Angeles on the Weather home page, the forecast page for that city is http://weather.yahoo.com/forecast/USCA0638.html. The location ID is USCA0638. + + units: type of units. 'metric' for metric and '' for non-metric + Note that choosing metric units changes all the weather units to metric, for example, wind speed will be reported as kilometers per hour and barometric pressure as millibars. + + Returns: + weather_data: a dictionary of weather data that exists in XML feed. See http://developer.yahoo.com/weather/#channel + """ + location_id = quote(location_id) + if units == 'metric': + unit = 'c' + else: + unit = 'f' + url = YAHOO_WEATHER_URL % (location_id, unit) + handler = urllib2.urlopen(url) + dom = minidom.parse(handler) + handler.close() + + weather_data = {} + weather_data['title'] = dom.getElementsByTagName('title')[0].firstChild.data + weather_data['link'] = dom.getElementsByTagName('link')[0].firstChild.data + + ns_data_structure = { + 'location': ('city', 'region', 'country'), + 'units': ('temperature', 'distance', 'pressure', 'speed'), + 'wind': ('chill', 'direction', 'speed'), + 'atmosphere': ('humidity', 'visibility', 'pressure', 'rising'), + 'astronomy': ('sunrise', 'sunset'), + 'condition': ('text', 'code', 'temp', 'date') + } + + for (tag, attrs) in ns_data_structure.iteritems(): + weather_data[tag] = xml_get_ns_yahoo_tag(dom, YAHOO_WEATHER_NS, tag, attrs) + + weather_data['geo'] = {} + weather_data['geo']['lat'] = dom.getElementsByTagName('geo:lat')[0].firstChild.data + weather_data['geo']['long'] = dom.getElementsByTagName('geo:long')[0].firstChild.data + + weather_data['condition']['title'] = dom.getElementsByTagName('item')[0].getElementsByTagName('title')[0].firstChild.data + weather_data['html_description'] = dom.getElementsByTagName('item')[0].getElementsByTagName('description')[0].firstChild.data + + forecasts = [] + for forecast in dom.getElementsByTagNameNS(YAHOO_WEATHER_NS, 'forecast'): + forecasts.append(xml_get_attrs(forecast,('date', 'low', 'high', 'text', 'code'))) + weather_data['forecasts'] = forecasts + + dom.unlink() + + return weather_data + + + +def get_weather_from_noaa(station_id): + """ + Fetches weather report from NOAA: National Oceanic and Atmospheric Administration (United States) + + Parameter: + station_id: the ID of the weather station near the necessary location + To find your station ID, perform the following steps: + 1. Open this URL: http://www.weather.gov/xml/current_obs/seek.php?state=az&Find=Find + 2. Select the necessary state state. Click 'Find'. + 3. Find the necessary station in the 'Observation Location' column. + 4. The station ID is in the URL for the weather page for that station. + For example if the weather page is http://weather.noaa.gov/weather/current/KPEO.html -- the station ID is KPEO. + + Other way to get the station ID: use this library: http://code.google.com/p/python-weather/ and 'Weather.location2station' function. + + Returns: + weather_data: a dictionary of weather data that exists in XML feed. + + (useful icons: http://www.weather.gov/xml/current_obs/weather.php) + """ + station_id = quote(station_id) + url = NOAA_WEATHER_URL % (station_id) + handler = urllib2.urlopen(url) + dom = minidom.parse(handler) + handler.close() + + data_structure = ('suggested_pickup', + 'suggested_pickup_period', + 'location', + 'station_id', + 'latitude', + 'longitude', + 'observation_time', + 'observation_time_rfc822', + 'weather', + 'temperature_string', + 'temp_f', + 'temp_c', + 'relative_humidity', + 'wind_string', + 'wind_dir', + 'wind_degrees', + 'wind_mph', + 'wind_gust_mph', + 'pressure_string', + 'pressure_mb', + 'pressure_in', + 'dewpoint_string', + 'dewpoint_f', + 'dewpoint_c', + 'heat_index_string', + 'heat_index_f', + 'heat_index_c', + 'windchill_string', + 'windchill_f', + 'windchill_c', + 'icon_url_base', + 'icon_url_name', + 'two_day_history_url', + 'ob_url' + ) + weather_data = {} + current_observation = dom.getElementsByTagName('current_observation')[0] + for tag in data_structure: + try: + weather_data[tag] = current_observation.getElementsByTagName(tag)[0].firstChild.data + except IndexError: + pass + + dom.unlink() + return weather_data + + + +def xml_get_ns_yahoo_tag(dom, ns, tag, attrs): + """ + Parses the necessary tag and returns the dictionary with values + + Parameters: + dom - DOM + ns - namespace + tag - necessary tag + attrs - tuple of attributes + + Returns: a dictionary of elements + """ + element = dom.getElementsByTagNameNS(ns, tag)[0] + return xml_get_attrs(element,attrs) + + +def xml_get_attrs(xml_element, attrs): + """ + Returns the list of necessary attributes + + Parameters: + element: xml element + attrs: tuple of attributes + + Return: a dictionary of elements + """ + + result = {} + for attr in attrs: + result[attr] = xml_element.getAttribute(attr) + return result diff --git a/plugins/hu_googleweather/pywapi.pyc b/plugins/hu_googleweather/pywapi.pyc new file mode 100755 index 0000000000000000000000000000000000000000..81e0631fbb74b75b9ec5473dcaaeebe9023a861e GIT binary patch literal 11571 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 QGq2B_o_TxvJJW~$2O9W!hyVZp literal 0 HcmV?d00001 diff --git a/plugins/hu_lastfm/__init__.py b/plugins/hu_lastfm/__init__.py new file mode 100644 index 0000000..ce915fb --- /dev/null +++ b/plugins/hu_lastfm/__init__.py @@ -0,0 +1 @@ +import hu_lastfm \ No newline at end of file diff --git a/plugins/hu_lastfm/__init__.pyc b/plugins/hu_lastfm/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..251f20b63a2f7be70d786e853028118d4fe8d65a GIT binary patch literal 176 zcmcckiI?l{ now: # track was played < 10 minutes (600 seconds) ago. + tracksFromLastTenMinutes.append(track) + return xmlify(tracksFromLastTenMinutes) + except: + return "" + + + +def xmlify(tracks): + """ + takes a list of Tracks, turns them into a nice XML-ish format. + """ + if len(tracks) != 0: + xmlifiedTracks = "\n" + for track in tracks: + trackDetails = u'' + trackDetails = asciify(unicode(str(track[0]))) + artistNameEnd = trackDetails.find(" - ") + artistName = trackDetails[:artistNameEnd] + startTitleName = artistNameEnd + 3 + titleName = trackDetails[startTitleName:] + + artist = "artist=\"" + artistName + "\" " + title = "title=\"" + titleName + "\" " + timestamp = "time=\"" + track[2] + "\" " + + convertedTrack = "\n" + xmlifiedTracks += convertedTrack + xmlifiedTracks += "" + return xmlifiedTracks + else: + return "" + +def asciify(string): + """ + gets rid of pesky things like umlauts and tildes and other accents. ascii all the way, baby. + """ + temp = u'' + for char in string: + decomp = decomposition(char) + if decomp: # Not an empty string + temp += unichr(int(decomp.split()[0], 16)) + else: + temp += char + return temp \ No newline at end of file diff --git a/plugins/hu_lastfm/hu_lastfm.pyc b/plugins/hu_lastfm/hu_lastfm.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6af6fa932fe4c1e2ca2dd37c93e9a59250f17b4b GIT binary patch literal 2332 zcmb_d&2Aev5FW06Y$>+$(>9LL03m_^@gcVApha9XF^bmBp-mh>PLaSs;I5XG#Att% zB{zux>69M&5WV%7$LJIE1q$@g7w8ML-^?oU12iiwcQ`X7XUOjx{_)Rxcl7sPj}lrw zEquSm>mQ;qL?>t!nTRHePNH{mgH9SEH)ztNv_&T^Ql?2iV?disON13NZ4p*^{PFe* zW(=!nMm9Hb{?faD@f>X{*01w&HYu`c;azY=E)nq%9`P++{}#>cS6+630YpyXnqypp zLPT#US}4&=cdqD7M2m>#5xFKIxHlNspb^ocNw4qIyg`T04rtM$&?48ucv`e6v}xYr z47T8o_&4WmdfuesBB^Uaf#Q%qSRq*#Ye-eGnZGv`rU4)+lh0-##}$I%*c8@ zE2>@nDt6(P&I`S>17jR+dhK!@>tPkD((0GC^w>#V4E4CeEI}ifq4|98%R7U?-e8#Q ze{p*+vF7$5-rqBKJ{y|-z1xBJJf6DKiJi%5Ro<3)oLToV3@+m4DOwx=(UByHJtL|! z3zr}w+ta#xn%nTIC@10kdymt1^MxjKKVtH5aXF^6iOs`lDC1<}#T;B_Ljz`)2U#Va zP5~YfUuUTs&T3R~0Eqbqgv{Z;Ta{Pf<-!_-diKZl6NgzkGI7sl0~H?Hts?!lR~JFUaR{v5_3 z=1Bcjp1R#oZFNOmQs-5>u|jRNrao1ls14On3A2O4=M;qoe?q|K1NV$jMb8^l{0{a3 z?22B$Wy9gK-x&Ge1ppsFiinU3hyZuSb^V7895~}fle+-31|{$T2R1n?K76p_{Fu3v#Q~Ui zn1wu3@oo*tgKi}%MwQ;tj=!F3Ok6S%?_Ku{@$1_2@dOuA3l(oZWAWZ~A%NKK3VyWb z33>61@ob;u7Vp~{zV($#ii&n+OBFv!Vl*8 zdw!gv!*>M8(67nxJ(GbcgG)m00ARYfiGmJh2sxMr4ZZ+CqAjon7Y7^#rR(B`k;@)q z_IR5Y^gp3lmiE#a2ItiJ$qb-GVfR4n#Of+b<4S5j%C}1mV;Pma0B}nkCtUCRu8tWM zI!;ptG^+aSmL9}|8Q?2e6%0Ejg)H2c3zPF@1dYqS;Dw>)r>P6#Bk%#J5J_D~El!T3 zPR2NW9$R0wAMoh7r0#PN94IyGgIiN8>L!j%?ukCrOctNcm?wRXqkoQ-+${NEP#Pu0 q{$(~ZBKkb{S*RlY?@G+U+|3G8rS^b#)Jv6()=+#eM4c<0t^WYo4;798 literal 0 HcmV?d00001 diff --git a/plugins/hu_lastfm/pylast.py b/plugins/hu_lastfm/pylast.py new file mode 100644 index 0000000..a939f19 --- /dev/null +++ b/plugins/hu_lastfm/pylast.py @@ -0,0 +1,3702 @@ +# -*- coding: utf-8 -*- +# +# 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 +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# +# http://code.google.com/p/pylast/ + +__version__ = '0.4' +__author__ = 'Amr Hassan' +__copyright__ = "Copyright (C) 2008-2009 Amr Hassan" +__license__ = "gpl" +__email__ = 'amr.hassan@gmail.com' + +import hashlib +import httplib +import urllib +import threading +from xml.dom import minidom +import xml.dom +import time +import shelve +import tempfile +import sys +import htmlentitydefs + +try: + import collections +except ImportError: + pass + +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_INVALID_SIGNATURE = 13 +STATUS_TOKEN_UNAUTHORIZED = 14 +STATUS_TOKEN_EXPIRED = 15 + +EVENT_ATTENDING = '0' +EVENT_MAYBE_ATTENDING = '1' +EVENT_NOT_ATTENDING = '2' + +PERIOD_OVERALL = 'overall' +PERIOD_7DAYS = "7day" +PERIOD_3MONTHS = '3month' +PERIOD_6MONTHS = '6month' +PERIOD_12MONTHS = '12month' + +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' + +SCROBBLE_SOURCE_USER = "P" +SCROBBLE_SOURCE_NON_PERSONALIZED_BROADCAST = "R" +SCROBBLE_SOURCE_PERSONALIZED_BROADCAST = "E" +SCROBBLE_SOURCE_LASTFM = "L" +SCROBBLE_SOURCE_UNKNOWN = "U" + +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) + +class _Request(object): + """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. 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"]) +LovedTrack = _namedtuple("LovedTrack", ["track", "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 + +def _pad_list(given_list, desired_length, padding = None): + """ + Pads a list to be of the desired_length. + """ + + while len(given_list) < desired_length: + given_list.append(padding) + + return given_list + +class _BaseObject(object): + """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) + + 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): + """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): + """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): + """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.""" + + 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.""" + + 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.""" + + 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) + + 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.""" + + 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.""" + + 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.""" + + 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.""" + + 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 this user's loved track as a sequence of LovedTrack objects + in reverse order of their timestamp, all the way back to the first track. + + If limit==None, it will try to pull all the available data. + + This method uses caching. Enable caching only if you're pulling a + large amount of data. + + 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) + + seq = [] + for track in _collect_nodes(limit, self, "user.getLovedTracks", True, params): + + title = _extract(track, "name") + artist = _extract(track, "name", 1) + date = _extract(track, "date") + timestamp = track.getElementsByTagName("date")[0].getAttribute("uts") + + seq.append(LovedTrack(Track(artist, title, self.network), date, timestamp)) + + return seq + + 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 = 10): + """Returns this user's played track as a sequence of PlayedTrack objects + in reverse order of their playtime, all the way back to the first track. + + If limit==None, it will try to pull all the available data. + + This method uses caching. Enable caching only if you're pulling a + large amount of data. + + 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) + + seq = [] + for track in _collect_nodes(limit, self, "user.getRecentTracks", True, params): + + if track.hasAttribute('nowplaying'): + continue #to prevent the now playing track from sneaking in here + + title = _extract(track, "name") + artist = _extract(track, "artist") + date = _extract(track, "date") + timestamp = track.getElementsByTagName("date")[0].getAttribute("uts") + + seq.append(PlayedTrack(Track(artist, title, self.network), date, timestamp)) + + return seq + + def get_id(self): + """Returns the user id.""" + + doc = self._request("user.getInfo", True) + + return _extract(doc, "id") + + 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_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_7DAYS + 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_7DAYS + 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_7DAYS + 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_image(self): + """Returns the user's avatar.""" + + doc = self._request("user.getInfo", True) + + return _extract(doc, "image") + + 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, 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_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, 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, 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, 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, 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, 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, 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. + + 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.""" + + 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 _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 _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.""" + + 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: + 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.""" + + seq = [] + for i in topitems_or_libraryitems: + seq.append(i.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, 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, 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())) + + if self.password and self.username: + token = md5(self.password + timestamp) + elif self.network.api_key and self.network.api_secret and self.network.session_key: + if not self.username: + self.username = self.network.get_authenticated_user().get_name() + token = md5(self.network.api_secret + timestamp) + + params = {"hs": "true", "p": "1.2.1", "c": self.client_id, + "v": self.client_version, "u": self.username, "t": timestamp, + "a": token} + + if self.network.session_key and self.network.api_key: + params["sk"] = self.network.session_key + params["api_key"] = self.network.api_key + + 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() + + def scrobble_many(self, tracks): + """ + Scrobble several tracks at once. + + tracks: A sequence of a sequence of parameters for each trach. The order of parameters + is the same as if passed to the scrobble() method. + """ + + remainder = [] + + if len(tracks) > 50: + remainder = tracks[50:] + tracks = tracks[:50] + + params = {"s": self._get_session_id()} + + i = 0 + for t in tracks: + _pad_list(t, 9, "") + params["a[%s]" % str(i)] = _string(t[0]) + params["t[%s]" % str(i)] = _string(t[1]) + params["i[%s]" % str(i)] = str(t[2]) + params["o[%s]" % str(i)] = t[3] + params["r[%s]" % str(i)] = t[4] + params["l[%s]" % str(i)] = str(t[5]) + params["b[%s]" % str(i)] = _string(t[6]) + params["n[%s]" % str(i)] = t[7] + params["m[%s]" % str(i)] = t[8] + + i += 1 + + _ScrobblerRequest(self.submissions_url, params, self.network).execute() + + if remainder: + self.scrobble_many(remainder) diff --git a/plugins/hu_lastfm/pylast.pyc b/plugins/hu_lastfm/pylast.pyc new file mode 100644 index 0000000000000000000000000000000000000000..599667330cce0e841992a16059dd721dc86eaa00 GIT binary patch literal 133343 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\n" + openApps + "" + return "" \ No newline at end of file diff --git a/plugins/hu_openapps/hu_openapps.pyc b/plugins/hu_openapps/hu_openapps.pyc new file mode 100644 index 0000000000000000000000000000000000000000..26e0ca48789edadcef6bfbe5126215ca1dc04cc8 GIT binary patch literal 623 zcmb_Zy-ve05I*;Z(m>S(sbVu*%8)FKRRK|n1%cFxg$1RtX$(mm^_)tTV1gIq0T>W( zzze`#3ToN0>@Vlv3cON@sJW) zj4+;?f>h-#Cm{`nzed0jdL=NV2D3xp7&heCrTbtbIS#!Q7ycn>bO4{<(eXgB%`$2j zU7Id5?QH3a()s*im7kk9rEYv8paMK5E4)TM$56#OP0Kho;mF2irgMkk%($eShDk|# zJ +" + end repeat + get appList +end tell \ No newline at end of file diff --git a/plugins/hu_openbrowsertabs/__init__.py b/plugins/hu_openbrowsertabs/__init__.py new file mode 100644 index 0000000..b3e3892 --- /dev/null +++ b/plugins/hu_openbrowsertabs/__init__.py @@ -0,0 +1 @@ +import hu_openbrowsertabs \ No newline at end of file diff --git a/plugins/hu_openbrowsertabs/__init__.pyc b/plugins/hu_openbrowsertabs/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f55d90bd45e825e32812052950811a51ab957ac6 GIT binary patch literal 194 zcmcckiI?l{lIX%Z~)D*$<0qG%}KQbSzipq3;>%VG\n" + urls + "" + return "" \ No newline at end of file diff --git a/plugins/hu_openbrowsertabs/hu_openbrowsertabs.pyc b/plugins/hu_openbrowsertabs/hu_openbrowsertabs.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1e69c44b382dfea6ac6984d583c88df2337d6c9f GIT binary patch literal 661 zcmcIh%Sr<=6uoKdqk}~dLEOczX4ToatVL)+5FgOejT@OxVkbJ&40CTRg0A#`{1!jK zUvT46Z^jW9T^o{<+}F8}ynS?n*U$IM1nSk``E*3LQSFfDDayM_tC>@bjrRR#=L-LEMJ!H*dI z$~*{w&`*_odx_0+ZU(8%!d#PeR5%L#{HVp?s|opr3h +" + end repeat + on error + -- not all windows have tabs + end try + end repeat + get urls +end tell diff --git a/plugins/hu_ssidname/__init__.py b/plugins/hu_ssidname/__init__.py new file mode 100644 index 0000000..1ef6596 --- /dev/null +++ b/plugins/hu_ssidname/__init__.py @@ -0,0 +1 @@ +import hu_ssidname \ No newline at end of file diff --git a/plugins/hu_ssidname/__init__.pyc b/plugins/hu_ssidname/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..13479f2900bda4a426b3f3e137c2f83be07cee39 GIT binary patch literal 180 zcmcckiI?l{" + return "" \ No newline at end of file diff --git a/plugins/hu_ssidname/hu_ssidname.pyc b/plugins/hu_ssidname/hu_ssidname.pyc new file mode 100644 index 0000000000000000000000000000000000000000..47e4b93e3d3884fc385fd04f896c3810f4937011 GIT binary patch literal 653 zcmb_Z&1xGl5Z2lbjZI9BAvfXEUToDlg^<5e69|N~cxel{gpGD$E7@Hwno*0vJ+&{` z2go7h4e|o*%+k<2K(*4R`O(ZbGyCg#ICyw^{9x7J)#3Y;5AHG)yc2b=)D;UI^^=7} zELqxRulvpGvW-kwmB{yeaLhpLZlC^0)OV%UZTwwV`JqaRuIM{OPwiw0r_It03H!n@ z=+A*-s8Q1rbe&hA{cS5}+4<~6u7SS!aNpXkubM~@v_oxU2vAeG($?2CSb{>6pA7og zaBh>Wkf1Ub?k416Wj+S?H77U=%p&Q~yla{g_TTPJr<2~>He5Uz+G)I93r}+?dE8NJvNNAIZkC>w8_9NRC!$> z^;uZ}`AFPSDl;W{Ezv38ss%h@$$UaC%M=#MGFt1*%-dLT?`Rf|mhr|cBkyYol_|cK xC(Vl9a1Lgg<&?{xL@7h{wv!=$ZDDg^p9}jx=Kn`8bcdBMVhP7Wgffe*&L3YZsp9|u literal 0 HcmV?d00001 From ffadf889ccab04d22cce739da78088ff7d80d486 Mon Sep 17 00:00:00 2001 From: Daniel Cellucci Date: Wed, 20 Jun 2012 16:03:03 -0400 Subject: [PATCH 3/4] privacy regarding the commit of hu-notes --- hu-notes copy.xml | 147 ---------------------------------------------- 1 file changed, 147 deletions(-) delete mode 100644 hu-notes copy.xml diff --git a/hu-notes copy.xml b/hu-notes copy.xml deleted file mode 100644 index ec91ff3..0000000 --- a/hu-notes copy.xml +++ /dev/null @@ -1,147 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From 6943e87c3ec96036819d6c777536176e829dad88 Mon Sep 17 00:00:00 2001 From: Daniel Cellucci Date: Thu, 21 Jun 2012 10:32:40 -0400 Subject: [PATCH 4/4] fixed rm/git rm thing --- hu_currentlyplaying/__init__.py | 1 - hu_currentlyplaying/__init__.pyc | Bin 196 -> 0 bytes .../currentlyPlaying.applescript | 3 - hu_currentlyplaying/hu_currentlyplaying.py | 10 - hu_currentlyplaying/hu_currentlyplaying.pyc | Bin 594 -> 0 bytes hu_googleweather/__init__.py | 1 - hu_googleweather/__init__.pyc | Bin 190 -> 0 bytes hu_googleweather/hu_googleweather.py | 16 - hu_googleweather/hu_googleweather.pyc | Bin 749 -> 0 bytes hu_googleweather/pywapi.py | 334 -- hu_googleweather/pywapi.pyc | Bin 11571 -> 0 bytes hu_lastfm/__init__.py | 1 - hu_lastfm/__init__.pyc | Bin 176 -> 0 bytes hu_lastfm/hu_lastfm.py | 67 - hu_lastfm/hu_lastfm.pyc | Bin 2332 -> 0 bytes hu_lastfm/pylast.py | 3702 ----------------- hu_lastfm/pylast.pyc | Bin 133343 -> 0 bytes hu_openapps/__init__.py | 1 - hu_openapps/__init__.pyc | Bin 180 -> 0 bytes hu_openapps/hu_openapps.py | 10 - hu_openapps/hu_openapps.pyc | Bin 623 -> 0 bytes hu_openapps/openapps.applescript | 8 - hu_openbrowsertabs/__init__.py | 1 - hu_openbrowsertabs/__init__.pyc | Bin 194 -> 0 bytes hu_openbrowsertabs/hu_openbrowsertabs.py | 10 - hu_openbrowsertabs/hu_openbrowsertabs.pyc | Bin 645 -> 0 bytes hu_openbrowsertabs/urls.applescript | 15 - hu_ssidname/__init__.py | 1 - hu_ssidname/__init__.pyc | Bin 180 -> 0 bytes hu_ssidname/hu_ssidname.py | 10 - hu_ssidname/hu_ssidname.pyc | Bin 653 -> 0 bytes 31 files changed, 4191 deletions(-) delete mode 100644 hu_currentlyplaying/__init__.py delete mode 100644 hu_currentlyplaying/__init__.pyc delete mode 100644 hu_currentlyplaying/currentlyPlaying.applescript delete mode 100644 hu_currentlyplaying/hu_currentlyplaying.py delete mode 100644 hu_currentlyplaying/hu_currentlyplaying.pyc delete mode 100644 hu_googleweather/__init__.py delete mode 100644 hu_googleweather/__init__.pyc delete mode 100644 hu_googleweather/hu_googleweather.py delete mode 100644 hu_googleweather/hu_googleweather.pyc delete mode 100755 hu_googleweather/pywapi.py delete mode 100755 hu_googleweather/pywapi.pyc delete mode 100644 hu_lastfm/__init__.py delete mode 100644 hu_lastfm/__init__.pyc delete mode 100644 hu_lastfm/hu_lastfm.py delete mode 100644 hu_lastfm/hu_lastfm.pyc delete mode 100644 hu_lastfm/pylast.py delete mode 100644 hu_lastfm/pylast.pyc delete mode 100644 hu_openapps/__init__.py delete mode 100644 hu_openapps/__init__.pyc delete mode 100644 hu_openapps/hu_openapps.py delete mode 100644 hu_openapps/hu_openapps.pyc delete mode 100644 hu_openapps/openapps.applescript delete mode 100644 hu_openbrowsertabs/__init__.py delete mode 100644 hu_openbrowsertabs/__init__.pyc delete mode 100644 hu_openbrowsertabs/hu_openbrowsertabs.py delete mode 100644 hu_openbrowsertabs/hu_openbrowsertabs.pyc delete mode 100644 hu_openbrowsertabs/urls.applescript delete mode 100644 hu_ssidname/__init__.py delete mode 100644 hu_ssidname/__init__.pyc delete mode 100644 hu_ssidname/hu_ssidname.py delete mode 100644 hu_ssidname/hu_ssidname.pyc diff --git a/hu_currentlyplaying/__init__.py b/hu_currentlyplaying/__init__.py deleted file mode 100644 index 28f9eae..0000000 --- a/hu_currentlyplaying/__init__.py +++ /dev/null @@ -1 +0,0 @@ -import hu_currentlyplaying \ No newline at end of file diff --git a/hu_currentlyplaying/__init__.pyc b/hu_currentlyplaying/__init__.pyc deleted file mode 100644 index c7a7683bdb1a19e0762d2af969bc152ebcfb2729..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 196 zcmcckiI?l{" -end tell \ No newline at end of file diff --git a/hu_currentlyplaying/hu_currentlyplaying.py b/hu_currentlyplaying/hu_currentlyplaying.py deleted file mode 100644 index 5855e6a..0000000 --- a/hu_currentlyplaying/hu_currentlyplaying.py +++ /dev/null @@ -1,10 +0,0 @@ -import commands - -def getData(): - """ - gets the currently-playing track from iTunes. - """ - track = commands.getoutput("osascript hu_currentlyplaying/currentlyPlaying.applescript") - if track[0:12] == "hu_currently": - return "" - return track \ No newline at end of file diff --git a/hu_currentlyplaying/hu_currentlyplaying.pyc b/hu_currentlyplaying/hu_currentlyplaying.pyc deleted file mode 100644 index 3d15c648406d51fee2548062a545de453b28415a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 594 zcmcIgu};G<5Iv_2(5i@qg)M9upb;xV2#L-VwXm=tW1Ge_iKF;DRDub9kPl!$`~g1z zccCgsuwtE`&v$;lclZ3h*?)O^9V=L_h|Uw*euqGl377+1krTlaIf5h8v_o0@LF=q# z3c7OWl(yd^KuWhy9|b%B+_!w)fnD8&y9itiLdP%_l$fS#%sL?G_htrtg1JdlXc|*t zvDmK*xvY5P?%$$gXijl=o8J8oPYoT diff --git a/hu_googleweather/__init__.py b/hu_googleweather/__init__.py deleted file mode 100644 index 73b61b5..0000000 --- a/hu_googleweather/__init__.py +++ /dev/null @@ -1 +0,0 @@ -import hu_googleweather \ No newline at end of file diff --git a/hu_googleweather/__init__.pyc b/hu_googleweather/__init__.pyc deleted file mode 100644 index 3b3041eada90d47e56b4fd601c37a4e6dfea4879..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 190 zcmcckiI?l{Qb`YFk&IXR`t$(i~t`N^fZsd**E`stY^ s8Kp`38KppkYLI?>d}dx|NqoFsL1hUC&=i~8{FKt1R6CH>#X!se0FT@" - except: - return "" \ No newline at end of file diff --git a/hu_googleweather/hu_googleweather.pyc b/hu_googleweather/hu_googleweather.pyc deleted file mode 100644 index a4b7f867094584ef4a4e89582891919796c09f77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 749 zcmcIh!A{#i5S_J2NZdlDf)sJ874?tZm0 z{e(*W0)C)<<0NqCxohw2zS)_1Z)bl#?M2hyzpgZGMu7DK)4zi;M7OYtOhDfh-C`5c z4>$wy%qKe%cnFj6!j}Fx&TZA?E<28|=O5f3jA>UmL`Vr9UhyufLFHw}nLM!HhoN4m-l% - -#Permission is hereby granted, free of charge, to any person -#obtaining a copy of this software and associated documentation -#files (the "Software"), to deal in the Software without -#restriction, including without limitation the rights to use, -#copy, modify, merge, publish, distribute, sublicense, and/or sell -#copies of the Software, and to permit persons to whom the -#Software is furnished to do so, subject to the following -#conditions: - -#The above copyright notice and this permission notice shall be -#included in all copies or substantial portions of the Software. - -#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -#EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -#OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -#NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -#HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -#WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -#FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -#OTHER DEALINGS IN THE SOFTWARE. - -""" -Fetches weather reports from Google Weather, Yahoo Wheather and NOAA -""" - -import urllib2, re -from xml.dom import minidom -from urllib import quote - -GOOGLE_WEATHER_URL = 'http://www.google.com/ig/api?weather=%s&hl=%s' -GOOGLE_COUNTRIES_URL = 'http://www.google.com/ig/countries?output=xml&hl=%s' -GOOGLE_CITIES_URL = 'http://www.google.com/ig/cities?output=xml&country=%s&hl=%s' - -YAHOO_WEATHER_URL = 'http://xml.weather.yahoo.com/forecastrss?p=%s&u=%s' -YAHOO_WEATHER_NS = 'http://xml.weather.yahoo.com/ns/rss/1.0' - -NOAA_WEATHER_URL = 'http://www.weather.gov/xml/current_obs/%s.xml' - -def get_weather_from_google(location_id, hl = ''): - """ - Fetches weather report from Google - - Parameters - location_id: a zip code (10001); city name, state (weather=woodland,PA); city name, country (weather=london, england); - latitude/longitude(weather=,,,30670000,104019996) or possibly other. - hl: the language parameter (language code). Default value is empty string, in this case Google will use English. - - Returns: - weather_data: a dictionary of weather data that exists in XML feed. - """ - location_id, hl = map(quote, (location_id, hl)) - url = GOOGLE_WEATHER_URL % (location_id, hl) - handler = urllib2.urlopen(url) - content_type = handler.info().dict['content-type'] - charset = re.search('charset\=(.*)',content_type).group(1) - if not charset: - charset = 'utf-8' - if charset.lower() != 'utf-8': - xml_response = handler.read().decode(charset).encode('utf-8') - else: - xml_response = handler.read() - dom = minidom.parseString(xml_response) - handler.close() - - weather_data = {} - weather_dom = dom.getElementsByTagName('weather')[0] - - data_structure = { - 'forecast_information': ('city', 'postal_code', 'latitude_e6', 'longitude_e6', 'forecast_date', 'current_date_time', 'unit_system'), - 'current_conditions': ('condition','temp_f', 'temp_c', 'humidity', 'wind_condition', 'icon') - } - for (tag, list_of_tags2) in data_structure.iteritems(): - tmp_conditions = {} - for tag2 in list_of_tags2: - try: - tmp_conditions[tag2] = weather_dom.getElementsByTagName(tag)[0].getElementsByTagName(tag2)[0].getAttribute('data') - except IndexError: - pass - weather_data[tag] = tmp_conditions - - forecast_conditions = ('day_of_week', 'low', 'high', 'icon', 'condition') - forecasts = [] - - for forecast in dom.getElementsByTagName('forecast_conditions'): - tmp_forecast = {} - for tag in forecast_conditions: - tmp_forecast[tag] = forecast.getElementsByTagName(tag)[0].getAttribute('data') - forecasts.append(tmp_forecast) - - weather_data['forecasts'] = forecasts - dom.unlink() - - return weather_data - -def get_countries_from_google(hl = ''): - """ - Get list of countries in specified language from Google - - Parameters - hl: the language parameter (language code). Default value is empty string, in this case Google will use English. - Returns: - countries: a list of elements(all countries that exists in XML feed). Each element is a dictionary with 'name' and 'iso_code' keys. - For example: [{'iso_code': 'US', 'name': 'USA'}, {'iso_code': 'FR', 'name': 'France'}] - """ - url = GOOGLE_COUNTRIES_URL % hl - - handler = urllib2.urlopen(url) - content_type = handler.info().dict['content-type'] - charset = re.search('charset\=(.*)',content_type).group(1) - if not charset: - charset = 'utf-8' - if charset.lower() != 'utf-8': - xml_response = handler.read().decode(charset).encode('utf-8') - else: - xml_response = handler.read() - dom = minidom.parseString(xml_response) - handler.close() - - countries = [] - countries_dom = dom.getElementsByTagName('country') - - for country_dom in countries_dom: - country = {} - country['name'] = country_dom.getElementsByTagName('name')[0].getAttribute('data') - country['iso_code'] = country_dom.getElementsByTagName('iso_code')[0].getAttribute('data') - countries.append(country) - - dom.unlink() - return countries - -def get_cities_from_google(country_code, hl = ''): - """ - Get list of cities of necessary country in specified language from Google - - Parameters - country_code: code of the necessary country. For example 'de' or 'fr'. - hl: the language parameter (language code). Default value is empty string, in this case Google will use English. - Returns: - cities: a list of elements(all cities that exists in XML feed). Each element is a dictionary with 'name', 'latitude_e6' and 'longitude_e6' keys. For example: [{'longitude_e6': '1750000', 'name': 'Bourges', 'latitude_e6': '47979999'}] - """ - url = GOOGLE_CITIES_URL % (country_code.lower(), hl) - - handler = urllib2.urlopen(url) - content_type = handler.info().dict['content-type'] - charset = re.search('charset\=(.*)',content_type).group(1) - if not charset: - charset = 'utf-8' - if charset.lower() != 'utf-8': - xml_response = handler.read().decode(charset).encode('utf-8') - else: - xml_response = handler.read() - dom = minidom.parseString(xml_response) - handler.close() - - cities = [] - cities_dom = dom.getElementsByTagName('city') - - for city_dom in cities_dom: - city = {} - city['name'] = city_dom.getElementsByTagName('name')[0].getAttribute('data') - city['latitude_e6'] = city_dom.getElementsByTagName('latitude_e6')[0].getAttribute('data') - city['longitude_e6'] = city_dom.getElementsByTagName('longitude_e6')[0].getAttribute('data') - cities.append(city) - - dom.unlink() - - return cities - -def get_weather_from_yahoo(location_id, units = 'metric'): - """ - Fetches weather report from Yahoo! - - Parameters - location_id: A five digit US zip code or location ID. To find your location ID, - browse or search for your city from the Weather home page(http://weather.yahoo.com/) - The weather ID is in the URL for the forecast page for that city. You can also get the location ID by entering your zip code on the home page. For example, if you search for Los Angeles on the Weather home page, the forecast page for that city is http://weather.yahoo.com/forecast/USCA0638.html. The location ID is USCA0638. - - units: type of units. 'metric' for metric and '' for non-metric - Note that choosing metric units changes all the weather units to metric, for example, wind speed will be reported as kilometers per hour and barometric pressure as millibars. - - Returns: - weather_data: a dictionary of weather data that exists in XML feed. See http://developer.yahoo.com/weather/#channel - """ - location_id = quote(location_id) - if units == 'metric': - unit = 'c' - else: - unit = 'f' - url = YAHOO_WEATHER_URL % (location_id, unit) - handler = urllib2.urlopen(url) - dom = minidom.parse(handler) - handler.close() - - weather_data = {} - weather_data['title'] = dom.getElementsByTagName('title')[0].firstChild.data - weather_data['link'] = dom.getElementsByTagName('link')[0].firstChild.data - - ns_data_structure = { - 'location': ('city', 'region', 'country'), - 'units': ('temperature', 'distance', 'pressure', 'speed'), - 'wind': ('chill', 'direction', 'speed'), - 'atmosphere': ('humidity', 'visibility', 'pressure', 'rising'), - 'astronomy': ('sunrise', 'sunset'), - 'condition': ('text', 'code', 'temp', 'date') - } - - for (tag, attrs) in ns_data_structure.iteritems(): - weather_data[tag] = xml_get_ns_yahoo_tag(dom, YAHOO_WEATHER_NS, tag, attrs) - - weather_data['geo'] = {} - weather_data['geo']['lat'] = dom.getElementsByTagName('geo:lat')[0].firstChild.data - weather_data['geo']['long'] = dom.getElementsByTagName('geo:long')[0].firstChild.data - - weather_data['condition']['title'] = dom.getElementsByTagName('item')[0].getElementsByTagName('title')[0].firstChild.data - weather_data['html_description'] = dom.getElementsByTagName('item')[0].getElementsByTagName('description')[0].firstChild.data - - forecasts = [] - for forecast in dom.getElementsByTagNameNS(YAHOO_WEATHER_NS, 'forecast'): - forecasts.append(xml_get_attrs(forecast,('date', 'low', 'high', 'text', 'code'))) - weather_data['forecasts'] = forecasts - - dom.unlink() - - return weather_data - - - -def get_weather_from_noaa(station_id): - """ - Fetches weather report from NOAA: National Oceanic and Atmospheric Administration (United States) - - Parameter: - station_id: the ID of the weather station near the necessary location - To find your station ID, perform the following steps: - 1. Open this URL: http://www.weather.gov/xml/current_obs/seek.php?state=az&Find=Find - 2. Select the necessary state state. Click 'Find'. - 3. Find the necessary station in the 'Observation Location' column. - 4. The station ID is in the URL for the weather page for that station. - For example if the weather page is http://weather.noaa.gov/weather/current/KPEO.html -- the station ID is KPEO. - - Other way to get the station ID: use this library: http://code.google.com/p/python-weather/ and 'Weather.location2station' function. - - Returns: - weather_data: a dictionary of weather data that exists in XML feed. - - (useful icons: http://www.weather.gov/xml/current_obs/weather.php) - """ - station_id = quote(station_id) - url = NOAA_WEATHER_URL % (station_id) - handler = urllib2.urlopen(url) - dom = minidom.parse(handler) - handler.close() - - data_structure = ('suggested_pickup', - 'suggested_pickup_period', - 'location', - 'station_id', - 'latitude', - 'longitude', - 'observation_time', - 'observation_time_rfc822', - 'weather', - 'temperature_string', - 'temp_f', - 'temp_c', - 'relative_humidity', - 'wind_string', - 'wind_dir', - 'wind_degrees', - 'wind_mph', - 'wind_gust_mph', - 'pressure_string', - 'pressure_mb', - 'pressure_in', - 'dewpoint_string', - 'dewpoint_f', - 'dewpoint_c', - 'heat_index_string', - 'heat_index_f', - 'heat_index_c', - 'windchill_string', - 'windchill_f', - 'windchill_c', - 'icon_url_base', - 'icon_url_name', - 'two_day_history_url', - 'ob_url' - ) - weather_data = {} - current_observation = dom.getElementsByTagName('current_observation')[0] - for tag in data_structure: - try: - weather_data[tag] = current_observation.getElementsByTagName(tag)[0].firstChild.data - except IndexError: - pass - - dom.unlink() - return weather_data - - - -def xml_get_ns_yahoo_tag(dom, ns, tag, attrs): - """ - Parses the necessary tag and returns the dictionary with values - - Parameters: - dom - DOM - ns - namespace - tag - necessary tag - attrs - tuple of attributes - - Returns: a dictionary of elements - """ - element = dom.getElementsByTagNameNS(ns, tag)[0] - return xml_get_attrs(element,attrs) - - -def xml_get_attrs(xml_element, attrs): - """ - Returns the list of necessary attributes - - Parameters: - element: xml element - attrs: tuple of attributes - - Return: a dictionary of elements - """ - - result = {} - for attr in attrs: - result[attr] = xml_element.getAttribute(attr) - return result diff --git a/hu_googleweather/pywapi.pyc b/hu_googleweather/pywapi.pyc deleted file mode 100755 index 81e0631fbb74b75b9ec5473dcaaeebe9023a861e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11571 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 QGq2B_o_TxvJJW~$2O9W!hyVZp diff --git a/hu_lastfm/__init__.py b/hu_lastfm/__init__.py deleted file mode 100644 index ce915fb..0000000 --- a/hu_lastfm/__init__.py +++ /dev/null @@ -1 +0,0 @@ -import hu_lastfm \ No newline at end of file diff --git a/hu_lastfm/__init__.pyc b/hu_lastfm/__init__.pyc deleted file mode 100644 index 251f20b63a2f7be70d786e853028118d4fe8d65a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 176 zcmcckiI?l{ now: # track was played < 10 minutes (600 seconds) ago. - tracksFromLastTenMinutes.append(track) - return xmlify(tracksFromLastTenMinutes) - except: - return "" - - - -def xmlify(tracks): - """ - takes a list of Tracks, turns them into a nice XML-ish format. - """ - if len(tracks) != 0: - xmlifiedTracks = "\n" - for track in tracks: - trackDetails = u'' - trackDetails = asciify(unicode(str(track[0]))) - artistNameEnd = trackDetails.find(" - ") - artistName = trackDetails[:artistNameEnd] - startTitleName = artistNameEnd + 3 - titleName = trackDetails[startTitleName:] - - artist = "artist=\"" + artistName + "\" " - title = "title=\"" + titleName + "\" " - timestamp = "time=\"" + track[2] + "\" " - - convertedTrack = "\n" - xmlifiedTracks += convertedTrack - xmlifiedTracks += "" - return xmlifiedTracks - else: - return "" - -def asciify(string): - """ - gets rid of pesky things like umlauts and tildes and other accents. ascii all the way, baby. - """ - temp = u'' - for char in string: - decomp = decomposition(char) - if decomp: # Not an empty string - temp += unichr(int(decomp.split()[0], 16)) - else: - temp += char - return temp \ No newline at end of file diff --git a/hu_lastfm/hu_lastfm.pyc b/hu_lastfm/hu_lastfm.pyc deleted file mode 100644 index 6af6fa932fe4c1e2ca2dd37c93e9a59250f17b4b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2332 zcmb_d&2Aev5FW06Y$>+$(>9LL03m_^@gcVApha9XF^bmBp-mh>PLaSs;I5XG#Att% zB{zux>69M&5WV%7$LJIE1q$@g7w8ML-^?oU12iiwcQ`X7XUOjx{_)Rxcl7sPj}lrw zEquSm>mQ;qL?>t!nTRHePNH{mgH9SEH)ztNv_&T^Ql?2iV?disON13NZ4p*^{PFe* zW(=!nMm9Hb{?faD@f>X{*01w&HYu`c;azY=E)nq%9`P++{}#>cS6+630YpyXnqypp zLPT#US}4&=cdqD7M2m>#5xFKIxHlNspb^ocNw4qIyg`T04rtM$&?48ucv`e6v}xYr z47T8o_&4WmdfuesBB^Uaf#Q%qSRq*#Ye-eGnZGv`rU4)+lh0-##}$I%*c8@ zE2>@nDt6(P&I`S>17jR+dhK!@>tPkD((0GC^w>#V4E4CeEI}ifq4|98%R7U?-e8#Q ze{p*+vF7$5-rqBKJ{y|-z1xBJJf6DKiJi%5Ro<3)oLToV3@+m4DOwx=(UByHJtL|! z3zr}w+ta#xn%nTIC@10kdymt1^MxjKKVtH5aXF^6iOs`lDC1<}#T;B_Ljz`)2U#Va zP5~YfUuUTs&T3R~0Eqbqgv{Z;Ta{Pf<-!_-diKZl6NgzkGI7sl0~H?Hts?!lR~JFUaR{v5_3 z=1Bcjp1R#oZFNOmQs-5>u|jRNrao1ls14On3A2O4=M;qoe?q|K1NV$jMb8^l{0{a3 z?22B$Wy9gK-x&Ge1ppsFiinU3hyZuSb^V7895~}fle+-31|{$T2R1n?K76p_{Fu3v#Q~Ui zn1wu3@oo*tgKi}%MwQ;tj=!F3Ok6S%?_Ku{@$1_2@dOuA3l(oZWAWZ~A%NKK3VyWb z33>61@ob;u7Vp~{zV($#ii&n+OBFv!Vl*8 zdw!gv!*>M8(67nxJ(GbcgG)m00ARYfiGmJh2sxMr4ZZ+CqAjon7Y7^#rR(B`k;@)q z_IR5Y^gp3lmiE#a2ItiJ$qb-GVfR4n#Of+b<4S5j%C}1mV;Pma0B}nkCtUCRu8tWM zI!;ptG^+aSmL9}|8Q?2e6%0Ejg)H2c3zPF@1dYqS;Dw>)r>P6#Bk%#J5J_D~El!T3 zPR2NW9$R0wAMoh7r0#PN94IyGgIiN8>L!j%?ukCrOctNcm?wRXqkoQ-+${NEP#Pu0 q{$(~ZBKkb{S*RlY?@G+U+|3G8rS^b#)Jv6()=+#eM4c<0t^WYo4;798 diff --git a/hu_lastfm/pylast.py b/hu_lastfm/pylast.py deleted file mode 100644 index a939f19..0000000 --- a/hu_lastfm/pylast.py +++ /dev/null @@ -1,3702 +0,0 @@ -# -*- coding: utf-8 -*- -# -# 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 -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 -# USA -# -# http://code.google.com/p/pylast/ - -__version__ = '0.4' -__author__ = 'Amr Hassan' -__copyright__ = "Copyright (C) 2008-2009 Amr Hassan" -__license__ = "gpl" -__email__ = 'amr.hassan@gmail.com' - -import hashlib -import httplib -import urllib -import threading -from xml.dom import minidom -import xml.dom -import time -import shelve -import tempfile -import sys -import htmlentitydefs - -try: - import collections -except ImportError: - pass - -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_INVALID_SIGNATURE = 13 -STATUS_TOKEN_UNAUTHORIZED = 14 -STATUS_TOKEN_EXPIRED = 15 - -EVENT_ATTENDING = '0' -EVENT_MAYBE_ATTENDING = '1' -EVENT_NOT_ATTENDING = '2' - -PERIOD_OVERALL = 'overall' -PERIOD_7DAYS = "7day" -PERIOD_3MONTHS = '3month' -PERIOD_6MONTHS = '6month' -PERIOD_12MONTHS = '12month' - -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' - -SCROBBLE_SOURCE_USER = "P" -SCROBBLE_SOURCE_NON_PERSONALIZED_BROADCAST = "R" -SCROBBLE_SOURCE_PERSONALIZED_BROADCAST = "E" -SCROBBLE_SOURCE_LASTFM = "L" -SCROBBLE_SOURCE_UNKNOWN = "U" - -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) - -class _Request(object): - """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. 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"]) -LovedTrack = _namedtuple("LovedTrack", ["track", "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 - -def _pad_list(given_list, desired_length, padding = None): - """ - Pads a list to be of the desired_length. - """ - - while len(given_list) < desired_length: - given_list.append(padding) - - return given_list - -class _BaseObject(object): - """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) - - 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): - """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): - """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): - """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.""" - - 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.""" - - 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.""" - - 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) - - 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.""" - - 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.""" - - 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.""" - - 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.""" - - 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 this user's loved track as a sequence of LovedTrack objects - in reverse order of their timestamp, all the way back to the first track. - - If limit==None, it will try to pull all the available data. - - This method uses caching. Enable caching only if you're pulling a - large amount of data. - - 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) - - seq = [] - for track in _collect_nodes(limit, self, "user.getLovedTracks", True, params): - - title = _extract(track, "name") - artist = _extract(track, "name", 1) - date = _extract(track, "date") - timestamp = track.getElementsByTagName("date")[0].getAttribute("uts") - - seq.append(LovedTrack(Track(artist, title, self.network), date, timestamp)) - - return seq - - 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 = 10): - """Returns this user's played track as a sequence of PlayedTrack objects - in reverse order of their playtime, all the way back to the first track. - - If limit==None, it will try to pull all the available data. - - This method uses caching. Enable caching only if you're pulling a - large amount of data. - - 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) - - seq = [] - for track in _collect_nodes(limit, self, "user.getRecentTracks", True, params): - - if track.hasAttribute('nowplaying'): - continue #to prevent the now playing track from sneaking in here - - title = _extract(track, "name") - artist = _extract(track, "artist") - date = _extract(track, "date") - timestamp = track.getElementsByTagName("date")[0].getAttribute("uts") - - seq.append(PlayedTrack(Track(artist, title, self.network), date, timestamp)) - - return seq - - def get_id(self): - """Returns the user id.""" - - doc = self._request("user.getInfo", True) - - return _extract(doc, "id") - - 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_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_7DAYS - 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_7DAYS - 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_7DAYS - 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_image(self): - """Returns the user's avatar.""" - - doc = self._request("user.getInfo", True) - - return _extract(doc, "image") - - 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, 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_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, 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, 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, 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, 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, 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, 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. - - 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.""" - - 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 _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 _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.""" - - 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: - 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.""" - - seq = [] - for i in topitems_or_libraryitems: - seq.append(i.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, 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, 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())) - - if self.password and self.username: - token = md5(self.password + timestamp) - elif self.network.api_key and self.network.api_secret and self.network.session_key: - if not self.username: - self.username = self.network.get_authenticated_user().get_name() - token = md5(self.network.api_secret + timestamp) - - params = {"hs": "true", "p": "1.2.1", "c": self.client_id, - "v": self.client_version, "u": self.username, "t": timestamp, - "a": token} - - if self.network.session_key and self.network.api_key: - params["sk"] = self.network.session_key - params["api_key"] = self.network.api_key - - 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() - - def scrobble_many(self, tracks): - """ - Scrobble several tracks at once. - - tracks: A sequence of a sequence of parameters for each trach. The order of parameters - is the same as if passed to the scrobble() method. - """ - - remainder = [] - - if len(tracks) > 50: - remainder = tracks[50:] - tracks = tracks[:50] - - params = {"s": self._get_session_id()} - - i = 0 - for t in tracks: - _pad_list(t, 9, "") - params["a[%s]" % str(i)] = _string(t[0]) - params["t[%s]" % str(i)] = _string(t[1]) - params["i[%s]" % str(i)] = str(t[2]) - params["o[%s]" % str(i)] = t[3] - params["r[%s]" % str(i)] = t[4] - params["l[%s]" % str(i)] = str(t[5]) - params["b[%s]" % str(i)] = _string(t[6]) - params["n[%s]" % str(i)] = t[7] - params["m[%s]" % str(i)] = t[8] - - i += 1 - - _ScrobblerRequest(self.submissions_url, params, self.network).execute() - - if remainder: - self.scrobble_many(remainder) diff --git a/hu_lastfm/pylast.pyc b/hu_lastfm/pylast.pyc deleted file mode 100644 index 599667330cce0e841992a16059dd721dc86eaa00..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 133343 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\n" + openApps + "" - return "" \ No newline at end of file diff --git a/hu_openapps/hu_openapps.pyc b/hu_openapps/hu_openapps.pyc deleted file mode 100644 index 26e0ca48789edadcef6bfbe5126215ca1dc04cc8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 623 zcmb_Zy-ve05I*;Z(m>S(sbVu*%8)FKRRK|n1%cFxg$1RtX$(mm^_)tTV1gIq0T>W( zzze`#3ToN0>@Vlv3cON@sJW) zj4+;?f>h-#Cm{`nzed0jdL=NV2D3xp7&heCrTbtbIS#!Q7ycn>bO4{<(eXgB%`$2j zU7Id5?QH3a()s*im7kk9rEYv8paMK5E4)TM$56#OP0Kho;mF2irgMkk%($eShDk|# zJ\n" - end repeat - get appList -end tell \ No newline at end of file diff --git a/hu_openbrowsertabs/__init__.py b/hu_openbrowsertabs/__init__.py deleted file mode 100644 index b3e3892..0000000 --- a/hu_openbrowsertabs/__init__.py +++ /dev/null @@ -1 +0,0 @@ -import hu_openbrowsertabs \ No newline at end of file diff --git a/hu_openbrowsertabs/__init__.pyc b/hu_openbrowsertabs/__init__.pyc deleted file mode 100644 index f55d90bd45e825e32812052950811a51ab957ac6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 194 zcmcckiI?l{lIX%Z~)D*$<0qG%}KQbSzipq3;>%VG\n" + urls + "" - return "" \ No newline at end of file diff --git a/hu_openbrowsertabs/hu_openbrowsertabs.pyc b/hu_openbrowsertabs/hu_openbrowsertabs.pyc deleted file mode 100644 index f92006f9ef56d51c3246fbbc95e36d6fc9feb1a6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 645 zcmcIh%}N6?5T5kM+QlM>P`t(4UhG~xmLgaX#2;ws#f$82(l)x=EISjWpeKDnAHak7 z2EKq!wiORO8cKVK$qU!CCn^W!>&^{VlG!Uqo+6mJ3d05>eu;FSg8E#t7xUh~cC zs>&3sdebQ%JZ2zvSD`Ngo&lb!d{u`-)quM?*bWHQ^yaSGg6>$T78l+2+2;&F`%Du` znrlghGA-kUl9SPev{R`cbh(riC(;&rALlHMUm-MjNt?@dko}ZGXn!Q_tzx`!AgyOb$&QN=%+~jIjPNa zCXdu+Wv0nGDjbD=UesXl-9x^i0=#4?nlN>YRI0PgrKyRAHg&mP5JodYbC*PO$9v-; zo2Fi|q!w4?zQdSe|3T\n" - end repeat - on error - -- not all windows have tabs - end try - end repeat -get urls -end tell \ No newline at end of file diff --git a/hu_ssidname/__init__.py b/hu_ssidname/__init__.py deleted file mode 100644 index 1ef6596..0000000 --- a/hu_ssidname/__init__.py +++ /dev/null @@ -1 +0,0 @@ -import hu_ssidname \ No newline at end of file diff --git a/hu_ssidname/__init__.pyc b/hu_ssidname/__init__.pyc deleted file mode 100644 index 13479f2900bda4a426b3f3e137c2f83be07cee39..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 180 zcmcckiI?l{" - return "" \ No newline at end of file diff --git a/hu_ssidname/hu_ssidname.pyc b/hu_ssidname/hu_ssidname.pyc deleted file mode 100644 index 47e4b93e3d3884fc385fd04f896c3810f4937011..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 653 zcmb_Z&1xGl5Z2lbjZI9BAvfXEUToDlg^<5e69|N~cxel{gpGD$E7@Hwno*0vJ+&{` z2go7h4e|o*%+k<2K(*4R`O(ZbGyCg#ICyw^{9x7J)#3Y;5AHG)yc2b=)D;UI^^=7} zELqxRulvpGvW-kwmB{yeaLhpLZlC^0)OV%UZTwwV`JqaRuIM{OPwiw0r_It03H!n@ z=+A*-s8Q1rbe&hA{cS5}+4<~6u7SS!aNpXkubM~@v_oxU2vAeG($?2CSb{>6pA7og zaBh>Wkf1Ub?k416Wj+S?H77U=%p&Q~yla{g_TTPJr<2~>He5Uz+G)I93r}+?dE8NJvNNAIZkC>w8_9NRC!$> z^;uZ}`AFPSDl;W{Ezv38ss%h@$$UaC%M=#MGFt1*%-dLT?`Rf|mhr|cBkyYol_|cK xC(Vl9a1Lgg<&?{xL@7h{wv!=$ZDDg^p9}jx=Kn`8bcdBMVhP7Wgffe*&L3YZsp9|u