Merge lp:~smasher816/pithos/pithos-pause into lp:~kevin-mehall/pithos/trunk

Proposed by Smasher816
Status: Needs review
Proposed branch: lp:~smasher816/pithos/pithos-pause
Merge into: lp:~kevin-mehall/pithos/trunk
Diff against target: 7712 lines (+7521/-4)
27 files modified
bin/pithos (+11/-4)
build/lib.linux-x86_64-2.7/pithos/AboutPithosDialog.py (+72/-0)
build/lib.linux-x86_64-2.7/pithos/PreferencesPithosDialog.py (+229/-0)
build/lib.linux-x86_64-2.7/pithos/SearchDialog.py (+116/-0)
build/lib.linux-x86_64-2.7/pithos/StationsDialog.py (+219/-0)
build/lib.linux-x86_64-2.7/pithos/dbus_service.py (+92/-0)
build/lib.linux-x86_64-2.7/pithos/gobject_worker.py (+68/-0)
build/lib.linux-x86_64-2.7/pithos/pandora/__init__.py (+24/-0)
build/lib.linux-x86_64-2.7/pithos/pandora/blowfish.py (+171/-0)
build/lib.linux-x86_64-2.7/pithos/pandora/fake.py (+130/-0)
build/lib.linux-x86_64-2.7/pithos/pandora/pandora.py (+348/-0)
build/lib.linux-x86_64-2.7/pithos/pandora/pandora_keys.py (+362/-0)
build/lib.linux-x86_64-2.7/pithos/pandora/xmlrpc.py (+63/-0)
build/lib.linux-x86_64-2.7/pithos/pithosconfig.py (+66/-0)
build/lib.linux-x86_64-2.7/pithos/plugin.py (+98/-0)
build/lib.linux-x86_64-2.7/pithos/plugins/mediakeys.py (+68/-0)
build/lib.linux-x86_64-2.7/pithos/plugins/notification_icon.py (+144/-0)
build/lib.linux-x86_64-2.7/pithos/plugins/notify.py (+61/-0)
build/lib.linux-x86_64-2.7/pithos/plugins/screensaver_pause.py (+62/-0)
build/lib.linux-x86_64-2.7/pithos/plugins/scrobble.py (+141/-0)
build/lib.linux-x86_64-2.7/pithos/pylast.py (+3702/-0)
build/lib.linux-x86_64-2.7/pithos/sound_menu.py (+231/-0)
build/scripts-2.7/pithos (+821/-0)
build/share/applications/pithos.desktop (+8/-0)
debug_config/pithos.ini (+12/-0)
pithos/plugins/notification_icon.py (+1/-0)
po/pithos.pot (+201/-0)
To merge this branch: bzr merge lp:~smasher816/pithos/pithos-pause
Reviewer Review Type Date Requested Status
Kevin Mehall Pending
Review via email: mp+96936@code.launchpad.net

Description of the change

Added the '-s' command line option
to start pithos paused and hidden.

all of my changes have #Smasher816 at the end of the line, for easy adding
bin/pithos - contains the main changes
pithos/plugins/notification_icon.py - has a small change to fix "Show Pithos" if started hidden

This option is nice for me, and i'm sure many others could use it.
For instance. Pithos starts with my computer, then I can press play on my keyboard to get music at any time.

To post a comment you must log in.

Unmerged revisions

190. By Smasher816

