from __future__ import unicode_literals import random, string, 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 tornado.escape import json_encode, json_decode from spotipy import Spotify # import logger logger = logging.getLogger(__name__) class IrisCore(object): version = 0 is_root = ( os.geteuid() == 0 ) spotify_token = False queue_metadata = {} connections = {} radio = { "enabled": 0, "seed_artists": [], "seed_genres": [], "seed_tracks": [] } def on_start(self): logger.info('--- Starting Iris core '+self.version) ## # Generate a random string # # Used for connection_ids where none is provided by client # @return string ## def generateGuid(self, length): return ''.join(random.choice(string.lowercase) for i in range(length)) ## # Digest a protocol header into it's id/name parts # # @return dict ## def digest_protocol(self, protocol): # if we're a string, split into list # this handles the different ways we get this passed (select_subprotocols gives string, headers.get gives list) if isinstance(protocol, basestring): # make sure we strip any spaces (IE gives "element,element", proper browsers give "element, element") protocol = [i.strip() for i in protocol.split(',')] # if we've been given a valid array try: clientid = protocol[0] connection_id = protocol[1] username = protocol[2] generated = False # invalid, so just create a default connection, and auto-generate an ID except: clientid = self.generateGuid(12) connection_id = self.generateGuid(12) username = 'Anonymous' generated = True # construct our protocol object, and return return {"clientid": clientid, "connection_id": connection_id, "username": username, "generated": generated} def send_message(self, to, data): self.connections[to]['connection'].write_message( json_encode(data) ) def broadcast(self, data): for connection in self.connections.itervalues(): connection['connection'].write_message( json_encode(data) ) return {} ## # Connections # # Contains all our connections and client details. This requires updates # when new clients connect, and old ones disconnect. These events are broadcast # to all current connections ## def get_connections(self, data): connections = [] for connection in self.connections.itervalues(): connections.append(connection['client']) return { 'connections': connections } def add_connection(self, connection_id, connection, client): new_connection = { 'client': client, 'connection': connection } self.connections[connection_id] = new_connection self.broadcast({ 'type': 'client_connected', 'client': client }) def remove_connection(self, connection_id): if connection_id in self.connections: try: client = self.connections[connection_id]['client'] del self.connections[connection_id] self.broadcast({ 'type': 'client_disconnected', 'client': client }) except: logger.error('Failed to close connection to '+ connection_id) def set_username(self, data): connection_id = data['connection_id'] if connection_id in self.connections: self.connections[connection_id]['client']['username'] = data['username'] self.broadcast({ 'type': 'connection_updated', 'connection': self.connections[connection_id]['client'] }) return {} else: error = 'Connection "'+data['connection_id']+'" not found' logger.error(error) return { 'error': error } ## # System controls # # Faciitates upgrades and configuration fetching ## def get_config(self, data): config = { "spotify_username": self.config['spotify']['username'], "country": self.config['iris']['country'], "locale": self.config['iris']['locale'] } return { 'config': config } def get_version(self, data): 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 return { 'version': { 'current': self.version, 'latest': latest_version, 'is_root': self.is_root, 'upgrade_available': upgrade_available } } def perform_upgrade( self ): try: subprocess.check_call(["pip", "install", "--upgrade", "Mopidy-Iris"]) return True except subprocess.CalledProcessError: return False def restart( self ): os.execl(sys.executable, *([sys.executable]+sys.argv)) ## # Spotify Radio # # Accepts seed URIs and creates radio-like experience. When our tracklist is nearly # empty, we fetch more recommendations. This can result in duplicates. We keep the # recommendations limit low to avoid timeouts and slow UI ## def get_radio(self, data): return { 'radio': self.radio } def start_radio(self, data): self.radio = data self.radio['enabled'] = 1; self.core.tracklist.clear() self.core.tracklist.set_consume( True ) self.load_more_tracks() self.core.playback.play() self.broadcast({ 'type': 'radio_started', 'radio': self.radio }) return self.get_radio({}) def stop_radio(self, data): self.radio = { "enabled": 0, "seed_artists": [], "seed_genres": [], "seed_tracks": [] } self.core.playback.stop() self.broadcast({ 'type': 'radio_stopped', 'radio': self.radio }) return {} 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.broadcast({ 'error': 'Could not get radio tracks: 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: logger.error('IrisFrontend: Failed to fetch Spotify recommendations') self.broadcast({ 'error': 'Failed to fetch radio recommendations' }) ## # Additional queue metadata # # This maps tltracks with extra info for display in Iris, including # added_by and from_uri. ## def get_queue_metadata(self, data): return { 'queue_metadata': self.queue_metadata } def add_queue_metadata(self, data): for tlid in data['tlids']: item = { 'tlid': tlid, 'added_from': data['added_from'], 'added_by': data['added_by'] } self.queue_metadata['tlid_'+str(tlid)] = item self.broadcast({ 'type': 'queue_metadata_changed', 'queue_metadata': self.queue_metadata }) return {} def clean_queue_metadata( self ): cleaned_queue_metadata = {} 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)] self.queue_metadata = cleaned_queue_metadata self.broadcast({ 'type': 'queue_metadata_changed', 'queue_metadata': self.queue_metadata }) return {} ## # Spotify authentication # # 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 get_spotify_token(self, data): return { 'spotify_token': self.spotify_token } def refresh_spotify_token(self, data): 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 self.broadcast({ 'type': 'spotify_token_changed', 'spotify_token': self.spotify_token }) return self.get_spotify_token({}) except urllib2.HTTPError as e: return e