Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
branch: master
Fetching contributors…

Octocat-spinner-32-eaf2f5

Cannot retrieve contributors at this time

executable file 256 lines (212 sloc) 7.877 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
#!/usr/bin/env python
import web
from web.contrib.template import render_jinja

try:
  import json
except:
  import simplejson as json

import logging, random, os, wsgiref, time, urllib
from urlparse import urlparse
from StringIO import StringIO

from rdio import Rdio, RdioProtocolException

logging.basicConfig(level=logging.DEBUG)

CONSUMER_TOKEN = 'CONSUMER_TOKEN'
CONSUMER_SECRET = 'CONSUMER_SECRET'

HR_EXPIRY = (60*60*24) # cache HR for one day
HR_LIMIT = 25
HR_CACHE = False

ALBUM_FIELDS = frozenset(('key', 'artist', 'icon', 'shortUrl', 'tracks'))


render = render_jinja('templates', encoding='utf-8')

class Cookies(object):
  def __setitem__(self, key, value):
    web.setcookie(key, urllib.quote(json.dumps(value)))

  def __getitem__(self, key):
    value = web.cookies().get(key)
    if value is not None:
      try:
        value = json.loads(urllib.unquote(value))
      except:
        value = None
    return value

  def has_key(self, key):
    return web.cookies().get(key) is not None

  def __delitem__(self, key):
    web.setcookie(key, '', expires=-1)

class RdioRequestHandler(object):
    __cached_cookies = None
    @property
    def cookies(self):
        if self.__cached_cookies is None:
            self.__cached_cookies = Cookies()
        return self.__cached_cookies

    __cached_rdio = None
    @property
    def rdio(self):
        if self.__cached_rdio is None:
            self.__cached_rdio = Rdio(CONSUMER_TOKEN,
                                      CONSUMER_SECRET,
                                      self.cookies)
        return self.__cached_rdio

    @property
    def secret(self):
        if not self.cookies.has_key('secret') or \
           self.cookies['secret'] is None or \
           self.cookies['secret'] == '':
            # 128 bits of randomness as a big hex
            self.cookies['secret'] = '%x' % random.getrandbits(128)
        return self.cookies['secret']

    @property
    def user(self):
        if self.rdio.authenticated:
            try:
                return self.rdio.currentUser()
            except:
                logging.warning("failed to make authed call, logging out")
                self.rdio.logout()
        else:
            return None


class MainHandler(RdioRequestHandler):
    def GET(self):
      web.header('content-type', 'text/html')
      return render.welcome(signed_in=(self.user is not None))

