Merge lp:~smasher816/pithos/pithos-pause into lp:~kevin-mehall/pithos/trunk
- pithos-pause
- Merge into 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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Kevin Mehall | Pending | ||
Review via email: mp+96936@code.launchpad.net |
Commit message
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/
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.