Start Paused + Hidden
via '-s' command line flag

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bin/pithos'
2--- bin/pithos 2012-01-01 00:14:23 +0000
3+++ bin/pithos 2012-03-11 20:43:36 +0000
4@@ -168,6 +168,8 @@
5 self.prefs_dlg = PreferencesPithosDialog.NewPreferencesPithosDialog()
6 self.preferences = self.prefs_dlg.get_preferences()
7
8+ self.startPaused = self.cmdopts.hide #SMASHER816
9+
10 if self.prefs_dlg.fix_perms():
11 # Changes were made, save new config variable
12 self.prefs_dlg.save()
13@@ -184,7 +186,7 @@
14 self.show_preferences(is_startup=True)
15
16 self.set_proxy()
17- self.set_audio_format()
18+ self.set_audio_format()
19 self.pandora_connect()
20
21 def init_core(self):
22@@ -477,7 +479,10 @@
23 self.statusbar.pop(self.statusbar.get_context_id('net'))
24 if self.start_new_playlist:
25 self.start_song(start_index)
26-
27+ if self.startPaused: #SMASHER816
28+ self.startPaused=False
29+ self.user_pause()
30+
31 self.gstreamer_errorcount_2 = self.gstreamer_errorcount_1
32 self.gstreamer_errorcount_1 = 0
33 self.playcount = 0
34@@ -794,6 +799,7 @@
35 parser = optparse.OptionParser(version="Pithos %s"%(VERSION))
36 parser.add_option("-v", "--verbose", action="store_true", dest="verbose", help="Show debug messages")
37 parser.add_option("-t", "--test", action="store_true", dest="test", help="Use a mock web interface instead of connecting to the real Pandora server")
38+ parser.add_option("-s", "--hide", action="store_true", dest="hide", help="Start Hidden and Paused") #SMASHER816
39 (options, args) = parser.parse_args()
40
41 if not options.test and try_to_raise():
42@@ -808,7 +814,8 @@
43
44 logging.info("Pithos %s"%VERSION)
45
46- window = NewPithosWindow(options)
47- window.show()
48+ window = NewPithosWindow(options)
49+ if not window.startPaused: #SMASHER816
50+ window.show()
51 gtk.main()
52
53
54=== added directory 'build'
55=== added directory 'build/lib.linux-x86_64-2.7'
56=== added directory 'build/lib.linux-x86_64-2.7/pithos'
57=== added file 'build/lib.linux-x86_64-2.7/pithos/AboutPithosDialog.py'
58--- build/lib.linux-x86_64-2.7/pithos/AboutPithosDialog.py 1970-01-01 00:00:00 +0000
59+++ build/lib.linux-x86_64-2.7/pithos/AboutPithosDialog.py 2012-03-11 20:43:36 +0000
60@@ -0,0 +1,72 @@
61+# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
62+### BEGIN LICENSE
63+# Copyright (C) 2010 Kevin Mehall <km@kevinmehall.net>
64+#This program is free software: you can redistribute it and/or modify it
65+#under the terms of the GNU General Public License version 3, as published
66+#by the Free Software Foundation.
67+#
68+#This program is distributed in the hope that it will be useful, but
69+#WITHOUT ANY WARRANTY; without even the implied warranties of
70+#MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
71+#PURPOSE. See the GNU General Public License for more details.
72+#
73+#You should have received a copy of the GNU General Public License along
74+#with this program. If not, see <http://www.gnu.org/licenses/>.
75+### END LICENSE
76+
77+import sys
78+import os
79+import gtk
80+
81+from pithos.pithosconfig import getdatapath
82+
83+class AboutPithosDialog(gtk.AboutDialog):
84+ __gtype_name__ = "AboutPithosDialog"
85+
86+ def __init__(self):
87+ """__init__ - This function is typically not called directly.
88+ Creation of a AboutPithosDialog requires redeading the associated ui
89+ file and parsing the ui definition extrenally,
90+ and then calling AboutPithosDialog.finish_initializing().
91+
92+ Use the convenience function NewAboutPithosDialog to create
93+ NewAboutPithosDialog objects.
94+
95+ """
96+ pass
97+
98+ def finish_initializing(self, builder):
99+ """finish_initalizing should be called after parsing the ui definition
100+ and creating a AboutPithosDialog object with it in order to finish
101+ initializing the start of the new AboutPithosDialog instance.
102+
103+ """
104+ #get a reference to the builder and set up the signals
105+ self.builder = builder
106+ self.builder.connect_signals(self)
107+
108+ #code for other initialization actions should be added here
109+
110+def NewAboutPithosDialog():
111+ """NewAboutPithosDialog - returns a fully instantiated
112+ AboutPithosDialog object. Use this function rather than
113+ creating a AboutPithosDialog instance directly.
114+
115+ """
116+
117+ #look for the ui file that describes the ui
118+ ui_filename = os.path.join(getdatapath(), 'ui', 'AboutPithosDialog.ui')
119+ if not os.path.exists(ui_filename):
120+ ui_filename = None
121+
122+ builder = gtk.Builder()
123+ builder.add_from_file(ui_filename)
124+ dialog = builder.get_object("about_pithos_dialog")
125+ dialog.finish_initializing(builder)
126+ return dialog
127+
128+if __name__ == "__main__":
129+ dialog = NewAboutPithosDialog()
130+ dialog.show()
131+ gtk.main()
132+
133
134=== added file 'build/lib.linux-x86_64-2.7/pithos/PreferencesPithosDialog.py'
135--- build/lib.linux-x86_64-2.7/pithos/PreferencesPithosDialog.py 1970-01-01 00:00:00 +0000
136+++ build/lib.linux-x86_64-2.7/pithos/PreferencesPithosDialog.py 2012-03-11 20:43:36 +0000
137@@ -0,0 +1,229 @@
138+# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
139+### BEGIN LICENSE
140+# Copyright (C) 2010 Kevin Mehall <km@kevinmehall.net>
141+#This program is free software: you can redistribute it and/or modify it
142+#under the terms of the GNU General Public License version 3, as published
143+#by the Free Software Foundation.
144+#
145+#This program is distributed in the hope that it will be useful, but
146+#WITHOUT ANY WARRANTY; without even the implied warranties of
147+#MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
148+#PURPOSE. See the GNU General Public License for more details.
149+#
150+#You should have received a copy of the GNU General Public License along
151+#with this program. If not, see <http://www.gnu.org/licenses/>.
152+### END LICENSE
153+
154+import sys
155+import os
156+import stat
157+import logging
158+
159+import gtk
160+import gobject
161+
162+from pithos.pithosconfig import getdatapath, valid_audio_formats
163+from pithos.plugins.scrobble import LastFmAuth
164+
165+try:
166+ from xdg.BaseDirectory import xdg_config_home
167+ config_home = xdg_config_home
168+except ImportError:
169+ config_home = os.path.dirname(__file__)
170+
171+configfilename = os.path.join(config_home, 'pithos.ini')
172+
173+class PreferencesPithosDialog(gtk.Dialog):
174+ __gtype_name__ = "PreferencesPithosDialog"
175+ prefernces = {}
176+
177+ def __init__(self):
178+ """__init__ - This function is typically not called directly.
179+ Creation of a PreferencesPithosDialog requires redeading the associated ui
180+ file and parsing the ui definition extrenally,
181+ and then calling PreferencesPithosDialog.finish_initializing().
182+
183+ Use the convenience function NewPreferencesPithosDialog to create
184+ NewAboutPithosDialog objects.
185+ """
186+
187+ pass
188+
189+ def finish_initializing(self, builder):
190+ """finish_initalizing should be called after parsing the ui definition
191+ and creating a AboutPithosDialog object with it in order to finish
192+ initializing the start of the new AboutPithosDialog instance.
193+ """
194+
195+ #get a reference to the builder and set up the signals
196+ self.builder = builder
197+ self.builder.connect_signals(self)
198+
199+ self.__load_preferences()
200+
201+
202+ def get_preferences(self):
203+ """get_preferences - returns a dictionary object that contains
204+ preferences for pithos.
205+ """
206+ return self.__preferences
207+
208+ def __load_preferences(self):
209+ #default preferences that will be overwritten if some are saved
210+ self.__preferences = {
211+ "username":'',
212+ "password":'',
213+ "notify":True,
214+ "last_station_id":None,
215+ "proxy":'',
216+ "show_icon": False,
217+ "lastfm_key": False,
218+ "enable_mediakeys":True,
219+ "enable_screensaverpause":False,
220+ "volume": 1.0,
221+ # If set, allow insecure permissions. Implements CVE-2011-1500
222+ "unsafe_permissions": False,
223+ "audio_format": valid_audio_formats[0],
224+ }
225+
226+ try:
227+ f = open(configfilename)
228+ except IOError:
229+ f = []
230+
231+ for line in f:
232+ sep = line.find('=')
233+ key = line[:sep]
234+ val = line[sep+1:].strip()
235+ if val == 'None': val=None
236+ elif val == 'False': val=False
237+ elif val == 'True': val=True
238+ self.__preferences[key]=val
239+ self.setup_fields()
240+
241+ def fix_perms(self):
242+ """Apply new file permission rules, fixing CVE-2011-1500.
243+ If the file is 0644 and if "unsafe_permissions" is not True,
244+ chmod 0600
245+ If the file is world-readable (but not exactly 0644) and if
246+ "unsafe_permissions" is not True:
247+ chmod o-rw
248+ """
249+ def complain_unsafe():
250+ # Display this message iff permissions are unsafe, which is why
251+ # we don't just check once and be done with it.
252+ logging.warning("Ignoring potentially unsafe permissions due to user override.")
253+
254+ changed = False
255+
256+ if os.path.exists(configfilename):
257+ # We've already written the file, get current permissions
258+ config_perms = stat.S_IMODE(os.stat(configfilename).st_mode)
259+ if config_perms == (stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH):
260+ if self.__preferences["unsafe_permissions"]:
261+ return complain_unsafe()
262+ # File is 0644, set to 0600
263+ logging.warning("Removing world- and group-readable permissions, to fix CVE-2011-1500 in older software versions. To force, set unsafe_permissions to True in pithos.ini.")
264+ os.chmod(configfilename, stat.S_IRUSR | stat.S_IWUSR)
265+ changed = True
266+
267+ elif config_perms & stat.S_IROTH:
268+ if self.__preferences["unsafe_permissions"]:
269+ return complain_unsafe()
270+ # File is o+r,
271+ logging.warning("Removing world-readable permissions, configuration should not be globally readable. To force, set unsafe_permissions to True in pithos.ini.")
272+ config_perms ^= stat.S_IROTH
273+ os.chmod(configfilename, config_perms)
274+ changed = True
275+
276+ if config_perms & stat.S_IWOTH:
277+ if self.__preferences["unsafe_permissions"]:
278+ return complain_unsafe()
279+ logging.warning("Removing world-writable permissions, configuration should not be globally writable. To force, set unsafe_permissions to True in pithos.ini.")
280+ config_perms ^= stat.S_IWOTH
281+ os.chmod(configfilename, config_perms)
282+ changed = True
283+
284+ return changed
285+
286+ def save(self):
287+ existed = os.path.exists(configfilename)
288+ f = open(configfilename, 'w')
289+
290+ if not existed:
291+ # make the file owner-readable and writable only
292+ os.fchmod(f.fileno(), (stat.S_IRUSR | stat.S_IWUSR))
293+
294+ for key in self.__preferences:
295+ f.write('%s=%s\n'%(key, self.__preferences[key]))
296+ f.close()
297+
298+ def setup_fields(self):
299+ self.builder.get_object('prefs_username').set_text(self.__preferences["username"])
300+ self.builder.get_object('prefs_password').set_text(self.__preferences["password"])
301+ self.builder.get_object('prefs_proxy').set_text(self.__preferences["proxy"])
302+
303+ audio_format_combo = self.builder.get_object('prefs_audio_format')
304+ fmt_store = gtk.ListStore(gobject.TYPE_STRING)
305+ for audio_format in valid_audio_formats:
306+ fmt_store.append((audio_format,))
307+ audio_format_combo.set_model(fmt_store)
308+ render_text = gtk.CellRendererText()
309+ audio_format_combo.pack_start(render_text, expand=True)
310+ audio_format_combo.add_attribute(render_text, "text", 0)
311+ audio_pref_idx = list(valid_audio_formats).index(self.__preferences["audio_format"])
312+ audio_format_combo.set_active(audio_pref_idx)
313+
314+
315+ self.builder.get_object('checkbutton_notify').set_active(self.__preferences["notify"])
316+ self.builder.get_object('checkbutton_screensaverpause').set_active(self.__preferences["enable_screensaverpause"])
317+ self.builder.get_object('checkbutton_icon').set_active(self.__preferences["show_icon"])
318+
319+ self.lastfm_auth = LastFmAuth(self.__preferences, "lastfm_key", self.builder.get_object('lastfm_btn'))
320+
321+ def ok(self, widget, data=None):
322+ """ok - The user has elected to save the changes.
323+ Called before the dialog returns gtk.RESONSE_OK from run().
324+ """
325+
326+ self.__preferences["username"] = self.builder.get_object('prefs_username').get_text()
327+ self.__preferences["password"] = self.builder.get_object('prefs_password').get_text()
328+ self.__preferences["proxy"] = self.builder.get_object('prefs_proxy').get_text()
329+ self.__preferences["audio_format"] = valid_audio_formats[self.builder.get_object('prefs_audio_format').get_active()]
330+ self.__preferences["notify"] = self.builder.get_object('checkbutton_notify').get_active()
331+ self.__preferences["enable_screensaverpause"] = self.builder.get_object('checkbutton_screensaverpause').get_active()
332+ self.__preferences["show_icon"] = self.builder.get_object('checkbutton_icon').get_active()
333+
334+ self.save()
335+
336+ def cancel(self, widget, data=None):
337+ """cancel - The user has elected cancel changes.
338+ Called before the dialog returns gtk.RESPONSE_CANCEL for run()
339+ """
340+
341+ self.setup_fields() # restore fields to previous values
342+ pass
343+
344+
345+def NewPreferencesPithosDialog():
346+ """NewPreferencesPithosDialog - returns a fully instantiated
347+ PreferencesPithosDialog object. Use this function rather than
348+ creating a PreferencesPithosDialog instance directly.
349+ """
350+
351+ #look for the ui file that describes the ui
352+ ui_filename = os.path.join(getdatapath(), 'ui', 'PreferencesPithosDialog.ui')
353+ if not os.path.exists(ui_filename):
354+ ui_filename = None
355+
356+ builder = gtk.Builder()
357+ builder.add_from_file(ui_filename)
358+ dialog = builder.get_object("preferences_pithos_dialog")
359+ dialog.finish_initializing(builder)
360+ return dialog
361+
362+if __name__ == "__main__":
363+ dialog = NewPreferencesPithosDialog()
364+ dialog.show()
365+ gtk.main()
366+
367
368=== added file 'build/lib.linux-x86_64-2.7/pithos/SearchDialog.py'
369--- build/lib.linux-x86_64-2.7/pithos/SearchDialog.py 1970-01-01 00:00:00 +0000
370+++ build/lib.linux-x86_64-2.7/pithos/SearchDialog.py 2012-03-11 20:43:36 +0000
371@@ -0,0 +1,116 @@
372+# -*- coding: utf-8 -*-
373+### BEGIN LICENSE
374+# This file is in the public domain
375+### END LICENSE
376+
377+import sys
378+import os
379+import gtk, gobject
380+import cgi
381+
382+from pithos.pithosconfig import getdatapath
383+
384+class SearchDialog(gtk.Dialog):
385+ __gtype_name__ = "SearchDialog"
386+
387+ def __init__(self):
388+ """__init__ - This function is typically not called directly.
389+ Creation of a SearchDialog requires redeading the associated ui
390+ file and parsing the ui definition extrenally,
391+ and then calling SearchDialog.finish_initializing().
392+
393+ Use the convenience function NewSearchDialog to create
394+ a SearchDialog object.
395+
396+ """
397+ pass
398+
399+ def finish_initializing(self, builder, worker_run):
400+ """finish_initalizing should be called after parsing the ui definition
401+ and creating a SearchDialog object with it in order to finish
402+ initializing the start of the new SearchDialog instance.
403+
404+ """
405+ #get a reference to the builder and set up the signals
406+ self.builder = builder
407+ self.builder.connect_signals(self)
408+
409+ self.entry = self.builder.get_object('entry')
410+ self.treeview = self.builder.get_object('treeview')
411+ self.okbtn = self.builder.get_object('okbtn')
412+ self.searchbtn = self.builder.get_object('searchbtn')
413+ self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, str)
414+ self.treeview.set_model(self.model)
415+
416+ self.worker_run = worker_run
417+
418+ self.result = None
419+
420+
421+ def ok(self, widget, data=None):
422+ """ok - The user has elected to save the changes.
423+ Called before the dialog returns gtk.RESONSE_OK from run().
424+
425+ """
426+
427+
428+ def cancel(self, widget, data=None):
429+ """cancel - The user has elected cancel changes.
430+ Called before the dialog returns gtk.RESPONSE_CANCEL for run()
431+
432+ """
433+ pass
434+
435+ def search_clicked(self, widget):
436+ self.search(self.entry.get_text())
437+
438+ def search(self, query):
439+ if not query: return
440+ def callback(results):
441+ self.model.clear()
442+ for i in results:
443+ if i.resultType is 'song':
444+ mk = "<b>%s</b> by %s"%(cgi.escape(i.title), cgi.escape(i.artist))
445+ elif i.resultType is 'artist':
446+ mk = "<b>%s</b> (artist)"%(cgi.escape(i.name))
447+ self.model.append((i, mk))
448+ self.treeview.show()
449+ self.searchbtn.set_sensitive(True)
450+ self.searchbtn.set_label("Search")
451+ self.worker_run('search', (query,), callback, "Searching...")
452+ self.searchbtn.set_sensitive(False)
453+ self.searchbtn.set_label("Searching...")
454+
455+ def get_selected(self):
456+ sel = self.treeview.get_selection().get_selected()
457+ if sel:
458+ return self.treeview.get_model().get_value(sel[1], 0)
459+
460+ def cursor_changed(self, *ignore):
461+ self.result = self.get_selected()
462+ self.okbtn.set_sensitive(not not self.result)
463+
464+
465+def NewSearchDialog(worker_run):
466+ """NewSearchDialog - returns a fully instantiated
467+ dialog-camel_case_nameDialog object. Use this function rather than
468+ creating SearchDialog instance directly.
469+
470+ """
471+
472+ #look for the ui file that describes the ui
473+ ui_filename = os.path.join(getdatapath(), 'ui', 'SearchDialog.ui')
474+ if not os.path.exists(ui_filename):
475+ ui_filename = None
476+
477+ builder = gtk.Builder()
478+ builder.add_from_file(ui_filename)
479+ dialog = builder.get_object("search_dialog")
480+ dialog.finish_initializing(builder, worker_run)
481+ return dialog
482+
483+if __name__ == "__main__":
484+ dialog = NewSearchDialog()
485+ dialog.show()
486+ gtk.main()
487+
488
489=== added file 'build/lib.linux-x86_64-2.7/pithos/StationsDialog.py'
490--- build/lib.linux-x86_64-2.7/pithos/StationsDialog.py 1970-01-01 00:00:00 +0000
491+++ build/lib.linux-x86_64-2.7/pithos/StationsDialog.py 2012-03-11 20:43:36 +0000
492@@ -0,0 +1,219 @@
493+# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
494+### BEGIN LICENSE
495+# Copyright (C) 2010 Kevin Mehall <km@kevinmehall.net>
496+#This program is free software: you can redistribute it and/or modify it
497+#under the terms of the GNU General Public License version 3, as published
498+#by the Free Software Foundation.
499+#
500+#This program is distributed in the hope that it will be useful, but
501+#WITHOUT ANY WARRANTY; without even the implied warranties of
502+#MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
503+#PURPOSE. See the GNU General Public License for more details.
504+#
505+#You should have received a copy of the GNU General Public License along
506+#with this program. If not, see <http://www.gnu.org/licenses/>.
507+### END LICENSE
508+
509+import sys
510+import os
511+import gtk
512+import logging
513+import webbrowser
514+
515+from pithos.pithosconfig import getdatapath
516+from pithos import SearchDialog
517+
518+class StationsDialog(gtk.Dialog):
519+ __gtype_name__ = "StationsDialog"
520+
521+ def __init__(self):
522+ """__init__ - This function is typically not called directly.
523+ Creation of a StationsDialog requires redeading the associated ui
524+ file and parsing the ui definition extrenally,
525+ and then calling StationsDialog.finish_initializing().
526+
527+ Use the convenience function NewStationsDialog to create
528+ a StationsDialog object.
529+
530+ """
531+ pass
532+
533+ def finish_initializing(self, builder, pithos):
534+ """finish_initalizing should be called after parsing the ui definition
535+ and creating a StationsDialog object with it in order to finish
536+ initializing the start of the new StationsDialog instance.
537+
538+ """
539+ #get a reference to the builder and set up the signals
540+ self.builder = builder
541+ self.builder.connect_signals(self)
542+
543+ self.pithos = pithos
544+ self.model = pithos.stations_model
545+ self.worker_run = pithos.worker_run
546+ self.quickmix_changed = False
547+ self.searchDialog = None
548+
549+ self.modelfilter = self.model.filter_new()
550+ self.modelfilter.set_visible_func(lambda m, i: m.get_value(i, 0) and not m.get_value(i, 0).isQuickMix)
551+
552+ self.modelsortable = gtk.TreeModelSort(self.modelfilter)
553+ """
554+ @todo Leaving it as sorting by date added by default.
555+ Probably should make a radio select in the window or an option in program options for user preference
556+ """
557+# self.modelsortable.set_sort_column_id(1, gtk.SORT_ASCENDING)
558+
559+ self.treeview = self.builder.get_object("treeview")
560+ self.treeview.set_model(self.modelsortable)
561+ self.treeview.connect('button_press_event', self.on_treeview_button_press_event)
562+
563+ name_col = gtk.TreeViewColumn()
564+ name_col.set_title("Name")
565+ render_text = gtk.CellRendererText()
566+ render_text.set_property('editable', True)
567+ render_text.connect("edited", self.station_renamed)
568+ name_col.pack_start(render_text, expand=True)
569+ name_col.add_attribute(render_text, "text", 1)
570+ name_col.set_expand(True)
571+ name_col.set_sort_column_id(1)
572+ self.treeview.append_column(name_col)
573+
574+ qm_col = gtk.TreeViewColumn()
575+ qm_col.set_title("In QuickMix")
576+ render_toggle = gtk.CellRendererToggle()
577+ qm_col.pack_start(render_toggle, expand=True)
578+ def qm_datafunc(column, cell, model, iter):
579+ if model.get_value(iter,0).useQuickMix:
580+ cell.set_active(True)
581+ else:
582+ cell.set_active(False)
583+ qm_col.set_cell_data_func(render_toggle, qm_datafunc)
584+ render_toggle.connect("toggled", self.qm_toggled)
585+ self.treeview.append_column(qm_col)
586+
587+ self.station_menu = builder.get_object("station_menu")
588+
589+ def qm_toggled(self, renderer, path):
590+ station = self.modelfilter[path][0]
591+ station.useQuickMix = not station.useQuickMix
592+ self.quickmix_changed = True
593+
594+ def station_renamed(self, cellrenderertext, path, new_text):
595+ station = self.modelfilter[path][0]
596+ self.worker_run(station.rename, (new_text,), context='net', message="Renaming Station...")
597+ self.model[self.modelfilter.convert_path_to_child_path(path)][1] = new_text
598+
599+ def selected_station(self):
600+ sel = self.treeview.get_selection().get_selected()
601+ if sel:
602+ return self.treeview.get_model().get_value(sel[1], 0)
603+
604+ def on_treeview_button_press_event(self, treeview, event):
605+ if event.button == 3:
606+ x = int(event.x)
607+ y = int(event.y)
608+ time = event.time
609+ pthinfo = treeview.get_path_at_pos(x, y)
610+ if pthinfo is not None:
611+ path, col, cellx, celly = pthinfo
612+ treeview.grab_focus()
613+ treeview.set_cursor( path, col, 0)
614+ self.station_menu.popup( None, None, None, event.button, time)
615+ return True
616+
617+ def on_menuitem_listen(self, widget):
618+ station = self.selected_station()
619+ self.pithos.station_changed(station)
620+ self.hide()
621+
622+ def on_menuitem_info(self, widget):
623+ webbrowser.open(self.selected_station().info_url)
624+
625+ def on_menuitem_rename(self, widget):
626+ sel = self.treeview.get_selection().get_selected()
627+ path = self.modelfilter.get_path(sel[1])
628+ self.treeview.set_cursor(path, self.treeview.get_column(0) ,True)
629+
630+ def on_menuitem_delete(self, widget):
631+ station = self.selected_station()
632+
633+ dialog = self.builder.get_object("delete_confirm_dialog")
634+ dialog.set_property("text", "Are you sure you want to delete the station \"%s\"?"%(station.name))
635+ response = dialog.run()
636+ dialog.hide()
637+
638+ if response:
639+ self.worker_run(station.delete, context='net', message="Deleting Station...")
640+ del self.pithos.stations_model[self.pithos.station_index(station)]
641+ if self.pithos.current_station is station:
642+ self.pithos.station_changed(self.model[0][0])
643+
644+ def add_station(self, widget):
645+ if self.searchDialog:
646+ self.searchDialog.present()
647+ else:
648+ self.searchDialog = SearchDialog.NewSearchDialog(self.worker_run)
649+ self.searchDialog.show_all()
650+ self.searchDialog.connect("response", self.add_station_cb)
651+
652+ def refresh_stations(self, widget):
653+ self.pithos.refresh_stations(self.pithos)
654+
655+ def add_station_cb(self, dialog, response):
656+ print "in add_station_cb", dialog.result, response
657+ if response == 1:
658+ self.worker_run("add_station_by_music_id", (dialog.result.musicId,), self.station_added, "Creating station...")
659+ dialog.hide()
660+ dialog.destroy()
661+ self.searchDialog = None
662+
663+ def station_added(self, station):
664+ logging.debug("1 "+ repr(station))
665+ it = self.model.insert_after(self.model.get_iter(1), (station, station.name))
666+ logging.debug("2 "+ repr(it))
667+ self.pithos.station_changed(station)
668+ logging.debug("3 ")
669+ self.modelfilter.refilter()
670+ logging.debug("4")
671+ self.treeview.set_cursor(0)
672+ logging.debug("5 ")
673+
674+ def add_genre_station(self, widget):
675+ """
676+ This is just a stub for the non-completed buttn
677+ """
678+
679+ def on_close(self, widget, data=None):
680+ self.hide()
681+
682+ if self.quickmix_changed:
683+ self.worker_run("save_quick_mix", message="Saving QuickMix...")
684+ self.quickmix_changed = False
685+
686+ logging.info("closed dialog")
687+ return True
688+
689+def NewStationsDialog(pithos):
690+ """NewStationsDialog - returns a fully instantiated
691+ Dialog object. Use this function rather than
692+ creating StationsDialog instance directly.
693+
694+ """
695+
696+ #look for the ui file that describes the ui
697+ ui_filename = os.path.join(getdatapath(), 'ui', 'StationsDialog.ui')
698+ if not os.path.exists(ui_filename):
699+ ui_filename = None
700+
701+ builder = gtk.Builder()
702+ builder.add_from_file(ui_filename)
703+ dialog = builder.get_object("stations_dialog")
704+ dialog.finish_initializing(builder, pithos)
705+ return dialog
706+
707+if __name__ == "__main__":
708+ dialog = NewStationsDialog()
709+ dialog.show()
710+ gtk.main()
711+
712
713=== added file 'build/lib.linux-x86_64-2.7/pithos/__init__.py'
714=== added file 'build/lib.linux-x86_64-2.7/pithos/dbus_service.py'
715--- build/lib.linux-x86_64-2.7/pithos/dbus_service.py 1970-01-01 00:00:00 +0000
716+++ build/lib.linux-x86_64-2.7/pithos/dbus_service.py 2012-03-11 20:43:36 +0000
717@@ -0,0 +1,92 @@
718+# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
719+### BEGIN LICENSE
720+# Copyright (C) 2010 Kevin Mehall <km@kevinmehall.net>
721+#This program is free software: you can redistribute it and/or modify it
722+#under the terms of the GNU General Public License version 3, as published
723+#by the Free Software Foundation.
724+#
725+#This program is distributed in the hope that it will be useful, but
726+#WITHOUT ANY WARRANTY; without even the implied warranties of
727+#MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
728+#PURPOSE. See the GNU General Public License for more details.
729+#
730+#You should have received a copy of the GNU General Public License along
731+#with this program. If not, see <http://www.gnu.org/licenses/>.
732+### END LICENSE
733+
734+import dbus.service
735+
736+DBUS_BUS = "net.kevinmehall.Pithos"
737+DBUS_OBJECT_PATH = "/net/kevinmehall/Pithos"
738+
739+def song_to_dict(song):
740+ d = {}
741+ if song:
742+ for i in ['artist', 'title', 'album', 'songDetailURL']:
743+ d[i] = getattr(song, i)
744+ return d
745+
746+class PithosDBusProxy(dbus.service.Object):
747+ def __init__(self, window):
748+ self.bus = dbus.SessionBus()
749+ bus_name = dbus.service.BusName(DBUS_BUS, bus=self.bus)
750+ dbus.service.Object.__init__(self, bus_name, DBUS_OBJECT_PATH)
751+ self.window = window
752+ self.window.connect("song-changed", self.songchange_handler)
753+ self.window.connect("play-state-changed", self.playstate_handler)
754+
755+ def playstate_handler(self, window, state):
756+ self.PlayStateChanged(state)
757+
758+ def songchange_handler(self, window, song):
759+ self.SongChanged(song_to_dict(song))
760+
761+ @dbus.service.method(DBUS_BUS)
762+ def PlayPause(self):
763+ self.window.playpause()
764+
765+ @dbus.service.method(DBUS_BUS)
766+ def SkipSong(self):
767+ self.window.next_song()
768+
769+ @dbus.service.method(DBUS_BUS)
770+ def LoveCurrentSong(self):
771+ self.window.love_song()
772+
773+ @dbus.service.method(DBUS_BUS)
774+ def BanCurrentSong(self):
775+ self.window.ban_song()
776+
777+ @dbus.service.method(DBUS_BUS)
778+ def TiredCurrentSong(self):
779+ self.window.tired_song()
780+
781+ @dbus.service.method(DBUS_BUS)
782+ def Present(self):
783+ self.window.bring_to_top()
784+
785+ @dbus.service.method(DBUS_BUS, out_signature='a{sv}')
786+ def GetCurrentSong(self):
787+ return song_to_dict(self.window.current_song)
788+
789+ @dbus.service.method(DBUS_BUS, out_signature='b')
790+ def IsPlaying(self):
791+ return self.window.playing
792+
793+ @dbus.service.signal(DBUS_BUS, signature='b')
794+ def PlayStateChanged(self, state):
795+ pass
796+
797+ @dbus.service.signal(DBUS_BUS, signature='a{sv}')
798+ def SongChanged(self, songinfo):
799+ pass
800+
801+
802+def try_to_raise():
803+ bus = dbus.SessionBus()
804+ try:
805+ proxy = bus.get_object(DBUS_BUS, DBUS_OBJECT_PATH)
806+ proxy.Present()
807+ return True
808+ except dbus.exceptions.DBusException as e:
809+ return False
810
811=== added file 'build/lib.linux-x86_64-2.7/pithos/gobject_worker.py'
812--- build/lib.linux-x86_64-2.7/pithos/gobject_worker.py 1970-01-01 00:00:00 +0000
813+++ build/lib.linux-x86_64-2.7/pithos/gobject_worker.py 2012-03-11 20:43:36 +0000
814@@ -0,0 +1,68 @@
815+# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
816+### BEGIN LICENSE
817+# Copyright (C) 2010 Kevin Mehall <km@kevinmehall.net>
818+#This program is free software: you can redistribute it and/or modify it
819+#under the terms of the GNU General Public License version 3, as published
820+#by the Free Software Foundation.
821+#
822+#This program is distributed in the hope that it will be useful, but
823+#WITHOUT ANY WARRANTY; without even the implied warranties of
824+#MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
825+#PURPOSE. See the GNU General Public License for more details.
826+#
827+#You should have received a copy of the GNU General Public License along
828+#with this program. If not, see <http://www.gnu.org/licenses/>.
829+### END LICENSE
830+
831+import threading
832+import Queue
833+import gobject
834+import traceback
835+gobject.threads_init()
836+
837+class GObjectWorker():
838+ def __init__(self):
839+ self.thread = threading.Thread(target=self._run)
840+ self.thread.daemon = True
841+ self.queue = Queue.Queue()
842+ self.thread.start()
843+
844+ def _run(self):
845+ while True:
846+ command, args, callback, errorback = self.queue.get()
847+ try:
848+ result = command(*args)
849+ if callback:
850+ gobject.idle_add(callback, result)
851+ except Exception, e:
852+ e.traceback = traceback.format_exc()
853+ if errorback:
854+ gobject.idle_add(errorback, e)
855+
856+ def send(self, command, args=(), callback=None, errorback=None):
857+ if errorback is None: errorback = self._default_errorback
858+ self.queue.put((command, args, callback, errorback))
859+
860+ def _default_errorback(self, error):
861+ print "Unhandled exception in worker thread:\n", error.traceback
862+
863+if __name__ == '__main__':
864+ worker = GObjectWorker()
865+ import time, gtk
866+
867+ def test_cmd(a, b):
868+ print "running..."
869+ time.sleep(5)
870+ print "done"
871+ return a*b
872+
873+ def test_cb(result):
874+ print "got result", result
875+
876+ print "sending"
877+ worker.send(test_cmd, (3,4), test_cb)
878+ worker.send(test_cmd, ((), ()), test_cb) #trigger exception in worker to test error handling
879+
880+ gtk.main()
881+
882+
883
884=== added directory 'build/lib.linux-x86_64-2.7/pithos/pandora'
885=== added file 'build/lib.linux-x86_64-2.7/pithos/pandora/__init__.py'
886--- build/lib.linux-x86_64-2.7/pithos/pandora/__init__.py 1970-01-01 00:00:00 +0000
887+++ build/lib.linux-x86_64-2.7/pithos/pandora/__init__.py 2012-03-11 20:43:36 +0000
888@@ -0,0 +1,24 @@
889+# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
890+### BEGIN LICENSE
891+# Copyright (C) 2010 Kevin Mehall <km@kevinmehall.net>
892+#This program is free software: you can redistribute it and/or modify it
893+#under the terms of the GNU General Public License version 3, as published
894+#by the Free Software Foundation.
895+#
896+#This program is distributed in the hope that it will be useful, but
897+#WITHOUT ANY WARRANTY; without even the implied warranties of
898+#MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
899+#PURPOSE. See the GNU General Public License for more details.
900+#
901+#You should have received a copy of the GNU General Public License along
902+#with this program. If not, see <http://www.gnu.org/licenses/>.
903+### END LICENSE
904+
905+from pithos.pandora.pandora import *
906+
907+def make_pandora(testing=False):
908+ if testing:
909+ from pithos.pandora.fake import FakePandora
910+ return FakePandora()
911+ else:
912+ return Pandora()
913
914=== added file 'build/lib.linux-x86_64-2.7/pithos/pandora/blowfish.py'
915--- build/lib.linux-x86_64-2.7/pithos/pandora/blowfish.py 1970-01-01 00:00:00 +0000
916+++ build/lib.linux-x86_64-2.7/pithos/pandora/blowfish.py 2012-03-11 20:43:36 +0000
917@@ -0,0 +1,171 @@
918+#
919+# blowfish.py
920+# Copyright (C) 2002 Michael Gilfix <mgilfix@eecs.tufts.edu>
921+#
922+# This module is open source; you can redistribute it and/or
923+# modify it under the terms of the GPL or Artistic License.
924+# These licenses are available at http://www.opensource.org
925+#
926+# This software must be used and distributed in accordance
927+# with the law. The author claims no liability for its
928+# misuse.
929+#
930+# This program is distributed in the hope that it will be useful,
931+# but WITHOUT ANY WARRANTY; without even the implied warranty of
932+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
933+#
934+
935+"""
936+Blowfish Encryption
937+
938+This module is a pure python implementation of Bruce Schneier's
939+encryption scheme 'Blowfish'. Blowish is a 16-round Feistel Network
940+cipher and offers substantial speed gains over DES.
941+
942+The key is a string of length anywhere between 64 and 448 bits, or
943+equivalently 8 and 56 bytes. The encryption and decryption functions operate
944+on 64-bit blocks, or 8 byte strings.
945+
946+Send questions, comments, bugs my way:
947+ Michael Gilfix <mgilfix@eecs.tufts.edu>
948+
949+This version is modified by Kevin Mehall <km@kevinmehall.net> to accept the
950+S and P boxes directly, rather than computing them from a key
951+"""
952+
953+__author__ = "Michael Gilfix <mgilfix@eecs.tufts.edu>"
954+
955+class Blowfish:
956+
957+ """Blowfish encryption Scheme
958+
959+ This class implements the encryption and decryption
960+ functionality of the Blowfish cipher.
961+
962+ Public functions:
963+
964+ def __init__ (self, key)
965+ Creates an instance of blowfish using 'key'
966+ as the encryption key. Key is a string of
967+ length ranging from 8 to 56 bytes (64 to 448
968+ bits). Once the instance of the object is
969+ created, the key is no longer necessary.
970+
971+ def encrypt (self, data):
972+ Encrypt an 8 byte (64-bit) block of text
973+ where 'data' is an 8 byte string. Returns an
974+ 8-byte encrypted string.
975+
976+ def decrypt (self, data):
977+ Decrypt an 8 byte (64-bit) encrypted block
978+ of text, where 'data' is the 8 byte encrypted
979+ string. Returns an 8-byte string of plaintext.
980+
981+ def cipher (self, xl, xr, direction):
982+ Encrypts a 64-bit block of data where xl is
983+ the upper 32-bits and xr is the lower 32-bits.
984+ 'direction' is the direction to apply the
985+ cipher, either ENCRYPT or DECRYPT constants.
986+ returns a tuple of either encrypted or decrypted
987+ data of the left half and right half of the
988+ 64-bit block.
989+
990+ Private members:
991+
992+ def __round_func (self, xl)
993+ Performs an obscuring function on the 32-bit
994+ block of data 'xl', which is the left half of
995+ the 64-bit block of data. Returns the 32-bit
996+ result as a long integer.
997+
998+ """
999+
1000+ # Cipher directions
1001+ ENCRYPT = 0
1002+ DECRYPT = 1
1003+
1004+ # For the __round_func
1005+ modulus = long (2) ** 32
1006+
1007+ def __init__ (self, p_boxes, s_boxes):
1008+ self.p_boxes = p_boxes
1009+ self.s_boxes = s_boxes
1010+
1011+
1012+ def cipher (self, xl, xr, direction):
1013+
1014+ if direction == self.ENCRYPT:
1015+ for i in range (16):
1016+ xl = xl ^ self.p_boxes[i]
1017+ xr = self.__round_func (xl) ^ xr
1018+ xl, xr = xr, xl
1019+ xl, xr = xr, xl
1020+ xr = xr ^ self.p_boxes[16]
1021+ xl = xl ^ self.p_boxes[17]
1022+ else:
1023+ for i in range (17, 1, -1):
1024+ xl = xl ^ self.p_boxes[i]
1025+ xr = self.__round_func (xl) ^ xr
1026+ xl, xr = xr, xl
1027+ xl, xr = xr, xl
1028+ xr = xr ^ self.p_boxes[1]
1029+ xl = xl ^ self.p_boxes[0]
1030+ return xl, xr
1031+
1032+ def __round_func (self, xl):
1033+ a = (xl & 0xFF000000) >> 24
1034+ b = (xl & 0x00FF0000) >> 16
1035+ c = (xl & 0x0000FF00) >> 8
1036+ d = xl & 0x000000FF
1037+
1038+ # Perform all ops as longs then and out the last 32-bits to
1039+ # obtain the integer
1040+ f = (long (self.s_boxes[0][a]) + long (self.s_boxes[1][b])) % self.modulus
1041+ f = f ^ long (self.s_boxes[2][c])
1042+ f = f + long (self.s_boxes[3][d])
1043+ f = (f % self.modulus) & 0xFFFFFFFF
1044+
1045+ return f
1046+
1047+ def encrypt (self, data):
1048+
1049+ if not len (data) == 8:
1050+ raise RuntimeError, "Attempted to encrypt data of invalid block length: %s" %len (data)
1051+
1052+ # Use big endianess since that's what everyone else uses
1053+ xl = ord (data[3]) | (ord (data[2]) << 8) | (ord (data[1]) << 16) | (ord (data[0]) << 24)
1054+ xr = ord (data[7]) | (ord (data[6]) << 8) | (ord (data[5]) << 16) | (ord (data[4]) << 24)
1055+
1056+ cl, cr = self.cipher (xl, xr, self.ENCRYPT)
1057+ chars = ''.join ([
1058+ chr ((cl >> 24) & 0xFF), chr ((cl >> 16) & 0xFF), chr ((cl >> 8) & 0xFF), chr (cl & 0xFF),
1059+ chr ((cr >> 24) & 0xFF), chr ((cr >> 16) & 0xFF), chr ((cr >> 8) & 0xFF), chr (cr & 0xFF)
1060+ ])
1061+ return chars
1062+
1063+ def decrypt (self, data):
1064+
1065+ if not len (data) == 8:
1066+ raise RuntimeError, "Attempted to encrypt data of invalid block length: %s" %len (data)
1067+
1068+ # Use big endianess since that's what everyone else uses
1069+ cl = ord (data[3]) | (ord (data[2]) << 8) | (ord (data[1]) << 16) | (ord (data[0]) << 24)
1070+ cr = ord (data[7]) | (ord (data[6]) << 8) | (ord (data[5]) << 16) | (ord (data[4]) << 24)
1071+
1072+ xl, xr = self.cipher (cl, cr, self.DECRYPT)
1073+ chars = ''.join ([
1074+ chr ((xl >> 24) & 0xFF), chr ((xl >> 16) & 0xFF), chr ((xl >> 8) & 0xFF), chr (xl & 0xFF),
1075+ chr ((xr >> 24) & 0xFF), chr ((xr >> 16) & 0xFF), chr ((xr >> 8) & 0xFF), chr (xr & 0xFF)
1076+ ])
1077+ return chars
1078+
1079+ def blocksize (self):
1080+ return 8
1081+
1082+ def key_length (self):
1083+ return 56
1084+
1085+ def key_bits (self):
1086+ return 56 * 8
1087+
1088+
1089
1090=== added file 'build/lib.linux-x86_64-2.7/pithos/pandora/fake.py'
1091--- build/lib.linux-x86_64-2.7/pithos/pandora/fake.py 1970-01-01 00:00:00 +0000
1092+++ build/lib.linux-x86_64-2.7/pithos/pandora/fake.py 2012-03-11 20:43:36 +0000
1093@@ -0,0 +1,130 @@
1094+# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
1095+### BEGIN LICENSE
1096+# Copyright (C) 2010 Kevin Mehall <km@kevinmehall.net>
1097+#This program is free software: you can redistribute it and/or modify it
1098+#under the terms of the GNU General Public License version 3, as published
1099+#by the Free Software Foundation.
1100+#
1101+#This program is distributed in the hope that it will be useful, but
1102+#WITHOUT ANY WARRANTY; without even the implied warranties of
1103+#MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1104+#PURPOSE. See the GNU General Public License for more details.
1105+#
1106+#You should have received a copy of the GNU General Public License along
1107+#with this program. If not, see <http://www.gnu.org/licenses/>.
1108+### END LICENSE
1109+
1110+from pithos.pandora.pandora import *
1111+import gtk
1112+import logging
1113+
1114+class FakePandora(Pandora):
1115+ def __init__(self):
1116+ super(FakePandora, self).__init__()
1117+ self.counter = 0
1118+ self.show_fail_window()
1119+ logging.info("Using test mode")
1120+
1121+ def count(self):
1122+ self.counter +=1
1123+ return self.counter
1124+
1125+ def show_fail_window(self):
1126+ self.window = gtk.Window()
1127+ self.window.set_size_request(200, 100)
1128+ self.window.set_title("Pithos failure tester")
1129+ self.window.set_opacity(0.7)
1130+ self.auth_check = gtk.CheckButton("Authenticated")
1131+ self.time_check = gtk.CheckButton("Be really slow")
1132+ vbox = gtk.VBox()
1133+ self.window.add(vbox)
1134+ vbox.pack_start(self.auth_check)
1135+ vbox.pack_start(self.time_check)
1136+ self.window.show_all()
1137+
1138+ def maybe_fail(self):
1139+ if self.time_check.get_active():
1140+ logging.info("fake: Going to sleep for 10s")
1141+ time.sleep(10)
1142+ if not self.auth_check.get_active():
1143+ logging.info("fake: We're deauthenticated...")
1144+ raise PandoraAuthTokenInvalid("Auth token invalid", "AUTH_INVALID_TOKEN")
1145+
1146+ def set_authenticated(self):
1147+ self.auth_check.set_active(True)
1148+
1149+ def xmlrpc_call(self, method, args=[], url_args=True, secure=False):
1150+ time.sleep(1)
1151+ if method != 'listener.authenticateListener':
1152+ self.maybe_fail()
1153+
1154+ if method == 'listener.authenticateListener':
1155+ self.set_authenticated()
1156+ return {'webAuthToken': '123', 'listenerId':'456', 'authToken':'789'}
1157+
1158+ elif method == 'station.getStations':
1159+ return [
1160+ {'stationId':'987', 'stationIdToken':'345434', 'isCreator':True, 'isQuickMix':False, 'stationName':"Test Station 1"},
1161+ {'stationId':'321', 'stationIdToken':'453544', 'isCreator':True, 'isQuickMix':True, 'stationName':"Fake's QuickMix",
1162+ 'quickMixStationIds':['987', '343']},
1163+ {'stationId':'432', 'stationIdToken':'345485', 'isCreator':True, 'isQuickMix':False, 'stationName':"Test Station 2"},
1164+ {'stationId':'254', 'stationIdToken':'345415', 'isCreator':True, 'isQuickMix':False, 'stationName':"Test Station 4 - Out of Order"},
1165+ {'stationId':'343', 'stationIdToken':'345435', 'isCreator':True, 'isQuickMix':False, 'stationName':"Test Station 3"},
1166+ ]
1167+ elif method == 'playlist.getFragment':
1168+ return [self.makeFakeSong(args) for i in range(4)]
1169+ elif method == 'music.search':
1170+ return {'artists': [
1171+ {'score':90, 'musicId':'988', 'artistName':"artistName"},
1172+ ],
1173+ 'songs':[
1174+ {'score':80, 'musicId':'238', 'songTitle':"SongName", 'artistSummary':"ArtistName"},
1175+ ],
1176+ }
1177+ elif method == 'station.createStation':
1178+ return {'stationId':'999', 'stationIdToken':'345433', 'isCreator':True, 'isQuickMix':False, 'stationName':"Added Station"}
1179+ elif method in ('station.setQuickMix',
1180+ 'station.addFeedback',
1181+ 'station.transformShared',
1182+ 'station.setStationName',
1183+ 'station.removeStation',
1184+ 'listener.addTiredSong',
1185+ 'station.createBookmark',
1186+ 'station.createArtistBookmark',
1187+ ):
1188+ return 1
1189+ else:
1190+ logging.error("Invalid method %s" % method)
1191+
1192+ def connect(self, user, password):
1193+ self.listenerId = self.authToken = None
1194+
1195+ user = self.xmlrpc_call('listener.authenticateListener', [user, password], [], secure=True)
1196+
1197+ self.webAuthToken = user['webAuthToken']
1198+ self.listenerId = user['listenerId']
1199+ self.authToken = user['authToken']
1200+
1201+ self.get_stations(self)
1202+
1203+ def makeFakeSong(self, args):
1204+ c = self.count()
1205+ return {
1206+ 'albumTitle':"AlbumName",
1207+ 'artistSummary':"ArtistName",
1208+ 'artistMusicId':'4324',
1209+ 'audioURL':'http://kevinmehall.net/p/pithos/testfile.aac?val='+'0'*48,
1210+ 'fileGain':0,
1211+ 'identity':'5908540384',
1212+ 'musicId':'4543',
1213+ 'rating': 1 if c%3 == 0 else 0,
1214+ 'stationId': args[0],
1215+ 'songTitle': 'Test song %i'%c,
1216+ 'userSeed': '54543',
1217+ 'songDetailURL': 'http://kevinmehall.net/p/pithos/',
1218+ 'albumDetailURL':'http://kevinmehall.net/p/pithos/',
1219+ 'artRadio':'http://i.imgur.com/H3Z8x.jpg',
1220+ 'songType':0,
1221+ 'trackToken':12345,
1222+ }
1223+
1224
1225=== added file 'build/lib.linux-x86_64-2.7/pithos/pandora/pandora.py'
1226--- build/lib.linux-x86_64-2.7/pithos/pandora/pandora.py 1970-01-01 00:00:00 +0000
1227+++ build/lib.linux-x86_64-2.7/pithos/pandora/pandora.py 2012-03-11 20:43:36 +0000
1228@@ -0,0 +1,348 @@
1229+# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
1230+### BEGIN LICENSE
1231+# Copyright (C) 2010 Kevin Mehall <km@kevinmehall.net>
1232+#This program is free software: you can redistribute it and/or modify it
1233+#under the terms of the GNU General Public License version 3, as published
1234+#by the Free Software Foundation.
1235+#
1236+#This program is distributed in the hope that it will be useful, but
1237+#WITHOUT ANY WARRANTY; without even the implied warranties of
1238+#MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1239+#PURPOSE. See the GNU General Public License for more details.
1240+#
1241+#You should have received a copy of the GNU General Public License along
1242+#with this program. If not, see <http://www.gnu.org/licenses/>.
1243+### END LICENSE
1244+import logging
1245+import time
1246+import urllib2, urllib
1247+import re
1248+import xml.etree.ElementTree as etree
1249+
1250+from pithos.pandora.xmlrpc import *
1251+from pithos.pandora.blowfish import Blowfish
1252+
1253+PROTOCOL_VERSION = "33"
1254+RPC_URL = "www.pandora.com/radio/xmlrpc/v"+PROTOCOL_VERSION+"?"
1255+USER_AGENT = "Mozilla/5.0 (X11; U; Linux i586; de; rv:5.0) Gecko/20100101 Firefox/5.0 (compatible; Pithos/0.3)"
1256+HTTP_TIMEOUT = 30
1257+AUDIO_FORMAT = 'aacplus'
1258+
1259+RATE_BAN = 'ban'
1260+RATE_LOVE = 'love'
1261+RATE_NONE = None
1262+
1263+PLAYLIST_VALIDITY_TIME = 60*60*3
1264+
1265+class PandoraError(IOError):
1266+ def __init__(self, message, status=None, submsg=None):
1267+ self.status = status
1268+ self.message = message
1269+ self.submsg = submsg
1270+
1271+class PandoraAuthTokenInvalid(PandoraError): pass
1272+class PandoraNetError(PandoraError): pass
1273+class PandoraAPIVersionError(PandoraError): pass
1274+class PandoraTimeout(PandoraNetError): pass
1275+
1276+from pithos.pandora import pandora_keys
1277+
1278+blowfish_encode = Blowfish(pandora_keys.out_key_p, pandora_keys.out_key_s)
1279+
1280+def pad(s, l):
1281+ return s + "\0" * (l - len(s))
1282+
1283+def pandora_encrypt(s):
1284+ return "".join([blowfish_encode.encrypt(pad(s[i:i+8], 8)).encode('hex') for i in xrange(0, len(s), 8)])
1285+
1286+blowfish_decode = Blowfish(pandora_keys.in_key_p, pandora_keys.in_key_s)
1287+
1288+def pandora_decrypt(s):
1289+ return "".join([blowfish_decode.decrypt(pad(s[i:i+16].decode('hex'), 8)) for i in xrange(0, len(s), 16)]).rstrip('\x08')
1290+
1291+
1292+def format_url_arg(v):
1293+ if v is True:
1294+ return 'true'
1295+ elif v is False:
1296+ return 'false'
1297+ elif isinstance(v, list):
1298+ return "%2C".join(v)
1299+ else:
1300+ return urllib.quote(str(v))
1301+
1302+class Pandora(object):
1303+ def __init__(self):
1304+ self.rid = self.listenerId = self.authToken = None
1305+ self.set_proxy(None)
1306+ self.set_audio_format(AUDIO_FORMAT)
1307+
1308+ def xmlrpc_call(self, method, args=[], url_args=True, secure=False, includeTime=True):
1309+ if url_args is True:
1310+ url_args = args
1311+
1312+ args = args[:]
1313+
1314+ if includeTime:
1315+ args.insert(0, int(time.time()+self.time_offset))
1316+
1317+ if self.authToken:
1318+ args.insert(1, self.authToken)
1319+
1320+ xml = xmlrpc_make_call(method, args)
1321+ data = pandora_encrypt(xml)
1322+
1323+ url_arg_strings = []
1324+
1325+ if self.rid and includeTime:
1326+ url_arg_strings.append('rid=%s'%self.rid)
1327+ if self.listenerId:
1328+ url_arg_strings.append('lid=%s'%self.listenerId)
1329+ method = method[method.find('.')+1:] # method in URL is only last component
1330+ url_arg_strings.append('method=%s'%method)
1331+ count = 1
1332+ for i in url_args:
1333+ url_arg_strings.append("arg%i=%s"%(count, format_url_arg(i)))
1334+ count+=1
1335+
1336+ if secure:
1337+ proto = 'https://'
1338+ else:
1339+ proto = 'http://'
1340+
1341+ url = proto + RPC_URL + '&'.join(url_arg_strings)
1342+
1343+ logging.debug(url)
1344+ logging.debug(xml)
1345+
1346+ try:
1347+ req = urllib2.Request(url, data, {'User-agent': USER_AGENT, 'Content-type': 'text/xml'})
1348+ response = self.opener.open(req, timeout=HTTP_TIMEOUT)
1349+ text = response.read()
1350+ except urllib2.URLError as e:
1351+ logging.error("Network error: %s", e)
1352+ if e.reason[0] == 'timed out':
1353+ raise PandoraTimeout("Network error", submsg="Timeout")
1354+ else:
1355+ raise PandoraNetError("Network error", submsg=e.reason[1])
1356+
1357+ logging.debug(text)
1358+
1359+ tree = etree.fromstring(text)
1360+
1361+ fault = tree.findtext('fault/value/struct/member/value')
1362+ if fault:
1363+ logging.error('fault: ' + fault)
1364+ code, msg = fault.split('|')[2:]
1365+ if code == 'AUTH_INVALID_TOKEN':
1366+ raise PandoraAuthTokenInvalid(msg)
1367+ elif code == 'INCOMPATIBLE_VERSION':
1368+ raise PandoraAPIVersionError(msg)
1369+ elif code == 'OUT_OF_SYNC':
1370+ raise PandoraError("Out of sync", code,
1371+ submsg="Correct your system's clock. If the problem persists, a Pithos update may be required")
1372+ elif code == 'AUTH_INVALID_USERNAME_PASSWORD':
1373+ raise PandoraError("Login Error", code, submsg="Invalid username or password")
1374+ else:
1375+ raise PandoraError("Pandora returned an error", code, "%s: %s"%(code, msg))
1376+ else:
1377+ return xmlrpc_parse(tree)
1378+
1379+ def set_audio_format(self, fmt):
1380+ self.audio_format = fmt
1381+
1382+ def set_proxy(self, proxy):
1383+ if proxy:
1384+ proxy_handler = urllib2.ProxyHandler({'http': proxy})
1385+ self.opener = urllib2.build_opener(proxy_handler)
1386+ else:
1387+ self.opener = urllib2.build_opener()
1388+
1389+ def connect(self, user, password):
1390+ self.rid = "%07iP"%(int(time.time()) % 10000000)
1391+ self.listenerId = self.authToken = None
1392+
1393+ pandora_time = self.xmlrpc_call('misc.sync', [], [], secure=True, includeTime=False)
1394+ pandora_time = int(re.sub(r"\D", "", pandora_decrypt(pandora_time)))
1395+ self.time_offset = pandora_time - time.time()
1396+
1397+ user = self.xmlrpc_call('listener.authenticateListener', [user, password], [], secure=True)
1398+
1399+ self.webAuthToken = user['webAuthToken']
1400+ self.listenerId = user['listenerId']
1401+ self.authToken = user['authToken']
1402+
1403+ self.get_stations(self)
1404+
1405+ def get_stations(self, *ignore):
1406+ stations = self.xmlrpc_call('station.getStations')
1407+ self.quickMixStationIds = None
1408+ self.stations = [Station(self, i) for i in stations]
1409+
1410+ if self.quickMixStationIds:
1411+ for i in self.stations:
1412+ if i.id in self.quickMixStationIds:
1413+ i.useQuickMix = True
1414+
1415+ def save_quick_mix(self):
1416+ stationIds = []
1417+ for i in self.stations:
1418+ if i.useQuickMix:
1419+ stationIds.append(i.id)
1420+ self.xmlrpc_call('station.setQuickMix', ['RANDOM', stationIds])
1421+
1422+ def search(self, query):
1423+ results = self.xmlrpc_call('music.search', [query])
1424+
1425+ l = [SearchResult('artist', i) for i in results['artists']]
1426+ l += [SearchResult('song', i) for i in results['songs']]
1427+ l.sort(key=lambda i: i.score, reverse=True)
1428+
1429+ return l
1430+
1431+ def create_station(self, reqType, id):
1432+ assert(reqType == 'mi' or requestType == 'sh') # music id or shared station id
1433+ d = self.xmlrpc_call('station.createStation', [reqType+id, ''])
1434+ station = Station(self, d)
1435+ self.stations.append(station)
1436+ return station
1437+
1438+ def add_station_by_music_id(self, musicid):
1439+ return self.create_station('mi', musicid)
1440+
1441+ def add_feedback(self, stationId, trackToken, rating):
1442+ logging.info("pandora: addFeedback")
1443+ if rating == RATE_NONE:
1444+ logging.error("Can't set rating to none")
1445+ return
1446+ rating_bool = True if rating == RATE_LOVE else False
1447+ self.xmlrpc_call('station.addFeedback', [stationId, trackToken, rating_bool])
1448+
1449+ def get_station_by_id(self, id):
1450+ for i in self.stations:
1451+ if i.id == id:
1452+ return i
1453+
1454+ def get_feedback_id(self, stationId, musicId):
1455+ station = self.xmlrpc_call('station.getStation', [stationId])
1456+ feedback = station['feedback']
1457+ for i in feedback:
1458+ if musicId == i['musicId']:
1459+ return i['feedbackId']
1460+
1461+ def delete_feedback(self, feedbackId):
1462+ self.xmlrpc_call('station.deleteFeedback', [feedbackId])
1463+
1464+class Station(object):
1465+ def __init__(self, pandora, d):
1466+ self.pandora = pandora
1467+
1468+ self.id = d['stationId']
1469+ self.idToken = d['stationIdToken']
1470+ self.isCreator = d['isCreator']
1471+ self.isQuickMix = d['isQuickMix']
1472+ self.name = d['stationName']
1473+ self.useQuickMix = False
1474+
1475+ if self.isQuickMix:
1476+ self.pandora.quickMixStationIds = d.get('quickMixStationIds', [])
1477+
1478+ def transformIfShared(self):
1479+ if not self.isCreator:
1480+ logging.info("pandora: transforming station")
1481+ self.pandora.xmlrpc_call('station.transformShared', [self.id])
1482+ self.isCreator = True
1483+
1484+ def get_playlist(self):
1485+ logging.info("pandora: Get Playlist")
1486+ playlist = self.pandora.xmlrpc_call('playlist.getFragment', [self.id, '0', '', '', self.pandora.audio_format, '0', '0'])
1487+ return [Song(self.pandora, i) for i in playlist]
1488+
1489+ @property
1490+ def info_url(self):
1491+ return 'http://www.pandora.com/stations/'+self.idToken
1492+
1493+ def rename(self, new_name):
1494+ if new_name != self.name:
1495+ logging.info("pandora: Renaming station")
1496+ self.pandora.xmlrpc_call('station.setStationName', [self.id, new_name])
1497+ self.name = new_name
1498+
1499+ def delete(self):
1500+ logging.info("pandora: Deleting Station")
1501+ self.pandora.xmlrpc_call('station.removeStation', [self.id])
1502+
1503+class Song(object):
1504+ def __init__(self, pandora, d):
1505+ self.pandora = pandora
1506+
1507+ self.album = d['albumTitle']
1508+ self.artist = d['artistSummary']
1509+ self.artistMusicId = d['artistMusicId']
1510+ self.audioUrl = d['audioURL'][:-48] + pandora_decrypt(d['audioURL'][-48:])
1511+ self.fileGain = d['fileGain']
1512+ self.identity = d['identity']
1513+ self.musicId = d['musicId']
1514+ self.trackToken = d['trackToken']
1515+ self.rating = RATE_LOVE if d['rating'] else RATE_NONE # banned songs won't play, so we don't care about them
1516+ self.stationId = d['stationId']
1517+ self.title = d['songTitle']
1518+ self.userSeed = d['userSeed']
1519+ self.songDetailURL = d['songDetailURL']
1520+ self.albumDetailURL = d['albumDetailURL']
1521+ self.artRadio = d['artRadio']
1522+
1523+ self.tired=False
1524+ self.message=''
1525+ self.start_time = None
1526+ self.finished = False
1527+ self.playlist_time = time.time()
1528+
1529+ @property
1530+ def station(self):
1531+ return self.pandora.get_station_by_id(self.stationId)
1532+
1533+ @property
1534+ def feedbackId(self):
1535+ return self.pandora.get_feedback_id(self.stationId, self.musicId)
1536+
1537+ def rate(self, rating):
1538+ if self.rating != rating:
1539+ self.station.transformIfShared()
1540+ if rating == RATE_NONE:
1541+ self.pandora.delete_feedback(self.feedbackId)
1542+ else:
1543+ self.pandora.add_feedback(self.stationId, self.trackToken, rating)
1544+ self.rating = rating
1545+
1546+ def set_tired(self):
1547+ if not self.tired:
1548+ self.pandora.xmlrpc_call('listener.addTiredSong', [self.identity])
1549+ self.tired = True
1550+
1551+ def bookmark(self):
1552+ self.pandora.xmlrpc_call('station.createBookmark', [self.stationId, self.musicId])
1553+
1554+ def bookmark_artist(self):
1555+ self.pandora.xmlrpc_call('station.createArtistBookmark', [self.artistMusicId])
1556+
1557+ @property
1558+ def rating_str(self):
1559+ return self.rating
1560+
1561+ def is_still_valid(self):
1562+ return (time.time() - self.playlist_time) < PLAYLIST_VALIDITY_TIME
1563+
1564+class SearchResult(object):
1565+ def __init__(self, resultType, d):
1566+ self.resultType = resultType
1567+ self.score = d['score']
1568+ self.musicId = d['musicId']
1569+
1570+ if resultType == 'song':
1571+ self.title = d['songTitle']
1572+ self.artist = d['artistSummary']
1573+ elif resultType == 'artist':
1574+ self.name = d['artistName']
1575+
1576+
1577
1578=== added file 'build/lib.linux-x86_64-2.7/pithos/pandora/pandora_keys.py'
1579--- build/lib.linux-x86_64-2.7/pithos/pandora/pandora_keys.py 1970-01-01 00:00:00 +0000
1580+++ build/lib.linux-x86_64-2.7/pithos/pandora/pandora_keys.py 2012-03-11 20:43:36 +0000
1581@@ -0,0 +1,362 @@
1582+#credit: ZigZagJoe
1583+
1584+out_key_p = [0xD8A1A847, 0xBCDA04F4, 0x54684D7B, 0xCDFD2D53, 0xADAD96BA, 0x83F7C7D2,
1585+ 0x97A48912, 0xA9D594AD, 0x6B4F3733, 0x0657C13E, 0xFCAE0687, 0x700858E4,
1586+ 0x34601911, 0x2A9DC589, 0xE3D08D11, 0x29B2D6AB, 0xC9657084, 0xFB5B9AF0]
1587+
1588+out_key_s = [[0x4EE44D9D, 0xCCEEAB0F, 0xD86488F6, 0x25FDD9B7, 0xB0DE3A97, 0x66EADF2F,
1589+ 0xC0D3DCA4, 0xEE72A5FA, 0x54074DEC, 0xCBAD83AD, 0x4B1771A3, 0xD92AE545,
1590+ 0xB5FCE937, 0x26AD96D9, 0x5D615D68, 0xF2994B82, 0xE668D342, 0x61051D4C,
1591+ 0xCFB29CA4, 0x8B421D38, 0xDA3B4EB9, 0xD92D6A55, 0xF7D940C7, 0x99C4BC83,
1592+ 0xAB896E79, 0x77C7039B, 0x1215B24A, 0x0C0EBC0D, 0xE9F082B2, 0x6B7DFE9C,
1593+ 0x4A714E76, 0x91280D88, 0xA422A361, 0x3E674D4A, 0x6EBC2D42, 0x6838580B,
1594+ 0xBAE461AB, 0xE8FEDD17, 0xEFD6E5E0, 0x690D3E93, 0x32FADEB0, 0x1B99EE04,
1595+ 0xBE9FA7D9, 0x7997DFC6, 0xFD1B8025, 0x667B35D8, 0x2D909996, 0xFE487FF0,
1596+ 0x628BCFE1, 0xA534C620, 0x6644DEFE, 0x8BF9236D, 0xE943DD51, 0xF4615657,
1597+ 0x605D4F80, 0x2E02FC45, 0xD924D2D0, 0xFD4AB9E3, 0x5AEB18F0, 0x7A8D7C92,
1598+ 0x6CA40CA6, 0xD8AD4139, 0xCA5E7EC2, 0x69BE3C59, 0x554A4DD6, 0xBA474DD1,
1599+ 0xE113576B, 0xCB89A6BD, 0xF366EC0C, 0x876661AB, 0xD85E5381, 0x79A93327,
1600+ 0x5A4E5D92, 0xE3301F23, 0xF211DD61, 0x6F0140D0, 0xDBA134BF, 0x3C623008,
1601+ 0xD5FCE976, 0x6EDE648E, 0x814CF920, 0xB38878E1, 0x6232D49C, 0x2310373B,
1602+ 0xA8C6EBFC, 0xCD506842, 0x62BEF441, 0x1324C803, 0x69D1F137, 0x3907EE67,
1603+ 0x47967932, 0xC3C3F280, 0xC4B036B9, 0x5EC264B4, 0x9484AA3C, 0x5FEF9C53,
1604+ 0xC1B9030F, 0xE86C6BBA, 0x3AE49DAE, 0xBBAC421C, 0x54D06D99, 0xBA13A2B2,
1605+ 0x3132FA87, 0x2FDDB5E2, 0x4B751219, 0x5B59778F, 0xEFFA2E62, 0x3BD56164,
1606+ 0xE7EDFC1D, 0xCF4D5FDB, 0xC6310BDA, 0x0CAE8B8F, 0x53196C2F, 0xAC951D1F,
1607+ 0x32FD1D71, 0x7D9D5956, 0x2EA62C92, 0x9FA4A4C8, 0xE491DC41, 0x7E5F2507,
1608+ 0x4568300F, 0xF210AAA8, 0xB6980949, 0x017405E7, 0x5EBF3350, 0x44B863F6,
1609+ 0xDF96854A, 0xFA8A8390, 0x342BDFFA, 0x93096350, 0xCD0F0083, 0xBE295FDD,
1610+ 0x549AA9C9, 0x8554D31B, 0x2F2FE138, 0x30E8C78D, 0xED603733, 0x4B47F4C2,
1611+ 0x03D481DC, 0x8BE4479C, 0x9A307E98, 0x73CFC5DC, 0x71DE3DFB, 0x55DA2605,
1612+ 0x2CC97898, 0x13F0CC6F, 0x5F30FEE1, 0xF65D36D0, 0x99D05438, 0xB6A1DF23,
1613+ 0x2EA6EF9B, 0x12D3A110, 0xF1C89B1A, 0x522BAA1F, 0xE39AC7B3, 0xAFC153D1,
1614+ 0x2A565274, 0x51530B46, 0x1291387D, 0x15BC2B63, 0xA73AD01F, 0x13EBC4A7,
1615+ 0x849583D7, 0x4A9E1AE6, 0x430C9A05, 0xEB2A78FB, 0xFA3A817D, 0x6D1D7AE5,
1616+ 0xB99588F5, 0x6D2C571B, 0xF975441C, 0x1348927D, 0xB069BDE2, 0x0771A398,
1617+ 0x4B93EDCC, 0x3C167223, 0xC3BBCFDF, 0x40C406DA, 0x81C867B1, 0xEB20C3D2,
1618+ 0x2476ED54, 0xB581F042, 0x1160A8B8, 0xBCA1AD0F, 0xD8F18C9F, 0x708BC7C6,
1619+ 0x0579D83C, 0x29BAA2B8, 0x45B845EE, 0xA57F5579, 0xE52E4A8A, 0x48365478,
1620+ 0xC6CCBFB4, 0x2F53D280, 0x8E1FF972, 0xF4E02067, 0x3F878869, 0x5879FF3C,
1621+ 0x1EDFAB0F, 0xD4FE52E3, 0x630AC960, 0xABD69092, 0xFAA3BF43, 0xF1CA3317,
1622+ 0x9CFF48D2, 0x8FE33F83, 0x260C1DE3, 0x89DB0B0B, 0xF127E1E3, 0x7DA503FF,
1623+ 0x01C9A821, 0x30573A67, 0x8A567A2E, 0xE47B86CF, 0xB8709ADE, 0xB19ADD3A,
1624+ 0x46A37074, 0x134CE184, 0x1F73191B, 0xE22B39F6, 0xE9D35D3D, 0x996390AF,
1625+ 0xADBBCCDB, 0xC9312071, 0xD442107D, 0x0B50C70A, 0xB9B6CC8C, 0x60A51E0E,
1626+ 0xA1076443, 0x215F1292, 0x5A53C644, 0xEA96EA2E, 0xE9F3B4BC, 0xBA5F45D2,
1627+ 0x454B65D6, 0x2CF04D9C, 0x05EF1D0F, 0xCD1ABBEE, 0xE86697B0, 0xFB92F164,
1628+ 0xEBEDADBF, 0x69282B8D, 0x65C91F0D, 0x6215AB51, 0x87E7BDF6, 0xC663D502,
1629+ 0x6EF4864E, 0xDC3BDCC9, 0x97184DBB, 0xCD315EED, 0x64001E09, 0x6F7DE8CE,
1630+ 0x38435D03, 0x840B5C82, 0x23CDBC8A, 0x7FA0D4FB
1631+],[
1632+ 0xEBCBE20D, 0x09FADAEC, 0x98FF9F63, 0x16D0DFE1, 0x54B65FA8, 0x8C58D07C,
1633+ 0xEAACBEA0, 0xEA8BC5B7, 0xD343B8ED, 0x46D416FC, 0x0247DCBB, 0x527CA3F5,
1634+ 0x22DAF183, 0x6684CF7F, 0xA2D5D9F6, 0xC507E43B, 0x7B368AE6, 0xFC8179EC,
1635+ 0x47E959C4, 0xDADF15F2, 0x92E48145, 0xD9CFA8B3, 0x94F209E8, 0x10F93D6D,
1636+ 0x3BAAF7B5, 0x9E5009B4, 0xE7E66FD8, 0x10F6D58F, 0x1EAFFF4D, 0x0423FCE5,
1637+ 0xE860C60A, 0x7713B2B4, 0x7C5EEF7E, 0x430801CF, 0x46613A77, 0xFADEC916,
1638+ 0x58AB09B3, 0xEE05C51F, 0xD4C6331F, 0x9BCA1941, 0x15BF041F, 0xC3B04E8D,
1639+ 0x6CD037AF, 0x11C81E53, 0xB38393DF, 0xB1D07B52, 0x067D02F7, 0xA9E5798B,
1640+ 0x4E5C10A6, 0x790DD862, 0xDEA21AD1, 0x3C0C90BF, 0xB05D8240, 0xFEA81F59,
1641+ 0x832F19FF, 0x17190D1C, 0x03E07FDC, 0x43A6AEAC, 0xFE0C8A2E, 0x216813A6,
1642+ 0xF0428728, 0xC1D21DCF, 0x54109ACB, 0x68FB51BB, 0x3F5AEE69, 0x557FEA14,
1643+ 0x07965E16, 0x58E2A204, 0x6E765B0C, 0x3B8D920F, 0xDD712180, 0xDD0F67CA,
1644+ 0x37F9D475, 0x91815CCF, 0xC31A34BB, 0x8F710EF2, 0xF2DA2F82, 0x2A24931B,
1645+ 0x41CFF29F, 0x16C9BECF, 0x1AEB93FB, 0x090DF533, 0xC10D27B6, 0xF7EE2303,
1646+ 0xF82A0ED0, 0x57031132, 0x88AFF451, 0x574A8BFF, 0xF1ACA4F0, 0xDD556F49,
1647+ 0x90D7CF52, 0x4BCA4AA3, 0xC917557C, 0x4BB6B151, 0x52CD8251, 0x7C7ED836,
1648+ 0x3488ED59, 0xC50C6A0B, 0x675413ED, 0x6368583D, 0x98B61BAE, 0x1AF59261,
1649+ 0x46590022, 0xA4C70187, 0x4658F3EB, 0x80A61049, 0x8F120E7A, 0xBEAC09D8,
1650+ 0x195ACD49, 0x6BE1DE45, 0x6EF1E32D, 0xB8A4B816, 0xC18758B8, 0xCA7AD046,
1651+ 0xD475BFE1, 0xCC3AB8AF, 0x45AB9AD7, 0xC37C62AC, 0x9AAD7E2E, 0xB9D87862,
1652+ 0x28F3CD26, 0xA0577A0E, 0x75859ECE, 0x4A6E5B86, 0xE61E36B3, 0xA00E0CA4,
1653+ 0x3E2CC99C, 0x581DF442, 0xCE40B79B, 0x17BAB635, 0x73F1C282, 0x7C009CE0,
1654+ 0x1A8BBC5A, 0xBBB87ECD, 0x162ED0AC, 0x8DB76F5A, 0xD5AD1234, 0xD0D7A773,
1655+ 0x41CBDEFB, 0x7197AFF4, 0x5C60E777, 0x5D9141D4, 0xF43D5211, 0xA4F064D9,
1656+ 0x40C13CB3, 0xE9DE900D, 0xBF733203, 0xC00F2E89, 0x095D476F, 0x277A825D,
1657+ 0x4B6A61D3, 0xFF857740, 0xE34705C0, 0x65F8372C, 0x497AC161, 0x1231CA4A,
1658+ 0xFB385036, 0x24B36150, 0x6CB9FA2D, 0xCBAB3399, 0x3832629E, 0x1BB815EE,
1659+ 0x6AAA74C7, 0x8FFA22B8, 0x64093F28, 0x973BBA95, 0x831A8195, 0x48B2923D,
1660+ 0x9680C36E, 0x16BA5344, 0x1F190542, 0xBCB0DFCC, 0xCCC24623, 0xFA503EAD,
1661+ 0x7189956C, 0x80B3C715, 0xFA9F4685, 0x36CF833E, 0x19A53ADF, 0xA5A4BD79,
1662+ 0x187ADC8D, 0x8AEFA6B6, 0xF64FF62A, 0x88A590BA, 0xE30C75BE, 0xA3BFBCC7,
1663+ 0xAC669722, 0xC4AEAFF2, 0x822DC5FA, 0xAA73C1D5, 0x422EFD93, 0x946FE915,
1664+ 0xEF623E46, 0x24395A31, 0xF28FF488, 0xB4D7CA7E, 0x27703504, 0x9F390B73,
1665+ 0xA6999558, 0x8AE04A20, 0xDD6FE7DB, 0x55963137, 0xCFEF70BB, 0x708CA677,
1666+ 0x804CF78B, 0xD5AC1CA2, 0x88D7CCFC, 0x5FE056DF, 0x25B390EA, 0x11550845,
1667+ 0x15A58C0B, 0x7C3530A3, 0x24550544, 0xD395EDD0, 0xEB046782, 0x7E3CCE71,
1668+ 0x25A8640C, 0x96A955DE, 0x4BF7614E, 0x3014FD08, 0xE2AC1E2E, 0x7D3AB3C3,
1669+ 0xB63CB59C, 0x9E92D401, 0x859B2C44, 0x1F893940, 0xEE81B9BB, 0x7F430589,
1670+ 0xAF2CC2EC, 0x0FA273E2, 0x3E5C6FAA, 0xE580E6A9, 0x64D73FE6, 0xE7C5A28A,
1671+ 0x99B760BC, 0xC0FCBA71, 0xDB521C76, 0xDBC7C1F8, 0x4968CF63, 0xD4928D17,
1672+ 0x6DBBCC5F, 0x681EB668, 0xC326CEB9, 0x7C6B0EBB, 0xF071C193, 0x5CC6A08C,
1673+ 0xFA4B95EB, 0x0BED345D, 0x16854F61, 0x22ECDDA9, 0x77335F2D, 0xCC016EE5,
1674+ 0x4CE1D7F6, 0x32B1409B, 0x2197B046, 0x73CD94F3
1675+],[
1676+ 0x56D997EE, 0x92FA3097, 0xA1AF0D9D, 0x11FCBB9C, 0xA2673993, 0x3860F1CE,
1677+ 0xB2B70A39, 0x5BC90183, 0xBFA62ADC, 0x58E257F2, 0xD221A704, 0x0A876CE4,
1678+ 0xD7B0FCA9, 0x80D3D874, 0x696A6CFD, 0xB989EFF1, 0xEAA5F132, 0xA29ECB5D,
1679+ 0x674B7380, 0x0BAD725F, 0x59D55508, 0x8DB40E2A, 0x003EBD12, 0x871AD00E,
1680+ 0x7ACE20A9, 0xE670BA85, 0x43D53997, 0x79461049, 0x806C102B, 0xB21337BD,
1681+ 0x791483E8, 0x6ECA44EA, 0x959CF50D, 0x8D87166D, 0xFA939DF8, 0xB0E519DE,
1682+ 0x8C069B44, 0x0A47F71A, 0x8D7AD1CA, 0x24E6FEDD, 0xCEF2173E, 0xB46A57F1,
1683+ 0x9DD9C775, 0x549B2E5D, 0x67A37485, 0x38F7FC18, 0xA269F5A1, 0x1B04F14E,
1684+ 0x4550E006, 0x8F5E0E14, 0x5EB9992C, 0x88D780A5, 0x334FFA1E, 0x473A75C1,
1685+ 0x9D96E913, 0x7DB16188, 0xE699B708, 0x88D087FA, 0x06E44D4E, 0xCB29E519,
1686+ 0x68529AB8, 0xBC74B1FD, 0xDA074140, 0x557B9936, 0x80BB557E, 0x42522D24,
1687+ 0x909E967F, 0x7D578A28, 0x7F78EBD7, 0xB793DC4B, 0x08498F07, 0x8A77FC08,
1688+ 0xFFFDA0C1, 0x2ECA4123, 0xB63861DC, 0xD909606E, 0x29A545E4, 0xB37539D6,
1689+ 0x292FAC93, 0xBDC6C4F3, 0xDAC7CE05, 0x68201C9D, 0xE08DC67A, 0xE0FB0327,
1690+ 0x17554D62, 0x636D9040, 0x0612D29F, 0xAF250475, 0xB8961740, 0xBE3E4408,
1691+ 0x3AF166E6, 0x3B16CC87, 0x2DC77141, 0x3C874024, 0x0E409623, 0xC7576B7A,
1692+ 0x35CAF7DA, 0x0AA9AED6, 0x6C5F2CC0, 0x23AAB90F, 0x74A41C51, 0xDAA1B557,
1693+ 0x412EC422, 0xD9E55CF0, 0x7F6A804E, 0x9256A133, 0xF3FD2639, 0x42C9A68A,
1694+ 0xB20588E4, 0x33339C04, 0xCB9B9300, 0xCCA198E9, 0x849A2FFF, 0xF2B71118,
1695+ 0xD27C41DF, 0xF1453CD9, 0xEB94D640, 0x9CE6A69E, 0x1561C1BD, 0x8A8F7E07,
1696+ 0x1FA3989C, 0x601C3440, 0x95DE5ED8, 0xB2F2AE94, 0x831BA7C3, 0x6831E3ED,
1697+ 0x5C5C0BD8, 0x628A0E89, 0x2726D7A3, 0x82B6E434, 0xB729A5C7, 0x5AB563C2,
1698+ 0xA4119CE6, 0x4459E404, 0x0B3E858A, 0x080C2DF9, 0x6EBE3FFB, 0xC1D64BCE,
1699+ 0xB2C90336, 0x998AE507, 0xC152879A, 0x31B99F23, 0x37769978, 0xF5C78668,
1700+ 0x2B954114, 0x54169F1A, 0xBF9E6E7D, 0x41BEBC39, 0x35BC63BD, 0x77E91F12,
1701+ 0x89909690, 0xCB17B79D, 0xCCBF4A25, 0x3E5E653E, 0x3B4531F1, 0x31AF6109,
1702+ 0x027DC03F, 0x334AE2A7, 0x8A685A70, 0xD82C335D, 0x7D73C193, 0xF0311C79,
1703+ 0xE8091EAF, 0x64B12983, 0x85CEB9A6, 0x402AB7C9, 0xA95E4546, 0x85CE4FD7,
1704+ 0x21968004, 0x0846E117, 0xD290B888, 0xCE2888FC, 0xE2F318F1, 0x89B189DD,
1705+ 0x7A2D73BA, 0xE28937E5, 0x6D857435, 0x8A2F05FA, 0xA19B966F, 0x37EF297F,
1706+ 0xC50696F5, 0xA7C3DE1A, 0x988D3850, 0x24007793, 0xB94C792C, 0x4DA98736,
1707+ 0xA04EB570, 0x4AA44F84, 0x7124E7C6, 0x13B9026E, 0x27AC2D15, 0xFBB9AD93,
1708+ 0x2F94AA1C, 0x98587A3D, 0x9C9DB996, 0x7E3487D5, 0xA819272C, 0x32AA5E43,
1709+ 0xE0DB72F5, 0x4DB4853C, 0x7350C7EC, 0xB1626C73, 0x07130A5F, 0xC3DAA529,
1710+ 0xD6422735, 0x8559200D, 0x1046E85C, 0x326CFB54, 0xAD42DB6A, 0xAE4CC364,
1711+ 0xA49F5718, 0xF472F8A0, 0x3C002484, 0x013067BE, 0xC88A1317, 0x4C3C209B,
1712+ 0x7CBB8BB3, 0x41FB8DAF, 0x236591B3, 0xDC974A45, 0x8639E738, 0x97C38B19,
1713+ 0xD7FF5725, 0xE7094458, 0xF28B223F, 0xF73C878B, 0x7F7502D9, 0x52F7FD09,
1714+ 0x4A661B36, 0x62814D8E, 0xBBDD1D16, 0x002598D9, 0x56B17A84, 0x87A331B7,
1715+ 0x6C2898C2, 0xAFCBA795, 0x4EFEE9AE, 0xEAE3A4F1, 0xC3D4D9CD, 0x5EFD7C32,
1716+ 0xB1B31E64, 0x95245686, 0x21A7DA12, 0x7155E041, 0x7362B475, 0x36486BD5,
1717+ 0xA97E5D7C, 0x8871303B, 0x93199D52, 0x246F919E, 0x5A581359, 0x6AE746DD,
1718+ 0x3CA9098C, 0x56DA5714, 0xAA0B674A, 0x08C89A5D
1719+],[
1720+ 0x7DD47329, 0xF270A704, 0x71BF31DA, 0x3B57772E, 0xFBE90F4B, 0x87FC23F6,
1721+ 0xCF413D71, 0x4FFEA8EC, 0xEFBA20C2, 0xEB53E0C1, 0xFFE7633E, 0x854E28E8,
1722+ 0xFBFFE904, 0x8A7841BE, 0x94E99960, 0xA3E69064, 0x365C57AB, 0xBEE976CC,
1723+ 0x596B94C2, 0x8C5E90E2, 0x074B3C54, 0x89B5E926, 0xDF192C71, 0xAF631D85,
1724+ 0x67A8EDEC, 0x24BE4919, 0x81EB9C8A, 0xFDB13471, 0xEE61A4A1, 0x1EE368DE,
1725+ 0x8C55C255, 0xD273A000, 0x12A24DCD, 0x22A6708E, 0x6BB4C19A, 0xF2599FDE,
1726+ 0xE84B8A95, 0xDD578159, 0x1F666F1E, 0x483BBCE2, 0x46E340BA, 0x8B7D6490,
1727+ 0xE65BD77D, 0xA50F2282, 0x4B455D23, 0x9B5D486B, 0x95CEA1A3, 0x4B7A484A,
1728+ 0x2E16BE82, 0x096A8E05, 0x5494AF5E, 0x1EBA1525, 0x84FDB773, 0xD47CE143,
1729+ 0xC1254007, 0x1CE4CBBE, 0x8049402D, 0x114D7B59, 0x64D760AD, 0x6AEECE49,
1730+ 0x83DC9867, 0x36FF9C28, 0x6FFB709D, 0xB22F7301, 0x6E6CAD92, 0x0001F394,
1731+ 0xB560CDE7, 0xEA02FDDA, 0x40609266, 0x7F599B81, 0x1B8FD59A, 0xA562FF5C,
1732+ 0xA01750C6, 0x78A35114, 0x789F8094, 0xF46594B8, 0xFF3A12BE, 0x29DDEB50,
1733+ 0xE3CF5A2C, 0x8E440B20, 0xBFBF3DD8, 0x649DB58A, 0xC48A8A51, 0x97F139C3,
1734+ 0x0BB07943, 0x548C90BD, 0x8153FCF1, 0x13098DEF, 0x812EA492, 0xFC0AC487,
1735+ 0xC5EAE50A, 0x7A02481B, 0xC75279D7, 0x59CBC149, 0x6AB39416, 0x39331E1A,
1736+ 0x233BE50B, 0x7F09C1BD, 0xECC11E6E, 0xA6647D03, 0x06BD33AD, 0xD717C795,
1737+ 0xE07E2D67, 0x2688D40B, 0xE23E349F, 0x8C7F559E, 0x3BA698C2, 0xEB5FCD3C,
1738+ 0xE94E2DE5, 0x3C0FE4DF, 0x55454456, 0x12731019, 0x21AF58D7, 0x2555CE03,
1739+ 0x17BBC647, 0xF0C66012, 0xE02D87F8, 0x340DB0CE, 0x72A3766F, 0xE2724C51,
1740+ 0x3636A5FD, 0xC226C419, 0x1A5F0464, 0xA543817B, 0x0B850A8D, 0xD5A6F88B,
1741+ 0xCE3715B8, 0xB73918A2, 0x6AC92E61, 0x0FCD43EA, 0xF559EEDE, 0x3482C340,
1742+ 0x447D9924, 0xF95D6EB2, 0xB22E6C6F, 0x935740D2, 0x7C04B228, 0xB90ABD1A,
1743+ 0x8D9D01C9, 0x43B63B2D, 0xE0EBEDAC, 0x7C219604, 0x8479756F, 0xB67355FE,
1744+ 0xA056539B, 0xAF1D5A02, 0x6660BB07, 0xD1A0593C, 0x5AABEF47, 0x73802FC5,
1745+ 0xAADB5251, 0x92556CFF, 0x5BF44BDC, 0x4DC171CF, 0x1EE4E879, 0x516BC896,
1746+ 0xCDBB21EA, 0xF513BD04, 0x94267720, 0x6B29DAC1, 0x1D778D67, 0x9625EA42,
1747+ 0x23946BBC, 0xF23D2E0A, 0x001C2CFB, 0xEF121203, 0x963A0C2B, 0x1AAE960B,
1748+ 0x13F2D588, 0xAE6BFEAE, 0x77424AC8, 0x1E0B2A9F, 0x9074C626, 0x9BCDE764,
1749+ 0xF8539561, 0xC14A5B05, 0xD88D9FAE, 0x2C5C4C67, 0x2C63BAE5, 0x99CCF4CB,
1750+ 0x3563CA53, 0x0CE7A114, 0xCB8938D3, 0x7C61537F, 0xE717A35E, 0xB69D3832,
1751+ 0xE47931C3, 0xD5C9D409, 0x355E0B97, 0xC60EB27E, 0xB17978F6, 0x77CCBCEA,
1752+ 0x85AEFA12, 0x59DFA376, 0x36DB61D2, 0x96832915, 0xCC4411F3, 0xB81F1EF9,
1753+ 0x2C54E5E1, 0xDD3CE944, 0x02D92E29, 0x1D4795B1, 0x27F900B0, 0x97A516CC,
1754+ 0xA2DB2CC8, 0x3125B863, 0xBF44DC77, 0x211A0226, 0x3A98AB5F, 0x2612396E,
1755+ 0xA1BEF080, 0x708B7433, 0x5D457230, 0xED03C4EB, 0xA84D73AE, 0x89D5582D,
1756+ 0x95F0C7FA, 0xEF51B8C9, 0xF9DCA97D, 0xCB2E49FD, 0xC12B4ADD, 0x611C9AD5,
1757+ 0x35D1D7CE, 0xA77E13BE, 0x207C1B88, 0x0AC289D4, 0x4B553B81, 0x4940991A,
1758+ 0x23D9F9D5, 0xDFD93925, 0xB924E9D2, 0xBFA61D10, 0x861FDF0F, 0xBBD30811,
1759+ 0x953CE5DA, 0x92B48334, 0x5E5B44FC, 0x5B949533, 0x31A5D165, 0x99339641,
1760+ 0x2737671F, 0x512EB25C, 0x54408346, 0xA090A7FE, 0x1D9CA5F9, 0x470C19E4,
1761+ 0x720F936E, 0xA8628453, 0x364D29CC, 0x42E472DF, 0x54949196, 0x6C7C46EA,
1762+ 0x12797418, 0x7D775295, 0xC46A7C32, 0x69CE8560]]
1763+
1764+in_key_p = [0xD825B592, 0xA73D0737, 0x3F7C28AE, 0xF91F7116, 0xEF6B001E, 0xD38524FD,
1765+ 0x547F01EC, 0xE9C9DF7C, 0x25DE8E97, 0x9F45FF21, 0x87479245, 0x74A5FE7A,
1766+ 0x9D4A1EDB, 0x7EEFCA76, 0x58B117C2, 0x0E7F33BE, 0x1C840B7A, 0x15EEE858]
1767+
1768+
1769+in_key_s = [[0x4A0BABBD, 0x46B75CE1, 0xDA4E60D7, 0x62DD8F22, 0x668D7291, 0x70330C71,
1770+ 0x8ECE2964, 0xD4D1D24F, 0x2247D3FF, 0x33BD2D83, 0x2A7DF912, 0xB9E711C8,
1771+ 0x0E6544DB, 0x3A45B663, 0x8A9AB3DB, 0xD1A18732, 0x12700956, 0x25D9E559,
1772+ 0x9A425C80, 0xCE51C1BD, 0x73D8583D, 0x22B93DE3, 0x3FF39FAF, 0x3B2E11F8,
1773+ 0xA29731D0, 0x4F2C315F, 0xB46182CA, 0xB28C562E, 0xC37A96D9, 0x8AF05087,
1774+ 0x745FB822, 0xEBC3F308, 0x2F40C980, 0xE16657A1, 0xBDECD15A, 0xC9948113,
1775+ 0xD13F34A9, 0x526A159F, 0xF5982C49, 0x55E2220D, 0x0EAA7A36, 0x90771A3B,
1776+ 0x852D659D, 0xF42B33CE, 0xBC55AB70, 0xD58495F9, 0xA01A146A, 0x146E0960,
1777+ 0xDCC3EAFB, 0x45561BA0, 0x978C2955, 0x040E66A1, 0x54A12BDC, 0x86EB78C4,
1778+ 0x317B7AB9, 0xC1131F78, 0xBC67C341, 0xF8850A30, 0x26EBAEB3, 0xA21BAB96,
1779+ 0xF252C98A, 0xE8B913F2, 0x9237DBF2, 0xC549F051, 0xB37EBAEB, 0xE1BBC775,
1780+ 0xC1655D75, 0x630A0946, 0xAEE49047, 0x4EC591BF, 0xB155B99A, 0x2EEC5B73,
1781+ 0xDE713B06, 0x12A634B7, 0x13CE8AB7, 0x938D6C99, 0x3596EEE0, 0x500F9066,
1782+ 0xF6F80717, 0x40D95E18, 0xC1D47A2E, 0x8DEE1E05, 0xE37E36A2, 0x54C7FFD3,
1783+ 0x4AFFB9CF, 0x214CF5B8, 0xAB68B547, 0xB7CC1821, 0x5C7DA859, 0xB2DA7DDF,
1784+ 0x846494FA, 0x8CB04D15, 0x1A4A71A2, 0x5FF08D8A, 0x8768DF65, 0x924E2FE3,
1785+ 0x04B8109A, 0xE418742A, 0x58CD8113, 0x0EF7AAC9, 0x92B0CF79, 0xB01FBED2,
1786+ 0x4DE3CE22, 0x6B779E20, 0x1CF126CF, 0x1789A794, 0xA8463DA6, 0xF3CCE7AC,
1787+ 0xD225730A, 0x2D1B121E, 0x3F2C0850, 0xE327010B, 0x56D95881, 0x8639B3AE,
1788+ 0x01B3CAEB, 0x1F6F9304, 0x7D4BF367, 0xE4EC20D9, 0x6D05AA51, 0x882B710D,
1789+ 0xFC409837, 0x65E212C6, 0xA4C94A87, 0xB11A90AE, 0xB575256C, 0x9C12AF15,
1790+ 0x682F72EA, 0xC4C8CE09, 0x43F6C164, 0x4BD015D9, 0x9B9B1FCE, 0x9C108425,
1791+ 0x40A8DF10, 0xB0BC8633, 0x974EBB7A, 0xDE6FCE1C, 0xE005634D, 0x708C699E,
1792+ 0x7D5BFC2F, 0xBC8D5BD0, 0xF0CCE026, 0xB5F50E04, 0x84124BC7, 0x9A34780C,
1793+ 0x34E8E954, 0x64E165A3, 0x3007959D, 0x8DA44C08, 0xAA10D841, 0x7E876AE0,
1794+ 0xB6020431, 0x47F4EA6A, 0xF4825054, 0x16C42BDB, 0xD1965562, 0x3777B610,
1795+ 0x74B5741E, 0xD60C40D9, 0x3A5EF183, 0x8716250D, 0x91797C4D, 0x01A28965,
1796+ 0xF9161DBC, 0x44A9E602, 0xABECEF05, 0xAA90BA96, 0x76607DBD, 0x48BF5BB9,
1797+ 0x25840A8A, 0xD1AC56A6, 0x6F9B4274, 0xC56846D3, 0x88CF0D6B, 0x324EEFC0,
1798+ 0xEA8304DF, 0xA27E4A81, 0x9AAB617B, 0x3301C499, 0x50E8FF17, 0x67B48F3F,
1799+ 0x5AFB3B82, 0xF316F7D3, 0xBA425BB4, 0x083E12F4, 0x1CFAB3DB, 0x78C088A9,
1800+ 0x6EC83605, 0xB46D30E9, 0x7A45D4FE, 0x1A31BAD8, 0x29242D72, 0x71EFF75F,
1801+ 0x6DCFAC98, 0x79A1261A, 0x3865EEF5, 0x619461A3, 0xC25A0231, 0x750472D2,
1802+ 0x7B714CBD, 0x7E57A0B8, 0xB3A7319F, 0x3FDDBEAB, 0xC4A71E85, 0x44D38329,
1803+ 0xA21B0851, 0xBD44AE96, 0xFA3AF1D0, 0x3D6766B9, 0xA6884E85, 0xD3C9A0C0,
1804+ 0xCAE680FC, 0x41C31A2C, 0x091EDF2D, 0x9EA4E645, 0x1CA034CC, 0xE10DBA18,
1805+ 0x922AA9DC, 0x2DC318CB, 0xC49D6F10, 0xEEE7A768, 0x8173DD96, 0x96552598,
1806+ 0xAB254BF2, 0xA5BFDEE8, 0x898E4896, 0x049DCDBA, 0x57BDCC74, 0x0F3399A4,
1807+ 0x63909424, 0x6FEF1E46, 0x36A47384, 0x51F036DE, 0xAE707EA0, 0xDA83353C,
1808+ 0xFB9E7298, 0xF9DCF17E, 0x4F6FEF42, 0x2A41B840, 0x7DCD2662, 0x381FEF90,
1809+ 0x1773E656, 0xF6029EC1, 0xB1592A3B, 0x10A656D4, 0x6DDB2E61, 0x8E02EDFE,
1810+ 0xE32782EA, 0x973D37F5, 0x1594C629, 0x8B73C632, 0x1680A138, 0x3F5B1228,
1811+ 0x8C29E932, 0x49D384A2, 0x02794495, 0x118FF2E5
1812+ ],[
1813+ 0xAE22E435, 0xBF7344EB, 0xFBF3614E, 0xB52AF67A, 0x15EC7E8B, 0x0615CD74,
1814+ 0x310931B6, 0x069D7297, 0x455368A0, 0xEEB001F7, 0x0E925151, 0xDEB13D62,
1815+ 0x939071EE, 0x0BD2D2D2, 0x22AFF084, 0xF3D5BA75, 0xE8B78473, 0xE164B152,
1816+ 0x8DD31804, 0x1AABFA34, 0xF1837C1B, 0xF50D14CB, 0x44F64DC6, 0x25C4CAC7,
1817+ 0x59A2FE4B, 0x0C9E6026, 0x96A8908C, 0x49C0551E, 0x6C952FD7, 0x24BAEAA2,
1818+ 0x6B9DC320, 0x727310AD, 0x09873DE7, 0xA815543B, 0x106743C6, 0xF4CDF032,
1819+ 0xFB0208AF, 0xD58B37B0, 0xB8916BBB, 0xF31DC932, 0xB2273875, 0x94BFDD76,
1820+ 0x2165879B, 0xA652B81E, 0x584698E4, 0xB42AF09E, 0x4DFF38F3, 0x29845BD7,
1821+ 0x14F3B627, 0x5ECA5580, 0xFBDDDBB3, 0xD8392CB3, 0xF6B4FBE5, 0xFDCF2025,
1822+ 0x31B61951, 0x9E588D9D, 0x25A07E35, 0xDCA8D140, 0x8C973391, 0x70C83894,
1823+ 0x1B2F0DC5, 0x248E1EA0, 0x0760336A, 0x8F8BB40F, 0x0D1A660E, 0x0D1640F6,
1824+ 0xA32B1F16, 0x3809890F, 0x82F7F0EA, 0x6CCC940F, 0xB9F2C42E, 0xB661A456,
1825+ 0x5901ECB5, 0x76B92DF6, 0x21DB5718, 0xE23F3AED, 0xF94A8A63, 0x89F64664,
1826+ 0x7335C780, 0x9FF900EB, 0xAC5D42D8, 0xF6049DC4, 0xB99BED2A, 0x3FE7D758,
1827+ 0xDB69E89D, 0xCC9C3333, 0x382BB23F, 0xB07FB181, 0x9B20B744, 0x4E4ECE5D,
1828+ 0xED8E3B98, 0x4AB66852, 0x78BCC661, 0xC1D8A28C, 0x71E08EB0, 0x9ECAE411,
1829+ 0xA4FD636D, 0xA064969A, 0x185B05EC, 0x54896D93, 0x8110F40B, 0xD4D7E57C,
1830+ 0x1E928C46, 0xF9CE1F29, 0x3E72FB51, 0xA79C044C, 0xADB9445E, 0x4AA394F9,
1831+ 0x129534D2, 0xDF211944, 0x6267B4DA, 0xF534A6F8, 0xD45D2B3E, 0x5F1894E4,
1832+ 0xCC27E376, 0xFE866D4F, 0x956E7C8E, 0x0A630CE7, 0x4734673B, 0xBF8AB871,
1833+ 0x432C515F, 0xA2D7B37F, 0xCF48F9F8, 0x31F7209B, 0xB6E792D9, 0x5E136600,
1834+ 0xF583A945, 0xAAD4705F, 0xE8E30373, 0xA5E5AB27, 0xCAC6FA4A, 0x48F3A109,
1835+ 0xB21A697B, 0x45B9B7F0, 0x2E7BB193, 0x9603F1BB, 0xE0BA7A40, 0xCE7EB62E,
1836+ 0x334B24C5, 0x0261817E, 0x4EB792AB, 0xB85BC2B6, 0x6D47D0D9, 0xD2A1433B,
1837+ 0x7523942B, 0x39BF5CE7, 0x72223B25, 0xC28F0913, 0xC01CFE44, 0x500F27E5,
1838+ 0x055379CB, 0x786C7B08, 0x7C75295A, 0x6294932D, 0xF1B01EF0, 0xE79358F6,
1839+ 0x89C4E49F, 0xE7F73D07, 0xEF6FE437, 0xF2CDA93F, 0xB0994E68, 0xADF6AC19,
1840+ 0x00925578, 0x06454AF8, 0xCE4628AB, 0x9F983E8C, 0x393E59A4, 0x3896C188,
1841+ 0x6A0A5EF5, 0xBA378BF3, 0xD27590ED, 0xCFD20FF9, 0x1728237A, 0x8DABD973,
1842+ 0xE22C9842, 0x29A1264B, 0xFFC8569C, 0x17DDEBAA, 0xF9D62958, 0x11028197,
1843+ 0xC42BD84F, 0x6F81854B, 0x061FD297, 0xD26BA792, 0x63F71BAA, 0xAD36BDCC,
1844+ 0x06262269, 0x5711A1F5, 0xAA746143, 0x75242F8D, 0x5817A84D, 0x639ED1B6,
1845+ 0x6D18D6C4, 0x4FFEFE01, 0x0689F6D4, 0x5997D712, 0xD761CF5B, 0x9211216C,
1846+ 0x5D8006F3, 0xDDE24FEA, 0x1E60F66B, 0x18D41ABC, 0xDC72B4CB, 0x9FBCBA7D,
1847+ 0xEAA807C5, 0x700B7E32, 0xCF4173D6, 0xE354A49E, 0x12EB5466, 0x63DCC4E4,
1848+ 0x1DC24D6E, 0x1C9F2A83, 0x3DF6F311, 0xFFA91651, 0x3A1A9645, 0x30D568A4,
1849+ 0xC51A55B4, 0xEE032227, 0x0F0A28AC, 0x4516C9B2, 0xD7323F98, 0x547C3B78,
1850+ 0x5BCA33E1, 0xF28A15C2, 0xDFFEFAAD, 0xD6E87BBD, 0xE820C21D, 0xFB1C97C3,
1851+ 0xE40869F7, 0xD4A167F0, 0x79A2EFFC, 0xD67A0D93, 0xE862FF06, 0x54BCFB17,
1852+ 0x3420A670, 0x9219126B, 0x54930637, 0x55E84ED3, 0xA8C4D3C6, 0xFC2C2BD2,
1853+ 0x73B116F0, 0xE7A2900C, 0x801E1978, 0xC6741B16, 0xFADB0E61, 0xB5315FC8,
1854+ 0x76D1E2BD, 0x2C3452EB, 0x9F42E977, 0xBBE3B7FF, 0x3E5A0B7F, 0x4E1E865A,
1855+ 0x24D40A06, 0x202F6EDB, 0x9BAEBAFA, 0x6272D57D
1856+ ],[
1857+ 0x72B35463, 0xF3AD17A4, 0x5631E69E, 0x407BBD75, 0x3508A084, 0x88AAF1CE,
1858+ 0x4B04905F, 0x2F2FEBE0, 0x09AA8992, 0xDE1EA57A, 0x1FDBFAFD, 0x3827C109,
1859+ 0x764DFD38, 0x5C4A2FB0, 0xA1E90138, 0xF4814033, 0xD086FC2A, 0xB388B360,
1860+ 0xFEA5D332, 0x47202A75, 0xCD25CB9A, 0xA351B744, 0xB7F4A6B6, 0xEE1658C0,
1861+ 0x5016807B, 0x12574F7A, 0xB517B43C, 0x741262F5, 0x6F00F349, 0xDAA44579,
1862+ 0xE58B8090, 0x6FC04F54, 0x8451D14A, 0x84C352B4, 0x710C2858, 0x0FE8F84C,
1863+ 0xAF5E81F8, 0x0865497B, 0x552638F9, 0x3109EBED, 0x13C29D74, 0x276CB543,
1864+ 0x8CA65E96, 0xE983C198, 0xB85CDC58, 0xB522FBCD, 0x21506FF9, 0x8C703690,
1865+ 0x322A3026, 0xB87B9BA5, 0x49FF5C4F, 0x3B3E927D, 0xCC9E8227, 0x064CEA69,
1866+ 0x88F21343, 0x4EDCB6EC, 0x0F7DA6B8, 0x2ABCEF9C, 0x86866163, 0xBF4B1B4F,
1867+ 0x94E0E0E7, 0x8CFE5DF6, 0x4A8D073B, 0x43916370, 0xBCED0841, 0x2D8EF63A,
1868+ 0xFEFC6AE8, 0x7A3284CD, 0x51FAE69D, 0x34996231, 0x15599997, 0x5A8D2DF2,
1869+ 0xDC8F5265, 0x2B23915D, 0xAC578846, 0x0B9BC885, 0xE5AEF368, 0xE352A2EB,
1870+ 0x67330720, 0xFD176A18, 0x78BB578F, 0xB5422A51, 0x68F35707, 0x47570020,
1871+ 0x8A4F5321, 0xB6774AB5, 0x06CE136C, 0xADED4447, 0xAE8A1FEE, 0xB97BAB2D,
1872+ 0x95B81706, 0x493E002C, 0x0D9DC930, 0xF70BF46D, 0x229C90F8, 0xAF450341,
1873+ 0x12B799C0, 0x98656508, 0x8C8D6417, 0x491D1ED4, 0x1AF33256, 0xF3275DE1,
1874+ 0x08454958, 0x321A8D4E, 0x4F516FE5, 0x1D076087, 0x94311B59, 0xED369CB9,
1875+ 0x4FF94F65, 0x87129FB4, 0x8D065220, 0x7B11F2D5, 0x976138B0, 0x030962FB,
1876+ 0x8E68ED5F, 0xE26B5272, 0x045BCBEB, 0x9350F121, 0x522DDFEB, 0x1EEB70B2,
1877+ 0xCC2EABF8, 0x22BA4ED7, 0x30B0C3BC, 0x64DF8A4C, 0xF58B3FF2, 0x56B3BBE6,
1878+ 0x3E71A700, 0x65D765C7, 0x87A50635, 0x9156084B, 0x85CD7CAA, 0x7BFF8FFF,
1879+ 0x7B79A939, 0x69277DCA, 0x5A7C5DF2, 0x37886A35, 0x177CA1B3, 0x96871FD4,
1880+ 0xB869BA3D, 0x1C2697A1, 0xD59109DD, 0x6B870F45, 0xC9ADBA95, 0xDB8323D8,
1881+ 0x0A777311, 0x25E2DF2D, 0x18C684D7, 0x645521B0, 0x58ABD6FC, 0x7EC9EF2D,
1882+ 0x11B4DB17, 0xAE1A2EE9, 0x84E961A4, 0x1627F703, 0x8C23EC6D, 0xF0A8A31C,
1883+ 0x8E53A703, 0xB1131710, 0x5723CF12, 0x991DF0FF, 0xA30498B8, 0x02EF0D79,
1884+ 0x2B3DD3EF, 0xD38C2931, 0xE5745386, 0xF41AE34F, 0x10499E3C, 0xF79569CE,
1885+ 0x8ED71CA2, 0x98B0DC30, 0x9F6E5321, 0x42EB67BA, 0x2742BCEC, 0x8DF9EF50,
1886+ 0xAAA09BA7, 0xF9695506, 0xA294E5EF, 0xDC12C945, 0x0F7A7CEA, 0x9E19DD96,
1887+ 0xABD27434, 0xD26E3C3F, 0x13B45FF0, 0xCF8379E7, 0xF85FF493, 0x985079D6,
1888+ 0xBDB81502, 0xA2C60C5C, 0x22B632AC, 0x3D3986EE, 0x45825465, 0xAEEB58F9,
1889+ 0xF283D84B, 0x80517003, 0x942B1C72, 0xE3E70F85, 0x3F8E843D, 0x7454E06D,
1890+ 0x31B2159B, 0x568A8365, 0x23DCC43E, 0x37871B58, 0x60C69863, 0xD1889242,
1891+ 0x520946C2, 0x929B3286, 0xD95FED2C, 0x2A208340, 0x928DF232, 0xC028DF59,
1892+ 0x0D35A23F, 0x00EDC91C, 0x3979AED9, 0xAA29C007, 0x84BA7B37, 0x7E3CA9CB,
1893+ 0x54F00B03, 0x61691F7C, 0x5834387D, 0x770BB788, 0xB162F9C0, 0x43618701,
1894+ 0x9E059974, 0x06E5F826, 0x0CF7163D, 0xBBE0FE4D, 0x7F106BA3, 0x71A5CDA3,
1895+ 0x7044BD36, 0x54DB9C6D, 0xCEB6C2E7, 0x4DE29FAE, 0x136CB930, 0xEA408A55,
1896+ 0x7ADB6970, 0xC89057DA, 0x70B27C55, 0xFD9DD49F, 0x71945289, 0x29F6F8CA,
1897+ 0xDFD414FF, 0x3A073C7A, 0x610CB803, 0xF63DB2F3, 0xDAB51606, 0x1D98C2CB,
1898+ 0x8EAED1E3, 0x2713BE03, 0xA7740897, 0x4184FD27, 0xF4FB8A06, 0x42B5EE24,
1899+ 0x16CC60A1, 0x58C7FA50, 0x0D3A4CB4, 0x40C16EFA
1900+ ],[
1901+ 0xF0B6672A, 0x2FBFF845, 0xF8DAE2E1, 0x1E62ADD9, 0xE386B139, 0xCE326446,
1902+ 0x1F85E0A2, 0x876AC684, 0xF89F69ED, 0xB9C1F96B, 0x0F350297, 0x5D5C5727,
1903+ 0xE4D11EDE, 0xCB7964CC, 0xE1646E44, 0x413DB994, 0x50169691, 0x2A75843F,
1904+ 0xE56CE654, 0x307C49E8, 0xABBF759E, 0x18E30B64, 0x1DBD8903, 0xED9C2D59,
1905+ 0x2DDFF02B, 0x6EF2C41D, 0x3FF08CED, 0x5C4B7F31, 0xB26984CA, 0xB0B1EFEA,
1906+ 0x9C11734E, 0x680B8B66, 0xDC671C46, 0x2F8DD9C2, 0xCACF3975, 0xC3F6D8DB,
1907+ 0x38DDF820, 0x421E0652, 0x9FBE5AFA, 0xB4F43AE3, 0x957FC205, 0xFFA58375,
1908+ 0x55286205, 0x7EE974B5, 0x31DECAF4, 0x3E7425ED, 0xACD2BC1B, 0xFBB39E17,
1909+ 0xE99CE668, 0xC3DE689F, 0xE8BC3A39, 0xC327F086, 0xB3F2E894, 0x13CDC849,
1910+ 0xA7D8DACF, 0x2987F368, 0x7FC52A15, 0x2C0DE867, 0xC39020AE, 0x6B9A5BEB,
1911+ 0x1116EB0C, 0x56FAD5FD, 0xE13E5C39, 0x167F6C42, 0x1462F7F4, 0x5D1875F4,
1912+ 0x82A56F78, 0x2DFB88E6, 0xD0191186, 0x0850D44A, 0xCEFA0FC4, 0x3FB97E0B,
1913+ 0x3225B980, 0x3D33D41F, 0x595FA8D5, 0x3FB1945B, 0x94D5E9FA, 0xDDEF2BCB,
1914+ 0x4C35A2D9, 0xCF328015, 0x7DEEB93C, 0xF76A0735, 0x6CBE97D8, 0xE1E32FD2,
1915+ 0x72537C1C, 0x6946111C, 0x689A0958, 0x2A82EBAD, 0x5A70B4F6, 0xCA2C98CF,
1916+ 0x500B4CB5, 0xE4F72532, 0x317ABF2E, 0x9B16512E, 0x612F8F0B, 0xBF323BE6,
1917+ 0x7A9A4827, 0xE401B548, 0x4776626E, 0xCB602107, 0xEC189B17, 0x94A36CB9,
1918+ 0x00C86686, 0xDA12464F, 0x0172E4EC, 0x9779E7E2, 0x45349B40, 0x7286BCE1,
1919+ 0x1913C435, 0x8BF1E440, 0x16BB54D3, 0xE700E633, 0xF524785A, 0xB27F3F09,
1920+ 0x25B6406D, 0xA19BF68C, 0x3B144DA2, 0x816B70C0, 0xFF86B5D6, 0x1E75EBB3,
1921+ 0x57B4F242, 0x46771F2A, 0x53776C5F, 0x6257CECD, 0x27D7A26B, 0x74AD749F,
1922+ 0x0E8DF172, 0xFA7C4A26, 0x8F978578, 0xF3265767, 0xA08160FB, 0x8A13431D,
1923+ 0x22213265, 0x4A018EF2, 0x526BB8F8, 0x5FC7F38C, 0xFAB62CD2, 0x50EF8674,
1924+ 0x3E4FC6E1, 0x08553D35, 0x2BCC6BD6, 0x06D22116, 0x25F16EE8, 0xB51D172B,
1925+ 0x8E657082, 0xD102410A, 0x6B8DE05D, 0x22AEA76E, 0x0B692D8D, 0xC5E7C0F1,
1926+ 0xB3499478, 0xAB3AE1FF, 0xC16DCFB5, 0xB5F67469, 0x31C015AE, 0x7807E2FF,
1927+ 0x67E17C48, 0x56BB3693, 0xB11ABE01, 0x4AC2383A, 0x54D6BFDF, 0xBA1F540E,
1928+ 0xF4CEFE7B, 0x7F9CD102, 0x82C1AB14, 0x4D32978E, 0xE3832FE2, 0x1E63A9AA,
1929+ 0xCB1BB7CB, 0x478E6C6D, 0xD8C0CB68, 0xB907DA30, 0x05BC439C, 0x79872F07,
1930+ 0x74040F93, 0x5EE6EABD, 0xC700C640, 0xE2740CF2, 0x7D9611A6, 0x28C34433,
1931+ 0x9519AF00, 0xACAE30BA, 0xD1ED6770, 0x72557C82, 0x50D9BCCD, 0xBCDEE88E,
1932+ 0x97E36EFF, 0xCC382EF8, 0x71B71FC2, 0x6EED0311, 0x17B065C2, 0x38BE59A6,
1933+ 0x6D47222A, 0x9940BBDD, 0x342463F0, 0x98723D62, 0x59EC6C09, 0x13E077BB,
1934+ 0x6CDD2887, 0x763D4A95, 0x51D7A260, 0x7615C8BF, 0x7BAA1B0D, 0x85E2F0B8,
1935+ 0x29A14FB7, 0x90BC2A4F, 0x15A17CE1, 0x95BBC7DA, 0x98384B5B, 0x964E6B18,
1936+ 0x1341FD22, 0x64BC571D, 0x856E353D, 0x92846808, 0xBDBC4F81, 0x11C5A93F,
1937+ 0x9B00D8AB, 0xE9C7F68C, 0x002C42BE, 0x79B7F2E8, 0xCC450F36, 0x106D1921,
1938+ 0x8306ECBF, 0xB3E092E3, 0xFB4A2813, 0xC46D7B9F, 0x7CF4F348, 0x1BEB4962,
1939+ 0xFEC844F2, 0xD5FE7D0A, 0xF28A1872, 0xA0E433FE, 0x5EDB808D, 0x01591BE1,
1940+ 0xDB4E08F3, 0x298A0DFB, 0xC638BB36, 0x59F96184, 0xA344CD21, 0x39BAFAD3,
1941+ 0x83112A1C, 0x1892FC88, 0x856E9D5C, 0x13FD4F16, 0x81802AB7, 0x79FB698E,
1942+ 0xAB8AF3E1, 0x986C72C1, 0xF1565939, 0x332EFF7D, 0xD0D7746C, 0xDCF7CA0E,
1943+ 0x61E6931A, 0xF582D866, 0x2C3410D9, 0x21D70463]]
1944
1945=== added file 'build/lib.linux-x86_64-2.7/pithos/pandora/xmlrpc.py'
1946--- build/lib.linux-x86_64-2.7/pithos/pandora/xmlrpc.py 1970-01-01 00:00:00 +0000
1947+++ build/lib.linux-x86_64-2.7/pithos/pandora/xmlrpc.py 2012-03-11 20:43:36 +0000
1948@@ -0,0 +1,63 @@
1949+# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
1950+### BEGIN LICENSE
1951+# Copyright (C) 2010 Kevin Mehall <km@kevinmehall.net>
1952+#This program is free software: you can redistribute it and/or modify it
1953+#under the terms of the GNU General Public License version 3, as published
1954+#by the Free Software Foundation.
1955+#
1956+#This program is distributed in the hope that it will be useful, but
1957+#WITHOUT ANY WARRANTY; without even the implied warranties of
1958+#MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1959+#PURPOSE. See the GNU General Public License for more details.
1960+#
1961+#You should have received a copy of the GNU General Public License along
1962+#with this program. If not, see <http://www.gnu.org/licenses/>.
1963+### END LICENSE
1964+
1965+from cgi import escape
1966+
1967+def xmlrpc_value(v):
1968+ if isinstance(v, str):
1969+ return "<value><string>%s</string></value>"%escape(v)
1970+ elif v is True:
1971+ return "<value><boolean>1</boolean></value>"
1972+ elif v is False:
1973+ return "<value><boolean>0</boolean></value>"
1974+ elif isinstance(v, int) or isinstance(v, long):
1975+ return "<value><int>%i</int></value>"%v
1976+ elif isinstance(v, list):
1977+ return "<value><array><data>%s</data></array></value>"%("".join([xmlrpc_value(i) for i in v]))
1978+ else:
1979+ raise ValueError("Can't encode %s of type %s to XMLRPC"%(v, type(v)))
1980+
1981+def xmlrpc_make_call(method, args):
1982+ args = "".join(["<param>%s</param>"%xmlrpc_value(i) for i in args])
1983+ return "<?xml version=\"1.0\"?><methodCall><methodName>%s</methodName><params>%s</params></methodCall>"%(method, args)
1984+
1985+def xmlrpc_parse_value(tree):
1986+ b = tree.findtext('boolean')
1987+ if b is not None:
1988+ return bool(int(b))
1989+ i = tree.findtext('int')
1990+ if i is not None:
1991+ return int(i)
1992+ a = tree.find('array')
1993+ if a is not None:
1994+ return xmlrpc_parse_array(a)
1995+ s = tree.find('struct')
1996+ if s is not None:
1997+ return xmlrpc_parse_struct(s)
1998+ return tree.text
1999+
2000+def xmlrpc_parse_struct(tree):
2001+ d = {}
2002+ for member in tree.findall('member'):
2003+ name = member.findtext('name')
2004+ d[name] = xmlrpc_parse_value(member.find('value'))
2005+ return d
2006+
2007+def xmlrpc_parse_array(tree):
2008+ return [xmlrpc_parse_value(item) for item in tree.findall('data/value')]
2009+
2010+def xmlrpc_parse(tree):
2011+ return xmlrpc_parse_value(tree.find('params/param/value'))
2012
2013=== added file 'build/lib.linux-x86_64-2.7/pithos/pithosconfig.py'
2014--- build/lib.linux-x86_64-2.7/pithos/pithosconfig.py 1970-01-01 00:00:00 +0000
2015+++ build/lib.linux-x86_64-2.7/pithos/pithosconfig.py 2012-03-11 20:43:36 +0000
2016@@ -0,0 +1,66 @@
2017+# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
2018+### BEGIN LICENSE
2019+# Copyright (C) 2010 Kevin Mehall <km@kevinmehall.net>
2020+#This program is free software: you can redistribute it and/or modify it
2021+#under the terms of the GNU General Public License version 3, as published
2022+#by the Free Software Foundation.
2023+#
2024+#This program is distributed in the hope that it will be useful, but
2025+#WITHOUT ANY WARRANTY; without even the implied warranties of
2026+#MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
2027+#PURPOSE. See the GNU General Public License for more details.
2028+#
2029+#You should have received a copy of the GNU General Public License along
2030+#with this program. If not, see <http://www.gnu.org/licenses/>.
2031+### END LICENSE
2032+
2033+# where your project will head for your data (for instance, images and ui files)
2034+# by default, this is ../data, relative your trunk layout
2035+__pithos_data_directory__ = '/usr/local/share/pithos/'
2036+__license__ = 'GPL-3'
2037+
2038+VERSION = '0.3.14'
2039+
2040+
2041+import os
2042+
2043+class project_path_not_found(Exception):
2044+ pass
2045+
2046+valid_audio_formats = [
2047+ 'aacplus',
2048+ 'mp3',
2049+ 'mp3-hifi',
2050+]
2051+
2052+def get_data_file(*path_segments):
2053+ """Get the full path to a data file.
2054+
2055+ Returns the path to a file underneath the data directory (as defined by
2056+ `get_data_path`). Equivalent to os.path.join(get_data_path(),
2057+ *path_segments).
2058+ """
2059+ return os.path.join(getdatapath(), *path_segments)
2060+
2061+def getdatapath():
2062+ """Retrieve pithos data path
2063+
2064+ This path is by default <pithos_lib_path>/../data/ in trunk
2065+ and /usr/share/pithos in an installed version but this path
2066+ is specified at installation time.
2067+ """
2068+
2069+ # get pathname absolute or relative
2070+ if __pithos_data_directory__.startswith('/'):
2071+ pathname = __pithos_data_directory__
2072+ else:
2073+ pathname = os.path.dirname(__file__) + '/' + __pithos_data_directory__
2074+
2075+ abs_data_path = os.path.abspath(pathname)
2076+ if os.path.exists(abs_data_path):
2077+ return abs_data_path
2078+ else:
2079+ raise project_path_not_found
2080+
2081+if __name__=='__main__':
2082+ print VERSION
2083
2084=== added file 'build/lib.linux-x86_64-2.7/pithos/plugin.py'
2085--- build/lib.linux-x86_64-2.7/pithos/plugin.py 1970-01-01 00:00:00 +0000
2086+++ build/lib.linux-x86_64-2.7/pithos/plugin.py 2012-03-11 20:43:36 +0000
2087@@ -0,0 +1,98 @@
2088+#!/usr/bin/python
2089+# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
2090+### BEGIN LICENSE
2091+# Copyright (C) 2010 Kevin Mehall <km@kevinmehall.net>
2092+#This program is free software: you can redistribute it and/or modify it
2093+#under the terms of the GNU General Public License version 3, as published
2094+#by the Free Software Foundation.
2095+#
2096+#This program is distributed in the hope that it will be useful, but
2097+#WITHOUT ANY WARRANTY; without even the implied warranties of
2098+#MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
2099+#PURPOSE. See the GNU General Public License for more details.
2100+#
2101+#You should have received a copy of the GNU General Public License along
2102+#with this program. If not, see <http://www.gnu.org/licenses/>.
2103+### END LICENSE
2104+
2105+import logging
2106+import glob
2107+import os
2108+
2109+class PithosPlugin(object):
2110+ _PITHOS_PLUGIN = True # used to find the plugin class in a module
2111+ preference = None
2112+ def __init__(self, name, window):
2113+ self.name = name
2114+ self.window = window
2115+ self.prepared = False
2116+ self.enabled = False
2117+
2118+ def enable(self):
2119+ if not self.prepared:
2120+ self.error = self.on_prepare()
2121+ self.prepared = True
2122+ if not self.error and not self.enabled:
2123+ logging.info("Enabling module %s"%(self.name))
2124+ self.on_enable()
2125+ self.enabled = True
2126+
2127+ def disable(self):
2128+ if self.enabled:
2129+ logging.info("Disabling module %s"%(self.name))
2130+ self.on_disable()
2131+ self.enabled = False
2132+
2133+ def on_prepare(self):
2134+ pass
2135+
2136+ def on_enable(self):
2137+ pass
2138+
2139+ def on_disable(self):
2140+ pass
2141+
2142+class ErrorPlugin(PithosPlugin):
2143+ def __init__(self, name, error):
2144+ logging.error("Error loading plugin %s: %s"%(name, error))
2145+ self.prepared = True
2146+ self.error = error
2147+ self.name = name
2148+ self.enabled = False
2149+
2150+def load_plugin(name, window):
2151+ try:
2152+ module = __import__('pithos.plugins.'+name)
2153+ module = getattr(module.plugins, name)
2154+
2155+ except ImportError as e:
2156+ return ErrorPlugin(name, e.message)
2157+
2158+ # find the class object for the actual plugin
2159+ for key, item in module.__dict__.iteritems():
2160+ if hasattr(item, '_PITHOS_PLUGIN') and key != "PithosPlugin":
2161+ plugin_class = item
2162+ break
2163+ else:
2164+ return ErrorPlugin(name, "Could not find module class")
2165+
2166+ return plugin_class(name, window)
2167+
2168+def load_plugins(window):
2169+ plugins = window.plugins
2170+ prefs = window.preferences
2171+
2172+ plugins_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "plugins")
2173+ discovered_plugins = [ fname.replace(".py", "") for fname in glob.glob1(plugins_dir, "*.py") if not fname.startswith("__") ]
2174+
2175+ for name in discovered_plugins:
2176+ if not name in plugins:
2177+ plugin = plugins[name] = load_plugin(name, window)
2178+ else:
2179+ plugin = plugins[name]
2180+
2181+ if plugin.preference and prefs.get(plugin.preference, False):
2182+ plugin.enable()
2183+ else:
2184+ plugin.disable()
2185+
2186
2187=== added directory 'build/lib.linux-x86_64-2.7/pithos/plugins'
2188=== added file 'build/lib.linux-x86_64-2.7/pithos/plugins/__init__.py'
2189=== added file 'build/lib.linux-x86_64-2.7/pithos/plugins/mediakeys.py'
2190--- build/lib.linux-x86_64-2.7/pithos/plugins/mediakeys.py 1970-01-01 00:00:00 +0000
2191+++ build/lib.linux-x86_64-2.7/pithos/plugins/mediakeys.py 2012-03-11 20:43:36 +0000
2192@@ -0,0 +1,68 @@
2193+# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
2194+### BEGIN LICENSE
2195+# Copyright (C) 2010 Kevin Mehall <km@kevinmehall.net>
2196+#This program is free software: you can redistribute it and/or modify it
2197+#under the terms of the GNU General Public License version 3, as published
2198+#by the Free Software Foundation.
2199+#
2200+#This program is distributed in the hope that it will be useful, but
2201+#WITHOUT ANY WARRANTY; without even the implied warranties of
2202+#MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
2203+#PURPOSE. See the GNU General Public License for more details.
2204+#
2205+#You should have received a copy of the GNU General Public License along
2206+#with this program. If not, see <http://www.gnu.org/licenses/>.
2207+### END LICENSE
2208+
2209+from pithos.plugin import PithosPlugin
2210+import dbus
2211+import logging
2212+
2213+APP_ID = 'Pithos'
2214+
2215+class MediaKeyPlugin(PithosPlugin):
2216+ preference = 'enable_mediakeys'
2217+
2218+ def bind_dbus(self):
2219+ try:
2220+ bus = dbus.Bus(dbus.Bus.TYPE_SESSION)
2221+ mk = bus.get_object("org.gnome.SettingsDaemon","/org/gnome/SettingsDaemon/MediaKeys")
2222+ mk.GrabMediaPlayerKeys(APP_ID, 0, dbus_interface='org.gnome.SettingsDaemon.MediaKeys')
2223+ mk.connect_to_signal("MediaPlayerKeyPressed", self.mediakey_pressed)
2224+ logging.info("Bound media keys with DBUS")
2225+ self.method = 'dbus'
2226+ return True
2227+ except dbus.DBusException:
2228+ return False
2229+
2230+ def mediakey_pressed(self, app, action):
2231+ if app == APP_ID:
2232+ if action == 'Play':
2233+ self.window.playpause()
2234+ elif action == 'Next':
2235+ self.window.next_song()
2236+ elif action == 'Stop':
2237+ self.window.user_pause()
2238+ elif action == 'Previous':
2239+ self.window.bring_to_top()
2240+
2241+ def bind_keybinder(self):
2242+ try:
2243+ import keybinder
2244+ except:
2245+ return False
2246+
2247+ keybinder.bind('XF86AudioPlay', self.window.playpause, None)
2248+ keybinder.bind('XF86AudioStop', self.window.user_pause, None)
2249+ keybinder.bind('XF86AudioNext', self.window.next_song, None)
2250+ keybinder.bind('XF86AudioPrev', self.window.bring_to_top, None)
2251+
2252+ logging.info("Bound media keys with keybinder")
2253+ self.method = 'keybinder'
2254+ return True
2255+
2256+ def on_enable(self):
2257+ self.bind_dbus() or self.bind_keybinder() or logging.error("Could not bind media keys")
2258+
2259+ def on_disable(self):
2260+ logging.error("Not implemented: Can't disable media keys")
2261
2262=== added file 'build/lib.linux-x86_64-2.7/pithos/plugins/notification_icon.py'
2263--- build/lib.linux-x86_64-2.7/pithos/plugins/notification_icon.py 1970-01-01 00:00:00 +0000
2264+++ build/lib.linux-x86_64-2.7/pithos/plugins/notification_icon.py 2012-03-11 20:43:36 +0000
2265@@ -0,0 +1,144 @@
2266+#!/usr/bin/python
2267+# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
2268+### BEGIN LICENSE
2269+# Copyright (C) 2010 Kevin Mehall <km@kevinmehall.net>
2270+#This program is free software: you can redistribute it and/or modify it
2271+#under the terms of the GNU General Public License version 3, as published
2272+#by the Free Software Foundation.
2273+#
2274+#This program is distributed in the hope that it will be useful, but
2275+#WITHOUT ANY WARRANTY; without even the implied warranties of
2276+#MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
2277+#PURPOSE. See the GNU General Public License for more details.
2278+#
2279+#You should have received a copy of the GNU General Public License along
2280+#with this program. If not, see <http://www.gnu.org/licenses/>.
2281+### END LICENSE
2282+
2283+import gtk
2284+from pithos.pithosconfig import get_data_file
2285+from pithos.plugin import PithosPlugin
2286+
2287+# Check if appindicator is available on the system
2288+try:
2289+ import appindicator
2290+ indicator_capable = True
2291+except:
2292+ indicator_capable = False
2293+
2294+class PithosNotificationIcon(PithosPlugin):
2295+ preference = 'show_icon'
2296+
2297+ def on_prepare(self):
2298+ if indicator_capable:
2299+ self.ind = appindicator.Indicator("pithos", \
2300+ "pithos-mono", \
2301+ appindicator.CATEGORY_APPLICATION_STATUS, \
2302+ get_data_file('media'))
2303+
2304+ def on_enable(self):
2305+ self.visible = True
2306+ self.delete_callback_handle = self.window.connect("delete-event", self.toggle_visible)
2307+ self.state_callback_handle = self.window.connect("play-state-changed", self.play_state_changed)
2308+ self.song_callback_handle = self.window.connect("song-changed", self.song_changed)
2309+
2310+ if indicator_capable:
2311+ self.ind.set_status(appindicator.STATUS_ACTIVE)
2312+ else:
2313+ self.statusicon = gtk.status_icon_new_from_file(get_data_file('media', 'icon.png'))
2314+ self.statusicon.connect('activate', self.toggle_visible)
2315+
2316+ self.build_context_menu()
2317+
2318+ def build_context_menu(self):
2319+ menu = gtk.Menu()
2320+
2321+ def button(text, action, icon=None):
2322+ if icon == 'check':
2323+ item = gtk.CheckMenuItem(text)
2324+ item.set_active(True)
2325+ elif icon:
2326+ item = gtk.ImageMenuItem(text)
2327+ item.set_image(gtk.image_new_from_stock(icon, gtk.ICON_SIZE_MENU))
2328+ else:
2329+ item = gtk.MenuItem(text)
2330+ item.connect('activate', action)
2331+ item.show()
2332+ menu.append(item)
2333+ return item
2334+
2335+ if indicator_capable:
2336+ # We have to add another entry for show / hide Pithos window
2337+ self.visible_check = button("Show Pithos", self._toggle_visible, 'check')
2338+ self.visible_check.set_active(not self.window.startPaused) #SMASHER816
2339+
2340+ self.playpausebtn = button("Pause", self.window.playpause, gtk.STOCK_MEDIA_PAUSE)
2341+ button("Skip", self.window.next_song, gtk.STOCK_MEDIA_NEXT)
2342+ button("Love", (lambda *i: self.window.love_song()), gtk.STOCK_ABOUT)
2343+ button("Ban", (lambda *i: self.window.ban_song()), gtk.STOCK_CANCEL)
2344+ button("Tired", (lambda *i: self.window.tired_song()), gtk.STOCK_JUMP_TO)
2345+ button("Quit", self.window.quit, gtk.STOCK_QUIT )
2346+
2347+ # connect our new menu to the statusicon or the appindicator
2348+ if indicator_capable:
2349+ self.ind.set_menu(menu)
2350+ else:
2351+ self.statusicon.connect('popup-menu', self.context_menu, menu)
2352+
2353+ self.menu = menu
2354+
2355+
2356+ def play_state_changed(self, window, playing):
2357+ """ play or pause and rotate the text """
2358+
2359+ button = self.playpausebtn
2360+ if not playing:
2361+ button.set_label("Play")
2362+ button.set_image(gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_MENU))
2363+
2364+ else:
2365+ button.set_label("Pause")
2366+ button.set_image(gtk.image_new_from_stock(gtk.STOCK_MEDIA_PAUSE, gtk.ICON_SIZE_MENU))
2367+
2368+ if indicator_capable: # menu needs to be reset to get updated icon
2369+ self.ind.set_menu(self.menu)
2370+
2371+ def song_changed(self, window, song):
2372+ if not indicator_capable:
2373+ self.statusicon.set_tooltip("%s by %s"%(song.title, song.artist))
2374+
2375+ def _toggle_visible(self, *args):
2376+ if self.visible:
2377+ self.window.hide()
2378+ else:
2379+ self.window.bring_to_top()
2380+
2381+ self.visible = not self.visible
2382+
2383+ def toggle_visible(self, *args):
2384+ if hasattr(self, 'visible_check'):
2385+ self.visible_check.set_active(not self.visible)
2386+ else:
2387+ self._toggle_visible()
2388+
2389+ return True
2390+
2391+ def context_menu(self, widget, button, time, data=None):
2392+ if button == 3:
2393+ if data:
2394+ data.show_all()
2395+ data.popup(None, None, None, 3, time)
2396+
2397+ def on_disable(self):
2398+ if indicator_capable:
2399+ self.ind.set_status(appindicator.STATUS_PASSIVE)
2400+ else:
2401+ self.statusicon.set_visible(False)
2402+
2403+ self.window.disconnect(self.delete_callback_handle)
2404+ self.window.disconnect(self.state_callback_handle)
2405+ self.window.disconnect(self.song_callback_handle)
2406+
2407+ # Pithos window needs to be reconnected to on_destro()
2408+ self.window.connect('delete-event',self.window.on_destroy)
2409+
2410
2411=== added file 'build/lib.linux-x86_64-2.7/pithos/plugins/notify.py'
2412--- build/lib.linux-x86_64-2.7/pithos/plugins/notify.py 1970-01-01 00:00:00 +0000
2413+++ build/lib.linux-x86_64-2.7/pithos/plugins/notify.py 2012-03-11 20:43:36 +0000
2414@@ -0,0 +1,61 @@
2415+# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
2416+### BEGIN LICENSE
2417+# Copyright (C) 2010 Kevin Mehall <km@kevinmehall.net>
2418+#This program is free software: you can redistribute it and/or modify it
2419+#under the terms of the GNU General Public License version 3, as published
2420+#by the Free Software Foundation.
2421+#
2422+#This program is distributed in the hope that it will be useful, but
2423+#WITHOUT ANY WARRANTY; without even the implied warranties of
2424+#MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
2425+#PURPOSE. See the GNU General Public License for more details.
2426+#
2427+#You should have received a copy of the GNU General Public License along
2428+#with this program. If not, see <http://www.gnu.org/licenses/>.
2429+### END LICENSE
2430+
2431+import pynotify, gtk
2432+from cgi import escape
2433+from pithos.plugin import PithosPlugin
2434+from pithos.pithosconfig import get_data_file
2435+
2436+class NotifyPlugin(PithosPlugin):
2437+ preference = 'notify'
2438+
2439+ def on_prepare(self):
2440+ pynotify.init('pithos')
2441+ self.notification = pynotify.Notification("Pithos","Pithos")
2442+
2443+ def on_enable(self):
2444+ self.song_callback_handle = self.window.connect("song-changed", self.song_changed)
2445+ self.state_changed_handle = self.window.connect("user-changed-play-state", self.playstate_changed)
2446+
2447+ def set_for_song(self, song):
2448+ self.notification.clear_hints()
2449+ msg = escape("by %s from %s"%(song.artist, song.album))
2450+ self.notification.update(song.title, msg, 'audio-x-generic')
2451+
2452+ def song_changed(self, window, song):
2453+ if not self.window.is_active():
2454+ self.set_for_song(song)
2455+ if song.art_pixbuf:
2456+ #logging.debug("has albumart", song.art_pixbuf, song.art_pixbuf.get_width())
2457+ self.notification.set_icon_from_pixbuf(song.art_pixbuf)
2458+ else:
2459+ self.notification.props.icon_name = get_data_file('media/pithos-mono.png')
2460+ self.notification.show()
2461+
2462+ def playstate_changed(self, window, state):
2463+ if not self.window.is_active():
2464+ self.set_for_song(window.current_song)
2465+ if state:
2466+ self.notification.props.icon_name = 'gtk-media-play-ltr'
2467+ else:
2468+ self.notification.props.icon_name = 'gtk-media-pause'
2469+
2470+ self.notification.show()
2471+
2472+
2473+ def on_disable(self):
2474+ self.window.disconnect(self.song_callback_handle)
2475+ self.window.disconnect(self.state_changed_handle)
2476
2477=== added file 'build/lib.linux-x86_64-2.7/pithos/plugins/screensaver_pause.py'
2478--- build/lib.linux-x86_64-2.7/pithos/plugins/screensaver_pause.py 1970-01-01 00:00:00 +0000
2479+++ build/lib.linux-x86_64-2.7/pithos/plugins/screensaver_pause.py 2012-03-11 20:43:36 +0000
2480@@ -0,0 +1,62 @@
2481+# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
2482+### BEGIN LICENSE
2483+# Copyright (C) 2010 Kevin Mehall <km@kevinmehall.net>
2484+#This program is free software: you can redistribute it and/or modify it
2485+#under the terms of the GNU General Public License version 3, as published
2486+#by the Free Software Foundation.
2487+#
2488+#This program is distributed in the hope that it will be useful, but
2489+#WITHOUT ANY WARRANTY; without even the implied warranties of
2490+#MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
2491+#PURPOSE. See the GNU General Public License for more details.
2492+#
2493+#You should have received a copy of the GNU General Public License along
2494+#with this program. If not, see <http://www.gnu.org/licenses/>.
2495+### END LICENSE
2496+
2497+from pithos.plugin import PithosPlugin
2498+import dbus
2499+import logging
2500+
2501+
2502+class ScreenSaverPausePlugin(PithosPlugin):
2503+ preference = 'enable_screensaverpause'
2504+
2505+ def bind_session_bus(self):
2506+ try:
2507+ self.session_bus = dbus.SessionBus()
2508+ return True
2509+ except dbus.DBusException:
2510+ return False
2511+
2512+ def on_enable(self):
2513+ self.bind_session_bus() or logging.error("Could not bind session bus")
2514+ self.connect_events() or logging.error("Could not connect events")
2515+
2516+ def on_disable(self):
2517+ self.disconnect_events()
2518+ self.session_bus = None
2519+
2520+ def connect_events(self):
2521+ try:
2522+ self.session_bus.add_signal_receiver(self.playPause, 'ActiveChanged', 'org.gnome.ScreenSaver')
2523+ return True
2524+ except dbus.DBusException:
2525+ logging.info("Enable failed")
2526+ return False
2527+
2528+ def disconnect_events(self):
2529+ try:
2530+ self.session_bus.remove_signal_receiver(self.playPause, 'ActiveChanged', 'org.gnome.ScreenSaver')
2531+ return True
2532+ except dbus.DBusException:
2533+ return False
2534+
2535+
2536+ def playPause(self,state):
2537+ if not state:
2538+ if self.wasplaying:
2539+ self.window.user_play()
2540+ else:
2541+ self.wasplaying = self.window.playing
2542+ self.window.pause()
2543
2544=== added file 'build/lib.linux-x86_64-2.7/pithos/plugins/scrobble.py'
2545--- build/lib.linux-x86_64-2.7/pithos/plugins/scrobble.py 1970-01-01 00:00:00 +0000
2546+++ build/lib.linux-x86_64-2.7/pithos/plugins/scrobble.py 2012-03-11 20:43:36 +0000
2547@@ -0,0 +1,141 @@
2548+#!/usr/bin/python
2549+# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
2550+### BEGIN LICENSE
2551+# Copyright (C) 2010 Kevin Mehall <km@kevinmehall.net>
2552+#This program is free software: you can redistribute it and/or modify it
2553+#under the terms of the GNU General Public License version 3, as published
2554+#by the Free Software Foundation.
2555+#
2556+#This program is distributed in the hope that it will be useful, but
2557+#WITHOUT ANY WARRANTY; without even the implied warranties of
2558+#MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
2559+#PURPOSE. See the GNU General Public License for more details.
2560+#
2561+#You should have received a copy of the GNU General Public License along
2562+#with this program. If not, see <http://www.gnu.org/licenses/>.
2563+### END LICENSE
2564+
2565+from pithos import pylast
2566+import webbrowser
2567+import logging
2568+from pithos.gobject_worker import GObjectWorker
2569+from pithos.plugin import PithosPlugin
2570+
2571+#getting an API account: http://www.last.fm/api/account
2572+API_KEY = '997f635176130d5d6fe3a7387de601a8'
2573+API_SECRET = '3243b876f6bf880b923a3c9fb955720c'
2574+
2575+#client id, client version info: http://www.last.fm/api/submissions#1.1
2576+CLIENT_ID = 'pth'
2577+CLIENT_VERSION = '1.0'
2578+
2579+_worker = None
2580+def get_worker():
2581+ # so it can be shared between the plugin and the authorizer
2582+ global _worker
2583+ if not _worker:
2584+ _worker = GObjectWorker()
2585+ return _worker
2586+
2587+class LastfmPlugin(PithosPlugin):
2588+ preference='lastfm_key'
2589+
2590+ def on_prepare(self):
2591+ self.worker = get_worker()
2592+
2593+ def on_enable(self):
2594+ self.connect(self.window.preferences['lastfm_key'])
2595+ self.song_ended_handle = self.window.connect('song-ended', self.song_ended)
2596+ self.song_changed_handle = self.window.connect('song-changed', self.song_changed)
2597+
2598+ def on_disable(self):
2599+ self.window.disconnect(self.song_ended_handle)
2600+ self.window.disconnect(self.song_rating_changed_handle)
2601+ self.window.disconnect(self.song_changed_handle)
2602+
2603+ def song_ended(self, window, song):
2604+ self.scrobble(song)
2605+
2606+ def connect(self, session_key):
2607+ self.network = pylast.get_lastfm_network(
2608+ api_key=API_KEY, api_secret=API_SECRET,
2609+ session_key = session_key
2610+ )
2611+ self.scrobbler = self.network.get_scrobbler(CLIENT_ID, CLIENT_VERSION)
2612+
2613+ def song_changed(self, window, song):
2614+ self.worker.send(self.scrobbler.report_now_playing, (song.artist, song.title, song.album))
2615+
2616+ def send_rating(self, song, rating):
2617+ if song.rating:
2618+ track = self.network.get_track(song.artist, song.title)
2619+ if rating == 'love':
2620+ self.worker.send(track.love)
2621+ elif rating == 'ban':
2622+ self.worker.send(track.ban)
2623+ logging.info("Sending song rating to last.fm")
2624+
2625+ def scrobble(self, song):
2626+ if song.duration > 30 and (song.position > 240 or song.position > song.duration/2):
2627+ logging.info("Scrobbling song")
2628+ mode = pylast.SCROBBLE_MODE_PLAYED
2629+ source = pylast.SCROBBLE_SOURCE_PERSONALIZED_BROADCAST
2630+ self.worker.send(self.scrobbler.scrobble, (song.artist, song.title, int(song.start_time), source, mode, song.duration, song.album))
2631+
2632+
2633+class LastFmAuth:
2634+ def __init__(self, d, prefname, button):
2635+ self.button = button
2636+ self.dict = d
2637+ self.prefname = prefname
2638+
2639+ self.auth_url= False
2640+ self.set_button_text()
2641+ self.button.connect('clicked', self.clicked)
2642+
2643+ @property
2644+ def enabled(self):
2645+ return self.dict[self.prefname]
2646+
2647+ def setkey(self, key):
2648+ self.dict[self.prefname] = key
2649+ self.set_button_text()
2650+
2651+ def set_button_text(self):
2652+ self.button.set_sensitive(True)
2653+ if self.auth_url:
2654+ self.button.set_label("Click once authorized on web site")
2655+ elif self.enabled:
2656+ self.button.set_label("Disable")
2657+ else:
2658+ self.button.set_label("Authorize")
2659+
2660+ def clicked(self, *ignore):
2661+ if self.auth_url:
2662+ def err(e):
2663+ logging.error(e)
2664+ self.set_button_text()
2665+
2666+ get_worker().send(self.sg.get_web_auth_session_key, (self.auth_url,), self.setkey, err)
2667+ self.button.set_label("Checking...")
2668+ self.button.set_sensitive(False)
2669+ self.auth_url = False
2670+
2671+ elif self.enabled:
2672+ self.setkey(False)
2673+ else:
2674+ self.network = pylast.get_lastfm_network(api_key=API_KEY, api_secret=API_SECRET)
2675+ self.sg = pylast.SessionKeyGenerator(self.network)
2676+
2677+ def callback(url):
2678+ self.auth_url = url
2679+ self.set_button_text()
2680+ webbrowser.open(self.auth_url)
2681+
2682+ get_worker().send(self.sg.get_web_auth_url, (), callback)
2683+ self.button.set_label("Connecting...")
2684+ self.button.set_sensitive(False)
2685+
2686+
2687+
2688+
2689
2690=== added file 'build/lib.linux-x86_64-2.7/pithos/pylast.py'
2691--- build/lib.linux-x86_64-2.7/pithos/pylast.py 1970-01-01 00:00:00 +0000
2692+++ build/lib.linux-x86_64-2.7/pithos/pylast.py 2012-03-11 20:43:36 +0000
2693@@ -0,0 +1,3702 @@
2694+# -*- coding: utf-8 -*-
2695+#
2696+# pylast - A Python interface to Last.fm (and other API compatible social networks)
2697+# Copyright (C) 2008-2009 Amr Hassan
2698+#
2699+# This program is free software; you can redistribute it and/or modify
2700+# it under the terms of the GNU General Public License as published by
2701+# the Free Software Foundation; either version 2 of the License, or
2702+# (at your option) any later version.
2703+#
2704+# This program is distributed in the hope that it will be useful,
2705+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2706+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2707+# GNU General Public License for more details.
2708+#
2709+# You should have received a copy of the GNU General Public License
2710+# along with this program; if not, write to the Free Software
2711+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
2712+# USA
2713+#
2714+# http://code.google.com/p/pylast/
2715+
2716+__version__ = '0.4'
2717+__author__ = 'Amr Hassan'
2718+__copyright__ = "Copyright (C) 2008-2009 Amr Hassan"
2719+__license__ = "gpl"
2720+__email__ = 'amr.hassan@gmail.com'
2721+
2722+import hashlib
2723+import httplib
2724+import urllib
2725+import threading
2726+from xml.dom import minidom
2727+import xml.dom
2728+import time
2729+import shelve
2730+import tempfile
2731+import sys
2732+import htmlentitydefs
2733+
2734+try:
2735+ import collections
2736+except ImportError:
2737+ pass
2738+
2739+STATUS_INVALID_SERVICE = 2
2740+STATUS_INVALID_METHOD = 3
2741+STATUS_AUTH_FAILED = 4
2742+STATUS_INVALID_FORMAT = 5
2743+STATUS_INVALID_PARAMS = 6
2744+STATUS_INVALID_RESOURCE = 7
2745+STATUS_TOKEN_ERROR = 8
2746+STATUS_INVALID_SK = 9
2747+STATUS_INVALID_API_KEY = 10
2748+STATUS_OFFLINE = 11
2749+STATUS_SUBSCRIBERS_ONLY = 12
2750+STATUS_INVALID_SIGNATURE = 13
2751+STATUS_TOKEN_UNAUTHORIZED = 14
2752+STATUS_TOKEN_EXPIRED = 15
2753+
2754+EVENT_ATTENDING = '0'
2755+EVENT_MAYBE_ATTENDING = '1'
2756+EVENT_NOT_ATTENDING = '2'
2757+
2758+PERIOD_OVERALL = 'overall'
2759+PERIOD_7DAYS = "7day"
2760+PERIOD_3MONTHS = '3month'
2761+PERIOD_6MONTHS = '6month'
2762+PERIOD_12MONTHS = '12month'
2763+
2764+DOMAIN_ENGLISH = 0
2765+DOMAIN_GERMAN = 1
2766+DOMAIN_SPANISH = 2
2767+DOMAIN_FRENCH = 3
2768+DOMAIN_ITALIAN = 4
2769+DOMAIN_POLISH = 5
2770+DOMAIN_PORTUGUESE = 6
2771+DOMAIN_SWEDISH = 7
2772+DOMAIN_TURKISH = 8
2773+DOMAIN_RUSSIAN = 9
2774+DOMAIN_JAPANESE = 10
2775+DOMAIN_CHINESE = 11
2776+
2777+COVER_SMALL = 0
2778+COVER_MEDIUM = 1
2779+COVER_LARGE = 2
2780+COVER_EXTRA_LARGE = 3
2781+COVER_MEGA = 4
2782+
2783+IMAGES_ORDER_POPULARITY = "popularity"
2784+IMAGES_ORDER_DATE = "dateadded"
2785+
2786+
2787+USER_MALE = 'Male'
2788+USER_FEMALE = 'Female'
2789+
2790+SCROBBLE_SOURCE_USER = "P"
2791+SCROBBLE_SOURCE_NON_PERSONALIZED_BROADCAST = "R"
2792+SCROBBLE_SOURCE_PERSONALIZED_BROADCAST = "E"
2793+SCROBBLE_SOURCE_LASTFM = "L"
2794+SCROBBLE_SOURCE_UNKNOWN = "U"
2795+
2796+SCROBBLE_MODE_PLAYED = ""
2797+SCROBBLE_MODE_LOVED = "L"
2798+SCROBBLE_MODE_BANNED = "B"
2799+SCROBBLE_MODE_SKIPPED = "S"
2800+
2801+"""
2802+A list of the implemented webservices (from http://www.last.fm/api/intro)
2803+=====================================
2804+# Album
2805+
2806+ * album.addTags DONE
2807+ * album.getInfo DONE
2808+ * album.getTags DONE
2809+ * album.removeTag DONE
2810+ * album.search DONE
2811+
2812+# Artist
2813+
2814+ * artist.addTags DONE
2815+ * artist.getEvents DONE
2816+ * artist.getImages DONE
2817+ * artist.getInfo DONE
2818+ * artist.getPodcast TODO
2819+ * artist.getShouts DONE
2820+ * artist.getSimilar DONE
2821+ * artist.getTags DONE
2822+ * artist.getTopAlbums DONE
2823+ * artist.getTopFans DONE
2824+ * artist.getTopTags DONE
2825+ * artist.getTopTracks DONE
2826+ * artist.removeTag DONE
2827+ * artist.search DONE
2828+ * artist.share DONE
2829+ * artist.shout DONE
2830+
2831+# Auth
2832+
2833+ * auth.getMobileSession DONE
2834+ * auth.getSession DONE
2835+ * auth.getToken DONE
2836+
2837+# Event
2838+
2839+ * event.attend DONE
2840+ * event.getAttendees DONE
2841+ * event.getInfo DONE
2842+ * event.getShouts DONE
2843+ * event.share DONE
2844+ * event.shout DONE
2845+
2846+# Geo
2847+
2848+ * geo.getEvents
2849+ * geo.getTopArtists
2850+ * geo.getTopTracks
2851+
2852+# Group
2853+
2854+ * group.getMembers DONE
2855+ * group.getWeeklyAlbumChart DONE
2856+ * group.getWeeklyArtistChart DONE
2857+ * group.getWeeklyChartList DONE
2858+ * group.getWeeklyTrackChart DONE
2859+
2860+# Library
2861+
2862+ * library.addAlbum DONE
2863+ * library.addArtist DONE
2864+ * library.addTrack DONE
2865+ * library.getAlbums DONE
2866+ * library.getArtists DONE
2867+ * library.getTracks DONE
2868+
2869+# Playlist
2870+
2871+ * playlist.addTrack DONE
2872+ * playlist.create DONE
2873+ * playlist.fetch DONE
2874+
2875+# Radio
2876+
2877+ * radio.getPlaylist
2878+ * radio.tune
2879+
2880+# Tag
2881+
2882+ * tag.getSimilar DONE
2883+ * tag.getTopAlbums DONE
2884+ * tag.getTopArtists DONE
2885+ * tag.getTopTags DONE
2886+ * tag.getTopTracks DONE
2887+ * tag.getWeeklyArtistChart DONE
2888+ * tag.getWeeklyChartList DONE
2889+ * tag.search DONE
2890+
2891+# Tasteometer
2892+
2893+ * tasteometer.compare DONE
2894+
2895+# Track
2896+
2897+ * track.addTags DONE
2898+ * track.ban DONE
2899+ * track.getInfo DONE
2900+ * track.getSimilar DONE
2901+ * track.getTags DONE
2902+ * track.getTopFans DONE
2903+ * track.getTopTags DONE
2904+ * track.love DONE
2905+ * track.removeTag DONE
2906+ * track.search DONE
2907+ * track.share DONE
2908+
2909+# User
2910+
2911+ * user.getEvents DONE
2912+ * user.getFriends DONE
2913+ * user.getInfo DONE
2914+ * user.getLovedTracks DONE
2915+ * user.getNeighbours DONE
2916+ * user.getPastEvents DONE
2917+ * user.getPlaylists DONE
2918+ * user.getRecentStations TODO
2919+ * user.getRecentTracks DONE
2920+ * user.getRecommendedArtists DONE
2921+ * user.getRecommendedEvents DONE
2922+ * user.getShouts DONE
2923+ * user.getTopAlbums DONE
2924+ * user.getTopArtists DONE
2925+ * user.getTopTags DONE
2926+ * user.getTopTracks DONE
2927+ * user.getWeeklyAlbumChart DONE
2928+ * user.getWeeklyArtistChart DONE
2929+ * user.getWeeklyChartList DONE
2930+ * user.getWeeklyTrackChart DONE
2931+ * user.shout DONE
2932+
2933+# Venue
2934+
2935+ * venue.getEvents DONE
2936+ * venue.getPastEvents DONE
2937+ * venue.search DONE
2938+"""
2939+
2940+class Network(object):
2941+ """
2942+ A music social network website that is Last.fm or one exposing a Last.fm compatible API
2943+ """
2944+
2945+ def __init__(self, name, homepage, ws_server, api_key, api_secret, session_key, submission_server, username, password_hash,
2946+ domain_names, urls):
2947+ """
2948+ name: the name of the network
2949+ homepage: the homepage url
2950+ ws_server: the url of the webservices server
2951+ api_key: a provided API_KEY
2952+ api_secret: a provided API_SECRET
2953+ session_key: a generated session_key or None
2954+ submission_server: the url of the server to which tracks are submitted (scrobbled)
2955+ username: a username of a valid user
2956+ password_hash: the output of pylast.md5(password) where password is the user's password thingy
2957+ domain_names: a dict mapping each DOMAIN_* value to a string domain name
2958+ urls: a dict mapping types to urls
2959+
2960+ if username and password_hash were provided and not session_key, session_key will be
2961+ generated automatically when needed.
2962+
2963+ Either a valid session_key or a combination of username and password_hash must be present for scrobbling.
2964+
2965+ You should use a preconfigured network object through a get_*_network(...) method instead of creating an object
2966+ of this class, unless you know what you're doing.
2967+ """
2968+
2969+ self.ws_server = ws_server
2970+ self.submission_server = submission_server
2971+ self.name = name
2972+ self.homepage = homepage
2973+ self.api_key = api_key
2974+ self.api_secret = api_secret
2975+ self.session_key = session_key
2976+ self.username = username
2977+ self.password_hash = password_hash
2978+ self.domain_names = domain_names
2979+ self.urls = urls
2980+
2981+ self.cache_backend = None
2982+ self.proxy_enabled = False
2983+ self.proxy = None
2984+ self.last_call_time = 0
2985+
2986+ #generate a session_key if necessary
2987+ if (self.api_key and self.api_secret) and not self.session_key and (self.username and self.password_hash):
2988+ sk_gen = SessionKeyGenerator(self)
2989+ self.session_key = sk_gen.get_session_key(self.username, self.password_hash)
2990+
2991+ def get_artist(self, artist_name):
2992+ """
2993+ Return an Artist object
2994+ """
2995+
2996+ return Artist(artist_name, self)
2997+
2998+ def get_track(self, artist, title):
2999+ """
3000+ Return a Track object
3001+ """
3002+
3003+ return Track(artist, title, self)
3004+
3005+ def get_album(self, artist, title):
3006+ """
3007+ Return an Album object
3008+ """
3009+
3010+ return Album(artist, title, self)
3011+
3012+ def get_authenticated_user(self):
3013+ """
3014+ Returns the authenticated user
3015+ """
3016+
3017+ return AuthenticatedUser(self)
3018+
3019+ def get_country(self, country_name):
3020+ """
3021+ Returns a country object
3022+ """
3023+
3024+ return Country(country_name, self)
3025+
3026+ def get_group(self, name):
3027+ """
3028+ Returns a Group object
3029+ """
3030+
3031+ return Group(name, self)
3032+
3033+ def get_user(self, username):
3034+ """
3035+ Returns a user object
3036+ """
3037+
3038+ return User(username, self)
3039+
3040+ def get_tag(self, name):
3041+ """
3042+ Returns a tag object
3043+ """
3044+
3045+ return Tag(name, self)
3046+
3047+ def get_scrobbler(self, client_id, client_version):
3048+ """
3049+ Returns a Scrobbler object used for submitting tracks to the server
3050+
3051+ Quote from http://www.last.fm/api/submissions:
3052+ ========
3053+ Client identifiers are used to provide a centrally managed database of
3054+ the client versions, allowing clients to be banned if they are found to
3055+ be behaving undesirably. The client ID is associated with a version
3056+ number on the server, however these are only incremented if a client is
3057+ banned and do not have to reflect the version of the actual client application.
3058+
3059+ During development, clients which have not been allocated an identifier should
3060+ use the identifier tst, with a version number of 1.0. Do not distribute code or
3061+ client implementations which use this test identifier. Do not use the identifiers
3062+ used by other clients.
3063+ =========
3064+
3065+ To obtain a new client identifier please contact:
3066+ * Last.fm: submissions@last.fm
3067+ * # TODO: list others
3068+
3069+ ...and provide us with the name of your client and its homepage address.
3070+ """
3071+
3072+ return Scrobbler(self, client_id, client_version)
3073+
3074+ def _get_language_domain(self, domain_language):
3075+ """
3076+ Returns the mapped domain name of the network to a DOMAIN_* value
3077+ """
3078+
3079+ if domain_language in self.domain_names:
3080+ return self.domain_names[domain_language]
3081+
3082+ def _get_url(self, domain, type):
3083+ return "http://%s/%s" %(self._get_language_domain(domain), self.urls[type])
3084+
3085+ def _get_ws_auth(self):
3086+ """
3087+ Returns a (API_KEY, API_SECRET, SESSION_KEY) tuple.
3088+ """
3089+ return (self.api_key, self.api_secret, self.session_key)
3090+
3091+ def _delay_call(self):
3092+ """
3093+ Makes sure that web service calls are at least a second apart
3094+ """
3095+
3096+ # delay time in seconds
3097+ DELAY_TIME = 1.0
3098+ now = time.time()
3099+
3100+ if (now - self.last_call_time) < DELAY_TIME:
3101+ time.sleep(1)
3102+
3103+ self.last_call_time = now
3104+
3105+ def create_new_playlist(self, title, description):
3106+ """
3107+ Creates a playlist for the authenticated user and returns it
3108+ title: The title of the new playlist.
3109+ description: The description of the new playlist.
3110+ """
3111+
3112+ params = {}
3113+ params['title'] = _unicode(title)
3114+ params['description'] = _unicode(description)
3115+
3116+ doc = _Request(self, 'playlist.create', params).execute(False)
3117+
3118+ e_id = doc.getElementsByTagName("id")[0].firstChild.data
3119+ user = doc.getElementsByTagName('playlists')[0].getAttribute('user')
3120+
3121+ return Playlist(user, e_id, self)
3122+
3123+ def get_top_tags(self, limit=None):
3124+ """Returns a sequence of the most used tags as a sequence of TopItem objects."""
3125+
3126+ doc = _Request(self, "tag.getTopTags").execute(True)
3127+ seq = []
3128+ for node in doc.getElementsByTagName("tag"):
3129+ tag = Tag(_extract(node, "name"), self)
3130+ weight = _number(_extract(node, "count"))
3131+
3132+ if len(seq) < limit:
3133+ seq.append(TopItem(tag, weight))
3134+
3135+ return seq
3136+
3137+ def enable_proxy(self, host, port):
3138+ """Enable a default web proxy"""
3139+
3140+ self.proxy = [host, _number(port)]
3141+ self.proxy_enabled = True
3142+
3143+ def disable_proxy(self):
3144+ """Disable using the web proxy"""
3145+
3146+ self.proxy_enabled = False
3147+
3148+ def is_proxy_enabled(self):
3149+ """Returns True if a web proxy is enabled."""
3150+
3151+ return self.proxy_enabled
3152+
3153+ def _get_proxy(self):
3154+ """Returns proxy details."""
3155+
3156+ return self.proxy
3157+
3158+ def enable_caching(self, file_path = None):
3159+ """Enables caching request-wide for all cachable calls.
3160+ In choosing the backend used for caching, it will try _SqliteCacheBackend first if
3161+ the module sqlite3 is present. If not, it will fallback to _ShelfCacheBackend which uses shelve.Shelf objects.
3162+
3163+ * file_path: A file path for the backend storage file. If
3164+ None set, a temp file would probably be created, according the backend.
3165+ """
3166+
3167+ if not file_path:
3168+ file_path = tempfile.mktemp(prefix="pylast_tmp_")
3169+
3170+ self.cache_backend = _ShelfCacheBackend(file_path)
3171+
3172+ def disable_caching(self):
3173+ """Disables all caching features."""
3174+
3175+ self.cache_backend = None
3176+
3177+ def is_caching_enabled(self):
3178+ """Returns True if caching is enabled."""
3179+
3180+ return not (self.cache_backend == None)
3181+
3182+ def _get_cache_backend(self):
3183+
3184+ return self.cache_backend
3185+
3186+ def search_for_album(self, album_name):
3187+ """Searches for an album by its name. Returns a AlbumSearch object.
3188+ Use get_next_page() to retreive sequences of results."""
3189+
3190+ return AlbumSearch(album_name, self)
3191+
3192+ def search_for_artist(self, artist_name):
3193+ """Searches of an artist by its name. Returns a ArtistSearch object.
3194+ Use get_next_page() to retreive sequences of results."""
3195+
3196+ return ArtistSearch(artist_name, self)
3197+
3198+ def search_for_tag(self, tag_name):
3199+ """Searches of a tag by its name. Returns a TagSearch object.
3200+ Use get_next_page() to retreive sequences of results."""
3201+
3202+ return TagSearch(tag_name, self)
3203+
3204+ def search_for_track(self, artist_name, track_name):
3205+ """Searches of a track by its name and its artist. Set artist to an empty string if not available.
3206+ Returns a TrackSearch object.
3207+ Use get_next_page() to retreive sequences of results."""
3208+
3209+ return TrackSearch(artist_name, track_name, self)
3210+
3211+ def search_for_venue(self, venue_name, country_name):
3212+ """Searches of a venue by its name and its country. Set country_name to an empty string if not available.
3213+ Returns a VenueSearch object.
3214+ Use get_next_page() to retreive sequences of results."""
3215+
3216+ return VenueSearch(venue_name, country_name, self)
3217+
3218+ def get_track_by_mbid(self, mbid):
3219+ """Looks up a track by its MusicBrainz ID"""
3220+
3221+ params = {"mbid": _unicode(mbid)}
3222+
3223+ doc = _Request(self, "track.getInfo", params).execute(True)
3224+
3225+ return Track(_extract(doc, "name", 1), _extract(doc, "name"), self)
3226+
3227+ def get_artist_by_mbid(self, mbid):
3228+ """Loooks up an artist by its MusicBrainz ID"""
3229+
3230+ params = {"mbid": _unicode(mbid)}
3231+
3232+ doc = _Request(self, "artist.getInfo", params).execute(True)
3233+
3234+ return Artist(_extract(doc, "name"), self)
3235+
3236+ def get_album_by_mbid(self, mbid):
3237+ """Looks up an album by its MusicBrainz ID"""
3238+
3239+ params = {"mbid": _unicode(mbid)}
3240+
3241+ doc = _Request(self, "album.getInfo", params).execute(True)
3242+
3243+ return Album(_extract(doc, "artist"), _extract(doc, "name"), self)
3244+
3245+def get_lastfm_network(api_key="", api_secret="", session_key = "", username = "", password_hash = ""):
3246+ """
3247+ Returns a preconfigured Network object for Last.fm
3248+
3249+ api_key: a provided API_KEY
3250+ api_secret: a provided API_SECRET
3251+ session_key: a generated session_key or None
3252+ username: a username of a valid user
3253+ password_hash: the output of pylast.md5(password) where password is the user's password
3254+
3255+ if username and password_hash were provided and not session_key, session_key will be
3256+ generated automatically when needed.
3257+
3258+ Either a valid session_key or a combination of username and password_hash must be present for scrobbling.
3259+
3260+ Most read-only webservices only require an api_key and an api_secret, see about obtaining them from:
3261+ http://www.last.fm/api/account
3262+ """
3263+
3264+ return Network (
3265+ name = "Last.fm",
3266+ homepage = "http://last.fm",
3267+ ws_server = ("ws.audioscrobbler.com", "/2.0/"),
3268+ api_key = api_key,
3269+ api_secret = api_secret,
3270+ session_key = session_key,
3271+ submission_server = "http://post.audioscrobbler.com:80/",
3272+ username = username,
3273+ password_hash = password_hash,
3274+ domain_names = {
3275+ DOMAIN_ENGLISH: 'www.last.fm',
3276+ DOMAIN_GERMAN: 'www.lastfm.de',
3277+ DOMAIN_SPANISH: 'www.lastfm.es',
3278+ DOMAIN_FRENCH: 'www.lastfm.fr',
3279+ DOMAIN_ITALIAN: 'www.lastfm.it',
3280+ DOMAIN_POLISH: 'www.lastfm.pl',
3281+ DOMAIN_PORTUGUESE: 'www.lastfm.com.br',
3282+ DOMAIN_SWEDISH: 'www.lastfm.se',
3283+ DOMAIN_TURKISH: 'www.lastfm.com.tr',
3284+ DOMAIN_RUSSIAN: 'www.lastfm.ru',
3285+ DOMAIN_JAPANESE: 'www.lastfm.jp',
3286+ DOMAIN_CHINESE: 'cn.last.fm',
3287+ },
3288+ urls = {
3289+ "album": "music/%(artist)s/%(album)s",
3290+ "artist": "music/%(artist)s",
3291+ "event": "event/%(id)s",
3292+ "country": "place/%(country_name)s",
3293+ "playlist": "user/%(user)s/library/playlists/%(appendix)s",
3294+ "tag": "tag/%(name)s",
3295+ "track": "music/%(artist)s/_/%(title)s",
3296+ "group": "group/%(name)s",
3297+ "user": "user/%(name)s",
3298+ }
3299+ )
3300+
3301+def get_librefm_network(api_key="", api_secret="", session_key = "", username = "", password_hash = ""):
3302+ """
3303+ Returns a preconfigured Network object for Libre.fm
3304+
3305+ api_key: a provided API_KEY
3306+ api_secret: a provided API_SECRET
3307+ session_key: a generated session_key or None
3308+ username: a username of a valid user
3309+ password_hash: the output of pylast.md5(password) where password is the user's password
3310+
3311+ if username and password_hash were provided and not session_key, session_key will be
3312+ generated automatically when needed.
3313+ """
3314+
3315+ return Network (
3316+ name = "Libre.fm",
3317+ homepage = "http://alpha.dev.libre.fm",
3318+ ws_server = ("alpha.dev.libre.fm", "/2.0/"),
3319+ api_key = api_key,
3320+ api_secret = api_secret,
3321+ session_key = session_key,
3322+ submission_server = "http://turtle.libre.fm:80/",
3323+ username = username,
3324+ password_hash = password_hash,
3325+ domain_names = {
3326+ DOMAIN_ENGLISH: "alpha.dev.libre.fm",
3327+ DOMAIN_GERMAN: "alpha.dev.libre.fm",
3328+ DOMAIN_SPANISH: "alpha.dev.libre.fm",
3329+ DOMAIN_FRENCH: "alpha.dev.libre.fm",
3330+ DOMAIN_ITALIAN: "alpha.dev.libre.fm",
3331+ DOMAIN_POLISH: "alpha.dev.libre.fm",
3332+ DOMAIN_PORTUGUESE: "alpha.dev.libre.fm",
3333+ DOMAIN_SWEDISH: "alpha.dev.libre.fm",
3334+ DOMAIN_TURKISH: "alpha.dev.libre.fm",
3335+ DOMAIN_RUSSIAN: "alpha.dev.libre.fm",
3336+ DOMAIN_JAPANESE: "alpha.dev.libre.fm",
3337+ DOMAIN_CHINESE: "alpha.dev.libre.fm",
3338+ },
3339+ urls = {
3340+ "album": "artist/%(artist)s/album/%(album)s",
3341+ "artist": "artist/%(artist)s",
3342+ "event": "event/%(id)s",
3343+ "country": "place/%(country_name)s",
3344+ "playlist": "user/%(user)s/library/playlists/%(appendix)s",
3345+ "tag": "tag/%(name)s",
3346+ "track": "music/%(artist)s/_/%(title)s",
3347+ "group": "group/%(name)s",
3348+ "user": "user/%(name)s",
3349+ }
3350+ )
3351+
3352+class _ShelfCacheBackend(object):
3353+ """Used as a backend for caching cacheable requests."""
3354+ def __init__(self, file_path = None):
3355+ self.shelf = shelve.open(file_path)
3356+
3357+ def get_xml(self, key):
3358+ return self.shelf[key]
3359+
3360+ def set_xml(self, key, xml_string):
3361+ self.shelf[key] = xml_string
3362+
3363+ def has_key(self, key):
3364+ return key in self.shelf.keys()
3365+
3366+class _ThreadedCall(threading.Thread):
3367+ """Facilitates calling a function on another thread."""
3368+
3369+ def __init__(self, sender, funct, funct_args, callback, callback_args):
3370+
3371+ threading.Thread.__init__(self)
3372+
3373+ self.funct = funct
3374+ self.funct_args = funct_args
3375+ self.callback = callback
3376+ self.callback_args = callback_args
3377+
3378+ self.sender = sender
3379+
3380+ def run(self):
3381+
3382+ output = []
3383+
3384+ if self.funct:
3385+ if self.funct_args:
3386+ output = self.funct(*self.funct_args)
3387+ else:
3388+ output = self.funct()
3389+
3390+ if self.callback:
3391+ if self.callback_args:
3392+ self.callback(self.sender, output, *self.callback_args)
3393+ else:
3394+ self.callback(self.sender, output)
3395+
3396+class _Request(object):
3397+ """Representing an abstract web service operation."""
3398+
3399+ def __init__(self, network, method_name, params = {}):
3400+
3401+ self.params = params
3402+ self.network = network
3403+
3404+ (self.api_key, self.api_secret, self.session_key) = network._get_ws_auth()
3405+
3406+ self.params["api_key"] = self.api_key
3407+ self.params["method"] = method_name
3408+
3409+ if network.is_caching_enabled():
3410+ self.cache = network._get_cache_backend()
3411+
3412+ if self.session_key:
3413+ self.params["sk"] = self.session_key
3414+ self.sign_it()
3415+
3416+ def sign_it(self):
3417+ """Sign this request."""
3418+
3419+ if not "api_sig" in self.params.keys():
3420+ self.params['api_sig'] = self._get_signature()
3421+
3422+ def _get_signature(self):
3423+ """Returns a 32-character hexadecimal md5 hash of the signature string."""
3424+
3425+ keys = self.params.keys()[:]
3426+
3427+ keys.sort()
3428+
3429+ string = ""
3430+
3431+ for name in keys:
3432+ string += name
3433+ string += self.params[name]
3434+
3435+ string += self.api_secret
3436+
3437+ return md5(string)
3438+
3439+ def _get_cache_key(self):
3440+ """The cache key is a string of concatenated sorted names and values."""
3441+
3442+ keys = self.params.keys()
3443+ keys.sort()
3444+
3445+ cache_key = str()
3446+
3447+ for key in keys:
3448+ if key != "api_sig" and key != "api_key" and key != "sk":
3449+ cache_key += key + _string(self.params[key])
3450+
3451+ return hashlib.sha1(cache_key).hexdigest()
3452+
3453+ def _get_cached_response(self):
3454+ """Returns a file object of the cached response."""
3455+
3456+ if not self._is_cached():
3457+ response = self._download_response()
3458+ self.cache.set_xml(self._get_cache_key(), response)
3459+
3460+ return self.cache.get_xml(self._get_cache_key())
3461+
3462+ def _is_cached(self):
3463+ """Returns True if the request is already in cache."""
3464+
3465+ return self.cache.has_key(self._get_cache_key())
3466+
3467+ def _download_response(self):
3468+ """Returns a response body string from the server."""
3469+
3470+ # Delay the call if necessary
3471+ #self.network._delay_call() # enable it if you want.
3472+
3473+ data = []
3474+ for name in self.params.keys():
3475+ data.append('='.join((name, urllib.quote_plus(_string(self.params[name])))))
3476+ data = '&'.join(data)
3477+
3478+ headers = {
3479+ "Content-type": "application/x-www-form-urlencoded",
3480+ 'Accept-Charset': 'utf-8',
3481+ 'User-Agent': "pylast" + '/' + __version__
3482+ }
3483+
3484+ (HOST_NAME, HOST_SUBDIR) = self.network.ws_server
3485+
3486+ if self.network.is_proxy_enabled():
3487+ conn = httplib.HTTPConnection(host = self._get_proxy()[0], port = self._get_proxy()[1])
3488+ conn.request(method='POST', url="http://" + HOST_NAME + HOST_SUBDIR,
3489+ body=data, headers=headers)
3490+ else:
3491+ conn = httplib.HTTPConnection(host=HOST_NAME)
3492+ conn.request(method='POST', url=HOST_SUBDIR, body=data, headers=headers)
3493+
3494+ response = conn.getresponse()
3495+ response_text = _unicode(response.read())
3496+ self._check_response_for_errors(response_text)
3497+ return response_text
3498+
3499+ def execute(self, cacheable = False):
3500+ """Returns the XML DOM response of the POST Request from the server"""
3501+
3502+ if self.network.is_caching_enabled() and cacheable:
3503+ response = self._get_cached_response()
3504+ else:
3505+ response = self._download_response()
3506+
3507+ return minidom.parseString(_string(response))
3508+
3509+ def _check_response_for_errors(self, response):
3510+ """Checks the response for errors and raises one if any exists."""
3511+
3512+ doc = minidom.parseString(_string(response))
3513+ e = doc.getElementsByTagName('lfm')[0]
3514+
3515+ if e.getAttribute('status') != "ok":
3516+ e = doc.getElementsByTagName('error')[0]
3517+ status = e.getAttribute('code')
3518+ details = e.firstChild.data.strip()
3519+ raise WSError(self.network, status, details)
3520+
3521+class SessionKeyGenerator(object):
3522+ """Methods of generating a session key:
3523+ 1) Web Authentication:
3524+ a. network = get_*_network(API_KEY, API_SECRET)
3525+ b. sg = SessionKeyGenerator(network)
3526+ c. url = sg.get_web_auth_url()
3527+ d. Ask the user to open the url and authorize you, and wait for it.
3528+ e. session_key = sg.get_web_auth_session_key(url)
3529+ 2) Username and Password Authentication:
3530+ a. network = get_*_network(API_KEY, API_SECRET)
3531+ b. username = raw_input("Please enter your username: ")
3532+ c. password_hash = pylast.md5(raw_input("Please enter your password: ")
3533+ d. session_key = SessionKeyGenerator(network).get_session_key(username, password_hash)
3534+
3535+ A session key's lifetime is infinie, unless the user provokes the rights of the given API Key.
3536+
3537+ If you create a Network object with just a API_KEY and API_SECRET and a username and a password_hash, a
3538+ SESSION_KEY will be automatically generated for that network and stored in it so you don't have to do this
3539+ manually, unless you want to.
3540+ """
3541+
3542+ def __init__(self, network):
3543+ self.network = network
3544+ self.web_auth_tokens = {}
3545+
3546+ def _get_web_auth_token(self):
3547+ """Retrieves a token from the network for web authentication.
3548+ The token then has to be authorized from getAuthURL before creating session.
3549+ """
3550+
3551+ request = _Request(self.network, 'auth.getToken')
3552+
3553+ # default action is that a request is signed only when
3554+ # a session key is provided.
3555+ request.sign_it()
3556+
3557+ doc = request.execute()
3558+
3559+ e = doc.getElementsByTagName('token')[0]
3560+ return e.firstChild.data
3561+
3562+ def get_web_auth_url(self):
3563+ """The user must open this page, and you first, then call get_web_auth_session_key(url) after that."""
3564+
3565+ token = self._get_web_auth_token()
3566+
3567+ url = '%(homepage)s/api/auth/?api_key=%(api)s&token=%(token)s' % \
3568+ {"homepage": self.network.homepage, "api": self.network.api_key, "token": token}
3569+
3570+ self.web_auth_tokens[url] = token
3571+
3572+ return url
3573+
3574+ def get_web_auth_session_key(self, url):
3575+ """Retrieves the session key of a web authorization process by its url."""
3576+
3577+ if url in self.web_auth_tokens.keys():
3578+ token = self.web_auth_tokens[url]
3579+ else:
3580+ token = "" #that's gonna raise a WSError of an unauthorized token when the request is executed.
3581+
3582+ request = _Request(self.network, 'auth.getSession', {'token': token})
3583+
3584+ # default action is that a request is signed only when
3585+ # a session key is provided.
3586+ request.sign_it()
3587+
3588+ doc = request.execute()
3589+
3590+ return doc.getElementsByTagName('key')[0].firstChild.data
3591+
3592+ def get_session_key(self, username, password_hash):
3593+ """Retrieve a session key with a username and a md5 hash of the user's password."""
3594+
3595+ params = {"username": username, "authToken": md5(username + password_hash)}
3596+ request = _Request(self.network, "auth.getMobileSession", params)
3597+
3598+ # default action is that a request is signed only when
3599+ # a session key is provided.
3600+ request.sign_it()
3601+
3602+ doc = request.execute()
3603+
3604+ return _extract(doc, "key")
3605+
3606+def _namedtuple(name, children):
3607+ """
3608+ collections.namedtuple is available in (python >= 2.6)
3609+ """
3610+
3611+ v = sys.version_info
3612+ if v[1] >= 6 and v[0] < 3:
3613+ return collections.namedtuple(name, children)
3614+ else:
3615+ def fancydict(*args):
3616+ d = {}
3617+ i = 0
3618+ for child in children:
3619+ d[child.strip()] = args[i]
3620+ i += 1
3621+ return d
3622+
3623+ return fancydict
3624+
3625+TopItem = _namedtuple("TopItem", ["item", "weight"])
3626+SimilarItem = _namedtuple("SimilarItem", ["item", "match"])
3627+LibraryItem = _namedtuple("LibraryItem", ["item", "playcount", "tagcount"])
3628+PlayedTrack = _namedtuple("PlayedTrack", ["track", "playback_date", "timestamp"])
3629+LovedTrack = _namedtuple("LovedTrack", ["track", "date", "timestamp"])
3630+ImageSizes = _namedtuple("ImageSizes", ["original", "large", "largesquare", "medium", "small", "extralarge"])
3631+Image = _namedtuple("Image", ["title", "url", "dateadded", "format", "owner", "sizes", "votes"])
3632+Shout = _namedtuple("Shout", ["body", "author", "date"])
3633+
3634+def _string_output(funct):
3635+ def r(*args):
3636+ return _string(funct(*args))
3637+
3638+ return r
3639+
3640+def _pad_list(given_list, desired_length, padding = None):
3641+ """
3642+ Pads a list to be of the desired_length.
3643+ """
3644+
3645+ while len(given_list) < desired_length:
3646+ given_list.append(padding)
3647+
3648+ return given_list
3649+
3650+class _BaseObject(object):
3651+ """An abstract webservices object."""
3652+
3653+ network = None
3654+
3655+ def __init__(self, network):
3656+ self.network = network
3657+
3658+ def _request(self, method_name, cacheable = False, params = None):
3659+ if not params:
3660+ params = self._get_params()
3661+
3662+ return _Request(self.network, method_name, params).execute(cacheable)
3663+
3664+ def _get_params(self):
3665+ """Returns the most common set of parameters between all objects."""
3666+
3667+ return {}
3668+
3669+ def __hash__(self):
3670+ return hash(self.network) + \
3671+ hash(str(type(self)) + "".join(self._get_params().keys() + self._get_params().values()).lower())
3672+
3673+class _Taggable(object):
3674+ """Common functions for classes with tags."""
3675+
3676+ def __init__(self, ws_prefix):
3677+ self.ws_prefix = ws_prefix
3678+
3679+ def add_tags(self, *tags):
3680+ """Adds one or several tags.
3681+ * *tags: Any number of tag names or Tag objects.
3682+ """
3683+
3684+ for tag in tags:
3685+ self._add_tag(tag)
3686+
3687+ def _add_tag(self, tag):
3688+ """Adds one or several tags.
3689+ * tag: one tag name or a Tag object.
3690+ """
3691+
3692+ if isinstance(tag, Tag):
3693+ tag = tag.get_name()
3694+
3695+ params = self._get_params()
3696+ params['tags'] = _unicode(tag)
3697+
3698+ self._request(self.ws_prefix + '.addTags', False, params)
3699+
3700+ def _remove_tag(self, single_tag):
3701+ """Remove a user's tag from this object."""
3702+
3703+ if isinstance(single_tag, Tag):
3704+ single_tag = single_tag.get_name()
3705+
3706+ params = self._get_params()
3707+ params['tag'] = _unicode(single_tag)
3708+
3709+ self._request(self.ws_prefix + '.removeTag', False, params)
3710+
3711+ def get_tags(self):
3712+ """Returns a list of the tags set by the user to this object."""
3713+
3714+ # Uncacheable because it can be dynamically changed by the user.
3715+ params = self._get_params()
3716+
3717+ doc = self._request(self.ws_prefix + '.getTags', False, params)
3718+ tag_names = _extract_all(doc, 'name')
3719+ tags = []
3720+ for tag in tag_names:
3721+ tags.append(Tag(tag, self.network))
3722+
3723+ return tags
3724+
3725+ def remove_tags(self, *tags):
3726+ """Removes one or several tags from this object.
3727+ * *tags: Any number of tag names or Tag objects.
3728+ """
3729+
3730+ for tag in tags:
3731+ self._remove_tag(tag)
3732+
3733+ def clear_tags(self):
3734+ """Clears all the user-set tags. """
3735+
3736+ self.remove_tags(*(self.get_tags()))
3737+
3738+ def set_tags(self, *tags):
3739+ """Sets this object's tags to only those tags.
3740+ * *tags: any number of tag names.
3741+ """
3742+
3743+ c_old_tags = []
3744+ old_tags = []
3745+ c_new_tags = []
3746+ new_tags = []
3747+
3748+ to_remove = []
3749+ to_add = []
3750+
3751+ tags_on_server = self.get_tags()
3752+
3753+ for tag in tags_on_server:
3754+ c_old_tags.append(tag.get_name().lower())
3755+ old_tags.append(tag.get_name())
3756+
3757+ for tag in tags:
3758+ c_new_tags.append(tag.lower())
3759+ new_tags.append(tag)
3760+
3761+ for i in range(0, len(old_tags)):
3762+ if not c_old_tags[i] in c_new_tags:
3763+ to_remove.append(old_tags[i])
3764+
3765+ for i in range(0, len(new_tags)):
3766+ if not c_new_tags[i] in c_old_tags:
3767+ to_add.append(new_tags[i])
3768+
3769+ self.remove_tags(*to_remove)
3770+ self.add_tags(*to_add)
3771+
3772+ def get_top_tags(self, limit = None):
3773+ """Returns a list of the most frequently used Tags on this object."""
3774+
3775+ doc = self._request(self.ws_prefix + '.getTopTags', True)
3776+
3777+ elements = doc.getElementsByTagName('tag')
3778+ seq = []
3779+
3780+ for element in elements:
3781+ if limit and len(seq) >= limit:
3782+ break
3783+ tag_name = _extract(element, 'name')
3784+ tagcount = _extract(element, 'count')
3785+
3786+ seq.append(TopItem(Tag(tag_name, self.network), tagcount))
3787+
3788+ return seq
3789+
3790+class WSError(Exception):
3791+ """Exception related to the Network web service"""
3792+
3793+ def __init__(self, network, status, details):
3794+ self.status = status
3795+ self.details = details
3796+ self.network = network
3797+
3798+ @_string_output
3799+ def __str__(self):
3800+ return self.details
3801+
3802+ def get_id(self):
3803+ """Returns the exception ID, from one of the following:
3804+ STATUS_INVALID_SERVICE = 2
3805+ STATUS_INVALID_METHOD = 3
3806+ STATUS_AUTH_FAILED = 4
3807+ STATUS_INVALID_FORMAT = 5
3808+ STATUS_INVALID_PARAMS = 6
3809+ STATUS_INVALID_RESOURCE = 7
3810+ STATUS_TOKEN_ERROR = 8
3811+ STATUS_INVALID_SK = 9
3812+ STATUS_INVALID_API_KEY = 10
3813+ STATUS_OFFLINE = 11
3814+ STATUS_SUBSCRIBERS_ONLY = 12
3815+ STATUS_TOKEN_UNAUTHORIZED = 14
3816+ STATUS_TOKEN_EXPIRED = 15
3817+ """
3818+
3819+ return self.status
3820+
3821+class Album(_BaseObject, _Taggable):
3822+ """An album."""
3823+
3824+ title = None
3825+ artist = None
3826+
3827+ def __init__(self, artist, title, network):
3828+ """
3829+ Create an album instance.
3830+ # Parameters:
3831+ * artist: An artist name or an Artist object.
3832+ * title: The album title.
3833+ """
3834+
3835+ _BaseObject.__init__(self, network)
3836+ _Taggable.__init__(self, 'album')
3837+
3838+ if isinstance(artist, Artist):
3839+ self.artist = artist
3840+ else:
3841+ self.artist = Artist(artist, self.network)
3842+
3843+ self.title = title
3844+
3845+ @_string_output
3846+ def __repr__(self):
3847+ return u"%s - %s" %(self.get_artist().get_name(), self.get_title())
3848+
3849+ def __eq__(self, other):
3850+ return (self.get_title().lower() == other.get_title().lower()) and (self.get_artist().get_name().lower() == other.get_artist().get_name().lower())
3851+
3852+ def __ne__(self, other):
3853+ return (self.get_title().lower() != other.get_title().lower()) or (self.get_artist().get_name().lower() != other.get_artist().get_name().lower())
3854+
3855+ def _get_params(self):
3856+ return {'artist': self.get_artist().get_name(), 'album': self.get_title(), }
3857+
3858+ def get_artist(self):
3859+ """Returns the associated Artist object."""
3860+
3861+ return self.artist
3862+
3863+ def get_title(self):
3864+ """Returns the album title."""
3865+
3866+ return self.title
3867+
3868+ def get_name(self):
3869+ """Returns the album title (alias to Album.get_title)."""
3870+
3871+ return self.get_title()
3872+
3873+ def get_release_date(self):
3874+ """Retruns the release date of the album."""
3875+
3876+ return _extract(self._request("album.getInfo", cacheable = True), "releasedate")
3877+
3878+ def get_cover_image(self, size = COVER_EXTRA_LARGE):
3879+ """
3880+ Returns a uri to the cover image
3881+ size can be one of:
3882+ COVER_MEGA
3883+ COVER_EXTRA_LARGE
3884+ COVER_LARGE
3885+ COVER_MEDIUM
3886+ COVER_SMALL
3887+ """
3888+
3889+ return _extract_all(self._request("album.getInfo", cacheable = True), 'image')[size]
3890+
3891+ def get_id(self):
3892+ """Returns the ID"""
3893+
3894+ return _extract(self._request("album.getInfo", cacheable = True), "id")
3895+
3896+ def get_playcount(self):
3897+ """Returns the number of plays on the network"""
3898+
3899+ return _number(_extract(self._request("album.getInfo", cacheable = True), "playcount"))
3900+
3901+ def get_listener_count(self):
3902+ """Returns the number of liteners on the network"""
3903+
3904+ return _number(_extract(self._request("album.getInfo", cacheable = True), "listeners"))
3905+
3906+ def get_top_tags(self, limit=None):
3907+ """Returns a list of the most-applied tags to this album."""
3908+
3909+ doc = self._request("album.getInfo", True)
3910+ e = doc.getElementsByTagName("toptags")[0]
3911+
3912+ seq = []
3913+ for name in _extract_all(e, "name"):
3914+ if len(seq) < limit:
3915+ seq.append(Tag(name, self.network))
3916+
3917+ return seq
3918+
3919+ def get_tracks(self):
3920+ """Returns the list of Tracks on this album."""
3921+
3922+ uri = 'lastfm://playlist/album/%s' %self.get_id()
3923+
3924+ return XSPF(uri, self.network).get_tracks()
3925+
3926+ def get_mbid(self):
3927+ """Returns the MusicBrainz id of the album."""
3928+
3929+ return _extract(self._request("album.getInfo", cacheable = True), "mbid")
3930+
3931+ def get_url(self, domain_name = DOMAIN_ENGLISH):
3932+ """Returns the url of the album page on the network.
3933+ # Parameters:
3934+ * domain_name str: The network's language domain. Possible values:
3935+ o DOMAIN_ENGLISH
3936+ o DOMAIN_GERMAN
3937+ o DOMAIN_SPANISH
3938+ o DOMAIN_FRENCH
3939+ o DOMAIN_ITALIAN
3940+ o DOMAIN_POLISH
3941+ o DOMAIN_PORTUGUESE
3942+ o DOMAIN_SWEDISH
3943+ o DOMAIN_TURKISH
3944+ o DOMAIN_RUSSIAN
3945+ o DOMAIN_JAPANESE
3946+ o DOMAIN_CHINESE
3947+ """
3948+
3949+ artist = _url_safe(self.get_artist().get_name())
3950+ album = _url_safe(self.get_title())
3951+
3952+ return self.network._get_url(domain_name, "album") %{'artist': artist, 'album': album}
3953+
3954+ def get_wiki_published_date(self):
3955+ """Returns the date of publishing this version of the wiki."""
3956+
3957+ doc = self._request("album.getInfo", True)
3958+
3959+ if len(doc.getElementsByTagName("wiki")) == 0:
3960+ return
3961+
3962+ node = doc.getElementsByTagName("wiki")[0]
3963+
3964+ return _extract(node, "published")
3965+
3966+ def get_wiki_summary(self):
3967+ """Returns the summary of the wiki."""
3968+
3969+ doc = self._request("album.getInfo", True)
3970+
3971+ if len(doc.getElementsByTagName("wiki")) == 0:
3972+ return
3973+
3974+ node = doc.getElementsByTagName("wiki")[0]
3975+
3976+ return _extract(node, "summary")
3977+
3978+ def get_wiki_content(self):
3979+ """Returns the content of the wiki."""
3980+
3981+ doc = self._request("album.getInfo", True)
3982+
3983+ if len(doc.getElementsByTagName("wiki")) == 0:
3984+ return
3985+
3986+ node = doc.getElementsByTagName("wiki")[0]
3987+
3988+ return _extract(node, "content")
3989+
3990+class Artist(_BaseObject, _Taggable):
3991+ """An artist."""
3992+
3993+ name = None
3994+
3995+ def __init__(self, name, network):
3996+ """Create an artist object.
3997+ # Parameters:
3998+ * name str: The artist's name.
3999+ """
4000+
4001+ _BaseObject.__init__(self, network)
4002+ _Taggable.__init__(self, 'artist')
4003+
4004+ self.name = name
4005+
4006+ @_string_output
4007+ def __repr__(self):
4008+ return self.get_name()
4009+
4010+ def __eq__(self, other):
4011+ return self.get_name().lower() == other.get_name().lower()
4012+
4013+ def __ne__(self, other):
4014+ return self.get_name().lower() != other.get_name().lower()
4015+
4016+ def _get_params(self):
4017+ return {'artist': self.get_name()}
4018+
4019+ def get_name(self):
4020+ """Returns the name of the artist."""
4021+
4022+ return self.name
4023+
4024+ def get_cover_image(self, size = COVER_LARGE):
4025+ """
4026+ Returns a uri to the cover image
4027+ size can be one of:
4028+ COVER_MEGA
4029+ COVER_EXTRA_LARGE
4030+ COVER_LARGE
4031+ COVER_MEDIUM
4032+ COVER_SMALL
4033+ """
4034+
4035+ return _extract_all(self._request("artist.getInfo", True), "image")[size]
4036+
4037+ def get_playcount(self):
4038+ """Returns the number of plays on the network."""
4039+
4040+ return _number(_extract(self._request("artist.getInfo", True), "playcount"))
4041+
4042+ def get_mbid(self):
4043+ """Returns the MusicBrainz ID of this artist."""
4044+
4045+ doc = self._request("artist.getInfo", True)
4046+
4047+ return _extract(doc, "mbid")
4048+
4049+ def get_listener_count(self):
4050+ """Returns the number of liteners on the network."""
4051+
4052+ return _number(_extract(self._request("artist.getInfo", True), "listeners"))
4053+
4054+ def is_streamable(self):
4055+ """Returns True if the artist is streamable."""
4056+
4057+ return bool(_number(_extract(self._request("artist.getInfo", True), "streamable")))
4058+
4059+ def get_bio_published_date(self):
4060+ """Returns the date on which the artist's biography was published."""
4061+
4062+ return _extract(self._request("artist.getInfo", True), "published")
4063+
4064+ def get_bio_summary(self):
4065+ """Returns the summary of the artist's biography."""
4066+
4067+ return _extract(self._request("artist.getInfo", True), "summary")
4068+
4069+ def get_bio_content(self):
4070+ """Returns the content of the artist's biography."""
4071+
4072+ return _extract(self._request("artist.getInfo", True), "content")
4073+
4074+ def get_upcoming_events(self):
4075+ """Returns a list of the upcoming Events for this artist."""
4076+
4077+ doc = self._request('artist.getEvents', True)
4078+
4079+ ids = _extract_all(doc, 'id')
4080+
4081+ events = []
4082+ for e_id in ids:
4083+ events.append(Event(e_id, self.network))
4084+
4085+ return events
4086+
4087+ def get_similar(self, limit = None):
4088+ """Returns the similar artists on the network."""
4089+
4090+ params = self._get_params()
4091+ if limit:
4092+ params['limit'] = _unicode(limit)
4093+
4094+ doc = self._request('artist.getSimilar', True, params)
4095+
4096+ names = _extract_all(doc, "name")
4097+ matches = _extract_all(doc, "match")
4098+
4099+ artists = []
4100+ for i in range(0, len(names)):
4101+ artists.append(SimilarItem(Artist(names[i], self.network), _number(matches[i])))
4102+
4103+ return artists
4104+
4105+ def get_top_albums(self):
4106+ """Retuns a list of the top albums."""
4107+
4108+ doc = self._request('artist.getTopAlbums', True)
4109+
4110+ seq = []
4111+
4112+ for node in doc.getElementsByTagName("album"):
4113+ name = _extract(node, "name")
4114+ artist = _extract(node, "name", 1)
4115+ playcount = _extract(node, "playcount")
4116+
4117+ seq.append(TopItem(Album(artist, name, self.network), playcount))
4118+
4119+ return seq
4120+
4121+ def get_top_tracks(self):
4122+ """Returns a list of the most played Tracks by this artist."""
4123+
4124+ doc = self._request("artist.getTopTracks", True)
4125+
4126+ seq = []
4127+ for track in doc.getElementsByTagName('track'):
4128+
4129+ title = _extract(track, "name")
4130+ artist = _extract(track, "name", 1)
4131+ playcount = _number(_extract(track, "playcount"))
4132+
4133+ seq.append( TopItem(Track(artist, title, self.network), playcount) )
4134+
4135+ return seq
4136+
4137+ def get_top_fans(self, limit = None):
4138+ """Returns a list of the Users who played this artist the most.
4139+ # Parameters:
4140+ * limit int: Max elements.
4141+ """
4142+
4143+ doc = self._request('artist.getTopFans', True)
4144+
4145+ seq = []
4146+
4147+ elements = doc.getElementsByTagName('user')
4148+
4149+ for element in elements:
4150+ if limit and len(seq) >= limit:
4151+ break
4152+
4153+ name = _extract(element, 'name')
4154+ weight = _number(_extract(element, 'weight'))
4155+
4156+ seq.append(TopItem(User(name, self.network), weight))
4157+
4158+ return seq
4159+
4160+ def share(self, users, message = None):
4161+ """Shares this artist (sends out recommendations).
4162+ # Parameters:
4163+ * users [User|str,]: A list that can contain usernames, emails, User objects, or all of them.
4164+ * message str: A message to include in the recommendation message.
4165+ """
4166+
4167+ #last.fm currently accepts a max of 10 recipient at a time
4168+ while(len(users) > 10):
4169+ section = users[0:9]
4170+ users = users[9:]
4171+ self.share(section, message)
4172+
4173+ nusers = []
4174+ for user in users:
4175+ if isinstance(user, User):
4176+ nusers.append(user.get_name())
4177+ else:
4178+ nusers.append(user)
4179+
4180+ params = self._get_params()
4181+ recipients = ','.join(nusers)
4182+ params['recipient'] = recipients
4183+ if message: params['message'] = _unicode(message)
4184+
4185+ self._request('artist.share', False, params)
4186+
4187+ def get_url(self, domain_name = DOMAIN_ENGLISH):
4188+ """Returns the url of the artist page on the network.
4189+ # Parameters:
4190+ * domain_name: The network's language domain. Possible values:
4191+ o DOMAIN_ENGLISH
4192+ o DOMAIN_GERMAN
4193+ o DOMAIN_SPANISH
4194+ o DOMAIN_FRENCH
4195+ o DOMAIN_ITALIAN
4196+ o DOMAIN_POLISH
4197+ o DOMAIN_PORTUGUESE
4198+ o DOMAIN_SWEDISH
4199+ o DOMAIN_TURKISH
4200+ o DOMAIN_RUSSIAN
4201+ o DOMAIN_JAPANESE
4202+ o DOMAIN_CHINESE
4203+ """
4204+
4205+ artist = _url_safe(self.get_name())
4206+
4207+ return self.network._get_url(domain_name, "artist") %{'artist': artist}
4208+
4209+ def get_images(self, order=IMAGES_ORDER_POPULARITY, limit=None):
4210+ """
4211+ Returns a sequence of Image objects
4212+ if limit is None it will return all
4213+ order can be IMAGES_ORDER_POPULARITY or IMAGES_ORDER_DATE
4214+ """
4215+
4216+ images = []
4217+
4218+ params = self._get_params()
4219+ params["order"] = order
4220+ nodes = _collect_nodes(limit, self, "artist.getImages", True, params)
4221+ for e in nodes:
4222+ if _extract(e, "name"):
4223+ user = User(_extract(e, "name"), self.network)
4224+ else:
4225+ user = None
4226+
4227+ images.append(Image(
4228+ _extract(e, "title"),
4229+ _extract(e, "url"),
4230+ _extract(e, "dateadded"),
4231+ _extract(e, "format"),
4232+ user,
4233+ ImageSizes(*_extract_all(e, "size")),
4234+ (_extract(e, "thumbsup"), _extract(e, "thumbsdown"))
4235+ )
4236+ )
4237+ return images
4238+
4239+ def get_shouts(self, limit=50):
4240+ """
4241+ Returns a sequqence of Shout objects
4242+ """
4243+
4244+ shouts = []
4245+ for node in _collect_nodes(limit, self, "artist.getShouts", False):
4246+ shouts.append(Shout(
4247+ _extract(node, "body"),
4248+ User(_extract(node, "author"), self.network),
4249+ _extract(node, "date")
4250+ )
4251+ )
4252+ return shouts
4253+
4254+ def shout(self, message):
4255+ """
4256+ Post a shout
4257+ """
4258+
4259+ params = self._get_params()
4260+ params["message"] = message
4261+
4262+ self._request("artist.Shout", False, params)
4263+
4264+
4265+class Event(_BaseObject):
4266+ """An event."""
4267+
4268+ id = None
4269+
4270+ def __init__(self, event_id, network):
4271+ _BaseObject.__init__(self, network)
4272+
4273+ self.id = _unicode(event_id)
4274+
4275+ @_string_output
4276+ def __repr__(self):
4277+ return "Event #" + self.get_id()
4278+
4279+ def __eq__(self, other):
4280+ return self.get_id() == other.get_id()
4281+
4282+ def __ne__(self, other):
4283+ return self.get_id() != other.get_id()
4284+
4285+ def _get_params(self):
4286+ return {'event': self.get_id()}
4287+
4288+ def attend(self, attending_status):
4289+ """Sets the attending status.
4290+ * attending_status: The attending status. Possible values:
4291+ o EVENT_ATTENDING
4292+ o EVENT_MAYBE_ATTENDING
4293+ o EVENT_NOT_ATTENDING
4294+ """
4295+
4296+ params = self._get_params()
4297+ params['status'] = _unicode(attending_status)
4298+
4299+ self._request('event.attend', False, params)
4300+
4301+ def get_attendees(self):
4302+ """
4303+ Get a list of attendees for an event
4304+ """
4305+
4306+ doc = self._request("event.getAttendees", False)
4307+
4308+ users = []
4309+ for name in _extract_all(doc, "name"):
4310+ users.append(User(name, self.network))
4311+
4312+ return users
4313+
4314+ def get_id(self):
4315+ """Returns the id of the event on the network. """
4316+
4317+ return self.id
4318+
4319+ def get_title(self):
4320+ """Returns the title of the event. """
4321+
4322+ doc = self._request("event.getInfo", True)
4323+
4324+ return _extract(doc, "title")
4325+
4326+ def get_headliner(self):
4327+ """Returns the headliner of the event. """
4328+
4329+ doc = self._request("event.getInfo", True)
4330+
4331+ return Artist(_extract(doc, "headliner"), self.network)
4332+
4333+ def get_artists(self):
4334+ """Returns a list of the participating Artists. """
4335+
4336+ doc = self._request("event.getInfo", True)
4337+ names = _extract_all(doc, "artist")
4338+
4339+ artists = []
4340+ for name in names:
4341+ artists.append(Artist(name, self.network))
4342+
4343+ return artists
4344+
4345+ def get_venue(self):
4346+ """Returns the venue where the event is held."""
4347+
4348+ doc = self._request("event.getInfo", True)
4349+
4350+ v = doc.getElementsByTagName("venue")[0]
4351+ venue_id = _number(_extract(v, "id"))
4352+
4353+ return Venue(venue_id, self.network)
4354+
4355+ def get_start_date(self):
4356+ """Returns the date when the event starts."""
4357+
4358+ doc = self._request("event.getInfo", True)
4359+
4360+ return _extract(doc, "startDate")
4361+
4362+ def get_description(self):
4363+ """Returns the description of the event. """
4364+
4365+ doc = self._request("event.getInfo", True)
4366+
4367+ return _extract(doc, "description")
4368+
4369+ def get_cover_image(self, size = COVER_LARGE):
4370+ """
4371+ Returns a uri to the cover image
4372+ size can be one of:
4373+ COVER_MEGA
4374+ COVER_EXTRA_LARGE
4375+ COVER_LARGE
4376+ COVER_MEDIUM
4377+ COVER_SMALL
4378+ """
4379+
4380+ doc = self._request("event.getInfo", True)
4381+
4382+ return _extract_all(doc, "image")[size]
4383+
4384+ def get_attendance_count(self):
4385+ """Returns the number of attending people. """
4386+
4387+ doc = self._request("event.getInfo", True)
4388+
4389+ return _number(_extract(doc, "attendance"))
4390+
4391+ def get_review_count(self):
4392+ """Returns the number of available reviews for this event. """
4393+
4394+ doc = self._request("event.getInfo", True)
4395+
4396+ return _number(_extract(doc, "reviews"))
4397+
4398+ def get_url(self, domain_name = DOMAIN_ENGLISH):
4399+ """Returns the url of the event page on the network.
4400+ * domain_name: The network's language domain. Possible values:
4401+ o DOMAIN_ENGLISH
4402+ o DOMAIN_GERMAN
4403+ o DOMAIN_SPANISH
4404+ o DOMAIN_FRENCH
4405+ o DOMAIN_ITALIAN
4406+ o DOMAIN_POLISH
4407+ o DOMAIN_PORTUGUESE
4408+ o DOMAIN_SWEDISH
4409+ o DOMAIN_TURKISH
4410+ o DOMAIN_RUSSIAN
4411+ o DOMAIN_JAPANESE
4412+ o DOMAIN_CHINESE
4413+ """
4414+
4415+ return self.network._get_url(domain_name, "event") %{'id': self.get_id()}
4416+
4417+ def share(self, users, message = None):
4418+ """Shares this event (sends out recommendations).
4419+ * users: A list that can contain usernames, emails, User objects, or all of them.
4420+ * message: A message to include in the recommendation message.
4421+ """
4422+
4423+ #last.fm currently accepts a max of 10 recipient at a time
4424+ while(len(users) > 10):
4425+ section = users[0:9]
4426+ users = users[9:]
4427+ self.share(section, message)
4428+
4429+ nusers = []
4430+ for user in users:
4431+ if isinstance(user, User):
4432+ nusers.append(user.get_name())
4433+ else:
4434+ nusers.append(user)
4435+
4436+ params = self._get_params()
4437+ recipients = ','.join(nusers)
4438+ params['recipient'] = recipients
4439+ if message: params['message'] = _unicode(message)
4440+
4441+ self._request('event.share', False, params)
4442+
4443+ def get_shouts(self, limit=50):
4444+ """
4445+ Returns a sequqence of Shout objects
4446+ """
4447+
4448+ shouts = []
4449+ for node in _collect_nodes(limit, self, "event.getShouts", False):
4450+ shouts.append(Shout(
4451+ _extract(node, "body"),
4452+ User(_extract(node, "author"), self.network),
4453+ _extract(node, "date")
4454+ )
4455+ )
4456+ return shouts
4457+
4458+ def shout(self, message):
4459+ """
4460+ Post a shout
4461+ """
4462+
4463+ params = self._get_params()
4464+ params["message"] = message
4465+
4466+ self._request("event.Shout", False, params)
4467+
4468+class Country(_BaseObject):
4469+ """A country at Last.fm."""
4470+
4471+ name = None
4472+
4473+ def __init__(self, name, network):
4474+ _BaseObject.__init__(self, network)
4475+
4476+ self.name = name
4477+
4478+ @_string_output
4479+ def __repr__(self):
4480+ return self.get_name()
4481+
4482+ def __eq__(self, other):
4483+ return self.get_name().lower() == other.get_name().lower()
4484+
4485+ def __ne__(self, other):
4486+ return self.get_name() != other.get_name()
4487+
4488+ def _get_params(self):
4489+ return {'country': self.get_name()}
4490+
4491+ def _get_name_from_code(self, alpha2code):
4492+ # TODO: Have this function lookup the alpha-2 code and return the country name.
4493+
4494+ return alpha2code
4495+
4496+ def get_name(self):
4497+ """Returns the country name. """
4498+
4499+ return self.name
4500+
4501+ def get_top_artists(self):
4502+ """Returns a sequence of the most played artists."""
4503+
4504+ doc = self._request('geo.getTopArtists', True)
4505+
4506+ seq = []
4507+ for node in doc.getElementsByTagName("artist"):
4508+ name = _extract(node, 'name')
4509+ playcount = _extract(node, "playcount")
4510+
4511+ seq.append(TopItem(Artist(name, self.network), playcount))
4512+
4513+ return seq
4514+
4515+ def get_top_tracks(self):
4516+ """Returns a sequence of the most played tracks"""
4517+
4518+ doc = self._request("geo.getTopTracks", True)
4519+
4520+ seq = []
4521+
4522+ for n in doc.getElementsByTagName('track'):
4523+
4524+ title = _extract(n, 'name')
4525+ artist = _extract(n, 'name', 1)
4526+ playcount = _number(_extract(n, "playcount"))
4527+
4528+ seq.append( TopItem(Track(artist, title, self.network), playcount))
4529+
4530+ return seq
4531+
4532+ def get_url(self, domain_name = DOMAIN_ENGLISH):
4533+ """Returns the url of the event page on the network.
4534+ * domain_name: The network's language domain. Possible values:
4535+ o DOMAIN_ENGLISH
4536+ o DOMAIN_GERMAN
4537+ o DOMAIN_SPANISH
4538+ o DOMAIN_FRENCH
4539+ o DOMAIN_ITALIAN
4540+ o DOMAIN_POLISH
4541+ o DOMAIN_PORTUGUESE
4542+ o DOMAIN_SWEDISH
4543+ o DOMAIN_TURKISH
4544+ o DOMAIN_RUSSIAN
4545+ o DOMAIN_JAPANESE
4546+ o DOMAIN_CHINESE
4547+ """
4548+
4549+ country_name = _url_safe(self.get_name())
4550+
4551+ return self.network._get_url(domain_name, "country") %{'country_name': country_name}
4552+
4553+
4554+class Library(_BaseObject):
4555+ """A user's Last.fm library."""
4556+
4557+ user = None
4558+
4559+ def __init__(self, user, network):
4560+ _BaseObject.__init__(self, network)
4561+
4562+ if isinstance(user, User):
4563+ self.user = user
4564+ else:
4565+ self.user = User(user, self.network)
4566+
4567+ self._albums_index = 0
4568+ self._artists_index = 0
4569+ self._tracks_index = 0
4570+
4571+ @_string_output
4572+ def __repr__(self):
4573+ return repr(self.get_user()) + "'s Library"
4574+
4575+ def _get_params(self):
4576+ return {'user': self.user.get_name()}
4577+
4578+ def get_user(self):
4579+ """Returns the user who owns this library."""
4580+
4581+ return self.user
4582+
4583+ def add_album(self, album):
4584+ """Add an album to this library."""
4585+
4586+ params = self._get_params()
4587+ params["artist"] = album.get_artist.get_name()
4588+ params["album"] = album.get_name()
4589+
4590+ self._request("library.addAlbum", False, params)
4591+
4592+ def add_artist(self, artist):
4593+ """Add an artist to this library."""
4594+
4595+ params = self._get_params()
4596+ params["artist"] = artist.get_name()
4597+
4598+ self._request("library.addArtist", False, params)
4599+
4600+ def add_track(self, track):
4601+ """Add a track to this library."""
4602+
4603+ params = self._get_params()
4604+ params["track"] = track.get_title()
4605+
4606+ self._request("library.addTrack", False, params)
4607+
4608+ def get_albums(self, limit=50):
4609+ """
4610+ Returns a sequence of Album objects
4611+ if limit==None it will return all (may take a while)
4612+ """
4613+
4614+ seq = []
4615+ for node in _collect_nodes(limit, self, "library.getAlbums", True):
4616+ name = _extract(node, "name")
4617+ artist = _extract(node, "name", 1)
4618+ playcount = _number(_extract(node, "playcount"))
4619+ tagcount = _number(_extract(node, "tagcount"))
4620+
4621+ seq.append(LibraryItem(Album(artist, name, self.network), playcount, tagcount))
4622+
4623+ return seq
4624+
4625+ def get_artists(self, limit=50):
4626+ """
4627+ Returns a sequence of Album objects
4628+ if limit==None it will return all (may take a while)
4629+ """
4630+
4631+ seq = []
4632+ for node in _collect_nodes(limit, self, "library.getArtists", True):
4633+ name = _extract(node, "name")
4634+
4635+ playcount = _number(_extract(node, "playcount"))
4636+ tagcount = _number(_extract(node, "tagcount"))
4637+
4638+ seq.append(LibraryItem(Artist(name, self.network), playcount, tagcount))
4639+
4640+ return seq
4641+
4642+ def get_tracks(self, limit=50):
4643+ """
4644+ Returns a sequence of Album objects
4645+ if limit==None it will return all (may take a while)
4646+ """
4647+
4648+ seq = []
4649+ for node in _collect_nodes(limit, self, "library.getTracks", True):
4650+ name = _extract(node, "name")
4651+ artist = _extract(node, "name", 1)
4652+ playcount = _number(_extract(node, "playcount"))
4653+ tagcount = _number(_extract(node, "tagcount"))
4654+
4655+ seq.append(LibraryItem(Track(artist, name, self.network), playcount, tagcount))
4656+
4657+ return seq
4658+
4659+
4660+class Playlist(_BaseObject):
4661+ """A Last.fm user playlist."""
4662+
4663+ id = None
4664+ user = None
4665+
4666+ def __init__(self, user, id, network):
4667+ _BaseObject.__init__(self, network)
4668+
4669+ if isinstance(user, User):
4670+ self.user = user
4671+ else:
4672+ self.user = User(user, self.network)
4673+
4674+ self.id = _unicode(id)
4675+
4676+ @_string_output
4677+ def __repr__(self):
4678+ return repr(self.user) + "'s playlist # " + repr(self.id)
4679+
4680+ def _get_info_node(self):
4681+ """Returns the node from user.getPlaylists where this playlist's info is."""
4682+
4683+ doc = self._request("user.getPlaylists", True)
4684+
4685+ for node in doc.getElementsByTagName("playlist"):
4686+ if _extract(node, "id") == str(self.get_id()):
4687+ return node
4688+
4689+ def _get_params(self):
4690+ return {'user': self.user.get_name(), 'playlistID': self.get_id()}
4691+
4692+ def get_id(self):
4693+ """Returns the playlist id."""
4694+
4695+ return self.id
4696+
4697+ def get_user(self):
4698+ """Returns the owner user of this playlist."""
4699+
4700+ return self.user
4701+
4702+ def get_tracks(self):
4703+ """Returns a list of the tracks on this user playlist."""
4704+
4705+ uri = u'lastfm://playlist/%s' %self.get_id()
4706+
4707+ return XSPF(uri, self.network).get_tracks()
4708+
4709+ def add_track(self, track):
4710+ """Adds a Track to this Playlist."""
4711+
4712+ params = self._get_params()
4713+ params['artist'] = track.get_artist().get_name()
4714+ params['track'] = track.get_title()
4715+
4716+ self._request('playlist.addTrack', False, params)
4717+
4718+ def get_title(self):
4719+ """Returns the title of this playlist."""
4720+
4721+ return _extract(self._get_info_node(), "title")
4722+
4723+ def get_creation_date(self):
4724+ """Returns the creation date of this playlist."""
4725+
4726+ return _extract(self._get_info_node(), "date")
4727+
4728+ def get_size(self):
4729+ """Returns the number of tracks in this playlist."""
4730+
4731+ return _number(_extract(self._get_info_node(), "size"))
4732+
4733+ def get_description(self):
4734+ """Returns the description of this playlist."""
4735+
4736+ return _extract(self._get_info_node(), "description")
4737+
4738+ def get_duration(self):
4739+ """Returns the duration of this playlist in milliseconds."""
4740+
4741+ return _number(_extract(self._get_info_node(), "duration"))
4742+
4743+ def is_streamable(self):
4744+ """Returns True if the playlist is streamable.
4745+ For a playlist to be streamable, it needs at least 45 tracks by 15 different artists."""
4746+
4747+ if _extract(self._get_info_node(), "streamable") == '1':
4748+ return True
4749+ else:
4750+ return False
4751+
4752+ def has_track(self, track):
4753+ """Checks to see if track is already in the playlist.
4754+ * track: Any Track object.
4755+ """
4756+
4757+ return track in self.get_tracks()
4758+
4759+ def get_cover_image(self, size = COVER_LARGE):
4760+ """
4761+ Returns a uri to the cover image
4762+ size can be one of:
4763+ COVER_MEGA
4764+ COVER_EXTRA_LARGE
4765+ COVER_LARGE
4766+ COVER_MEDIUM
4767+ COVER_SMALL
4768+ """
4769+
4770+ return _extract(self._get_info_node(), "image")[size]
4771+
4772+ def get_url(self, domain_name = DOMAIN_ENGLISH):
4773+ """Returns the url of the playlist on the network.
4774+ * domain_name: The network's language domain. Possible values:
4775+ o DOMAIN_ENGLISH
4776+ o DOMAIN_GERMAN
4777+ o DOMAIN_SPANISH
4778+ o DOMAIN_FRENCH
4779+ o DOMAIN_ITALIAN
4780+ o DOMAIN_POLISH
4781+ o DOMAIN_PORTUGUESE
4782+ o DOMAIN_SWEDISH
4783+ o DOMAIN_TURKISH
4784+ o DOMAIN_RUSSIAN
4785+ o DOMAIN_JAPANESE
4786+ o DOMAIN_CHINESE
4787+ """
4788+
4789+ english_url = _extract(self._get_info_node(), "url")
4790+ appendix = english_url[english_url.rfind("/") + 1:]
4791+
4792+ return self.network._get_url(domain_name, "playlist") %{'appendix': appendix, "user": self.get_user().get_name()}
4793+
4794+
4795+class Tag(_BaseObject):
4796+ """A Last.fm object tag."""
4797+
4798+ # TODO: getWeeklyArtistChart (too lazy, i'll wait for when someone requests it)
4799+
4800+ name = None
4801+
4802+ def __init__(self, name, network):
4803+ _BaseObject.__init__(self, network)
4804+
4805+ self.name = name
4806+
4807+ def _get_params(self):
4808+ return {'tag': self.get_name()}
4809+
4810+ @_string_output
4811+ def __repr__(self):
4812+ return self.get_name()
4813+
4814+ def __eq__(self, other):
4815+ return self.get_name().lower() == other.get_name().lower()
4816+
4817+ def __ne__(self, other):
4818+ return self.get_name().lower() != other.get_name().lower()
4819+
4820+ def get_name(self):
4821+ """Returns the name of the tag. """
4822+
4823+ return self.name
4824+
4825+ def get_similar(self):
4826+ """Returns the tags similar to this one, ordered by similarity. """
4827+
4828+ doc = self._request('tag.getSimilar', True)
4829+
4830+ seq = []
4831+ names = _extract_all(doc, 'name')
4832+ for name in names:
4833+ seq.append(Tag(name, self.network))
4834+
4835+ return seq
4836+
4837+ def get_top_albums(self):
4838+ """Retuns a list of the top albums."""
4839+
4840+ doc = self._request('tag.getTopAlbums', True)
4841+
4842+ seq = []
4843+
4844+ for node in doc.getElementsByTagName("album"):
4845+ name = _extract(node, "name")
4846+ artist = _extract(node, "name", 1)
4847+ playcount = _extract(node, "playcount")
4848+
4849+ seq.append(TopItem(Album(artist, name, self.network), playcount))
4850+
4851+ return seq
4852+
4853+ def get_top_tracks(self):
4854+ """Returns a list of the most played Tracks by this artist."""
4855+
4856+ doc = self._request("tag.getTopTracks", True)
4857+
4858+ seq = []
4859+ for track in doc.getElementsByTagName('track'):
4860+
4861+ title = _extract(track, "name")
4862+ artist = _extract(track, "name", 1)
4863+ playcount = _number(_extract(track, "playcount"))
4864+
4865+ seq.append( TopItem(Track(artist, title, self.network), playcount) )
4866+
4867+ return seq
4868+
4869+ def get_top_artists(self):
4870+ """Returns a sequence of the most played artists."""
4871+
4872+ doc = self._request('tag.getTopArtists', True)
4873+
4874+ seq = []
4875+ for node in doc.getElementsByTagName("artist"):
4876+ name = _extract(node, 'name')
4877+ playcount = _extract(node, "playcount")
4878+
4879+ seq.append(TopItem(Artist(name, self.network), playcount))
4880+
4881+ return seq
4882+
4883+ def get_weekly_chart_dates(self):
4884+ """Returns a list of From and To tuples for the available charts."""
4885+
4886+ doc = self._request("tag.getWeeklyChartList", True)
4887+
4888+ seq = []
4889+ for node in doc.getElementsByTagName("chart"):
4890+ seq.append( (node.getAttribute("from"), node.getAttribute("to")) )
4891+
4892+ return seq
4893+
4894+ def get_weekly_artist_charts(self, from_date = None, to_date = None):
4895+ """Returns the weekly artist charts for the week starting from the from_date value to the to_date value."""
4896+
4897+ params = self._get_params()
4898+ if from_date and to_date:
4899+ params["from"] = from_date
4900+ params["to"] = to_date
4901+
4902+ doc = self._request("tag.getWeeklyArtistChart", True, params)
4903+
4904+ seq = []
4905+ for node in doc.getElementsByTagName("artist"):
4906+ item = Artist(_extract(node, "name"), self.network)
4907+ weight = _number(_extract(node, "weight"))
4908+ seq.append(TopItem(item, weight))
4909+
4910+ return seq
4911+
4912+ def get_url(self, domain_name = DOMAIN_ENGLISH):
4913+ """Returns the url of the tag page on the network.
4914+ * domain_name: The network's language domain. Possible values:
4915+ o DOMAIN_ENGLISH
4916+ o DOMAIN_GERMAN
4917+ o DOMAIN_SPANISH
4918+ o DOMAIN_FRENCH
4919+ o DOMAIN_ITALIAN
4920+ o DOMAIN_POLISH
4921+ o DOMAIN_PORTUGUESE
4922+ o DOMAIN_SWEDISH
4923+ o DOMAIN_TURKISH
4924+ o DOMAIN_RUSSIAN
4925+ o DOMAIN_JAPANESE
4926+ o DOMAIN_CHINESE
4927+ """
4928+
4929+ name = _url_safe(self.get_name())
4930+
4931+ return self.network._get_url(domain_name, "tag") %{'name': name}
4932+
4933+class Track(_BaseObject, _Taggable):
4934+ """A Last.fm track."""
4935+
4936+ artist = None
4937+ title = None
4938+
4939+ def __init__(self, artist, title, network):
4940+ _BaseObject.__init__(self, network)
4941+ _Taggable.__init__(self, 'track')
4942+
4943+ if isinstance(artist, Artist):
4944+ self.artist = artist
4945+ else:
4946+ self.artist = Artist(artist, self.network)
4947+
4948+ self.title = title
4949+
4950+ @_string_output
4951+ def __repr__(self):
4952+ return self.get_artist().get_name() + ' - ' + self.get_title()
4953+
4954+ def __eq__(self, other):
4955+ return (self.get_title().lower() == other.get_title().lower()) and (self.get_artist().get_name().lower() == other.get_artist().get_name().lower())
4956+
4957+ def __ne__(self, other):
4958+ return (self.get_title().lower() != other.get_title().lower()) or (self.get_artist().get_name().lower() != other.get_artist().get_name().lower())
4959+
4960+ def _get_params(self):
4961+ return {'artist': self.get_artist().get_name(), 'track': self.get_title()}
4962+
4963+ def get_artist(self):
4964+ """Returns the associated Artist object."""
4965+
4966+ return self.artist
4967+
4968+ def get_title(self):
4969+ """Returns the track title."""
4970+
4971+ return self.title
4972+
4973+ def get_name(self):
4974+ """Returns the track title (alias to Track.get_title)."""
4975+
4976+ return self.get_title()
4977+
4978+ def get_id(self):
4979+ """Returns the track id on the network."""
4980+
4981+ doc = self._request("track.getInfo", True)
4982+
4983+ return _extract(doc, "id")
4984+
4985+ def get_duration(self):
4986+ """Returns the track duration."""
4987+
4988+ doc = self._request("track.getInfo", True)
4989+
4990+ return _number(_extract(doc, "duration"))
4991+
4992+ def get_mbid(self):
4993+ """Returns the MusicBrainz ID of this track."""
4994+
4995+ doc = self._request("track.getInfo", True)
4996+
4997+ return _extract(doc, "mbid")
4998+
4999+ def get_listener_count(self):
5000+ """Returns the listener count."""
The diff has been truncated for viewing.