class GameHandler(RdioRequestHandler):
  def heavyRotation(self):
    raise NotImplementedError("you need to implement heavyRotation")

  def cacheKey(self):
    raise NotImplementedError("you need to implement cacheKey")

  def sourceName(self):
    raise NotImplementedError("you need to implement sourceName")

  def requiresAuth(self):
    return True

  def processHeavyRotation(self, heavy_rotation):
    # get all non-various-artist albums from heavy rotation
    albums = [album for album in heavy_rotation
              if album['artist'] != 'Various Artists']
    # only want one album per artist
    artists = []
    the_albums = []
    for album in albums:
        if album['artist'] in artists: continue
        the_albums.append(album)
        artists.append(album['artist'])
    # just get the album keys
    albums_keys = [album['key'] for album in the_albums]

    # now get all of those albums including their tracks,
    # because that's what we actually want
    albums = self.rdio.call('get',
                            keys=(','.join(albums_keys)),
                            extras='tracks').values()

    # eliminate the album fields we don't use
    albums = [dict([(k,v) for k,v in album.items() if k in ALBUM_FIELDS])
      for album in albums
    ]
    # collapse album track objects into just their ids
    for album in albums:
      album['tracks'] = [track['key'] for track in album['tracks']]

    return albums

  def cachedHeavyRotation(self):
    '''use a cached version of heavy rotation or fetch a new one'''
    memcache_key = self.cacheKey()
    hr = memcache.get(memcache_key)
    timestamp = memcache.get(memcache_key+'_ts')
    if hr is not None and timestamp is not None:
      # parse the json from memcache
      hr = json.loads(hr)
      # check the timestamp
      if int(timestamp) + HR_EXPIRY < time.time():
        hr = None
    if hr is None:
      hr = self.processHeavyRotation(self.heavyRotation())
      memcache.add(memcache_key, json.dumps(hr), HR_EXPIRY)
      memcache.add(memcache_key+'_ts', time.time())
    return hr

  def GET(self):
    if self.requiresAuth() and not self.rdio.authenticated:
      web.seeother('/signin')
      return
    web.header('content-type', 'text/html')
    if HR_CACHE:
      albums = self.cachedHeavyRotation()
    else:
      albums = self.processHeavyRotation(self.heavyRotation())

    domain = urlparse(wsgiref.util.request_uri(web.ctx.environ)).netloc
    if domain.find(':') != -1:
      domain = domain[:domain.find(':')]
    playback_token = self.rdio.getPlaybackToken(domain=domain)


    return render.index(**{'user': self.user,
                                 'source': self.sourceName(),
                                 'albums': albums,
                                 'albums_json': json.dumps(albums, indent=True),
                                 'api_swf': 'http://rd.io/api/swf/',
                                 'domain': domain,
                                 'playbackToken': playback_token,
                                 })

class EveryoneHandler(GameHandler):
  def cacheKey(self):
    return 'hr_everyone'
  def heavyRotation(self):
    return self.rdio.getHeavyRotation(limit=HR_LIMIT)
  def sourceName(self):
    return 'Top Albums on Rdio'
  def requiresAuth(self):
    return False

class UserHandler(GameHandler):
  def cacheKey(self):
    return 'hr_user_%s' % self.user
  def heavyRotation(self):
    return self.rdio.getHeavyRotation(user=self.user['key'],
                                      friends=False,
                                      limit=HR_LIMIT)
  def sourceName(self):
    return 'Your Top Albums on Rdio'

class FriendsHandler(GameHandler):
  def cacheKey(self):
    return 'hr_friends_%s' % self.user
  def heavyRotation(self):
    return self.rdio.getHeavyRotation(user=self.user['key'],
                                      friends=True,
                                      limit=HR_LIMIT)
  def sourceName(self):
    return 'Your Friends\' Top Albums on Rdio'


class LoginHandler(RdioRequestHandler):
    def GET(self):
        web.header('content-type', 'text/html')
        if self.rdio.authenticated:
            self.rdio.logout()

        if not self.rdio.authenticating:
            # obtain a request token
            callback_url = wsgiref.util.request_uri(web.ctx.environ)
            web.seeother(self.rdio.begin_authentication(callback_url))
        else:
            try:
              self.rdio.complete_authentication(web.input().get('oauth_verifier'))
              web.seeother('/')
            except RdioProtocolException, e:
              logging.exception('completing authentication')
              self.rdio.logout()
              web.seeother('/signin')


class LogoutHandler(RdioRequestHandler):
    def GET(self):
        web.header('content-type', 'text/html')
        self.rdio.logout()
        if self.cookies.has_key('hr'):
            del self.cookies['hr']
        web.seeother('/')

application = web.application(
        (
        r'/', 'MainHandler',
        r'/user', 'UserHandler',
        r'/friends', 'FriendsHandler',
        r'/everyone', 'EveryoneHandler',
        r'/signin', 'LoginHandler',
        r'/signout', 'LogoutHandler',
        ), globals())


if __name__ == '__main__':
  web.config.debug = False
  import os
  if os.environ.has_key('GATEWAY_INTERFACE'):
    # running as a CGI
    application.cgirun()
  else:
    # running standalone
    application.run()
Something went wrong with that request. Please try again.