Revamp of backend method structure; Proxy for any URL

This commit is contained in:
James Barnsley
2017-10-04 16:47:15 +13:00
parent dcdecd989a
commit b2c7478cf9
12 changed files with 370 additions and 96 deletions

View File

@ -78,15 +78,20 @@ class IrisCore(object):
return {"clientid": clientid, "connection_id": connection_id, "username": username, "generated": generated}
def send_message(self, to, data):
def send_message(self, *args, **kwargs):
connection_id = kwargs.get('connection_id', None)
data = kwargs.get('data', {})
try:
self.connections[to]['connection'].write_message( json_encode(data) )
self.connections[connection_id]['connection'].write_message( json_encode(data) )
except:
self.raven_client.captureException()
logger.error('Failed to send message to '+ to)
logger.error('Failed to send message to '+ connection_id)
def broadcast(self, data):
def broadcast(self, *args, **kwargs):
data = kwargs.get('data', None)
for connection in self.connections.itervalues():
connection['connection'].write_message( json_encode(data) )
return {
@ -102,7 +107,7 @@ class IrisCore(object):
# to all current connections
##
def get_connections(self, data):
def get_connections(self, *args, **kwargs):
connections = []
for connection in self.connections.itervalues():
connections.append(connection['client'])
@ -112,7 +117,11 @@ class IrisCore(object):
'connections': connections
}
def add_connection(self, connection_id, connection, client):
def add_connection(self, *args, **kwargs):
connection_id = kwargs.get('connection_id', None)
connection = kwargs.get('connection', None)
client = kwargs.get('client', None)
new_connection = {
'client': client,
'connection': connection
@ -120,39 +129,48 @@ class IrisCore(object):
self.connections[connection_id] = new_connection
self.send_message(
connection_id, {
connection_id=connection_id,
data={
'type': 'connected',
'connection_id': connection_id,
'username': client['username']
}
)
self.broadcast({
self.broadcast(
data={
'type': 'connection_added',
'connection': 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({
self.broadcast(
data={
'type': 'connection_removed',
'connection': client
})
}
)
except:
self.raven_client.captureException()
logger.error('Failed to close connection to '+ connection_id)
def set_username(self, data):
def set_username(self, *args, **kwargs):
connection_id = data['connection_id']
data = kwargs.get('data', None)
if connection_id in self.connections:
self.connections[connection_id]['client']['username'] = data['username']
self.broadcast({
self.broadcast(
data={
'type': 'connection_updated',
'connection': self.connections[connection_id]['client']
})
}
)
return {
'status': 1,
'connection_id': connection_id,
@ -168,14 +186,14 @@ class IrisCore(object):
'message': error
}
def deliver_message(self, data):
to = data['to']
if to in self.connections:
self.send_message(to, data['message'])
def deliver_message(self, *args, **kwargs):
data = kwargs.get('data', "{}")
if data['connection_id'] in self.connections:
self.send_message(connection_id=data['connection_id'], data=data['message'])
return {
'status': 1
'status': 1,
'message': 'Sent message to '+data['connection_id']
}
else:
@ -196,7 +214,7 @@ class IrisCore(object):
# Faciitates upgrades and configuration fetching
##
def get_config(self, data):
def get_config(self, *args, **kwargs):
# handle config setups where there is no username/password
# Iris won't work properly anyway, but at least we won't get server errors
@ -215,7 +233,7 @@ class IrisCore(object):
'config': config
}
def get_version(self, data):
def get_version(self, *args, **kwargs):
url = 'https://pypi.python.org/pypi/Mopidy-Iris/json'
req = urllib2.Request(url)
@ -264,13 +282,14 @@ class IrisCore(object):
# recommendations limit low to avoid timeouts and slow UI
##
def get_radio(self, data):
def get_radio(self, *args, **kwargs):
return {
'status': 1,
'radio': self.radio
}
def change_radio(self, data):
def change_radio(self, *args, **kwargs):
data = kwargs.get('data', None)
# figure out if we're starting or updating radio mode
if data['update'] and self.radio['enabled']:
@ -301,15 +320,19 @@ class IrisCore(object):
if added.get():
if starting:
self.core.playback.play()
self.broadcast({
self.broadcast(
data={
'type': 'radio_started',
'radio': self.radio
})
}
)
else:
self.broadcast({
self.broadcast(
data={
'type': 'radio_changed',
'radio': self.radio
})
}
)
return self.get_radio({})
@ -322,7 +345,7 @@ class IrisCore(object):
}
def stop_radio(self, data):
def stop_radio(self, *args, **kwargs):
self.radio = {
"enabled": 0,
@ -336,10 +359,12 @@ class IrisCore(object):
self.core.tracklist.set_consume(self.initial_consume)
self.core.playback.stop()
self.broadcast({
self.broadcast(
data={
'type': 'radio_stopped',
'radio': self.radio
})
}
)
return {
'status': 1
@ -359,11 +384,13 @@ class IrisCore(object):
error = 'IrisFrontend: access_token missing or invalid'
self.raven_client.captureMessage(error)
logger.error(error)
self.broadcast({
self.broadcast(
data={
'type': 'error',
'message': 'Could not get radio tracks: access_token missing or invalid',
'source': 'load_more_tracks'
})
}
)
try:
url = 'https://api.spotify.com/v1/recommendations/'
@ -387,17 +414,19 @@ class IrisCore(object):
except:
self.raven_client.captureException()
logger.error('IrisFrontend: Failed to fetch Spotify recommendations')
self.broadcast({
self.broadcast(
data={
'type': 'error',
'message': 'Could not get radio tracks',
'source': 'load_more_tracks'
})
}
)
return []
def check_for_radio_update( self ):
tracklistLength = self.core.tracklist.length.get()
if( tracklistLength < 3 and self.radio['enabled'] == 1 ):
if (tracklistLength < 3 and self.radio['enabled'] == 1):
# Grab our loaded tracks
uris = self.radio['results']
@ -423,13 +452,14 @@ class IrisCore(object):
# added_by and from_uri.
##
def get_queue_metadata(self, data):
def get_queue_metadata(self, *args, **kwargs):
return {
'status': 1,
'queue_metadata': self.queue_metadata
}
def add_queue_metadata(self, data):
def add_queue_metadata(self, *args, **kwargs):
data = kwargs.get('data', None)
for tlid in data['tlids']:
item = {
@ -439,10 +469,12 @@ class IrisCore(object):
}
self.queue_metadata['tlid_'+str(tlid)] = item
self.broadcast({
self.broadcast(
data={
'type': 'queue_metadata_changed',
'queue_metadata': self.queue_metadata
})
}
)
return {
'status': 1
@ -459,10 +491,12 @@ class IrisCore(object):
self.queue_metadata = cleaned_queue_metadata
self.broadcast({
self.broadcast(
data={
'type': 'queue_metadata_changed',
'queue_metadata': self.queue_metadata
})
}
)
return {
'status': 1
@ -477,12 +511,12 @@ class IrisCore(object):
# passing token to frontend for javascript requests without use of the Authorization Code Flow.
##
def get_spotify_token(self, data):
def get_spotify_token(self, *args, **kwargs):
return {
'spotify_token': self.spotify_token
}
def refresh_spotify_token(self, data):
def refresh_spotify_token(self, *args, **kwargs):
# Use client_id and client_secret from config
# This was introduced in Mopidy-Spotify 3.1.0
@ -497,14 +531,16 @@ class IrisCore(object):
req = urllib2.Request(url, data_encoded)
try:
response = urllib2.urlopen(req, timeout=30).read()
response = urllib2.urlopen(req, timeout=15).read()
response_dict = json.loads(response)
self.spotify_token = response_dict
self.broadcast({
self.broadcast(
data={
'type': 'spotify_token_changed',
'spotify_token': self.spotify_token
})
}
)
return self.get_spotify_token({})
@ -517,3 +553,81 @@ class IrisCore(object):
'message': 'Could not refresh token: '+error['error_description'],
'source': 'refresh_spotify_token'
}
##
# 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):
data = kwargs.get('data', None)
origin_request = kwargs.get('request', None)
# Our request includes data, so make sure we POST the data
if 'url' not in data:
self.raven_client.captureException()
return {
'type': 'error',
'message': 'Could not complete proxy request',
'source': 'proxy_request',
'error': "Missing URL property",
'original_request': data
}
# Construct request headers
# If we have an original request, pass through it's headers
if origin_request:
target_request_headers = origin_request.headers
else:
target_request_headers = {}
# Adjust headers
target_request_headers["Accept-Encoding"] = "deflate"
if "Content-Type" in target_request_headers:
del target_request_headers["Content-Type"]
# Our request includes data, so make sure we POST the data
if ('data' in data and data['data']):
target_request = urllib2.Request(data['url'], data=urllib.urlencode(data['data']), headers=target_request_headers)
# No data, so just a simple GET request
else:
target_request = urllib2.Request(data['url'], headers=target_request_headers)
try:
target_response = urllib2.urlopen(target_request, timeout=15)
target_response_body = target_response.read()
try:
response = json.loads(target_response_body)
return {
'response': response
}
except:
return {
'response': target_response_body
}
except urllib2.HTTPError as e:
self.raven_client.captureException()
return {
'type': 'error',
'message': 'Could not complete proxy request',
'source': 'proxy_request',
'response': e.read(),
'original_request': data
}
except urllib2.URLError as e:
self.raven_client.captureException()
return {
'type': 'error',
'message': 'Could not complete proxy request',
'source': 'proxy_request',
'original_request': data
}

View File

@ -58,7 +58,7 @@ class WebsocketHandler(tornado.websocket.WebSocketHandler):
}
# add to connections
mem.iris.add_connection(connection_id, self, client)
mem.iris.add_connection(connection_id=connection_id, connection=self, client=client)
def on_message(self, message):
@ -87,11 +87,11 @@ class WebsocketHandler(tornado.websocket.WebSocketHandler):
if hasattr(mem.iris, message['method']):
# make the call, and return it's response
response = getattr(mem.iris, message['method'])(data)
response = getattr(mem.iris, message['method'])(data=data)
if response:
response['request_id'] = request_id
mem.iris.send_message(self.connection_id, response)
mem.iris.send_message(connection_id=self.connection_id, data=response)
else:
mem.iris.raven_client.captureMessage("Method "+message['method']+" does not exist")
response = {
@ -99,7 +99,7 @@ class WebsocketHandler(tornado.websocket.WebSocketHandler):
'message': 'Method "'+message['method']+'" does not exist',
'request_id': request_id
}
mem.iris.send_message(self.connection_id, response)
mem.iris.send_message(connection_id=self.connection_id, data=response)
else:
mem.iris.raven_client.captureMessage("Method key missing from request")
response = {
@ -107,11 +107,11 @@ class WebsocketHandler(tornado.websocket.WebSocketHandler):
'message': 'Method key missing from request',
'request_id': request_id
}
mem.iris.send_message(self.connection_id, response)
mem.iris.send_message(connection_id=self.connection_id, data=response)
def on_close(self):
mem.iris.remove_connection(self.connection_id)
mem.iris.remove_connection(connection_id=self.connection_id)
@ -133,7 +133,30 @@ class HttpHandler(tornado.web.RequestHandler):
if hasattr(mem.iris, slug):
# make the call, and return it's response
self.write(getattr(mem.iris, slug)({}))
self.write(getattr(mem.iris, slug)(data={}, request=self.request))
else:
mem.iris.raven_client.captureMessage("Method "+slug+" does not exist")
self.write({
'error': 'Method "'+slug+'" does not exist'
})
def post(self, slug=None):
# make sure the method exists
if hasattr(mem.iris, slug):
try:
data = json.loads(self.request.body.decode('utf-8'))
# make the call, and return it's response
self.write(getattr(mem.iris, slug)(data=data, request=self.request))
except urllib2.HTTPError as e:
self.raven_client.captureException()
self.write({
'error': 'Invalid JSON payload'
})
else:
mem.iris.raven_client.captureMessage("Method "+slug+" does not exist")
self.write({

10
src/js/bootstrap.js vendored
View File

@ -7,6 +7,7 @@ import pusher from './services/pusher/reducer'
import mopidy from './services/mopidy/reducer'
import lastfm from './services/lastfm/reducer'
import spotify from './services/spotify/reducer'
import genius from './services/genius/reducer'
import thunk from 'redux-thunk'
import coreMiddleware from './services/core/middleware'
@ -22,6 +23,7 @@ let reducers = combineReducers({
pusher,
mopidy,
lastfm,
genius,
spotify
});
@ -61,10 +63,10 @@ var initialState = {
}
},
lastfm: {
connected: false,
album: {},
artist: {},
track: {}
connected: false
},
genius: {
connected: false
},
spotify: {
connected: false,

View File

@ -16,12 +16,14 @@ import SearchURISchemesModal from './SearchURISchemesModal'
import VolumeModal from './VolumeModal'
import AuthorizationModal_Send from './AuthorizationModal_Send'
import AuthorizationModal_Receive from './AuthorizationModal_Receive'
import TrackInfoModal from './TrackInfoModal'
import * as coreActions from '../../services/core/actions'
import * as uiActions from '../../services/ui/actions'
import * as mopidyActions from '../../services/mopidy/actions'
import * as spotifyActions from '../../services/spotify/actions'
import * as pusherActions from '../../services/pusher/actions'
import * as geniusActions from '../../services/genius/actions'
class Modal extends React.Component{
@ -58,6 +60,7 @@ class Modal extends React.Component{
{ this.props.modal.name == 'kiosk_mode' ? <KioskModeModal uiActions={this.props.uiActions} data={this.props.modal.data} current_track={this.props.current_track} /> : null }
{ this.props.modal.name == 'search_uri_schemes' ? <SearchURISchemesModal uiActions={this.props.uiActions} coreActions={this.props.coreActions} search_uri_schemes={this.props.search_uri_schemes} available_uri_schemes={this.props.uri_schemes} data={this.props.modal.data} /> : null }
{ this.props.modal.name == 'volume' ? <VolumeModal uiActions={this.props.uiActions} mopidyActions={this.props.mopidyActions} volume={this.props.volume} mute={this.props.mute} /> : null }
{ this.props.modal.name == 'track_info' ? <TrackInfoModal uiActions={this.props.uiActions} geniusActions={this.props.geniusActions} current_track={this.props.current_track} /> : null }
</div>
</div>
@ -92,7 +95,8 @@ const mapDispatchToProps = (dispatch) => {
uiActions: bindActionCreators(uiActions, dispatch),
pusherActions: bindActionCreators(pusherActions, dispatch),
spotifyActions: bindActionCreators(spotifyActions, dispatch),
mopidyActions: bindActionCreators(mopidyActions, dispatch)
mopidyActions: bindActionCreators(mopidyActions, dispatch),
geniusActions: bindActionCreators(geniusActions, dispatch)
}
}

View File

@ -0,0 +1,32 @@
import React, { PropTypes } from 'react'
import FontAwesome from 'react-fontawesome'
import Icon from '../Icon'
import ArtistSentence from '../ArtistSentence'
import * as helpers from '../../helpers'
export default class TrackInfoModal extends React.Component{
constructor(props){
super(props)
}
componentDidMount(){
if (this.props.current_track && !this.props.current_track.annotations){
this.props.geniusActions.getTrackInfo(this.props.current_track);
}
}
render(){
var track = this.props.current_track;
return (
<div>
<h1>Track info</h1>
<h2 className="grey-text">{track.name} by <ArtistSentence artists={track.artists} /></h2>
{track.annotations ? track.annotations.id : "No annotations"}
</div>
)
}
}

View File

@ -1,13 +1,14 @@
import ReactGA from 'react-ga'
var coreActions = require('./actions.js')
var uiActions = require('../ui/actions.js')
var pusherActions = require('../pusher/actions.js')
var mopidyActions = require('../mopidy/actions.js')
var spotifyActions = require('../spotify/actions.js')
var lastfmActions = require('../lastfm/actions.js')
var helpers = require('../../helpers.js')
var coreActions = require('./actions.js');
var uiActions = require('../ui/actions.js');
var pusherActions = require('../pusher/actions.js');
var mopidyActions = require('../mopidy/actions.js');
var spotifyActions = require('../spotify/actions.js');
var lastfmActions = require('../lastfm/actions.js');
var geniusActions = require('../genius/actions.js');
var helpers = require('../../helpers.js');
const CoreMiddleware = (function(){
@ -71,9 +72,8 @@ const CoreMiddleware = (function(){
break;
case 'CORE_START_SERVICES':
store.dispatch(mopidyActions.connect())
store.dispatch(pusherActions.connect())
store.dispatch(lastfmActions.connect())
store.dispatch(mopidyActions.connect());
store.dispatch(pusherActions.connect());
next(action)
break

View File

@ -0,0 +1,73 @@
var coreActions = require('../core/actions')
var uiActions = require('../ui/actions')
var helpers = require('../../helpers')
/**
* Send an ajax request to the Spotify API
*
* @param dispatch obj
* @param getState obj
* @param endpoint params = the url params to send
**/
const sendRequest = (dispatch, getState, endpoint) => {
return new Promise( (resolve, reject) => {
var loader_key = helpers.generateGuid();
dispatch(uiActions.startLoading(loader_key, 'genius_'+endpoint));
var config = {
method: 'GET',
cache: false,
url: 'https://api.genius.com/'+endpoint+'&access_token=2AGP9sfzKQcxfKSZuGa_3lqsDIpuOiTGT7-vhJYcKaaDjHIIA2HICsxXCiC30Xxi'
};
$.ajax(config).then(
response => {
dispatch(uiActions.stopLoading(loader_key));
resolve(response.response);
},
(xhr, status, error) => {
dispatch(uiActions.stopLoading(loader_key));
reject({
config: config,
xhr: xhr,
status: status,
error: error
});
}
)
})
}
export function getTrackInfo(track){
return (dispatch, getState) => {
var query = '';
for (var i = 0; i < track.artists.length; i++){
query += track.artists[i].name+' ';
}
query += track.name;
sendRequest(dispatch, getState, 'search?q='+query)
.then(
response => {
if (response.hits && response.hits.length > 0){
dispatch({
type: 'TRACK_LOADED',
key: track.uri,
track: {
annotations: response.hits[0].result
}
});
}
},
error => {
dispatch(coreActions.handleException(
'Could not get track info',
error
));
}
)
}
}

View File

@ -0,0 +1,18 @@
export default function reducer(genius = {}, action){
switch (action.type) {
case 'GENIUS_CONNECT':
case 'GENIUS_CONNECTING':
return Object.assign({}, genius, { connected: false, connecting: true });
case 'GENIUS_CONNECTED':
return Object.assign({}, genius, { connected: true, connecting: false });
default:
return genius
}
}

View File

@ -1685,10 +1685,14 @@ const MopidyMiddleware = (function(){
if (action.data && action.data.track){
// Fire off our universal track index loader
store.dispatch({ type: 'TRACK_LOADED', key: action.data.track.uri, track: action.data.track })
store.dispatch({
type: 'TRACK_LOADED',
key: action.data.track.uri,
track: action.data.track
});
// We've got Spotify running, and it's a spotify track - go straight to the source!
if (helpers.uriSource(action.data.track.uri) == 'spotify' && store.getState().spotify.access != 'none'){
if (helpers.uriSource(action.data.track.uri) == 'spotify' && store.getState().spotify.enabled){
store.dispatch(spotifyActions.getTrack(action.data.track.uri))
// Some other source, rely on Mopidy backends to do their work

View File

@ -24,7 +24,9 @@ const sendRequest = ( dispatch, getState, endpoint, method = 'GET', data = false
// prepend the API baseurl, unless the endpoint already has it (ie pagination requests)
var url = 'https://api.spotify.com/v1/'+endpoint
if (endpoint.startsWith('https://api.spotify.com/')) url = endpoint;
if (endpoint.startsWith('https://api.spotify.com/')){
url = endpoint;
}
// create our ajax request config
var config = {

View File

@ -174,6 +174,7 @@ class Debug extends React.Component{
<option value='{"method":"set_username","data":{"connection_id":"CONNECTION_ID_HERE","username":"NewUsername"}}'>Change username</option>
<option value='{"method":"refresh_spotify_token"}'>Refresh Spotify token</option>
<option value='{"method":"perform_upgrade"}'>Perform upgrade (beta)</option>
<option value='{"method":"proxy_request","data":{"url":"https://jsonplaceholder.typicode.com/posts/1"}}'>Proxy request</option>
</select>
</div>
</div>

View File

@ -120,6 +120,7 @@ class Queue extends React.Component{
{ this.renderArtwork(image) }
<div className="title">
{this.props.current_track ? this.props.current_track.name : <span>-</span>}
&nbsp; <FontAwesome name="info-circle" onClick={e => this.props.uiActions.openModal('track_info')} />
</div>
{this.props.current_track ? <ArtistSentence artists={ this.props.current_track.artists } /> : <ArtistSentence />}
</div>