2017-02-18 12:45:48 +13:00
|
|
|
|
|
|
|
|
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
|
2017-10-13 10:02:06 +13:00
|
|
|
import tornado.httpclient
|
2017-10-06 22:01:58 +13:00
|
|
|
import requests
|
2017-10-13 10:02:06 +13:00
|
|
|
from promise import Promise
|
2017-02-18 12:45:48 +13:00
|
|
|
from mopidy import config, ext
|
|
|
|
|
from mopidy.core import CoreListener
|
|
|
|
|
from pkg_resources import parse_version
|
|
|
|
|
from tornado.escape import json_encode, json_decode
|
|
|
|
|
|
2017-09-25 14:10:46 -04:00
|
|
|
if sys.platform == 'win32':
|
|
|
|
|
import ctypes
|
|
|
|
|
|
2017-02-18 12:45:48 +13:00
|
|
|
# import logger
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
class IrisCore(object):
|
|
|
|
|
|
|
|
|
|
version = 0
|
2017-09-25 14:10:46 -04:00
|
|
|
if sys.platform == 'win32':
|
|
|
|
|
is_root = ctypes.windll.shell32.IsUserAnAdmin() != 0
|
|
|
|
|
else:
|
|
|
|
|
is_root = os.geteuid() == 0
|
2017-02-18 12:45:48 +13:00
|
|
|
spotify_token = False
|
|
|
|
|
queue_metadata = {}
|
|
|
|
|
connections = {}
|
2017-04-18 08:52:28 +12:00
|
|
|
initial_consume = False
|
2017-02-18 12:45:48 +13:00
|
|
|
radio = {
|
|
|
|
|
"enabled": 0,
|
|
|
|
|
"seed_artists": [],
|
|
|
|
|
"seed_genres": [],
|
2017-08-25 08:39:42 +12:00
|
|
|
"seed_tracks": [],
|
|
|
|
|
"results": []
|
2017-02-18 12:45:48 +13:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
##
|
|
|
|
|
# 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}
|
|
|
|
|
|
2017-02-18 22:04:05 +13:00
|
|
|
|
2017-10-04 16:47:15 +13:00
|
|
|
def send_message(self, *args, **kwargs):
|
|
|
|
|
connection_id = kwargs.get('connection_id', None)
|
|
|
|
|
data = kwargs.get('data', {})
|
|
|
|
|
|
2017-10-01 09:38:01 +13:00
|
|
|
try:
|
2017-10-04 16:47:15 +13:00
|
|
|
self.connections[connection_id]['connection'].write_message( json_encode(data) )
|
2017-10-01 09:38:01 +13:00
|
|
|
except:
|
|
|
|
|
self.raven_client.captureException()
|
2017-10-04 16:47:15 +13:00
|
|
|
logger.error('Failed to send message to '+ connection_id)
|
|
|
|
|
|
2017-02-18 12:45:48 +13:00
|
|
|
|
2017-10-04 16:47:15 +13:00
|
|
|
def broadcast(self, *args, **kwargs):
|
|
|
|
|
data = kwargs.get('data', None)
|
2017-02-18 12:45:48 +13:00
|
|
|
|
|
|
|
|
for connection in self.connections.itervalues():
|
|
|
|
|
connection['connection'].write_message( json_encode(data) )
|
2017-02-21 05:46:28 +13:00
|
|
|
return {
|
2017-02-21 08:58:49 +13:00
|
|
|
'status': 1,
|
2017-02-21 05:46:28 +13:00
|
|
|
'message': 'Broadcast to '+str(len(self.connections))+' connections'
|
|
|
|
|
}
|
2017-02-18 12:45:48 +13:00
|
|
|
|
|
|
|
|
##
|
2017-02-18 22:04:05 +13:00
|
|
|
# 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
|
2017-02-18 12:45:48 +13:00
|
|
|
##
|
2017-02-18 22:04:05 +13:00
|
|
|
|
2017-10-04 16:47:15 +13:00
|
|
|
def get_connections(self, *args, **kwargs):
|
2017-02-18 22:04:05 +13:00
|
|
|
connections = []
|
|
|
|
|
for connection in self.connections.itervalues():
|
|
|
|
|
connections.append(connection['client'])
|
|
|
|
|
|
|
|
|
|
return {
|
2017-02-21 08:58:49 +13:00
|
|
|
'status': 1,
|
2017-02-18 22:04:05 +13:00
|
|
|
'connections': connections
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-04 16:47:15 +13:00
|
|
|
def add_connection(self, *args, **kwargs):
|
|
|
|
|
connection_id = kwargs.get('connection_id', None)
|
|
|
|
|
connection = kwargs.get('connection', None)
|
|
|
|
|
client = kwargs.get('client', None)
|
|
|
|
|
|
2017-02-18 12:45:48 +13:00
|
|
|
new_connection = {
|
|
|
|
|
'client': client,
|
|
|
|
|
'connection': connection
|
|
|
|
|
}
|
|
|
|
|
self.connections[connection_id] = new_connection
|
|
|
|
|
|
2017-02-19 21:27:43 +13:00
|
|
|
self.send_message(
|
2017-10-04 16:47:15 +13:00
|
|
|
connection_id=connection_id,
|
|
|
|
|
data={
|
2017-02-19 21:27:43 +13:00
|
|
|
'type': 'connected',
|
|
|
|
|
'connection_id': connection_id,
|
|
|
|
|
'username': client['username']
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
2017-10-04 16:47:15 +13:00
|
|
|
self.broadcast(
|
|
|
|
|
data={
|
|
|
|
|
'type': 'connection_added',
|
|
|
|
|
'connection': client
|
|
|
|
|
}
|
|
|
|
|
)
|
2017-02-18 12:45:48 +13:00
|
|
|
|
|
|
|
|
def remove_connection(self, connection_id):
|
|
|
|
|
if connection_id in self.connections:
|
|
|
|
|
try:
|
2017-02-18 22:04:05 +13:00
|
|
|
client = self.connections[connection_id]['client']
|
2017-02-18 12:45:48 +13:00
|
|
|
del self.connections[connection_id]
|
2017-10-04 16:47:15 +13:00
|
|
|
self.broadcast(
|
|
|
|
|
data={
|
|
|
|
|
'type': 'connection_removed',
|
|
|
|
|
'connection': client
|
|
|
|
|
}
|
|
|
|
|
)
|
2017-02-18 12:45:48 +13:00
|
|
|
except:
|
2017-09-22 16:15:50 +12:00
|
|
|
self.raven_client.captureException()
|
2017-02-18 22:04:05 +13:00
|
|
|
logger.error('Failed to close connection to '+ connection_id)
|
|
|
|
|
|
2017-10-04 16:47:15 +13:00
|
|
|
def set_username(self, *args, **kwargs):
|
2017-10-05 08:58:02 +13:00
|
|
|
try:
|
|
|
|
|
data = kwargs.get('data', {})
|
|
|
|
|
except:
|
|
|
|
|
self.raven_client.captureException()
|
|
|
|
|
return {
|
|
|
|
|
'status': 0,
|
|
|
|
|
'message': 'Malformed data',
|
|
|
|
|
'source': 'set_username'
|
|
|
|
|
}
|
|
|
|
|
|
2017-02-18 22:04:05 +13:00
|
|
|
connection_id = data['connection_id']
|
2017-10-04 16:47:15 +13:00
|
|
|
|
2017-02-18 22:04:05 +13:00
|
|
|
if connection_id in self.connections:
|
|
|
|
|
self.connections[connection_id]['client']['username'] = data['username']
|
2017-10-04 16:47:15 +13:00
|
|
|
self.broadcast(
|
|
|
|
|
data={
|
|
|
|
|
'type': 'connection_updated',
|
|
|
|
|
'connection': self.connections[connection_id]['client']
|
|
|
|
|
}
|
|
|
|
|
)
|
2017-02-19 21:27:43 +13:00
|
|
|
return {
|
2017-02-21 08:58:49 +13:00
|
|
|
'status': 1,
|
2017-06-13 11:56:20 +12:00
|
|
|
'connection_id': connection_id,
|
2017-02-19 21:27:43 +13:00
|
|
|
'username': data['username']
|
|
|
|
|
}
|
2017-02-18 22:04:05 +13:00
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
error = 'Connection "'+data['connection_id']+'" not found'
|
2017-09-22 16:15:50 +12:00
|
|
|
self.raven_client.captureMessage(error)
|
2017-02-18 22:04:05 +13:00
|
|
|
logger.error(error)
|
|
|
|
|
return {
|
2017-02-21 08:58:49 +13:00
|
|
|
'status': 0,
|
|
|
|
|
'message': error
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-04 16:47:15 +13:00
|
|
|
def deliver_message(self, *args, **kwargs):
|
2017-10-05 08:58:02 +13:00
|
|
|
try:
|
|
|
|
|
data = kwargs.get('data', {})
|
|
|
|
|
except:
|
|
|
|
|
self.raven_client.captureException()
|
|
|
|
|
return {
|
|
|
|
|
'status': 0,
|
|
|
|
|
'message': 'Malformed data',
|
|
|
|
|
'source': 'deliver_message'
|
|
|
|
|
}
|
2017-02-21 08:58:49 +13:00
|
|
|
|
2017-10-04 16:47:15 +13:00
|
|
|
if data['connection_id'] in self.connections:
|
|
|
|
|
self.send_message(connection_id=data['connection_id'], data=data['message'])
|
2017-02-21 08:58:49 +13:00
|
|
|
return {
|
2017-10-04 16:47:15 +13:00
|
|
|
'status': 1,
|
|
|
|
|
'message': 'Sent message to '+data['connection_id']
|
2017-02-21 08:58:49 +13:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
error = 'Connection "'+data['connection_id']+'" not found'
|
2017-09-22 16:15:50 +12:00
|
|
|
self.raven_client.captureMessage(error)
|
2017-02-21 08:58:49 +13:00
|
|
|
logger.error(error)
|
|
|
|
|
return {
|
|
|
|
|
'status': 0,
|
|
|
|
|
'message': error
|
|
|
|
|
}
|
2017-02-18 22:04:05 +13:00
|
|
|
|
|
|
|
|
|
2017-02-18 12:45:48 +13:00
|
|
|
|
|
|
|
|
|
2017-02-18 22:04:05 +13:00
|
|
|
##
|
|
|
|
|
# System controls
|
|
|
|
|
#
|
|
|
|
|
# Faciitates upgrades and configuration fetching
|
|
|
|
|
##
|
2017-02-18 12:45:48 +13:00
|
|
|
|
2017-10-04 16:47:15 +13:00
|
|
|
def get_config(self, *args, **kwargs):
|
2017-03-31 08:46:44 +13:00
|
|
|
|
|
|
|
|
# handle config setups where there is no username/password
|
|
|
|
|
# Iris won't work properly anyway, but at least we won't get server errors
|
2017-03-31 08:33:27 +13:00
|
|
|
if 'spotify' in self.config and 'username' in self.config['spotify']:
|
|
|
|
|
spotify_username = self.config['spotify']['username']
|
|
|
|
|
else:
|
|
|
|
|
spotify_username = False
|
|
|
|
|
|
2017-02-18 12:45:48 +13:00
|
|
|
config = {
|
2017-03-31 08:33:27 +13:00
|
|
|
"spotify_username": spotify_username,
|
2017-02-18 12:45:48 +13:00
|
|
|
"country": self.config['iris']['country'],
|
2017-06-12 08:34:21 +12:00
|
|
|
"locale": self.config['iris']['locale'],
|
|
|
|
|
"authorization_url": self.config['iris']['authorization_url']
|
2017-02-18 12:45:48 +13:00
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
'config': config
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-04 16:47:15 +13:00
|
|
|
def get_version(self, *args, **kwargs):
|
2017-02-18 12:45:48 +13:00
|
|
|
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:
|
2017-09-22 16:15:50 +12:00
|
|
|
self.raven_client.captureException(e)
|
2017-02-18 12:45:48 +13:00
|
|
|
latest_version = '0.0.0'
|
|
|
|
|
upgrade_available = False
|
|
|
|
|
|
|
|
|
|
return {
|
2017-02-21 08:58:49 +13:00
|
|
|
'status': 1,
|
2017-02-18 12:45:48 +13:00
|
|
|
'version': {
|
|
|
|
|
'current': self.version,
|
|
|
|
|
'latest': latest_version,
|
|
|
|
|
'is_root': self.is_root,
|
|
|
|
|
'upgrade_available': upgrade_available
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-02-18 22:04:05 +13:00
|
|
|
def perform_upgrade( self ):
|
|
|
|
|
try:
|
|
|
|
|
subprocess.check_call(["pip", "install", "--upgrade", "Mopidy-Iris"])
|
|
|
|
|
return True
|
2017-09-22 16:15:50 +12:00
|
|
|
except subprocess.CalledProcessError as e:
|
|
|
|
|
self.raven_client.captureException(e)
|
2017-02-18 22:04:05 +13:00
|
|
|
return False
|
2017-02-18 12:45:48 +13:00
|
|
|
|
2017-02-18 22:04:05 +13:00
|
|
|
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
|
|
|
|
|
##
|
2017-02-18 12:45:48 +13:00
|
|
|
|
2017-10-04 16:47:15 +13:00
|
|
|
def get_radio(self, *args, **kwargs):
|
2017-02-18 12:45:48 +13:00
|
|
|
return {
|
2017-02-21 08:58:49 +13:00
|
|
|
'status': 1,
|
2017-02-18 12:45:48 +13:00
|
|
|
'radio': self.radio
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-04 16:47:15 +13:00
|
|
|
def change_radio(self, *args, **kwargs):
|
2017-10-05 08:58:02 +13:00
|
|
|
try:
|
|
|
|
|
data = kwargs.get('data', {})
|
|
|
|
|
except:
|
|
|
|
|
self.raven_client.captureException()
|
|
|
|
|
return {
|
|
|
|
|
'status': 0,
|
|
|
|
|
'message': 'Malformed data',
|
|
|
|
|
'source': 'change_radio'
|
|
|
|
|
}
|
2017-02-21 05:46:28 +13:00
|
|
|
|
2017-04-18 08:52:28 +12:00
|
|
|
# figure out if we're starting or updating radio mode
|
2017-04-19 06:58:41 +12:00
|
|
|
if data['update'] and self.radio['enabled']:
|
2017-04-18 08:52:28 +12:00
|
|
|
starting = False
|
|
|
|
|
self.initial_consume = self.core.tracklist.get_consume()
|
2017-02-21 05:46:28 +13:00
|
|
|
else:
|
2017-04-18 08:52:28 +12:00
|
|
|
starting = True
|
|
|
|
|
|
|
|
|
|
# fetch more tracks from Mopidy-Spotify
|
2017-04-18 04:44:39 +12:00
|
|
|
self.radio = data
|
|
|
|
|
self.radio['enabled'] = 1;
|
2017-08-25 08:39:42 +12:00
|
|
|
self.radio['results'] = [];
|
2017-04-18 04:44:39 +12:00
|
|
|
uris = self.load_more_tracks()
|
|
|
|
|
|
2017-04-18 08:52:28 +12:00
|
|
|
# make sure we got recommendations
|
|
|
|
|
if uris:
|
|
|
|
|
if starting:
|
|
|
|
|
self.core.tracklist.clear()
|
|
|
|
|
|
|
|
|
|
self.core.tracklist.set_consume(True)
|
2017-08-25 08:39:42 +12:00
|
|
|
|
|
|
|
|
# We only want to play the first batch
|
|
|
|
|
added = self.core.tracklist.add(uris = uris[0:3])
|
|
|
|
|
|
|
|
|
|
# Save results (minus first batch) for later use
|
|
|
|
|
self.radio['results'] = uris[3:]
|
2017-04-18 08:52:28 +12:00
|
|
|
|
|
|
|
|
if added.get():
|
|
|
|
|
if starting:
|
|
|
|
|
self.core.playback.play()
|
2017-10-04 16:47:15 +13:00
|
|
|
self.broadcast(
|
|
|
|
|
data={
|
|
|
|
|
'type': 'radio_started',
|
|
|
|
|
'radio': self.radio
|
|
|
|
|
}
|
|
|
|
|
)
|
2017-04-18 08:52:28 +12:00
|
|
|
else:
|
2017-10-04 16:47:15 +13:00
|
|
|
self.broadcast(
|
|
|
|
|
data={
|
|
|
|
|
'type': 'radio_changed',
|
|
|
|
|
'radio': self.radio
|
|
|
|
|
}
|
|
|
|
|
)
|
2017-04-18 08:52:28 +12:00
|
|
|
|
|
|
|
|
return self.get_radio({})
|
|
|
|
|
|
|
|
|
|
# failed fetching/adding tracks, so no-go
|
|
|
|
|
self.radio['enabled'] = 0;
|
|
|
|
|
return {
|
|
|
|
|
'status': 0,
|
|
|
|
|
'message': 'Could not start radio',
|
|
|
|
|
'radio': self.radio
|
|
|
|
|
}
|
2017-04-18 04:44:39 +12:00
|
|
|
|
2017-02-18 12:45:48 +13:00
|
|
|
|
2017-10-04 16:47:15 +13:00
|
|
|
def stop_radio(self, *args, **kwargs):
|
2017-04-18 08:52:28 +12:00
|
|
|
|
2017-02-18 12:45:48 +13:00
|
|
|
self.radio = {
|
|
|
|
|
"enabled": 0,
|
|
|
|
|
"seed_artists": [],
|
|
|
|
|
"seed_genres": [],
|
2017-08-25 08:39:42 +12:00
|
|
|
"seed_tracks": [],
|
|
|
|
|
"results": []
|
2017-02-18 12:45:48 +13:00
|
|
|
}
|
2017-04-18 08:52:28 +12:00
|
|
|
|
|
|
|
|
# restore initial consume state
|
|
|
|
|
self.core.tracklist.set_consume(self.initial_consume)
|
|
|
|
|
self.core.playback.stop()
|
2017-02-18 12:45:48 +13:00
|
|
|
|
2017-10-04 16:47:15 +13:00
|
|
|
self.broadcast(
|
|
|
|
|
data={
|
|
|
|
|
'type': 'radio_stopped',
|
|
|
|
|
'radio': self.radio
|
|
|
|
|
}
|
|
|
|
|
)
|
2017-02-18 12:45:48 +13:00
|
|
|
|
2017-02-21 08:58:49 +13:00
|
|
|
return {
|
|
|
|
|
'status': 1
|
|
|
|
|
}
|
2017-02-18 12:45:48 +13:00
|
|
|
|
2017-02-18 22:04:05 +13:00
|
|
|
|
|
|
|
|
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:
|
2017-09-22 16:15:50 +12:00
|
|
|
error = 'IrisFrontend: access_token missing or invalid'
|
|
|
|
|
self.raven_client.captureMessage(error)
|
|
|
|
|
logger.error(error)
|
2017-10-04 16:47:15 +13:00
|
|
|
self.broadcast(
|
|
|
|
|
data={
|
|
|
|
|
'type': 'error',
|
|
|
|
|
'message': 'Could not get radio tracks: access_token missing or invalid',
|
|
|
|
|
'source': 'load_more_tracks'
|
|
|
|
|
}
|
|
|
|
|
)
|
2017-02-18 22:04:05 +13:00
|
|
|
|
|
|
|
|
try:
|
2017-08-27 09:39:35 +12:00
|
|
|
url = 'https://api.spotify.com/v1/recommendations/'
|
|
|
|
|
url = url+'?seed_artists='+(",".join(self.radio['seed_artists'])).replace('spotify:artist:','')
|
|
|
|
|
url = url+'&seed_genres='+(",".join(self.radio['seed_genres'])).replace('spotify:genre:','')
|
|
|
|
|
url = url+'&seed_tracks='+(",".join(self.radio['seed_tracks'])).replace('spotify:track:','')
|
|
|
|
|
url = url+'&limit=50'
|
|
|
|
|
|
|
|
|
|
req = urllib2.Request(url)
|
|
|
|
|
req.add_header('Authorization', 'Bearer '+self.spotify_token['access_token'])
|
|
|
|
|
|
|
|
|
|
response = urllib2.urlopen(req, timeout=30).read()
|
|
|
|
|
response_dict = json.loads(response)
|
2017-02-18 22:04:05 +13:00
|
|
|
|
|
|
|
|
uris = []
|
2017-08-27 09:39:35 +12:00
|
|
|
for track in response_dict['tracks']:
|
2017-02-18 22:04:05 +13:00
|
|
|
uris.append( track['uri'] )
|
2017-08-25 08:39:42 +12:00
|
|
|
|
2017-02-21 05:46:28 +13:00
|
|
|
return uris
|
2017-08-25 08:39:42 +12:00
|
|
|
|
2017-02-18 22:04:05 +13:00
|
|
|
except:
|
2017-09-22 16:15:50 +12:00
|
|
|
self.raven_client.captureException()
|
2017-02-18 22:04:05 +13:00
|
|
|
logger.error('IrisFrontend: Failed to fetch Spotify recommendations')
|
2017-10-04 16:47:15 +13:00
|
|
|
self.broadcast(
|
|
|
|
|
data={
|
|
|
|
|
'type': 'error',
|
|
|
|
|
'message': 'Could not get radio tracks',
|
|
|
|
|
'source': 'load_more_tracks'
|
|
|
|
|
}
|
|
|
|
|
)
|
2017-08-25 08:39:42 +12:00
|
|
|
return []
|
2017-02-21 05:46:28 +13:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def check_for_radio_update( self ):
|
|
|
|
|
tracklistLength = self.core.tracklist.length.get()
|
2017-10-04 16:47:15 +13:00
|
|
|
if (tracklistLength < 3 and self.radio['enabled'] == 1):
|
2017-02-21 05:46:28 +13:00
|
|
|
|
2017-08-25 08:39:42 +12:00
|
|
|
# Grab our loaded tracks
|
|
|
|
|
uris = self.radio['results']
|
2017-02-21 05:46:28 +13:00
|
|
|
|
2017-08-25 08:39:42 +12:00
|
|
|
# We've run out of pre-fetched tracks, so we need to get more recommendations
|
|
|
|
|
if (len(uris) < 3):
|
|
|
|
|
uris = self.load_more_tracks()
|
|
|
|
|
|
|
|
|
|
# Remove the next batch, and update our results
|
|
|
|
|
self.radio['results'] = uris[3:]
|
|
|
|
|
|
|
|
|
|
# Only add the next set of uris
|
|
|
|
|
uris = uris[0:3]
|
2017-02-21 05:46:28 +13:00
|
|
|
|
2017-08-25 08:39:42 +12:00
|
|
|
self.core.tracklist.add(uris = uris)
|
2017-02-21 05:46:28 +13:00
|
|
|
|
2017-02-18 22:04:05 +13:00
|
|
|
|
|
|
|
|
|
|
|
|
|
##
|
|
|
|
|
# Additional queue metadata
|
|
|
|
|
#
|
|
|
|
|
# This maps tltracks with extra info for display in Iris, including
|
|
|
|
|
# added_by and from_uri.
|
|
|
|
|
##
|
|
|
|
|
|
2017-10-04 16:47:15 +13:00
|
|
|
def get_queue_metadata(self, *args, **kwargs):
|
2017-02-18 12:45:48 +13:00
|
|
|
return {
|
2017-02-21 08:58:49 +13:00
|
|
|
'status': 1,
|
2017-02-18 12:45:48 +13:00
|
|
|
'queue_metadata': self.queue_metadata
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-04 16:47:15 +13:00
|
|
|
def add_queue_metadata(self, *args, **kwargs):
|
2017-10-05 08:58:02 +13:00
|
|
|
try:
|
|
|
|
|
data = kwargs.get('data', {})
|
|
|
|
|
except:
|
|
|
|
|
self.raven_client.captureException()
|
|
|
|
|
return {
|
|
|
|
|
'status': 0,
|
|
|
|
|
'message': 'Malformed data',
|
|
|
|
|
'source': 'add_queue_metadata'
|
|
|
|
|
}
|
2017-02-18 22:04:05 +13:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2017-10-04 16:47:15 +13:00
|
|
|
self.broadcast(
|
|
|
|
|
data={
|
|
|
|
|
'type': 'queue_metadata_changed',
|
|
|
|
|
'queue_metadata': self.queue_metadata
|
|
|
|
|
}
|
|
|
|
|
)
|
2017-02-18 22:04:05 +13:00
|
|
|
|
2017-02-21 08:58:49 +13:00
|
|
|
return {
|
|
|
|
|
'status': 1
|
|
|
|
|
}
|
2017-02-18 22:04:05 +13:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2017-10-04 16:47:15 +13:00
|
|
|
self.broadcast(
|
|
|
|
|
data={
|
|
|
|
|
'type': 'queue_metadata_changed',
|
|
|
|
|
'queue_metadata': self.queue_metadata
|
|
|
|
|
}
|
|
|
|
|
)
|
2017-02-18 22:04:05 +13:00
|
|
|
|
2017-02-21 08:58:49 +13:00
|
|
|
return {
|
|
|
|
|
'status': 1
|
|
|
|
|
}
|
2017-02-18 22:04:05 +13:00
|
|
|
|
|
|
|
|
|
|
|
|
|
##
|
|
|
|
|
# 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.
|
|
|
|
|
##
|
|
|
|
|
|
2017-10-04 16:47:15 +13:00
|
|
|
def get_spotify_token(self, *args, **kwargs):
|
2017-02-18 22:04:05 +13:00
|
|
|
return {
|
|
|
|
|
'spotify_token': self.spotify_token
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-04 16:47:15 +13:00
|
|
|
def refresh_spotify_token(self, *args, **kwargs):
|
2017-06-13 08:58:10 +12:00
|
|
|
|
|
|
|
|
# Use client_id and client_secret from config
|
|
|
|
|
# This was introduced in Mopidy-Spotify 3.1.0
|
2017-08-06 20:22:16 +12:00
|
|
|
url = 'https://auth.mopidy.com/spotify/token'
|
|
|
|
|
data = {
|
|
|
|
|
'client_id': self.config['spotify']['client_id'],
|
|
|
|
|
'client_secret': self.config['spotify']['client_secret'],
|
|
|
|
|
'grant_type': 'client_credentials'
|
|
|
|
|
}
|
2017-06-13 08:58:10 +12:00
|
|
|
|
2017-02-18 22:04:05 +13:00
|
|
|
try:
|
2017-10-06 22:01:58 +13:00
|
|
|
response = requests.post(url, data)
|
|
|
|
|
self.spotify_token = response.json()
|
2017-02-18 22:04:05 +13:00
|
|
|
|
2017-10-04 16:47:15 +13:00
|
|
|
self.broadcast(
|
|
|
|
|
data={
|
|
|
|
|
'type': 'spotify_token_changed',
|
|
|
|
|
'spotify_token': self.spotify_token
|
|
|
|
|
}
|
|
|
|
|
)
|
2017-02-18 22:04:05 +13:00
|
|
|
|
|
|
|
|
return self.get_spotify_token({})
|
2017-06-13 08:58:10 +12:00
|
|
|
|
2017-02-18 22:04:05 +13:00
|
|
|
except urllib2.HTTPError as e:
|
2017-09-22 16:15:50 +12:00
|
|
|
self.raven_client.captureException()
|
2017-06-13 08:58:10 +12:00
|
|
|
error = json.loads(e.read())
|
|
|
|
|
|
|
|
|
|
return {
|
2017-10-05 08:58:02 +13:00
|
|
|
'status': 0,
|
2017-08-06 21:06:48 +12:00
|
|
|
'message': 'Could not refresh token: '+error['error_description'],
|
2017-06-13 08:58:10 +12:00
|
|
|
'source': 'refresh_spotify_token'
|
|
|
|
|
}
|
2017-10-04 16:47:15 +13:00
|
|
|
|
|
|
|
|
|
|
|
|
|
##
|
|
|
|
|
# Proxy a request to an external provider
|
|
|
|
|
#
|
|
|
|
|
# This is required when requesting to non-CORS providers. We simply make the request
|
|
|
|
|
# server-side and pass that back. All we change is the response's Access-Control-Allow-Origin
|
|
|
|
|
# to prevent CORS-blocking by the browser.
|
|
|
|
|
##
|
|
|
|
|
|
|
|
|
|
def proxy_request(self, *args, **kwargs):
|
2017-10-13 10:02:06 +13:00
|
|
|
callback = kwargs.get('callback', None)
|
|
|
|
|
origin_request = kwargs.get('request', None)
|
|
|
|
|
|
2017-10-05 08:58:02 +13:00
|
|
|
try:
|
|
|
|
|
data = kwargs.get('data', {})
|
|
|
|
|
except:
|
|
|
|
|
self.raven_client.captureException()
|
|
|
|
|
return {
|
|
|
|
|
'status': 0,
|
|
|
|
|
'message': 'Malformed data',
|
|
|
|
|
'source': 'proxy_request'
|
|
|
|
|
}
|
2017-10-04 16:47:15 +13:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# Our request includes data, so make sure we POST the data
|
|
|
|
|
if 'url' not in data:
|
|
|
|
|
self.raven_client.captureException()
|
|
|
|
|
return {
|
2017-10-05 08:58:02 +13:00
|
|
|
'status': 0,
|
|
|
|
|
'message': 'Malformed data (missing URL)',
|
2017-10-04 16:47:15 +13:00
|
|
|
'source': 'proxy_request',
|
|
|
|
|
'original_request': data
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Construct request headers
|
|
|
|
|
# If we have an original request, pass through it's headers
|
|
|
|
|
if origin_request:
|
2017-10-06 22:01:58 +13:00
|
|
|
headers = origin_request.headers
|
2017-10-04 16:47:15 +13:00
|
|
|
else:
|
2017-10-06 22:01:58 +13:00
|
|
|
headers = {}
|
2017-10-04 16:47:15 +13:00
|
|
|
|
|
|
|
|
# Adjust headers
|
2017-10-06 22:01:58 +13:00
|
|
|
headers["Accept-Language"] = "*"
|
|
|
|
|
headers["Accept-Encoding"] = "deflate"
|
|
|
|
|
if "Content-Type" in headers:
|
|
|
|
|
del headers["Content-Type"]
|
|
|
|
|
if "Host" in headers:
|
|
|
|
|
del headers["Host"]
|
|
|
|
|
if "X-Requested-With" in headers:
|
|
|
|
|
del headers["X-Requested-With"]
|
|
|
|
|
if "X-Forwarded-Server" in headers:
|
|
|
|
|
del headers["X-Forwarded-Server"]
|
|
|
|
|
if "X-Forwarded-Host" in headers:
|
|
|
|
|
del headers["X-Forwarded-Host"]
|
|
|
|
|
if "X-Forwarded-For" in headers:
|
|
|
|
|
del headers["X-Forwarded-For"]
|
|
|
|
|
if "Referrer" in headers:
|
|
|
|
|
del headers["Referrer"]
|
2017-10-04 16:47:15 +13:00
|
|
|
|
2017-10-06 22:01:58 +13:00
|
|
|
# Now actually attempt the request
|
|
|
|
|
try:
|
|
|
|
|
# Our request includes data, so make sure we POST the data
|
|
|
|
|
if ('data' in data and data['data']):
|
2017-10-13 10:02:06 +13:00
|
|
|
http_client = tornado.httpclient.AsyncHTTPClient()
|
|
|
|
|
request = tornado.httpclient.HTTPRequest(data['url'], method='POST', data=data['data'], headers=headers, validate_cert=False)
|
|
|
|
|
http_client.fetch(request, callback=callback)
|
2017-10-04 16:47:15 +13:00
|
|
|
|
2017-10-06 22:01:58 +13:00
|
|
|
# No data, so just a simple GET request
|
|
|
|
|
else:
|
2017-10-06 09:07:28 +13:00
|
|
|
|
2017-10-06 22:01:58 +13:00
|
|
|
# Strip out our origin content-length otherwise this confuses
|
|
|
|
|
# the target server as content-length doesn't apply to GET requests
|
|
|
|
|
if "Content-Length" in headers:
|
|
|
|
|
del headers["Content-Length"]
|
2017-10-06 09:07:28 +13:00
|
|
|
|
2017-10-13 10:02:06 +13:00
|
|
|
http_client = tornado.httpclient.AsyncHTTPClient()
|
|
|
|
|
request = tornado.httpclient.HTTPRequest(data['url'], headers=headers, validate_cert=False)
|
|
|
|
|
http_client.fetch(request, callback=callback)
|
2017-10-04 16:47:15 +13:00
|
|
|
|
|
|
|
|
|
2017-10-06 22:01:58 +13:00
|
|
|
# Attempt to decode body as JSON, otherwise just return plain text
|
2017-10-04 16:47:15 +13:00
|
|
|
try:
|
|
|
|
|
return {
|
2017-10-06 22:01:58 +13:00
|
|
|
'response_code': int(response.status_code),
|
|
|
|
|
'response': response.json()
|
2017-10-04 16:47:15 +13:00
|
|
|
}
|
|
|
|
|
except:
|
|
|
|
|
return {
|
2017-10-06 22:01:58 +13:00
|
|
|
'response_code': int(response.status_code),
|
|
|
|
|
'response': response.text
|
2017-10-04 16:47:15 +13:00
|
|
|
}
|
|
|
|
|
|
2017-10-06 22:01:58 +13:00
|
|
|
except requests.exceptions.RequestException as e:
|
2017-10-06 09:07:28 +13:00
|
|
|
return {
|
|
|
|
|
'status': 0,
|
|
|
|
|
'message': 'Could not complete proxy request',
|
|
|
|
|
'source': 'proxy_request',
|
2017-10-06 22:01:58 +13:00
|
|
|
'response': e.text,
|
|
|
|
|
'response_code': int(e.response_code),
|
2017-10-06 09:07:28 +13:00
|
|
|
'original_request': data
|
|
|
|
|
}
|
2017-10-13 10:02:06 +13:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test(self, *args, **kwargs):
|
|
|
|
|
|
|
|
|
|
print "running test 1"
|
|
|
|
|
response = requests.get("http://platform.james.plasticstudio.co/test.php?sleep=1")
|
|
|
|
|
|
|
|
|
|
return Promise(
|
|
|
|
|
lambda resolve, reject: resolve(response.text)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test2(self, *args, **kwargs):
|
|
|
|
|
|
|
|
|
|
print "running test 5"
|
|
|
|
|
response = requests.get("http://test.barnsley.nz/test.php?sleep=5")
|
|
|
|
|
|
|
|
|
|
return Promise(
|
|
|
|
|
lambda resolve, reject: resolve(response.text)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|