├── .gitignore ├── README.md └── pypandora.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | /.project 3 | /.pydevproject 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **PyPandora is longer actively maintained. There is still some good examples in the code (async webserver using only python builtins, streaming music to an html5 app), but the Pandora ad-hoc api is no longer functioning. Thanks for enjoying!** 2 | 3 | PyPandora is a cross-platform Pandora Radio player in a single python script. 4 | 5 | http://amoffat.github.com/pypandora 6 | 7 | Quickstart: 8 | 9 | $ python pypandora.py 10 | 11 | Result: 12 | 13 | ![Screenshot](http://i.imgur.com/Vo3kE.jpg) 14 | -------------------------------------------------------------------------------- /pypandora.py: -------------------------------------------------------------------------------- 1 | #=============================================================================== 2 | # Copyright (C) 2011 by Andrew Moffat 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | #=============================================================================== 22 | 23 | import time 24 | from xml.etree import ElementTree 25 | import xml.dom.minidom 26 | from xml.sax.saxutils import escape as xml_escape 27 | from string import Template 28 | import httplib 29 | import urllib 30 | from os.path import join, abspath, dirname, exists 31 | import re 32 | from urlparse import urlsplit 33 | import socket 34 | import logging 35 | import math 36 | from optparse import OptionParser 37 | from tempfile import gettempdir 38 | import struct 39 | from ctypes import c_uint32 40 | from pprint import pprint 41 | import select 42 | import errno 43 | import sys 44 | try: import simplejson as json 45 | except ImportError: import json 46 | from Queue import Queue 47 | from base64 import b64decode, b64encode 48 | import zlib 49 | from random import choice 50 | from webbrowser import open as webopen 51 | 52 | try: from urlparse import parse_qsl, parse_qs 53 | except ImportError: from cgi import parse_qsl, parse_qs 54 | 55 | 56 | 57 | 58 | THIS_DIR = dirname(abspath(__file__)) 59 | music_buffer_size = 20 60 | import_export_html_filename = "index.html" 61 | 62 | 63 | 64 | settings = { 65 | 'username': None, 66 | 'download_directory': '/tmp', 67 | 'https_proxy_port': None, 68 | 'http_proxy_port': None, 69 | 'http_proxy': None, 70 | 'volume': 60, 71 | 'password': None, 72 | 'pandora_protocol_version': 33, 73 | 'out_key_p': [3634473031L, 3168404724L, 1416121723, 3455921491L, 2913834682L, 2214053842L, 2544142610L, 2849346733L, 1800353587, 106414398, 4239263367L, 1879595236, 878713105, 714982793, 3822095633L, 699586219, 3378868356L, 4217084656L], 74 | 'out_key_s': u'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', 75 | 'https_proxy': None, 76 | 'tag_mp3s': True, 77 | 'version': u'0.2', 78 | 'last_station': None, 79 | 'in_key_p': [3626349970L, 2805794615L, 1065101486, 4179587350L, 4016767006L, 3548718333L, 1417609708, 3922321276L, 635342487, 2672164641L, 2269614661L, 1957035642, 2638880475L, 2129644150, 1488000962, 243217342, 478415738, 367978584], 80 | 'in_key_s': u'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', 81 | 'download_music': False, 82 | } 83 | 84 | 85 | 86 | 87 | def save_setting(**kwargs): 88 | """ saves a value persisitently *in the file itself* so that it can be 89 | used next time pypandora is fired up. of course there are better ways 90 | of storing values persistently, but i want to stick with the '1 file' 91 | idea """ 92 | global settings 93 | 94 | logging.info("saving values %r", kwargs) 95 | with open(abspath(__file__), "r") as h: lines = h.read() 96 | 97 | 98 | start = lines.index("settings = {\n") 99 | end = lines[start:].index("}\n") + start + 2 100 | 101 | chunks = [lines[:start], "", lines[end:]] 102 | 103 | settings.update(kwargs) 104 | new_settings = "settings = {\n" 105 | for k,v in settings.iteritems(): new_settings += " %r: %r,\n" % (k, v) 106 | new_settings += "}\n" 107 | 108 | chunks[1] = new_settings 109 | new_contents = "".join(chunks) 110 | 111 | with open(abspath(__file__), "w") as h: h.write(new_contents) 112 | 113 | 114 | 115 | 116 | 117 | class LoginFail(Exception): pass 118 | class PandoraException(Exception): pass 119 | 120 | 121 | class Connection(object): 122 | """ 123 | Handles all the direct communication to Pandora's servers 124 | """ 125 | 126 | _templates = { 127 | "sync": """ 128 | eNqzsa/IzVEoSy0qzszPs1Uy1DNQsrezyU0tychPcU7MyYGx/RJzU+1yM4uT9Yor85Jt9JFEbQoSixJz 129 | i+1s9OEMJP0Afngihg==""", 130 | "add_feedback": """ 131 | eNqdkssKwjAQRX9FSteNgo/NmC4El/6CTJuhhuZRkrT4+caSQl2IravMzdwzhLmB8qnVZiDnpTXnbFds 132 | s5KDpvCw4oJKTfUNNXEfMERbgUJciUSFdQts1ocOHWqfTg4Dqj7eShN4HqSmyOsO2FsDS02WvJ+ID06a 133 | JlK2JQMsyYVQeuZdirWk7r2s/+B83MZCJkfX7H+MraxVhGb0HoBNcgV1XEyN4UTi9CUXNmXKZp/iBbQI 134 | yo4=""", 135 | "authenticate": """ 136 | eNqNj8EKwkAMRH9FSs+N3uP24FX8h2CDDWx2yyZt/XwVtlAPgqdkJvNggv1T42HhYpLTuTl1x6YPqOxj 137 | Hi4U47bfSDlEMefEpaPZR04ud3K+VhNhl8SJCqnVGXChOL9dSR5aF2Vz0gnhoxHqEWr2GzEvkh6hZSWJ 138 | CFX+CU1ktuYy/OZgKwq7n1/FhWTE""", 139 | "get_playlist": """ 140 | eNq1ks8KwjAMxl9Fxs7LvMfuIHj0FSSwOItNO9o49O2t0MG8iDvslH+/j/CRYPcUt5s4Jhv8odo3bdUZ 141 | FNZb6I/k3JyfSdiMjl7OJm0G1lOkQdgrwgLAkSJJKtHgRO6Ru9arqdUKJyUZET41QhlCYb8lSaP1Q1aF 142 | O3uEUv4pyms027nYfqWyXclvi9fXEIV0Yw8/eJjPCYuHeAObkcrC""", 143 | "get_stations": """ 144 | eNpljrEOgzAMRH8FIWbc7iYM3bv0CyzVolHjBMUu4vNJ1SCBOvnOd082jquEZuGsPsWhvfaXdnQobK/0 145 | vFEIu76TsFMjK7V+Ynv8pCIccpwpk2idDhcKn7L10VxnXrjwMiN8PUINoXbPiFr2cSpUenNEqPYPgv0g 146 | HD7eAIijTD8=""" 147 | } 148 | 149 | def __init__(self): 150 | self.rid = "%07dP" % (time.time() % 10000000) # route id 151 | self.timeoffset = time.time() 152 | self.token = None 153 | self.lid = None # listener id 154 | self.log = logging.getLogger("pandora") 155 | 156 | @staticmethod 157 | def dump_xml(x): 158 | """ a convenience function for dumping xml from Pandora's servers """ 159 | #el = xml.dom.minidom.parseString(ElementTree.tostring(x)) 160 | el = xml.dom.minidom.parseString(x) 161 | return el.toprettyxml(indent=" ") 162 | 163 | 164 | def send(self, get_data, body, sync_on_error=True): 165 | authenticating = get_data["method"] == "authenticateListener" 166 | 167 | pandora_domain = "www.pandora.com" 168 | if authenticating: 169 | if settings['https_proxy']: 170 | conn = httplib.HTTPSConnection(settings['https_proxy'], settings['https_proxy_port']) 171 | else: 172 | conn = httplib.HTTPSConnection(pandora_domain) 173 | #method = "GET" 174 | else: 175 | if settings['http_proxy']: 176 | conn = httplib.HTTPConnection(settings['http_proxy'], settings['http_proxy_port']) 177 | else: 178 | conn = httplib.HTTPConnection(pandora_domain) 179 | #method = "POST" 180 | 181 | headers = {"Content-Type": "text/xml"} 182 | get_data_copy = get_data.copy() 183 | 184 | # pandora has a very specific way that the get params have to be ordered 185 | # otherwise we'll get a 500 error. so this orders them correctly. 186 | ordered = [] 187 | ordered.append(("rid", self.rid)) 188 | 189 | if "lid" in get_data_copy: 190 | ordered.append(("lid", get_data_copy["lid"])) 191 | del get_data_copy["lid"] 192 | 193 | ordered.append(("method", get_data_copy["method"])) 194 | del get_data_copy["method"] 195 | 196 | def sort_fn(item): 197 | k, v = item 198 | m = re.search("\d+$", k) 199 | if not m: return k 200 | else: return int(m.group(0)) 201 | 202 | kv = [(k, v) for k,v in get_data_copy.iteritems()] 203 | kv.sort(key=sort_fn) 204 | ordered.extend(kv) 205 | 206 | 207 | if authenticating and 0: path = "/radio/jsonp" 208 | else: path = "/radio/xmlrpc/v%d?%s" % (settings["pandora_protocol_version"], urllib.urlencode(ordered)) 209 | 210 | self.log.debug("talking to %s", path) 211 | 212 | # debug logging? 213 | self.log.debug("sending data %s" % self.dump_xml(body)) 214 | 215 | send_body = encrypt(body) 216 | if authenticating and settings['https_proxy']: 217 | conn.set_tunnel(pandora_domain, 443) 218 | conn.request("POST", path, send_body, headers) 219 | else: 220 | if settings['http_proxy']: 221 | conn.request("POST", "http://" + pandora_domain + path, send_body, headers) 222 | else: 223 | conn.request("POST", path, send_body, headers) 224 | resp = conn.getresponse() 225 | 226 | if resp.status != 200: raise Exception(resp.reason) 227 | 228 | ret_data = resp.read() 229 | 230 | # debug logging? 231 | self.log.debug("returned data %s" % self.dump_xml(ret_data)) 232 | 233 | conn.close() 234 | 235 | xml = ElementTree.fromstring(ret_data) 236 | fault = xml.find("fault/value/struct") 237 | if fault is not None: 238 | fault_data = {} 239 | for member in fault.findall("member"): 240 | name = member.find("name").text 241 | value = member.find("value") 242 | 243 | number = value.find("int") 244 | if number is not None: value = int(number.text) 245 | else: value = value.text 246 | 247 | fault_data[name] = value 248 | 249 | fault = fault_data.get("faultString", "Unknown error from Pandora Radio") 250 | if sync_on_error and "INCOMPATIBLE_VERSION" in fault: 251 | self.log.error("got 'incompatible version' from pandora! emergency sync") 252 | # sync out protocol version, our keys, and try again 253 | sync_everything() 254 | return self.send(get_data, body, sync_on_error=False) 255 | else: 256 | raise PandoraException, fault 257 | return xml 258 | 259 | 260 | def get_template(self, tmpl, params={}): 261 | tmpl = zlib.decompress(b64decode(self._templates[tmpl].strip().replace("\n", ""))) 262 | xml = Template(tmpl) 263 | return xml.substitute(params).strip() 264 | 265 | 266 | def sync(self): 267 | """ synchronizes the times between our clock and pandora's servers by 268 | recording the timeoffset value, so that for every call made to Pandora, 269 | we can specify the correct time of their servers in our call """ 270 | 271 | self.log.info("syncing time") 272 | get = {"method": "sync"} 273 | body = self.get_template("sync") 274 | timestamp = None 275 | 276 | 277 | while timestamp is None: 278 | xml = self.send(get.copy(), body) 279 | timestamp = xml.find("params/param/value").text 280 | timestamp = decrypt(timestamp) 281 | 282 | timestamp_chars = [] 283 | for c in timestamp: 284 | if c.isdigit(): timestamp_chars.append(c) 285 | timestamp = int(time.time() - int("".join(timestamp_chars))) 286 | 287 | self.timeoffset = timestamp 288 | return True 289 | 290 | 291 | def authenticate(self, email, password): 292 | """ logs us into Pandora. tries a few times, then fails if it doesn't 293 | get a listener id """ 294 | self.log.info("logging in with %s...", email) 295 | get = {"method": "authenticateListener"} 296 | 297 | 298 | body = self.get_template("authenticate", { 299 | "timestamp": int(time.time() - self.timeoffset), 300 | "email": xml_escape(email), 301 | "password": xml_escape(password) 302 | }) 303 | 304 | xml = self.send(get, body) 305 | 306 | for el in xml.findall("params/param/value/struct/member"): 307 | children = el.getchildren() 308 | if children[0].text == "authToken": 309 | self.token = children[1].text 310 | elif children[0].text == "listenerId": 311 | self.lid = children[1].text 312 | 313 | if self.lid: return True 314 | return False 315 | 316 | 317 | 318 | 319 | 320 | 321 | class Account(object): 322 | def __init__(self, reactor, email, password): 323 | self.reactor = reactor 324 | self.reactor.shared_data["pandora_account"] = self 325 | 326 | self.log = logging.getLogger("account %s" % email) 327 | self.connection = Connection() 328 | self.email = email 329 | self.password = password 330 | self._stations = {} 331 | self.recently_played = [] 332 | 333 | self.current_station = None 334 | self.msg_subscribers = [] 335 | 336 | self.login() 337 | self.start() 338 | 339 | 340 | def song_changer(): 341 | sd = self.reactor.shared_data 342 | 343 | if self.current_song and self.current_song.done_playing: 344 | self.current_station.next() 345 | sd["message"] = ["refresh_song"] 346 | 347 | self.reactor.add_callback(song_changer) 348 | 349 | 350 | def start(self): 351 | """ loads the last-played station and kicks it to start """ 352 | # load our previously-saved station 353 | station_id = settings.get("last_station", None) 354 | 355 | # ...or play a random one 356 | if not station_id or station_id not in self.stations: 357 | station_id = choice(self.stations.keys()) 358 | save_setting(last_station=station_id) 359 | 360 | self.play(station_id) 361 | 362 | 363 | def next(self): 364 | if self.current_station: self.current_station.next() 365 | 366 | def like(self): 367 | if self.current_station: self.current_station.like() 368 | 369 | def dislike(self): 370 | if self.current_station: self.current_station.dislike() 371 | 372 | def play(self, station_id): 373 | if self.current_station: self.current_station.stop() 374 | station = self.stations[station_id] 375 | station.play() 376 | return station 377 | 378 | @property 379 | def current_song(self): 380 | return self.current_station.current_song 381 | 382 | def login(self): 383 | logged_in = False 384 | for i in xrange(3): 385 | self.connection.sync() 386 | 387 | try: success = self.connection.authenticate(self.email, self.password) 388 | except PandoraException, p: success = False 389 | 390 | if success: 391 | logged_in = True 392 | break 393 | else: 394 | self.log.error("failed login (this happens quite a bit), trying again...") 395 | time.sleep(1) 396 | if not logged_in: 397 | self.reactor.shared_data["pandora_account"] = None 398 | raise LoginFail, "can't log in. wrong username or password?" 399 | self.log.info("logged in") 400 | 401 | @property 402 | def json_data(self): 403 | data = {} 404 | data["stations"] = [(id, station.name) for id,station in self.stations.iteritems()] 405 | data["stations"].sort(key=lambda s: s[1].lower()) 406 | data["current_station"] = getattr(self.current_station, "id", None) 407 | data["volume"] = settings["volume"] 408 | return data 409 | 410 | 411 | @property 412 | def stations(self): 413 | if self._stations: return self._stations 414 | 415 | self.log.info("fetching stations") 416 | get = {"method": "getStations", "lid": self.connection.lid} 417 | body = self.connection.get_template("get_stations", { 418 | "timestamp": int(time.time() - self.connection.timeoffset), 419 | "token": self.connection.token 420 | }) 421 | xml = self.connection.send(get, body) 422 | 423 | fresh_stations = {} 424 | station_params = {} 425 | Station._current_id = 0 426 | 427 | for el in xml.findall("params/param/value/array/data/value"): 428 | for member in el.findall("struct/member"): 429 | c = member.getchildren() 430 | station_params[c[0].text] = c[1].text 431 | 432 | station = Station(self, **station_params) 433 | fresh_stations[station.id] = station 434 | 435 | 436 | # remove any stations that pandora says we don't have anymore 437 | for id, station in self._stations.items(): 438 | if not fresh_stations.get(id): del self._stations[id] 439 | 440 | # add any new stations if they don't already exist 441 | for id, station in fresh_stations.iteritems(): 442 | self._stations.setdefault(id, station) 443 | 444 | self.log.info("got %d stations", len(self._stations)) 445 | 446 | return self._stations 447 | 448 | 449 | 450 | 451 | class Station(object): 452 | PLAYLIST_LENGTH = 3 453 | 454 | def __init__(self, account, stationId, stationIdToken, stationName, **kwargs): 455 | self.account = account 456 | self.id = stationId 457 | self.token = stationIdToken 458 | self.name = stationName 459 | self.current_song = None 460 | self._playlist = [] 461 | 462 | self.log = logging.getLogger(unicode(self)) 463 | 464 | def like(self): 465 | # normally we might do some logging here, but we let the song object 466 | # handle it 467 | self.current_song.like() 468 | 469 | def dislike(self): 470 | self.current_song.dislike() 471 | self.next() 472 | 473 | def stop(self): 474 | if self.current_song: self.current_song.stop() 475 | 476 | def play(self): 477 | # next() is an alias to play(), so we check if we're changing the 478 | # station before we output logging saying such 479 | if self.account.current_station and self.account.current_station is not self: 480 | self.log.info("changing station to %r", self) 481 | 482 | self.account.current_station = self 483 | self.stop() 484 | 485 | self.playlist.reverse() 486 | if self.current_song: self.account.recently_played.append(self.current_song) 487 | self.current_song = self.playlist.pop() 488 | 489 | self.log.info("playing %r", self.current_song) 490 | self.playlist.reverse() 491 | self.current_song.play() 492 | 493 | def next(self): 494 | self.account.reactor.shared_data["message"] = ["refresh_song"] 495 | self.play() 496 | 497 | @property 498 | def playlist(self): 499 | """ a playlist getter. each call to Pandora's station api returns maybe 500 | 3 songs in the playlist. so each time we access the playlist, we need 501 | to see if it's empty. if it's not, return it, if it is, get more 502 | songs for the station playlist """ 503 | 504 | if len(self._playlist) >= Station.PLAYLIST_LENGTH: return self._playlist 505 | 506 | self.log.info("getting playlist") 507 | format = "mp3-hifi" # always try to select highest quality sound 508 | get = { 509 | "method": "getFragment", "lid": self.account.connection.lid, 510 | "arg1": self.id, "arg2": 0, "arg3": "", "arg4": "", "arg5": format, 511 | "arg6": 0, "arg7": 0 512 | } 513 | 514 | got_playlist = False 515 | for i in xrange(2): 516 | body = self.account.connection.get_template("get_playlist", { 517 | "timestamp": int(time.time() - self.account.connection.timeoffset), 518 | "token": self.account.connection.token, 519 | "station_id": self.id, 520 | "format": format 521 | }) 522 | try: xml = self.account.connection.send(get, body) 523 | # pick a new station 524 | except PandoraException, e: 525 | if "PLAYLIST_END" in str(e): raise 526 | raise 527 | 528 | song_params = {} 529 | 530 | for el in xml.findall("params/param/value/array/data/value"): 531 | for member in el.findall("struct/member"): 532 | key = member[0].text 533 | value = member[1] 534 | 535 | number = value.find("int") 536 | if number is not None: value = int(number.text) 537 | else: value = value.text 538 | 539 | song_params[key] = value 540 | song = Song(self, **song_params) 541 | self._playlist.append(song) 542 | 543 | if self._playlist: 544 | got_playlist = True 545 | break 546 | else: 547 | self.log.error("failed to get playlist, trying again times") 548 | self.account.login() 549 | 550 | if not got_playlist: raise Exception, "can't get playlist!" 551 | return self._playlist 552 | 553 | def __unicode__(self): 554 | return u"" % (self.id, self.name) 555 | 556 | def __repr__(self): 557 | return "" % (self.id, self.name.encode("ascii", "ignore")) 558 | 559 | 560 | 561 | 562 | class Song(object): 563 | assume_bitrate = 128 564 | read_chunk_size = 1024 565 | kb_to_quick_stream = 256 566 | 567 | # states 568 | INITIALIZED = 0 569 | SENDING_REQUEST = 1 570 | READING_HEADERS = 2 571 | STREAMING = 3 572 | DONE = 4 573 | 574 | 575 | def __init__(self, station, **kwargs): 576 | self.station = station 577 | self.reactor = self.station.account.reactor 578 | 579 | self.__dict__.update(kwargs) 580 | #pprint(self.__dict__) 581 | 582 | self.seed = self.userSeed 583 | self.id = self.musicId 584 | self.title = self.songTitle 585 | self.album = self.albumTitle 586 | self.artist = self.artistSummary 587 | 588 | self.liked = bool(self.rating) 589 | 590 | # see if the big version of the album art exists 591 | if self.artRadio: 592 | art_url = self.artRadio.replace("130W_130H", "500W_500H") 593 | art_url_parts = urlsplit(art_url) 594 | 595 | test_art = httplib.HTTPConnection(art_url_parts.netloc) 596 | test_art.request("HEAD", art_url_parts.path) 597 | if test_art.getresponse().status != 200: art_url = self.artRadio 598 | else: 599 | art_url = self.artistArtUrl 600 | 601 | self.album_art = art_url 602 | 603 | 604 | self.purchase_itunes = kwargs.get("itunesUrl", "") 605 | if self.purchase_itunes: 606 | self.purchase_itunes = urllib.unquote(parse_qsl(self.purchase_itunes)[0][1]) 607 | 608 | self.purchase_amazon = kwargs.get("amazonUrl", "") 609 | 610 | 611 | try: self.gain = float(fileGain) 612 | except: self.gain = 0.0 613 | 614 | self.url = self._decrypt_url(self.audioURL) 615 | self.duration = 0 616 | self.song_size = 0 617 | self.download_progress = 0 618 | self.last_read = 0 619 | self.state = Song.INITIALIZED 620 | self.started_streaming = None 621 | self.sock = None 622 | self.bitrate = None 623 | 624 | 625 | # these are used to prevent .done_playing from reporting too early in 626 | # the case where we've closed the browser window (and are therefore not 627 | # streaming audio out of the buffer) 628 | self._done_playing_offset = 0 629 | self._done_playing_marker = 0 630 | 631 | def format_title(part): 632 | part = part.lower() 633 | part = part.replace(" ", "_") 634 | part = re.sub("\W", "", part) 635 | part = re.sub("_+", "_", part) 636 | return part 637 | 638 | self.filename = join(settings["download_directory"], "%s-%s.mp3" % (format_title(self.artist), format_title(self.title))) 639 | self.log = logging.getLogger(unicode(self)) 640 | 641 | 642 | 643 | @property 644 | def json_data(self): 645 | return { 646 | "id": self.id, 647 | "album_art": self.album_art, 648 | "title": self.title, 649 | "album": self.album, 650 | "artist": self.artist, 651 | "purchase_itunes": self.purchase_itunes, 652 | "purchase_amazon": self.purchase_amazon, 653 | "gain": self.gain, 654 | "duration": self.duration, 655 | "liked": self.liked, 656 | } 657 | 658 | 659 | @staticmethod 660 | def _decrypt_url(url): 661 | """ decrypts the song url where the song stream can be downloaded. """ 662 | e = url[-48:] 663 | d = decrypt(e) 664 | url = url.replace(e, d) 665 | return url[:-8] 666 | 667 | @property 668 | def position(self): 669 | if not self.song_size: return 0 670 | return self.duration * self.download_progress / float(self.song_size) 671 | 672 | @property 673 | def done_playing(self): 674 | # never finish playing if we're not actually pushing data through out 675 | # to the audio player 676 | if self._done_playing_marker: return False 677 | 678 | return self.started_streaming and self.duration\ 679 | and self.started_streaming + self.duration + self._done_playing_offset <= time.time() 680 | 681 | @property 682 | def done_downloading(self): 683 | return self.download_progress and self.download_progress == self.song_size 684 | 685 | def fileno(self): 686 | return self.sock.fileno() 687 | 688 | 689 | def stop(self): 690 | self.reactor.remove_all(self) 691 | if self.sock: 692 | try: self.sock.shutdown(socket.SHUT_RDWR) 693 | except: pass 694 | self.sock.close() 695 | 696 | 697 | def play(self): 698 | self.connect() 699 | 700 | # the first thing we do is send out the request for the music, so we 701 | # need the select reactor to know about us 702 | self.reactor.add_writer(self) 703 | 704 | 705 | def connect(self): 706 | # we stop the song just in case we're reconnecting...because we dont 707 | # want the old socket laying around, open, and in the reactor 708 | self.stop() 709 | 710 | self.log.info("downloading from byte %d", self.download_progress) 711 | 712 | split = urlsplit(self.url) 713 | host = split.netloc 714 | path = split.path + "?" + split.query 715 | 716 | req = """GET %s HTTP/1.0\r\nHost: %s\r\nRange: bytes=%d-\r\nUser-Agent: pypandora\r\nAccept: */*\r\n\r\n""" 717 | self.sock = MagicSocket(host=host, port=80) 718 | self.sock.write_string(req % (path, host, self.download_progress)) 719 | self.state = Song.SENDING_REQUEST 720 | 721 | # if we're reconnecting, we might be in a state of being in the readers 722 | # and not the writers, so let's just ensure that we're where we need 723 | # to be 724 | self.reactor.remove_reader(self) 725 | self.reactor.add_writer(self) 726 | 727 | 728 | 729 | def _calc_bitrate(self, chunk): 730 | """ takes a chunk of mp3 data, finds the sync frame in the header 731 | then filters out the bitrate (if it can be found) """ 732 | 733 | bitrate_lookup = { 734 | 144: 128, 735 | 160: 160, 736 | 176: 192, 737 | 192: 224, 738 | 208: 256, 739 | 224: 320 740 | } 741 | 742 | for i in xrange(0, len(chunk), 2): 743 | c = chunk[i:i+2] 744 | c = struct.unpack(">H", c)[0] 745 | 746 | if c & 65504: 747 | bitrate_byte = ord(chunk[i+2]) 748 | try: return bitrate_lookup[bitrate_byte & 240] 749 | except KeyError: return None 750 | 751 | return None 752 | 753 | 754 | def handle_write(self, shared, reactor): 755 | if self.state is Song.SENDING_REQUEST: 756 | done = self.sock.write() 757 | if done: 758 | self.reactor.remove_writer(self) 759 | self.reactor.add_reader(self) 760 | self.state = Song.READING_HEADERS 761 | self.sock.read_until("\r\n\r\n") 762 | return 763 | 764 | 765 | def handle_read(self, shared, reactor): 766 | if self.state is Song.DONE: 767 | return 768 | 769 | if self.state is Song.READING_HEADERS: 770 | status, headers = self.sock.read() 771 | if status is MagicSocket.DONE: 772 | # parse our headers 773 | headers = headers.strip().split("\r\n") 774 | headers = dict([h.split(": ") for h in headers[1:]]) 775 | 776 | #print headers 777 | 778 | # if we don't have a song size it means we're not doing 779 | # a reconnect, because if we were, we don't need to do 780 | # anything in this block 781 | if not self.song_size: 782 | # figure out how fast we should download and how long we need to sleep 783 | # in between reads. we have to do this so as to not stream to quickly 784 | # from pandora's servers. we lower it by 20% so we never suffer from 785 | # a buffer underrun. 786 | # 787 | # these values aren't necessarily correct, but we can't know that 788 | # until we get some mp3 data, from which we'll calculate the actual 789 | # bitrate, then the dependent values. but for now, using 790 | # Song.assume_bitrate is fine. 791 | bytes_per_second = Song.assume_bitrate * 125.0 792 | self.sleep_amt = Song.read_chunk_size * .8 / bytes_per_second 793 | 794 | # determine the size of the song, and from that, how long the 795 | # song is in seconds 796 | self.song_size = int(headers["Content-Length"]) 797 | self.duration = (self.song_size / bytes_per_second) + 1 798 | self.started_streaming = time.time() 799 | self._mp3_data = [] 800 | 801 | self.state = Song.STREAMING 802 | self.sock.read_amount(self.song_size - self.download_progress) 803 | return 804 | 805 | elif self.state is Song.STREAMING: 806 | # can we even put anything new on the music buffer? 807 | if shared_data["music_buffer"].full(): 808 | if not self._done_playing_marker: 809 | self._done_playing_marker = time.time() 810 | return 811 | 812 | # it's time to aggregate the time that we sat essentially paused 813 | # and add it to the offset. the offset is used to adjust the 814 | # time calculations to determine if we're done playing the song 815 | if self._done_playing_marker: 816 | self._done_playing_offset += time.time() - self._done_playing_marker 817 | self._done_playing_marker = 0 818 | 819 | # check if it's time to read more music yet. preload the 820 | # first N kilobytes quickly so songs play immediately 821 | now = time.time() 822 | if now - self.last_read < self.sleep_amt and\ 823 | self.download_progress > Song.kb_to_quick_stream * 1024: return 824 | 825 | self.last_read = now 826 | try: status, chunk = self.sock.read(Song.read_chunk_size, only_chunks=True) 827 | except: 828 | self.log.exception("error downloading chunk") 829 | self.connect() 830 | return 831 | 832 | if status is MagicSocket.BLOCKING: return 833 | 834 | 835 | if chunk: 836 | # calculate the actual bitrate from the mp3 stream data 837 | if not self.bitrate: 838 | self.log.debug("looking for bitrate...") 839 | self.bitrate = self._calc_bitrate(chunk) 840 | 841 | # now that we have the actual bitrate, let's recalculate the song 842 | # duration and how fast we should download the mp3 stream 843 | if self.bitrate: 844 | self.log.debug("found bitrate %d", self.bitrate) 845 | 846 | bytes_per_second = self.bitrate * 125.0 847 | self.sleep_amt = Song.read_chunk_size * .8 / bytes_per_second 848 | self.duration = (self.song_size / bytes_per_second) + 1 849 | 850 | 851 | self.download_progress += len(chunk) 852 | self._mp3_data.append(chunk) 853 | shared_data["music_buffer"].put(chunk) 854 | 855 | # disconnected? do we need to reconnect, or have we read everything 856 | # and the song is done? 857 | else: 858 | if not self.done_downloading: 859 | self.log.error("disconnected, reconnecting at byte %d of %d", self.download_progress, self.song_size) 860 | self.connect() 861 | return 862 | 863 | # done! 864 | else: 865 | self.status = Song.DONE 866 | self.reactor.remove_all(self) 867 | 868 | if settings["download_music"]: 869 | self.log.info("saving file to %s", self.filename) 870 | mp3_data = "".join(self._mp3_data) 871 | 872 | # save on memory 873 | self._mp3_data = [] 874 | 875 | # FIXME, id3 tags appear to be broken, disabling manually 876 | if settings["tag_mp3s"] and 0: 877 | # tag the mp3 878 | tag = ID3Tag() 879 | tag.add_id(self.id) 880 | tag.add_title(self.title) 881 | tag.add_album(self.album) 882 | tag.add_artist(self.artist) 883 | # can't get this working... 884 | #tag.add_image(self.album_art) 885 | 886 | print "\n\n", repr(tag.binary()), "\n\n" 887 | 888 | mp3_data = tag.binary() + mp3_data 889 | 890 | # and write it to the file 891 | h = open(self.filename, "wb") 892 | h.write(mp3_data) 893 | h.close() 894 | 895 | 896 | 897 | 898 | 899 | 900 | 901 | def new_station(self, station_name): 902 | """ create a new station from this song """ 903 | raise NotImplementedError 904 | 905 | def _add_feedback(self, like=True): 906 | """ common method called by both like and dislike """ 907 | conn = self.station.account.connection 908 | 909 | get = { 910 | "method": "addFeedback", 911 | "lid": conn.lid, 912 | "arg1": self.station.id, 913 | "arg2": self.id, 914 | "arg3": self.seed, 915 | "arg4": 0, "arg5": str(like).lower(), "arg6": "false", "arg7": 1 916 | } 917 | body = conn.get_template("add_feedback", { 918 | "timestamp": int(time.time() - conn.timeoffset), 919 | "station_id": self.station.id, 920 | "token": conn.token, 921 | "music_id": self.id, 922 | "seed": self.seed, 923 | "arg4": 0, "arg5": int(like), "arg6": 0, "arg7": 1 924 | }) 925 | xml = conn.send(get, body) 926 | 927 | def like(self): 928 | self.log.info("liking") 929 | self.liked = True 930 | self._add_feedback(like=True) 931 | 932 | def dislike(self): 933 | self.log.info("disliking") 934 | self.liked = False 935 | self._add_feedback(like=False) 936 | 937 | def __unicode__(self): 938 | return u"" % (self.title, self.artist) 939 | 940 | def __repr__(self): 941 | return "" % (self.title.encode("ascii", "ignore"), self.artist.encode("ascii", "ignore")) 942 | 943 | 944 | 945 | 946 | 947 | 948 | class ID3Tag(object): 949 | def __init__(self): 950 | self.frames = [] 951 | 952 | def add_frame(self, name, data): 953 | name = name.upper() 954 | # null byte means latin-1 encoding... 955 | # see section 4 http://www.id3.org/id3v2.4.0-structure 956 | header = struct.pack(">4siBB", name, self.sync_encode(len(data)), 0, 0) 957 | self.frames.append(header + data) 958 | 959 | def add_artist(self, artist): 960 | self.add_frame("tpe1", "\x00" + artist) 961 | 962 | def add_title(self, title): 963 | self.add_frame("tit2", "\x00" + title) 964 | 965 | def add_album(self, album): 966 | self.add_frame("talb", "\x00" + album) 967 | 968 | def add_id(self, id): 969 | self.add_frame("ufid", "\x00" + id) 970 | 971 | def add_image(self, image_url): 972 | mime_type = "\x00" + "-->" + "\x00" 973 | description = "cover image" + "\x00" 974 | # 3 for cover image 975 | data = struct.pack(">B5sB12s", 0, mime_type, 3, description) 976 | data += image_url 977 | self.add_frame("apic", data) 978 | 979 | def binary(self): 980 | total_size = sum([len(frame) for frame in self.frames]) 981 | header = struct.pack(">3s2BBi", "ID3", 4, 0, 0, self.sync_encode(total_size)) 982 | return header + "".join(self.frames) 983 | 984 | def add_to_file(self, f): 985 | h = open(f, "r+b") 986 | mp3_data = h.read() 987 | h.truncate(0) 988 | h.seek(0) 989 | h.write(self.binary() + mp3_data) 990 | h.close() 991 | 992 | def sync_decode(self, x): 993 | x_final = 0x00; 994 | a = x & 0xff; 995 | b = (x >> 8) & 0xff; 996 | c = (x >> 16) & 0xff; 997 | d = (x >> 24) & 0xff; 998 | 999 | x_final = x_final | a; 1000 | x_final = x_final | (b << 7); 1001 | x_final = x_final | (c << 14); 1002 | x_final = x_final | (d << 21); 1003 | return x_final 1004 | 1005 | def sync_encode(self, x): 1006 | x_final = 0x00; 1007 | a = x & 0x7f; 1008 | b = (x >> 7) & 0x7f; 1009 | c = (x >> 14) & 0x7f; 1010 | d = (x >> 21) & 0x7f; 1011 | 1012 | x_final = x_final | a; 1013 | x_final = x_final | (b << 8); 1014 | x_final = x_final | (c << 16); 1015 | x_final = x_final | (d << 24); 1016 | return x_final 1017 | 1018 | 1019 | 1020 | 1021 | 1022 | 1023 | 1024 | 1025 | 1026 | 1027 | 1028 | 1029 | 1030 | 1031 | 1032 | def encrypt(input): 1033 | """ encrypts data to be sent to pandora """ 1034 | 1035 | out_key_p = settings["out_key_p"] 1036 | out_key_s = struct.unpack("1024I", b64decode(settings["out_key_s"].replace("\n", "").strip())) 1037 | out_key_s = [out_key_s[i:i+256] for i in xrange(0, len(out_key_s), 256)] 1038 | 1039 | block_n = len(input) / 8 + 1 1040 | block_input = input 1041 | 1042 | # pad the string with null bytes 1043 | block_input += ("\x00" * ((block_n * 4 * 2) - len(block_input))) 1044 | 1045 | block_ptr = 0 1046 | hexmap = "0123456789abcdef" 1047 | str_hex = [] 1048 | 1049 | while block_n > 0: 1050 | # byte swap 1051 | l = struct.unpack(">L", block_input[block_ptr:block_ptr+4])[0] 1052 | r = struct.unpack(">L", block_input[block_ptr+4:block_ptr+8])[0] 1053 | 1054 | # encrypt blocks 1055 | for i in xrange(len(out_key_p) - 2): 1056 | l ^= out_key_p[i] 1057 | f = out_key_s[0][(l >> 24) & 0xff] + out_key_s[1][(l >> 16) & 0xff] 1058 | f ^= out_key_s[2][(l >> 8) & 0xff] 1059 | f += out_key_s[3][l & 0xff] 1060 | r ^= f 1061 | 1062 | lrExchange = l 1063 | l = r 1064 | r = lrExchange 1065 | 1066 | # exchange l & r again 1067 | lrExchange = l 1068 | l = r 1069 | r = lrExchange 1070 | r ^= out_key_p[len(out_key_p) - 2] 1071 | l ^= out_key_p[len(out_key_p) - 1] 1072 | 1073 | # swap bytes again... 1074 | l = c_uint32(l).value 1075 | l = struct.pack(">L", l) 1076 | l = struct.unpack("L", r) 1079 | r = struct.unpack("> 4]) 1084 | str_hex.append(hexmap[l & 0x0f]) 1085 | l >>= 8; 1086 | 1087 | for i in xrange(4): 1088 | str_hex.append(hexmap[(r & 0xf0) >> 4]) 1089 | str_hex.append(hexmap[r & 0x0f]) 1090 | r >>= 8; 1091 | 1092 | block_n -= 1 1093 | block_ptr += 8 1094 | 1095 | return "".join(str_hex) 1096 | 1097 | 1098 | 1099 | def decrypt(input): 1100 | """ decrypts data sent from pandora """ 1101 | 1102 | in_key_p = settings["in_key_p"] 1103 | in_key_s = struct.unpack("1024I", b64decode(settings["in_key_s"].replace("\n", "").strip())) 1104 | in_key_s = [in_key_s[i:i+256] for i in xrange(0, len(in_key_s), 256)] 1105 | 1106 | output = [] 1107 | 1108 | for i in xrange(0, len(input), 16): 1109 | chars = input[i:i+16] 1110 | 1111 | l = int(chars[:8], 16) 1112 | r = int(chars[8:], 16) 1113 | 1114 | for j in xrange(len(in_key_p) - 1, 1, -1): 1115 | l ^= in_key_p[j] 1116 | 1117 | f = in_key_s[0][(l >> 24) & 0xff] + in_key_s[1][(l >> 16) & 0xff] 1118 | f ^= in_key_s[2][(l >> 8) & 0xff] 1119 | f += in_key_s[3][l & 0xff] 1120 | r ^= f 1121 | 1122 | # exchange l & r 1123 | lrExchange = l 1124 | l = r 1125 | r = lrExchange 1126 | 1127 | # exchange l & r 1128 | lrExchange = l 1129 | l = r 1130 | r = lrExchange 1131 | r ^= in_key_p[1] 1132 | l ^= in_key_p[0] 1133 | 1134 | l = struct.pack(">L", c_uint32(l).value) 1135 | r = struct.pack(">L", c_uint32(r).value) 1136 | output.append(l) 1137 | output.append(r) 1138 | 1139 | return "".join(output) 1140 | 1141 | 1142 | 1143 | 1144 | 1145 | 1146 | 1147 | 1148 | 1149 | 1150 | 1151 | html_page = """ 1152 | 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""" 1266 | html_page = zlib.decompress(b64decode(html_page.replace("\n", ""))) 1267 | 1268 | 1269 | 1270 | 1271 | # i wish they had a CDN :) 1272 | jplayer_swf = """ 1273 | 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 1414 | fUSQ 1415 | """ 1416 | jplayer_swf = zlib.decompress(b64decode(jplayer_swf.replace("\n", ""))) 1417 | 1418 | 1419 | # i wish they had a CDN :) 1420 | jplayer_js = """ 1421 | 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 1592 | """ 1593 | jplayer_js = zlib.decompress(b64decode(jplayer_js.replace("\n", ""))) 1594 | 1595 | 1596 | 1597 | 1598 | 1599 | 1600 | 1601 | 1602 | 1603 | 1604 | 1605 | 1606 | 1607 | 1608 | 1609 | 1610 | 1611 | 1612 | 1613 | class MagicSocket(object): 1614 | """ a socket wrapper that allows for the non-blocking reads and writes. 1615 | the read methods include the ability to read up to a delimiter (like the 1616 | end of http headers) as well as reading a specified amount (like reading 1617 | the body of an http request) """ 1618 | 1619 | # statuses 1620 | DONE = 0 1621 | BLOCKING = 1 1622 | NOT_DONE = 2 1623 | 1624 | 1625 | def __init__(self, **kwargs): 1626 | self.read_buffer = "" 1627 | self.write_buffer = "" 1628 | 1629 | self._read_delim = "" 1630 | self._read_amount = 0 1631 | self._delim_cursor = 0 1632 | self._first_read = True 1633 | 1634 | # use an existing socket. useful for connection sockets created from 1635 | # a server socket 1636 | self.sock = kwargs.get("sock", None) 1637 | 1638 | # or use a new socket, useful for making new network connections 1639 | if not self.sock: 1640 | sock_type = kwargs.get("sock_type", (socket.AF_INET, socket.SOCK_STREAM)) 1641 | self.sock = socket.socket(*sock_type) 1642 | self.sock.connect((kwargs["host"], kwargs["port"])) 1643 | 1644 | self.sock.setblocking(0) 1645 | 1646 | 1647 | def read_until(self, delim, include_last=True): 1648 | self._read_amount = 0 1649 | self._delim_cursor = 0 1650 | self._read_delim = delim 1651 | self.include_last = include_last 1652 | self._first_read = True 1653 | 1654 | def read_amount(self, amount): 1655 | self._read_delim = "" 1656 | self._read_amount = amount 1657 | self._first_read = True 1658 | 1659 | 1660 | def _read_chunk(self, size): 1661 | try: data = self.sock.recv(size) 1662 | except socket.error, err: 1663 | if err.errno is errno.EWOULDBLOCK: return None 1664 | else: raise 1665 | return data 1666 | 1667 | def read(self, size=1024, only_chunks=False): 1668 | chunk = self._read_chunk(size) 1669 | if chunk is None: return MagicSocket.BLOCKING, "" 1670 | 1671 | # this is necessary for the case where we've overread some bytes 1672 | # in the process of reading an amount (or up to a delimiter), and 1673 | # we've stored those extra bytes on the read_buffer. we don't 1674 | # want to discard those bytes, but we DO want them to want them to 1675 | # be returned as part of the chunk, in the case that we're streaming 1676 | # chunks 1677 | if self._first_read and self.read_buffer: 1678 | chunk = self.read_buffer + chunk 1679 | self.read_buffer = "" 1680 | self._first_read = False 1681 | 1682 | self.read_buffer += chunk 1683 | 1684 | # do we have a delimiter we're waiting for? 1685 | if self._read_delim: 1686 | # look for our delimiter 1687 | found = self.read_buffer.find(self._read_delim, self._delim_cursor) 1688 | 1689 | # not found? mark where've last looked up until, taking into 1690 | # account that the delimiter might have gotten chopped up between 1691 | # consecutive reads 1692 | if found == -1: 1693 | self._delim_cursor = len(self.read_buffer) - len(self._read_delim) 1694 | return MagicSocket.NOT_DONE, chunk 1695 | 1696 | # found? chop out and return everything we've read up until that 1697 | # delimter 1698 | else: 1699 | end_cursor = self._delim_cursor + found 1700 | if self.include_last: end_cursor += len(self._read_delim) 1701 | try: return MagicSocket.DONE, self.read_buffer[:end_cursor] 1702 | finally: 1703 | self.read_buffer = self.read_buffer[end_cursor:] 1704 | self._read_delim = "" 1705 | self._delim_cursor = 0 1706 | 1707 | # or are we just reading until a specified amount 1708 | elif self._read_amount and len(self.read_buffer) >= self._read_amount: 1709 | try: 1710 | # returning only chunks is useful in the case where we're 1711 | # streaming content in real-time and don't want to be returning 1712 | # chunk, chunk, chunk (then when read_amount is reached), 1713 | # entire buffer. this keeps us returning.... only_chunks 1714 | if only_chunks: return MagicSocket.DONE, chunk 1715 | else: return MagicSocket.DONE, self.read_buffer[:self._read_amount] 1716 | finally: 1717 | self.read_buffer = self.read_buffer[self._read_amount:] 1718 | self._read_amount = 0 1719 | 1720 | return MagicSocket.NOT_DONE, chunk 1721 | 1722 | def _send_chunk(self, chunk): 1723 | try: sent = self.sock.send(chunk) 1724 | except socket.error, err: 1725 | if err.errno is errno.EWOULDBLOCK: return 0 1726 | else: raise 1727 | return sent 1728 | 1729 | 1730 | def write_string(self, data): 1731 | self.write_buffer += data 1732 | 1733 | def write(self, size=1024): 1734 | chunk = self.write_buffer[:size] 1735 | sent = self._send_chunk(chunk) 1736 | self.write_buffer = self.write_buffer[sent:] 1737 | if not self.write_buffer: return True 1738 | return False 1739 | 1740 | def __getattr__(self, name): 1741 | """ passes any non-existant methods down to the underlying socket """ 1742 | return getattr(self.sock, name) 1743 | 1744 | 1745 | 1746 | 1747 | 1748 | 1749 | 1750 | 1751 | 1752 | 1753 | 1754 | 1755 | 1756 | class WebConnection(object): 1757 | timeout = 60 1758 | 1759 | def __init__(self, sock, addr): 1760 | self.sock = sock 1761 | self.sock.read_until("\r\n\r\n") 1762 | 1763 | self.source, self.local_port = addr 1764 | self.local = self.source == "127.0.0.1" 1765 | 1766 | self.reading = True 1767 | self.writing = False 1768 | self.close_after_writing = True 1769 | 1770 | self.headers = None 1771 | self.path = None 1772 | self.params = {} 1773 | 1774 | self.connected = time.time() 1775 | self.log = logging.getLogger(repr(self)) 1776 | self.log.info("connected") 1777 | 1778 | 1779 | def __repr__(self): 1780 | path = "" 1781 | if self.path: path = " \"%s\"" % self.path 1782 | return "" % (self.source, self.local_port, path) 1783 | 1784 | def handle_read(self, shared_data, reactor): 1785 | if self.reading: 1786 | status, headers = self.sock.read() 1787 | if status is MagicSocket.DONE: 1788 | self.reading = False 1789 | 1790 | # parse the headers 1791 | headers = headers.strip().split("\r\n") 1792 | headers.reverse() 1793 | get_string = headers.pop() 1794 | headers.reverse() 1795 | 1796 | url = get_string.split()[1] 1797 | url = urlsplit(url) 1798 | 1799 | self.path = url.path 1800 | self.params = dict(parse_qsl(url.query, keep_blank_values=True)) 1801 | self.headers = dict([h.split(": ") for h in headers]) 1802 | 1803 | reactor.remove_reader(self) 1804 | reactor.add_writer(self) 1805 | self.log = logging.getLogger(repr(self)) 1806 | self.log.debug("done reading") 1807 | return 1808 | 1809 | 1810 | 1811 | def handle_write(self, shared_data, reactor): 1812 | pandora = shared_data.get("pandora_account", None) 1813 | 1814 | # have we already begun writing and must flush out what's in the write 1815 | # buffer? 1816 | if self.writing: 1817 | try: done = self.sock.write() 1818 | except socket.error, err: 1819 | if err.errno in (errno.ECONNRESET, errno.EPIPE): 1820 | self.log.info("peer closed connection") 1821 | self.close() 1822 | reactor.remove_all(self) 1823 | return 1824 | else: 1825 | self.log.exception("socket exception") 1826 | self.close() 1827 | reactor.remove_all(self) 1828 | return 1829 | 1830 | if done: 1831 | self.writing = False 1832 | if self.close_after_writing: 1833 | self.log.debug("closing") 1834 | self.close() 1835 | reactor.remove_all(self) 1836 | return 1837 | 1838 | 1839 | # no? ok let's process the request and queue up some data to be 1840 | # written the next time handle_write is called 1841 | 1842 | 1843 | # main page 1844 | if self.path == "/": 1845 | self.log.info("serving webpage") 1846 | 1847 | # do we use an overridden html page? 1848 | if exists(join(THIS_DIR, import_export_html_filename)): 1849 | with open(import_export_html_filename, "r") as h: page = h.read() 1850 | # or the embedded html page 1851 | else: page = html_page 1852 | 1853 | self.serve_content(page) 1854 | 1855 | elif self.path == "/jplayer.js": 1856 | self.serve_content(jplayer_js, "application/javascript") 1857 | 1858 | elif self.path == "/jplayer.swf": 1859 | self.serve_content(jplayer_swf, "application/x-shockwave-flash") 1860 | 1861 | # long-polling requests 1862 | elif self.path == "/events": 1863 | shared_data["long_pollers"].add(self) 1864 | return 1865 | 1866 | elif self.path == "/connection_info": 1867 | logged_in = bool(pandora) 1868 | self.send_json({"logged_in": logged_in}) 1869 | 1870 | # gets things like last volume, last station, and station list 1871 | elif self.path == "/account_info": 1872 | if pandora: self.send_json(pandora.json_data) 1873 | else: pass 1874 | 1875 | # what's currently playing 1876 | elif self.path == "/current_song_info": 1877 | self.send_json(pandora.current_song.json_data) 1878 | 1879 | # perform some action on the music player 1880 | elif self.path.startswith("/control/"): 1881 | command = self.path.replace("/control/", "") 1882 | if command == "next_song": 1883 | shared_data["music_buffer"] = Queue(music_buffer_size) 1884 | pandora.next() 1885 | self.send_json({"status": True}) 1886 | 1887 | elif command == "login": 1888 | username = self.params["username"] 1889 | password = self.params["password"] 1890 | 1891 | success = True 1892 | try: pandora_account = Account(reactor, username, password) 1893 | except LoginFail: success = False 1894 | 1895 | if success: 1896 | try: remember = bool(int(self.params["remember_login"])) 1897 | except: remember = False 1898 | if remember: save_setting(username=username, password=password) 1899 | shared_data["pandora_account"] = pandora_account 1900 | 1901 | self.send_json({"status": success}) 1902 | 1903 | elif command == "dislike_song": 1904 | shared_data["music_buffer"] = Queue(music_buffer_size) 1905 | pandora.dislike() 1906 | self.send_json({"status": True}) 1907 | 1908 | elif command == "like_song": 1909 | pandora.like() 1910 | self.send_json({"status": True}) 1911 | 1912 | elif command == "change_station": 1913 | station_id = self.params["station_id"]; 1914 | station = pandora.play(station_id) 1915 | save_setting(last_station=station_id) 1916 | 1917 | self.send_json({"status": True}) 1918 | 1919 | elif command == "volume": 1920 | self.log.info("changing volume") 1921 | try: level = int(self.params["level"]) 1922 | except: level = 60 1923 | save_setting(volume=level) 1924 | shared_data["message"] = ["update_volume", level] 1925 | 1926 | self.send_json({"status": True}) 1927 | 1928 | else: 1929 | self.send_json({"status": False}) 1930 | 1931 | 1932 | # this request is special in that it should never close after writing 1933 | # because it's a stream 1934 | elif self.path == "/m" and self.local: 1935 | try: chunk = shared_data["music_buffer"].get(False) 1936 | except: return 1937 | 1938 | if self.close_after_writing: 1939 | self.log.info("streaming music") 1940 | self.sock.write_string("HTTP/1.1 200 OK\r\nContent-Type: audio/mp3\r\n\r\n") 1941 | 1942 | self.sock.write_string(chunk) 1943 | self.close_after_writing = False 1944 | self.writing = True 1945 | 1946 | 1947 | 1948 | def fileno(self): 1949 | return self.sock.fileno() 1950 | 1951 | def close(self): 1952 | try: self.sock.shutdown(socket.SHUT_RDWR) 1953 | except: pass 1954 | self.sock.close() 1955 | 1956 | def send_json(self, data): 1957 | data = json.dumps(data) 1958 | self.serve_content(data, "application/json") 1959 | 1960 | def serve_content(self, data, mimetype="text/html"): 1961 | self.sock.write_string("HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Type: %s\r\nContent-Length: %s\r\n\r\n" % (mimetype, len(data))) 1962 | self.sock.write_string(data) 1963 | self.writing = True 1964 | 1965 | 1966 | 1967 | 1968 | 1969 | 1970 | 1971 | 1972 | class SocketReactor(object): 1973 | """ loops through all the readers and writers to see what sockets are ready 1974 | to be worked with """ 1975 | 1976 | def __init__(self, shared_data): 1977 | self.to_read = set() 1978 | self.to_write = set() 1979 | self.callbacks = set() 1980 | self.shared_data = shared_data 1981 | self.log = logging.getLogger("socket reactor") 1982 | 1983 | 1984 | def add_callback(self, fn): 1985 | self.callbacks.add(fn) 1986 | 1987 | def remove_callback(self, fn): 1988 | self.callbacks.discard(fn) 1989 | 1990 | def remove_all(self, o): 1991 | self.to_read.discard(o) 1992 | self.to_write.discard(o) 1993 | 1994 | def remove_reader(self, o): 1995 | self.to_read.discard(o) 1996 | 1997 | def remove_writer(self, o): 1998 | self.to_write.discard(o) 1999 | 2000 | def add_reader(self, o): 2001 | self.to_read.add(o) 2002 | 2003 | def add_writer(self, o): 2004 | self.to_write.add(o) 2005 | 2006 | 2007 | def run(self): 2008 | self.log.info("starting") 2009 | 2010 | while True: 2011 | read, write, err = select.select( 2012 | self.to_read, 2013 | self.to_write, 2014 | [], 2015 | 0 2016 | ) 2017 | 2018 | for sock in read: 2019 | try: sock.handle_read(self.shared_data, self) 2020 | except: 2021 | self.log.exception("error in readers") 2022 | self.to_read.remove(sock) 2023 | 2024 | for sock in write: 2025 | try: sock.handle_write(self.shared_data, self) 2026 | except: 2027 | self.log.exception("error in writers") 2028 | self.to_write.remove(sock) 2029 | 2030 | for cb in self.callbacks: 2031 | try: cb() 2032 | except: 2033 | self.log.exception("error in callbacks") 2034 | 2035 | time.sleep(.005) 2036 | 2037 | 2038 | 2039 | 2040 | 2041 | class WebServer(object): 2042 | """ serves as the entry point for all requests, spawning a new 2043 | WebConnection for each request and letting them handle what to do""" 2044 | 2045 | def __init__(self, reactor, port): 2046 | self.reactor = reactor 2047 | self.reactor.add_reader(self) 2048 | 2049 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 2050 | self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 2051 | self.sock.bind(('', port)) 2052 | self.sock.listen(100) 2053 | self.sock.setblocking(0) 2054 | 2055 | 2056 | def long_poll_writer(): 2057 | sd = self.reactor.shared_data 2058 | if sd["message"]: 2059 | for poller in sd["long_pollers"]: 2060 | poller.send_json({"event": sd["message"]}) 2061 | 2062 | sd["long_pollers"].clear() 2063 | sd["message"] = None 2064 | 2065 | self.reactor.add_callback(long_poll_writer) 2066 | 2067 | 2068 | def handle_read(self, shared_data, reactor): 2069 | conn, addr = self.sock.accept() 2070 | conn.setblocking(0) 2071 | 2072 | conn = WebConnection(MagicSocket(sock=conn), addr) 2073 | reactor.add_reader(conn) 2074 | 2075 | 2076 | def fileno(self): 2077 | return self.sock.fileno() 2078 | 2079 | 2080 | 2081 | 2082 | 2083 | 2084 | 2085 | def compress_encode_truncate(data): 2086 | data = b64encode(zlib.compress(data, 9)) 2087 | 2088 | # wrap it at 80 characters 2089 | data_chunks = [] 2090 | while True: 2091 | chunk = data[:80] 2092 | data = data[80:] 2093 | if not chunk: break 2094 | data_chunks.append(chunk) 2095 | 2096 | data = "\n".join(data_chunks) 2097 | return data 2098 | 2099 | 2100 | 2101 | 2102 | 2103 | def sync_everything(): 2104 | """ this syncs up with the github page http://amoffat.github.com/pypandora 2105 | the purpose is to provide quick updates to the static items in the code 2106 | (pandora protocol version, encryption and decryption keys). these are 2107 | things that pandora changes frequently to 'shrug off' software like 2108 | pypandora by forcing it to break """ 2109 | 2110 | global settings 2111 | 2112 | logging.info("syncing settings") 2113 | 2114 | conn = httplib.HTTPConnection("amoffat.github.com") 2115 | conn.request("GET", "/pypandora/") 2116 | github_page = conn.getresponse().read() 2117 | 2118 | m = re.search("SYNC START(.*?)SYNC END", github_page, re.S | re.M) 2119 | if not m: raise Exception, "problem syncing, fatal" 2120 | 2121 | sync = m.group(1) 2122 | sync = json.loads(zlib.decompress(b64decode(sync.strip()))) 2123 | 2124 | update_whitelist = [ 2125 | "pandora_protocol_version", "out_key_p", "out_key_s", "version", 2126 | "in_key_p", "in_key_s" 2127 | ] 2128 | updates = dict([(item, sync[item]) for item in update_whitelist]) 2129 | save_setting(**updates) 2130 | 2131 | 2132 | 2133 | 2134 | 2135 | 2136 | 2137 | 2138 | if __name__ == "__main__": 2139 | parser = OptionParser(usage=("%prog [options]")) 2140 | parser.add_option('-i', '--import', dest='import_html', action="store_true", default=False, help="Import index.html into pandora.py. See http://amoffat.github.com/pypandora/#extending") 2141 | parser.add_option('-e', '--export', dest='export_html', action="store_true", default=False, help="Export index.html from pandora.py. See http://amoffat.github.com/pypandora/#extending") 2142 | parser.add_option('-c', '--clean', dest='clean', action="store_true", default=False, help="Remove all account-specific details from the player. See http://amoffat.github.com/pypandora/#distributing") 2143 | parser.add_option('-n', '--no-browser', dest='no_browser', action="store_true", default=False, help="Don't start up a browser for me, I'll do it!") 2144 | parser.add_option('-p', '--port', type="int", dest='port', default=7000, help="The port to serve on") 2145 | parser.add_option('-b', '--b64', dest='encode_file', action="store", default=False, help="Zlib-compress and base64 encode an arbitrary file") 2146 | parser.add_option('-d', '--debug', dest='debug', action="store_true", default=False, help='Enable debug logging') 2147 | options, args = parser.parse_args() 2148 | 2149 | 2150 | log_level = logging.INFO 2151 | if options.debug: log_level = logging.DEBUG 2152 | 2153 | logging.basicConfig( 2154 | format="(%(process)d) %(asctime)s - %(name)s - %(levelname)s - %(message)s", 2155 | level=log_level 2156 | ) 2157 | 2158 | 2159 | # we're importing html to be embedded 2160 | if options.import_html: 2161 | html_file = join(THIS_DIR, import_export_html_filename) 2162 | logging.info("importing html from %s", html_file) 2163 | with open(html_file, "r") as h: html = h.read() 2164 | 2165 | html = compress_encode_truncate(html) 2166 | 2167 | with open(abspath(__file__), "r") as h: lines = h.read() 2168 | start_match = "html_page = \"\"\"\n" 2169 | end_match = "\"\"\"\n" 2170 | start = lines.index(start_match) 2171 | end = lines[start+len(start_match):].index(end_match) + start + len(start_match) + len(end_match) 2172 | 2173 | chunks = [lines[:start], start_match + html + end_match, lines[end:]] 2174 | new_contents = "".join(chunks) 2175 | 2176 | with open(abspath(__file__), "w") as h: h.write(new_contents) 2177 | exit() 2178 | 2179 | 2180 | if options.encode_file: 2181 | data = open(options.encode_file, "r").read() 2182 | data = compress_encode_truncate(data) 2183 | print data 2184 | exit() 2185 | 2186 | 2187 | # we're exporting the embedded html into index.html 2188 | if options.export_html: 2189 | html_file = join(THIS_DIR, import_export_html_filename) 2190 | if exists(html_file): 2191 | logging.error("\n\n*** html NOT exported, %s already exists! ***\n\n", html_file) 2192 | exit() 2193 | logging.info("exporting html to %s", html_file) 2194 | with open(html_file, "w") as h: h.write(html_page) 2195 | exit() 2196 | 2197 | 2198 | # cleaning up pandora.py for sharing 2199 | if options.clean: 2200 | logging.info("cleaning %s", __file__) 2201 | save_setting(**{ 2202 | "username": None, 2203 | "password": None, 2204 | "last_station": None, 2205 | "volume": 60, 2206 | "download_music": False, 2207 | "download_directory": '/tmp', 2208 | 'https_proxy': None, 2209 | 'https_proxy_port': None, 2210 | 'http_proxy': None, 2211 | 'http_proxy_port': None, 2212 | "tag_mp3s": True 2213 | }) 2214 | exit() 2215 | 2216 | 2217 | 2218 | # this is data shared between every socket-like object in the select 2219 | # reactor. for example, the socket that streams music to the browser 2220 | # uses the "music_buffer" key to read from, while the socket that reads 2221 | # music from pandora uses this same key to dump to 2222 | shared_data = { 2223 | "music_buffer": Queue(music_buffer_size), 2224 | "long_pollers": set(), 2225 | "message": None, 2226 | "pandora_account": None 2227 | } 2228 | 2229 | reactor = SocketReactor(shared_data) 2230 | WebServer(reactor, options.port) 2231 | 2232 | # do we have saved login settings? 2233 | username = settings.get("username") 2234 | password = settings.get("password") 2235 | if username and password: Account(reactor, username, password) 2236 | 2237 | 2238 | if not options.no_browser: 2239 | webopen("http://localhost:%d" % options.port) 2240 | reactor.run() 2241 | --------------------------------------------------------------------------------