from __future__ import unicode_literals import logging, json, pykka, pylast, urllib, urllib2, os, sys, mopidy_iris, subprocess import tornado.web import tornado.websocket import tornado.ioloop from mopidy import config, ext from mopidy.core import CoreListener from pkg_resources import parse_version from spotipy import Spotify from websocket import WebsocketHandler from http import HttpHandler # import logger logger = logging.getLogger(__name__) ### # Create our factory # # This hooks all our components into the Mopidy registry. Called from __init__.py ## def make_iris_factory(apps, statics): def iris_factory(config, core): path = os.path.join( os.path.dirname(__file__), 'static') frontend = IrisFrontend(config, core) return [ (r"/images/(.*)", tornado.web.StaticFileHandler, { "path": config['local-images']['image_dir'] }), (r'/http/([^/]*)', HttpHandler, { "frontend": frontend }), (r'/ws/?', WebsocketHandler, { "frontend": frontend }), (r'/(.*)', tornado.web.StaticFileHandler, { "path": path, "default_filename": "index.html" }), ] return iris_factory ### # Spotmop supporting frontend # # This provides a wrapping thread for the Pusher websocket, as well as the radio infrastructure ## class IrisFrontend(pykka.ThreadingActor, CoreListener): def __init__(self, config, core): super(IrisFrontend, self).__init__() self.config = config self.core = core self.version = mopidy_iris.__version__ self.is_root = ( os.geteuid() == 0 ) self.spotify_token = False self.queue_metadata = {} self.connections = {} self.radio = { "enabled": 0, "seed_artists": [], "seed_genres": [], "seed_tracks": [] } def on_start(self): logger.info('Starting Iris '+self.version) ## # Get a new spotify authentication token for server-side use # # Uses the Client Credentials Flow, so is invisible to the user. We need this token for # any backend spotify requests (we don't tap in to Mopidy-Spotify, yet). Also used for # passing token to frontend for javascript requests without use of the Authorization Code Flow. ## def refresh_spotify_token( self ): url = 'https://accounts.spotify.com/api/token' authorization = 'YTg3ZmI0ZGJlZDMwNDc1YjhjZWMzODUyM2RmZjUzZTI6ZDdjODlkMDc1M2VmNDA2OGJiYTE2NzhjNmNmMjZlZDY=' headers = {'Authorization' : 'Basic ' + authorization} data = {'grant_type': 'client_credentials'} data_encoded = urllib.urlencode( data ) req = urllib2.Request(url, data_encoded, headers) try: response = urllib2.urlopen(req, timeout=30).read() response_dict = json.loads(response) self.spotify_token = response_dict return response_dict except urllib2.HTTPError as e: return e ## # Listen for core events, and update our frontend as required ## def track_playback_ended( self, tl_track, time_position ): self.check_for_radio_update() def tracklist_changed( self ): self.clean_queue_metadata() ## # See if we need to perform updates to our radio # # We see if we've got one or two tracks left, if so, go get some more ## def check_for_radio_update( self ): try: tracklistLength = self.core.tracklist.length.get() if( tracklistLength <= 5 and self.radio['enabled'] == 1 ): self.load_more_tracks() except RuntimeError: self.websocket.broadcast('error', {'source': 'check_for_radio_update', 'message': 'Could not fetch tracklist length'}) logger.warning('IrisFrontend: Could not fetch tracklist length') pass ## # Load some more radio tracks # # We need to build a Spotify authentication token first, and then fetch recommendations ## def load_more_tracks( self ): # this is crude, but it means we don't need to handle expired tokens # TODO: address this when it's clear what Jodal and the team want to do with Pyspotify self.refresh_spotify_token() try: token = self.spotify_token token = token['access_token'] except: logger.error('IrisFrontend: access_token missing or invalid') self.websocket.broadcast('error', {'source': 'load_more_tracks', 'message': 'access_token missing or invalid'}) try: spotify = Spotify( auth = token ) response = spotify.recommendations(seed_artists = self.radio['seed_artists'], seed_genres = self.radio['seed_genres'], seed_tracks = self.radio['seed_tracks'], limit = 5) uris = [] for track in response['tracks']: uris.append( track['uri'] ) self.core.tracklist.add( uris = uris ) except: self.websocket.broadcast('error', {'source': 'load_more_tracks', 'message': 'Failed to fetch Spotify recommendations'}) logger.error('IrisFrontend: Failed to fetch Spotify recommendations') ## # Start radio # # Take the provided radio details, and start a new radio process ## def start_radio( self, new_state ): # set our new radio state self.radio = new_state self.radio['enabled'] = 1; # clear all tracks self.core.tracklist.clear() # explicitly set consume, to ensure we don't end up with a huge tracklist (and it's how a radio should 'feel') self.core.tracklist.set_consume( True ) # load me some tracks, and start playing! self.load_more_tracks() self.core.playback.play() # notify clients self.websocket.broadcast('radio', { 'radio': self.radio }) # return new radio state to initial call return self.radio ## # Stop radio ## def stop_radio( self ): # reset radio self.radio = { "enabled": 0, "seed_artists": [], "seed_genres": [], "seed_tracks": [] } # stop track playback self.core.playback.stop() # notify clients self.websocket.broadcast( 'radio', { 'radio': self.radio }) # return new radio state to initial call return self.radio # get our spotify token def get_spotify_token( self ): return self.spotify_token ## # Queue metadata ## def get_queue_metadata( self ): return self.queue_metadata def add_queue_metadata( self, tlids, added_from, added_by ): for tlid in tlids: item = { 'tlid': tlid, 'added_from': added_from, 'added_by': added_by } self.queue_metadata['tlid_'+str(tlid)] = item # broadcast to all clients self.websocket.broadcast('queue_metadata', {'queue_metadata': self.queue_metadata}) return self.queue_metadata # fetch our tracklist, and remove any metadata for tlids that don't exist anymore def clean_queue_metadata( self ): cleaned_queue_metadata = {} # get and loop all tltracks for tltrack in self.core.tracklist.get_tl_tracks().get(): # if we have metadata for this track, push it through to cleaned dictionary if 'tlid_'+str(tltrack.tlid) in self.queue_metadata: cleaned_queue_metadata['tlid_'+str(tltrack.tlid)] = self.queue_metadata['tlid_'+str(tltrack.tlid)] # update our cleaned store self.queue_metadata = cleaned_queue_metadata # broadcast to all clients self.websocket.broadcast('queue_metadata', {'queue_metadata': self.queue_metadata}) ## # System configuration # # This enables Iris to respect system config ## def get_config( self ): all_config = self.config config = { "spotify_username": all_config['spotify']['username'], "country": all_config['iris']['country'], "locale": all_config['iris']['locale'] } return config ## # Get Spotmop version, and check for updates # # We compare our version with the latest available on PyPi ## def get_version( self ): url = 'https://pypi.python.org/pypi/Mopidy-Iris/json' req = urllib2.Request(url) try: response = urllib2.urlopen(req, timeout=30).read() response = json.loads(response) latest_version = response['info']['version'] # compare our versions, and convert result to boolean upgrade_available = cmp( parse_version( latest_version ), parse_version( self.version ) ) upgrade_available = ( upgrade_available == 1 ) except urllib2.HTTPError as e: latest_version = '0.0.0' upgrade_available = False # prepare our response data = { 'current': self.version, 'latest': latest_version, 'is_root': self.is_root, 'upgrade_available': upgrade_available } return data ## # Upgrade Spotmop module # # Upgrade myself to the latest version available on PyPi ## def perform_upgrade( self ): try: subprocess.check_call(["pip", "install", "--upgrade", "Mopidy-Iris"]) return True except subprocess.CalledProcessError: return False ## # Restart Mopidy # # This is untested and may require installation of an upstart script to properly restart ## def restart( self ): os.execl(sys.executable, *([sys.executable]+sys.argv))