diff --git a/mopidy_iris/core.py b/mopidy_iris/core.py index 1195bb38..340c340a 100755 --- a/mopidy_iris/core.py +++ b/mopidy_iris/core.py @@ -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({ - 'type': 'connection_added', - 'connection': client - }) + 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({ - 'type': 'connection_removed', - 'connection': client - }) + 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({ - 'type': 'connection_updated', - 'connection': self.connections[connection_id]['client'] - }) + 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({ - 'type': 'radio_started', - 'radio': self.radio - }) + self.broadcast( + data={ + 'type': 'radio_started', + 'radio': self.radio + } + ) else: - self.broadcast({ - 'type': 'radio_changed', - 'radio': self.radio - }) + 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({ - 'type': 'radio_stopped', - 'radio': self.radio - }) + 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({ - 'type': 'error', - 'message': 'Could not get radio tracks: access_token missing or invalid', - 'source': 'load_more_tracks' - }) + 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({ - 'type': 'error', - 'message': 'Could not get radio tracks', - 'source': 'load_more_tracks' - }) + 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({ - 'type': 'queue_metadata_changed', - 'queue_metadata': self.queue_metadata - }) + 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({ - 'type': 'queue_metadata_changed', - 'queue_metadata': self.queue_metadata - }) + 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({ - 'type': 'spotify_token_changed', - 'spotify_token': self.spotify_token - }) + 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 + } diff --git a/mopidy_iris/handlers.py b/mopidy_iris/handlers.py index 488cae76..8f474f61 100755 --- a/mopidy_iris/handlers.py +++ b/mopidy_iris/handlers.py @@ -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({ diff --git a/src/js/bootstrap.js b/src/js/bootstrap.js index be671213..08527ba3 100755 --- a/src/js/bootstrap.js +++ b/src/js/bootstrap.js @@ -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, diff --git a/src/js/components/Modal/Modal.js b/src/js/components/Modal/Modal.js index 05acaebc..86ad28f4 100755 --- a/src/js/components/Modal/Modal.js +++ b/src/js/components/Modal/Modal.js @@ -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' ? : null } { this.props.modal.name == 'search_uri_schemes' ? : null } { this.props.modal.name == 'volume' ? : null } + { this.props.modal.name == 'track_info' ? : null } @@ -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) } } diff --git a/src/js/components/Modal/TrackInfoModal.js b/src/js/components/Modal/TrackInfoModal.js new file mode 100755 index 00000000..8d33c49e --- /dev/null +++ b/src/js/components/Modal/TrackInfoModal.js @@ -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 ( +
+

Track info

+

{track.name} by

+ {track.annotations ? track.annotations.id : "No annotations"} +
+ ) + } +} \ No newline at end of file diff --git a/src/js/services/core/middleware.js b/src/js/services/core/middleware.js index 06a9d05e..fdb6f3ea 100755 --- a/src/js/services/core/middleware.js +++ b/src/js/services/core/middleware.js @@ -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 diff --git a/src/js/services/genius/actions.js b/src/js/services/genius/actions.js new file mode 100755 index 00000000..466142f1 --- /dev/null +++ b/src/js/services/genius/actions.js @@ -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 + )); + } + ) + } +} diff --git a/src/js/services/genius/reducer.js b/src/js/services/genius/reducer.js new file mode 100755 index 00000000..8b190e91 --- /dev/null +++ b/src/js/services/genius/reducer.js @@ -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 + } +} + + + diff --git a/src/js/services/mopidy/middleware.js b/src/js/services/mopidy/middleware.js index ca4768a6..0a1f24bd 100755 --- a/src/js/services/mopidy/middleware.js +++ b/src/js/services/mopidy/middleware.js @@ -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 diff --git a/src/js/services/spotify/actions.js b/src/js/services/spotify/actions.js index 871f52ca..7c4fab26 100755 --- a/src/js/services/spotify/actions.js +++ b/src/js/services/spotify/actions.js @@ -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 = { diff --git a/src/js/views/Debug.js b/src/js/views/Debug.js index e24032a5..3975cd14 100755 --- a/src/js/views/Debug.js +++ b/src/js/views/Debug.js @@ -174,6 +174,7 @@ class Debug extends React.Component{ + diff --git a/src/js/views/Queue.js b/src/js/views/Queue.js index 419c3c13..031f7fb0 100755 --- a/src/js/views/Queue.js +++ b/src/js/views/Queue.js @@ -120,6 +120,7 @@ class Queue extends React.Component{ { this.renderArtwork(image) }
{this.props.current_track ? this.props.current_track.name : -} +   this.props.uiActions.openModal('track_info')} />
{this.props.current_track ? : }