From 5184d06f55b55fb97ee26487b8f8c3dc24b34f17 Mon Sep 17 00:00:00 2001 From: James Barnsley Date: Thu, 3 Aug 2017 15:26:54 +1200 Subject: [PATCH] Current track images to use Mopidy-Spotify so no immediate dependency on Spotify HTTP API --- src/js/App.js | 1 - src/js/components/Modal/KioskModeModal.js | 4 +- src/js/components/PlaybackControls.js | 4 +- src/js/helpers.js | 106 ++++++++++++++++------ src/js/services/core/middleware.js | 15 --- src/js/services/mopidy/actions.js | 8 ++ src/js/services/mopidy/middleware.js | 64 ++++++++++--- src/js/services/pusher/middleware.js | 1 + src/js/services/ui/middleware.js | 7 ++ src/js/views/Queue.js | 9 +- src/js/views/Search.js | 24 ++--- 11 files changed, 166 insertions(+), 77 deletions(-) diff --git a/src/js/App.js b/src/js/App.js index 0a4b00bf..c71266b5 100755 --- a/src/js/App.js +++ b/src/js/App.js @@ -101,7 +101,6 @@ class App extends React.Component{ // Listen for standalone key codes let keyCodes = [27,32,191] if (keyCodes.indexOf(e.keyCode) > -1){ - if (keyCode == 37) e.preventDefault() return true } diff --git a/src/js/components/Modal/KioskModeModal.js b/src/js/components/Modal/KioskModeModal.js index f5d38bf5..f868ff33 100755 --- a/src/js/components/Modal/KioskModeModal.js +++ b/src/js/components/Modal/KioskModeModal.js @@ -24,8 +24,8 @@ export default class KioskModeModal extends React.Component{ } render(){ - if (this.props.current_track && this.props.current_track.album && this.props.current_track.album.images){ - var images = this.props.current_track.album.images + if (this.props.current_track && this.props.current_track.images){ + var images = this.props.current_track.images } else { var images = [] } diff --git a/src/js/components/PlaybackControls.js b/src/js/components/PlaybackControls.js index d251af8a..7328adc3 100755 --- a/src/js/components/PlaybackControls.js +++ b/src/js/components/PlaybackControls.js @@ -75,8 +75,8 @@ class PlaybackControls extends React.Component{ render(){ var images = false - if (this.props.current_track && this.props.current_track.album && this.props.current_track.album.images){ - images = this.props.current_track.album.images + if (this.props.current_track && this.props.current_track.images){ + images = this.props.current_track.images } return ( diff --git a/src/js/helpers.js b/src/js/helpers.js index 8acd92d5..5977ce85 100755 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -13,50 +13,67 @@ export let sizedImages = function( images ){ huge: false } - if( images.length <= 0 ) return sizes; + if (images.length <= 0) return sizes; + + for (var i = 0; i < images.length; i++){ + let image = images[i] - for( var i = 0; i < images.length; i++ ){ - // Mopidy image object - if (typeof(images[i].__model__) !== 'undefined' && images[i].__model__ == 'Image'){ - sizes.small = images[i].uri + if (image.__model__ && image.__model__ == 'Image'){ + + if (image.width < 400){ + sizes.small = image.url; + }else if (image.width < 800){ + sizes.medium = image.url; + }else if (image.width < 1000){ + sizes.large = image.url; + }else{ + sizes.huge = image.url; + } // Mopidy image string - } else if (typeof(images[i]) == 'string'){ - sizes.small = images[i] - + } else if (typeof(image) == 'string'){ + sizes.small = image + // spotify-styled images - } else if (typeof(images[i].width) !== 'undefined'){ - if( images[i].width < 400 ){ - sizes.small = images[i].url; - }else if( images[i].width < 800 ){ - sizes.medium = images[i].url; - }else if( images[i].width < 1000 ){ - sizes.large = images[i].url; + } else if (image.width !== undefined){ + + if (image.width < 400){ + sizes.small = image.url; + }else if (image.width < 800){ + sizes.medium = image.url; + }else if (image.width < 1000){ + sizes.large = image.url; }else{ - sizes.huge = images[i].url; + sizes.huge = image.url; } // lastfm-styled images - } else if (typeof(images[i].size) !== 'undefined'){ - switch( images[i].size ){ + } else if (image.size !== undefined){ + switch( image.size ){ case 'mega': case 'extralarge': - sizes.huge = images[i]['#text'] + sizes.huge = image['#text'] break case 'large': - sizes.large = images[i]['#text'] + sizes.large = image['#text'] break case 'medium': - sizes.medium = images[i]['#text'] + sizes.medium = image['#text'] break case 'small': - sizes.small = images[i]['#text'] + sizes.small = image['#text'] break } } } + if (!sizes.small){ + if (sizes.medium) sizes.small = sizes.medium + else if (sizes.large) sizes.small = sizes.large + else if (sizes.huge) sizes.small = sizes.huge + else sizes.small = null + } if (!sizes.medium){ if (sizes.large) sizes.medium = sizes.large else if (sizes.huge) sizes.medium = sizes.huge @@ -68,6 +85,41 @@ export let sizedImages = function( images ){ return sizes; } + +/** + * Digest an array of Mopidy image objects into a universal format + * + * @param mopidy = obj (mopidy store object) + * @param images = array + * @return array + **/ +export let digestMopidyImages = function(mopidy, images){ + let digested = [] + + for (let i = 0; i < images.length; i++){ + + // Accommodate backends that provide URIs vs URLs + let url = images[i].url + if (!url && images[i].uri){ + url = images[i].uri + } +/* + // Replace local images to point directly to our Mopidy server + if (url.startsWith('/images/')){ + url = '//'+mopidy.host+':'+mopidy.port+url + } + */ + + // Amend our URL + images[i].url = url + + digested.push(images[i]) + } + + return digested +} + + export let generateGuid = function(format = 'xxxxxxxxxxxx'){ return format.replace(/[xy]/g, function(c) { var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); @@ -75,7 +127,6 @@ export let generateGuid = function(format = 'xxxxxxxxxxxx'){ }); } - export let getCurrentPusherConnection = function( connections, connectionid ){ function isCurrentConnection(connection){ return connection.connectionid == newProps.pusher.connectionid; @@ -93,15 +144,14 @@ export let getCurrentPusherConnection = function( connections, connectionid ){ * @param track object * @return string **/ -export let getTrackIcon = function(current_track = false, ui = false){ - if (!ui) return false +export let getTrackIcon = function(current_track = false, core = false){ + if (!core) return false if (!current_track) return false if (typeof(current_track.uri) == 'undefined') return false if (typeof(core.tracks[current_track.uri]) === 'undefined') return false var track = core.tracks[current_track.uri] - if (!track.album) return false - if (!track.album.images) return false - return sizedImages(track.album.images).small + if (!track.images) return false + return sizedImages(track.images).small } diff --git a/src/js/services/core/middleware.js b/src/js/services/core/middleware.js index 5e377476..f6d12636 100755 --- a/src/js/services/core/middleware.js +++ b/src/js/services/core/middleware.js @@ -24,16 +24,6 @@ const CoreMiddleware = (function(){ next(action) break - case 'MOPIDY_CONNECTED': - ReactGA.event({ category: 'Mopidy', action: 'Connected', label: window.location.hostname }) - next(action) - break - - case 'PUSHER_CONNECTED': - ReactGA.event({ category: 'Pusher', action: 'Connected', label: action.username }) - next(action) - break - case 'ALBUM_LOADED': if (action.data) ReactGA.event({ category: 'Album', action: 'Load', label: action.album.uri }) @@ -75,11 +65,6 @@ const CoreMiddleware = (function(){ next(action) break - case 'MOPIDY_DIRECTORY': - if (action.data) ReactGA.event({ category: 'Directory', action: 'Load', label: action.data.uri }) - next(action) - break - case 'PLAY_PLAYLIST': ReactGA.event({ category: 'Playlist', action: 'Play', label: action.uri }) next(action) diff --git a/src/js/services/mopidy/actions.js b/src/js/services/mopidy/actions.js index c65e25b7..8e3337bc 100755 --- a/src/js/services/mopidy/actions.js +++ b/src/js/services/mopidy/actions.js @@ -164,6 +164,14 @@ export function setTimePosition(time_position){ * Asset-oriented actions **/ +export function getImages( context, uris ){ + return { + type: 'MOPIDY_GET_IMAGES', + context: context, + uris: uris + } +} + export function deletePlaylist( uri ){ return { type: 'MOPIDY_DELETE_PLAYLIST', diff --git a/src/js/services/mopidy/middleware.js b/src/js/services/mopidy/middleware.js index 33686817..9b07c2e9 100755 --- a/src/js/services/mopidy/middleware.js +++ b/src/js/services/mopidy/middleware.js @@ -1,4 +1,5 @@ +import ReactGA from 'react-ga' import Mopidy from 'mopidy' import { hashHistory } from 'react-router' import * as helpers from '../../helpers' @@ -192,6 +193,11 @@ const MopidyMiddleware = (function(){ socket.on( (type, data) => handleMessage( socket, store, type, data ) ) break + case 'MOPIDY_CONNECTED': + ReactGA.event({ category: 'Mopidy', action: 'Connected', label: window.location.hostname }) + next(action) + break + case 'MOPIDY_DISCONNECT': if(socket != null) socket.close() socket = null @@ -238,7 +244,7 @@ const MopidyMiddleware = (function(){ type: 'browser_notification', title: 'Track skipped', body: store.getState().pusher.username +' skipped this track', - icon: (store.getState().core.current_track ? helpers.getTrackIcon(store.getState().core.current_track, store.getState().ui) : false) + icon: (store.getState().core.current_track ? helpers.getTrackIcon(store.getState().core.current_track, store.getState().core) : false) } store.dispatch( pusherActions.deliverBroadcast(data) ) break @@ -248,7 +254,7 @@ const MopidyMiddleware = (function(){ type: 'browser_notification', title: 'Playback stopped', body: store.getState().pusher.username +' stopped playback', - icon: (store.getState().core.current_track ? helpers.getTrackIcon(store.getState().core.current_track, store.getState().ui) : false) + icon: (store.getState().core.current_track ? helpers.getTrackIcon(store.getState().core.current_track, store.getState().core) : false) } store.dispatch( pusherActions.deliverBroadcast(data) ) break @@ -400,13 +406,6 @@ const MopidyMiddleware = (function(){ break - case 'CANCEL_PROCESS': - if (action.key == 'MOPIDY_ENQUEUE_URIS'){ - store.dispatch(mopidyActions.enqueueURIsCancel()) - } - next(action) - break - case 'MOPIDY_PLAY_URIS': // Stop the radio @@ -1033,17 +1032,49 @@ const MopidyMiddleware = (function(){ // Fire off our universal track index loader store.dispatch({ type: 'TRACK_LOADED', key: action.data.track.uri, track: action.data.track }) - // When current track is Spotify track, go get the full object - // This is because Mopidy doesn't give us full artist/album objects, without artwork - if (action.data.track.uri.substring(0,14) == 'spotify:track:'){ - store.dispatch( spotifyActions.getTrack( action.data.track.uri ) ) - } + // Get me some images + store.dispatch(mopidyActions.getImages('tracks',[action.data.track.uri])) } next(action) break + /** + * =============================================================== IMAGES =============== + * ====================================================================================== + **/ + + case 'MOPIDY_GET_IMAGES': + + instruct( socket, store, 'library.getImages', {uris: action.uris}) + .then( response => { + + let records = [] + for (var uri in response){ + if (response.hasOwnProperty(uri)){ + + let images = response[uri] + images = helpers.digestMopidyImages(store.getState().mopidy, images) + + records.push({ + uri: uri, + images: images + }) + } + } + + let action_data = { + type: (action.context+'_LOADED').toUpperCase() + } + action_data[action.context] = records + store.dispatch(action_data) + }) + + next(action) + break + + /** * =============================================================== LOCAL ================ * ====================================================================================== @@ -1060,6 +1091,11 @@ const MopidyMiddleware = (function(){ }) break + case 'MOPIDY_DIRECTORY': + if (action.data) ReactGA.event({ category: 'Directory', action: 'Load', label: action.data.uri }) + next(action) + break + // This action is irrelevant to us, pass it on to the next middleware default: return next(action) diff --git a/src/js/services/pusher/middleware.js b/src/js/services/pusher/middleware.js index 8c21883c..ec245e06 100755 --- a/src/js/services/pusher/middleware.js +++ b/src/js/services/pusher/middleware.js @@ -98,6 +98,7 @@ const PusherMiddleware = (function(){ break; case 'PUSHER_CONNECTED': + ReactGA.event({ category: 'Pusher', action: 'Connected', label: action.username }) request(store, 'get_config') .then( response => { diff --git a/src/js/services/ui/middleware.js b/src/js/services/ui/middleware.js index 2a36af7b..ac21c4a3 100755 --- a/src/js/services/ui/middleware.js +++ b/src/js/services/ui/middleware.js @@ -123,6 +123,13 @@ const UIMiddleware = (function(){ next(action) break + case 'CANCEL_PROCESS': + if (action.key == 'MOPIDY_ENQUEUE_URIS'){ + store.dispatch(mopidyActions.enqueueURIsCancel()) + } + next(action) + break + // This action is irrelevant to us, pass it on to the next middleware default: return next(action) diff --git a/src/js/views/Queue.js b/src/js/views/Queue.js index 6bda7fb7..0dbab57d 100755 --- a/src/js/views/Queue.js +++ b/src/js/views/Queue.js @@ -64,7 +64,7 @@ class Queue extends React.Component{ return ( {this.props.radio_enabled ? : null} - + ) } @@ -74,15 +74,16 @@ class Queue extends React.Component{ return ( {this.props.radio_enabled ? : null} - + ) } render(){ var image = null - if (this.props.current_track && this.props.current_track.album !== undefined && this.props.current_track.album.images){ - image = helpers.sizedImages(this.props.current_track.album.images).huge + if (this.props.current_track && this.props.current_track.images !== undefined){ + image = helpers.sizedImages(this.props.current_track.images) + image = image.large } var options = ( diff --git a/src/js/views/Search.js b/src/js/views/Search.js index d2ea4fe6..3a596760 100755 --- a/src/js/views/Search.js +++ b/src/js/views/Search.js @@ -16,6 +16,7 @@ import LazyLoadListener from '../components/LazyLoadListener' import SearchForm from '../components/SearchForm' import * as helpers from '../helpers' +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' @@ -30,7 +31,7 @@ class Search extends React.Component{ // Make sure we have search parameters to start with if (this.props.params && this.props.params.type && this.props.params.query){ - this.props.uiActions.startSearch(this.props.params.type, this.props.params.query) + this.props.coreActions.startSearch(this.props.params.type, this.props.params.query) } // Auto-focus on the input field @@ -43,12 +44,12 @@ class Search extends React.Component{ if (newProps.params && newProps.params.type && newProps.params.query){ if (this.props.params.query != newProps.params.query || this.props.params.type != newProps.params.type){ - this.props.uiActions.startSearch(newProps.params.type, newProps.params.query) + this.props.coreActions.startSearch(newProps.params.type, newProps.params.query) } // mopidy comes online if (!this.props.mopidy_connected && newProps.mopidy_connected){ - this.props.uiActions.startSearch(newProps.params.type, newProps.params.query, true) + this.props.coreActions.startSearch(newProps.params.type, newProps.params.query, true) } } } @@ -260,22 +261,23 @@ const mapStateToProps = (state, ownProps) => { return { mopidy_connected: state.mopidy.connected, search_settings: (state.ui.search_settings ? state.ui.search_settings : null), - tracks: (state.ui.search_results ? state.ui.search_results.tracks : []), - tracks_more: (state.ui.search_results && state.ui.search_results.tracks_more ? state.ui.search_results.tracks_more : null), + tracks: (state.core.search_results ? state.core.search_results.tracks : []), + tracks_more: (state.core.search_results && state.core.search_results.tracks_more ? state.core.search_results.tracks_more : null), artists: (state.core.artists ? state.core.artists : []), - artists_uris: (state.ui.search_results ? state.ui.search_results.artists_uris : []), - artists_more: (state.ui.search_results ? state.ui.search_results.artists_more : null), + artists_uris: (state.core.search_results ? state.core.search_results.artists_uris : []), + artists_more: (state.core.search_results ? state.core.search_results.artists_more : null), albums: (state.core.albums ? state.core.albums : []), - albums_uris: (state.ui.search_results ? state.ui.search_results.albums_uris : []), - albums_more: (state.ui.search_results ? state.ui.search_results.albums_more : null), + albums_uris: (state.core.search_results ? state.core.search_results.albums_uris : []), + albums_more: (state.core.search_results ? state.core.search_results.albums_more : null), playlists: (state.core.playlists ? state.core.playlists : []), - playlists_uris: (state.ui.search_results ? state.ui.search_results.playlists_uris : []), - playlists_more: (state.ui.search_results ? state.ui.search_results.playlists_more : null) + playlists_uris: (state.core.search_results ? state.core.search_results.playlists_uris : []), + playlists_more: (state.core.search_results ? state.core.search_results.playlists_more : null) } } const mapDispatchToProps = (dispatch) => { return { + coreActions: bindActionCreators(coreActions, dispatch), uiActions: bindActionCreators(uiActions, dispatch), mopidyActions: bindActionCreators(mopidyActions, dispatch), spotifyActions: bindActionCreators(spotifyActions, dispatch)