diff --git a/src/js/bootstrap.js b/src/js/bootstrap.js index 93bd6558..537b1f98 100755 --- a/src/js/bootstrap.js +++ b/src/js/bootstrap.js @@ -32,8 +32,9 @@ let reducers = combineReducers({ // TODO: Look at using propTypes in the component for these falsy initial states var initialState = { core: { - current_tracklist: [], - current_tltrack: false, + queue: [], + queue_metadata: {}, + current_track_uri: null, albums: {}, artists: {}, playlists: {}, diff --git a/src/js/components/LastfmAuthenticationFrame.js b/src/js/components/LastfmAuthenticationFrame.js index 9a0cb511..50e0e536 100755 --- a/src/js/components/LastfmAuthenticationFrame.js +++ b/src/js/components/LastfmAuthenticationFrame.js @@ -45,7 +45,7 @@ class LastfmAuthenticationFrame extends React.Component{ // Bounced with an error if (data.error !== undefined){ - this.props.uiActions.createNotification(data.error,'bad') + this.props.uiActions.createNotification(data.message,'bad') // No errors? We're in! } else { diff --git a/src/js/components/Modal/Modal.js b/src/js/components/Modal/Modal.js index 6133233d..4f0b1127 100755 --- a/src/js/components/Modal/Modal.js +++ b/src/js/components/Modal/Modal.js @@ -145,7 +145,7 @@ class Modal extends React.Component{ const mapStateToProps = (state, ownProps) => { return { - current_track: (state.core.current_track !== undefined && state.core.tracks !== undefined && state.core.tracks[state.core.current_track] !== undefined ? state.core.tracks[state.core.current_track] : null), + current_track: (state.core.tracks[state.core.current_track_uri] !== undefined ? state.core.tracks[state.core.current_track_uri] : null), uri_schemes: (state.mopidy.uri_schemes ? state.mopidy.uri_schemes : []), search_uri_schemes: (state.ui.search_uri_schemes ? state.ui.search_uri_schemes : []), volume: state.mopidy.volume, diff --git a/src/js/components/PlaybackControls.js b/src/js/components/PlaybackControls.js index a10c8616..f72beddf 100755 --- a/src/js/components/PlaybackControls.js +++ b/src/js/components/PlaybackControls.js @@ -135,7 +135,7 @@ class PlaybackControls extends React.Component{ const mapStateToProps = (state, ownProps) => { return { - current_track: (state.core.current_track !== undefined && state.core.tracks !== undefined && state.core.tracks[state.core.current_track] !== undefined ? state.core.tracks[state.core.current_track] : null), + current_track: (state.core.tracks[state.core.current_track_uri] !== undefined ? state.core.tracks[state.core.current_track_uri] : null), radio_enabled: (state.ui.radio && state.ui.radio.enabled ? true : false), play_state: state.mopidy.play_state, time_position: state.mopidy.time_position, diff --git a/src/js/services/core/actions.js b/src/js/services/core/actions.js index a2d324cf..7e25849d 100755 --- a/src/js/services/core/actions.js +++ b/src/js/services/core/actions.js @@ -81,8 +81,8 @@ export function set(data){ **/ export function reorderPlaylistTracks(uri, indexes, insert_before, snapshot_id = false){ - var range = helpers.createRange(indexes ); - switch(helpers.uriSource(uri )){ + var range = helpers.createRange(indexes); + switch(helpers.uriSource(uri)){ case 'spotify': return { diff --git a/src/js/services/core/middleware.js b/src/js/services/core/middleware.js index 0cbfbb0f..c9826205 100755 --- a/src/js/services/core/middleware.js +++ b/src/js/services/core/middleware.js @@ -186,81 +186,6 @@ const CoreMiddleware = (function(){ next(action) break - case 'MOPIDY_CURRENTTLTRACK': - if (action.data && action.data.track){ - helpers.setWindowTitle(action.data.track, store.getState().mopidy.play_state); - - // make sure our images use mopidy host:port - if (action.data.track.album && action.data.track.album.images && action.data.track.album.images.length > 0){ - var images = Object.assign([], action.data.track.album.images) - for (var i = 0; i < images.length; i++){ - if (typeof(images[i]) === 'string' && images[i].startsWith('/images/')){ - images[i] = '//'+store.getState().mopidy.host+':'+store.getState().mopidy.port+images[i] - } - } - action.data.track.album.images = images - } - } - - var current_tracklist = [] - Object.assign(current_tracklist, core.current_tracklist) - - for (var i = 0; i < current_tracklist.length; i++){ - Object.assign( - current_tracklist[i], - { playing: (current_tracklist[i].tlid == action.data.tlid) } - ) - } - - // Update action - action.current_tracklist = current_tracklist; - action.current_track = action.data.track.uri; - - next(action); - break; - - case 'MOPIDY_TLTRACKS': - var core = store.getState().core; - var tracklist = [] - for (var i = 0; i < action.data.length; i++){ - - var tltrack = helpers.formatTracks(action.data[i]); - - // load our metadata (if we have any for that tlid) - if (core.queue_metadata !== undefined && core.queue_metadata['tlid_'+tltrack.tlid] !== undefined){ - var metadata = core.queue_metadata['tlid_'+tltrack.tlid] - } else { - var metadata = {} - } - - var current_tlid = null; - if (core.current_track && core.tracks && core.tracks[core.current_track] !== undefined && core.tracks[core.current_track].tlid !== undefined){ - current_tlid = core.tracks[core.current_track].tlid; - } - - var track = Object.assign( - {}, - tltrack, - metadata, - { - playing: (tltrack.tlid == current_tlid) - }) - tracklist.push(track) - } - - // Append to our action - tracklist = helpers.formatTracks(tracklist); - action.tracklist = tracklist; - - // Load our tracks into index - store.dispatch({ - type: 'TRACKS_LOADED', - tracks: tracklist - }); - - next(action); - break - // Get assets from all of our providers case 'GET_LIBRARY_PLAYLISTS': if (store.getState().spotify.connected){ @@ -295,8 +220,115 @@ const CoreMiddleware = (function(){ break case 'RESTART': - location.reload() - break + location.reload(); + break; + + + /** + * Playlist manipulation + **/ + + case 'PLAYLIST_KEY_UPDATED': + var playlists = Object.assign({}, core.playlists); + + if (playlists[action.key] === undefined){ + dispatch(coreActions.handleException("Cannot change key of playlist not in index")); + } + + // Delete our old playlist by key, and add by new key + var playlist = Object.assign({}, playlists[action.key]); + delete playlists[action.key]; + playlists[action.new_key] = playlist; + + store.dispatch({ + type: 'UPDATE_PLAYLISTS_INDEX', + playlists: playlists + }); + break; + + case 'PLAYLIST_TRACKS_REORDERED': + var playlists = Object.assign({}, core.playlists); + var playlist = Object.assign({}, playlists[action.key]); + var tracks_uris = Object.assign([], playlist.tracks_uris); + + // handle insert_before offset if we're moving BENEATH where we're slicing tracks + var insert_before = action.insert_before + if (insert_before > action.range_start){ + insert_before = insert_before - action.range_length; + } + + // cut our moved tracks into a new array + var tracks_to_move = tracks_uris.splice(action.range_start, action.range_length) + tracks_to_move.reverse() + + for (i = 0; i < tracks_to_move.length; i++){ + tracks_uris.splice(insert_before, 0, tracks_to_move[i]) + } + + var snapshot_id = null; + if (action.snapshot_id){ + snapshot_id = action.snapshot_id; + } + + // Update our playlist + playlist.tracks_uris = tracks_uris; + playlist.snapshot_id = snapshot_id; + + // Trigger normal playlist updating + store.dispatch({ + type: 'PLAYLISTS_LOADED', + playlists: [playlist] + }); + break; + + case 'PLAYLIST_TRACKS_REMOVED': + var playlists = Object.assign({}, core.playlists); + var playlist = Object.assign({}, playlists[action.key]); + var tracks_uris = Object.assign([], playlist.tracks_uris); + + var indexes = action.tracks_indexes.reverse() + for(var i = 0; i < indexes.length; i++){ + tracks_uris.splice(indexes[i], 1); + } + + var snapshot_id = null; + if (action.snapshot_id){ + snapshot_id = action.snapshot_id; + } + + // Update our playlist + playlist.tracks_uris = tracks_uris; + playlist.snapshot_id = snapshot_id; + + // Trigger normal playlist updating + store.dispatch({ + type: 'PLAYLISTS_LOADED', + playlists: [playlist] + }); + break; + + + /** + * Queue and playback info + **/ + + case 'CURRENT_TRACK_LOADED': + store.dispatch({ + type: 'TRACKS_LOADED', + tracks: [action.current_track] + }); + next(action); + break; + + case 'QUEUE_LOADED': + store.dispatch({ + type: 'TRACKS_LOADED', + tracks: action.tracks + }); + + action.tracks_uris = helpers.arrayOf('uri',action.tracks); + next(action); + break; /** @@ -309,8 +341,7 @@ const CoreMiddleware = (function(){ store.dispatch({ type: 'TRACKS_LOADED', tracks: [action.track] - }) - next(action); + }); break; // Array wrapper for ALBUMS_LOADED @@ -319,7 +350,6 @@ const CoreMiddleware = (function(){ type: 'ALBUMS_LOADED', albums: [action.album] }); - next(action); break; // Array wrapper for ARTISTS_LOADED @@ -328,7 +358,6 @@ const CoreMiddleware = (function(){ type: 'ARTISTS_LOADED', artists: [action.artist] }); - next(action); break; // Array wrapper for PLAYLISTS_LOADED @@ -337,7 +366,6 @@ const CoreMiddleware = (function(){ type: 'PLAYLISTS_LOADED', playlists: [action.playlist] }); - next(action); break; // Array wrapper for USERS_LOADED @@ -346,7 +374,6 @@ const CoreMiddleware = (function(){ type: 'USERS_LOADED', users: [action.user] }); - next(action); break; case 'TRACKS_LOADED': @@ -361,6 +388,7 @@ const CoreMiddleware = (function(){ if (track.album && track.album.images && track.album.images.length > 0){ track.album.images = helpers.digestMopidyImages(store.getState().mopidy, track.album.images); + track.images = track.album.images; } tracks[track.uri] = track; @@ -523,7 +551,7 @@ const CoreMiddleware = (function(){ user = Object.assign({}, users[user.uri], user); } - users[action.key] = user; + users[user.uri] = user; } // Update index @@ -542,6 +570,7 @@ const CoreMiddleware = (function(){ **/ case 'LOADED_MORE': + console.log(action); var parent_type_plural = action.parent_type+'s'; var parent_index = Object.assign({}, core[action.parent_type+'s']); var parent = Object.assign({}, parent_index[action.parent_key]); @@ -569,7 +598,10 @@ const CoreMiddleware = (function(){ var records_uris = helpers.arrayOf('uri', records); // Append our records_uris array with our new records - var uris = [...parent[records_type_plural+'_uris'], ...records_uris]; + var uris = records_uris; + if (parent[records_type_plural+'_uris'] !== undefined){ + uris = [...parent[records_type_plural+'_uris'], ...uris]; + } parent[records_type_plural+'_uris'] = uris; if (action.records_data.next !== undefined){ parent[records_type_plural+'_more'] = action.records_data.next; diff --git a/src/js/services/core/reducer.js b/src/js/services/core/reducer.js index 05f496f6..4ce0b4a6 100755 --- a/src/js/services/core/reducer.js +++ b/src/js/services/core/reducer.js @@ -11,19 +11,14 @@ export default function reducer(core = {}, action){ * Current track and tracklist **/ - case 'MOPIDY_TLTRACKS': - if (!action.tracklist){ - return core; - } - return Object.assign({}, core, { current_tracklist: action.tracklist }); - - case 'MOPIDY_CURRENTTLTRACK': - if (!action.data){ - return core; - } + case 'CURRENT_TRACK_LOADED': return Object.assign({}, core, { - current_tracklist: action.current_tracklist, - current_track: action.current_track + current_track_uri: action.current_track_uri + }); + + case 'QUEUE_LOADED': + return Object.assign({}, core, { + queue: action.tracks_uris }); case 'PUSHER_QUEUE_METADATA': @@ -190,50 +185,6 @@ export default function reducer(core = {}, action){ * Playlists **/ - case 'PLAYLIST_KEY_UPDATED': - var playlists = Object.assign([], core.playlists) - - // URI not in our index? No change needed then - if (typeof(playlists[action.key]) === 'undefined'){ - return core - } - - // Delete our old playlist by key, and add by new key - var playlist = Object.assign({}, playlists[action.key]) - delete playlists[action.key] - playlists[playlist.uri] = playlist - - return Object.assign({}, core, { playlists: playlists }); - - case 'PLAYLIST_LOADED_MORE_TRACKS': - var playlists = Object.assign([], core.playlists) - var playlist = Object.assign( - {}, - playlists[action.key], - { - tracks: [...playlists[action.key].tracks, ...helpers.formatTracks(action.data.items)], - tracks_more: action.data.next, - tracks_total: action.data.total - } - ) - - playlists[action.key] = playlist - return Object.assign({}, core, { playlists: playlists }); - - case 'PLAYLIST_TRACKS_REMOVED': - var playlists = Object.assign([], core.playlists) - var playlist = Object.assign({}, playlists[action.key]) - var tracks = Object.assign([], playlist.tracks) - var indexes = action.tracks_indexes.reverse() - for(var i = 0; i < indexes.length; i++){ - tracks.splice(indexes[i], 1 ) - } - var snapshot_id = null - if (action.snapshot_id ) snapshot_id = action.snapshot_id - Object.assign(playlist, { tracks: tracks, snapshot_id: snapshot_id }) - playlists[action.key] = playlist - return Object.assign({}, core, { playlists: playlists }); - case 'PLAYLIST_TRACKS': var playlists = Object.assign([], core.playlists) var playlist = Object.assign({}, playlists[action.key], { tracks: helpers.formatTracks(action.tracks) }) @@ -241,29 +192,6 @@ export default function reducer(core = {}, action){ playlists[action.key] = playlist return Object.assign({}, core, { playlists: playlists }); - case 'PLAYLIST_TRACKS_REORDERED': - var playlists = Object.assign([], core.playlists) - var playlist = Object.assign({}, playlists[action.key]) - var tracks = Object.assign([], playlist.tracks) - - // handle insert_before offset if we're moving BENEATH where we're slicing tracks - var insert_before = action.insert_before - if (insert_before > action.range_start ) insert_before = insert_before - action.range_length - - // cut our moved tracks into a new array - var tracks_to_move = tracks.splice(action.range_start, action.range_length) - tracks_to_move.reverse() - - for(i = 0; i < tracks_to_move.length; i++){ - tracks.splice(insert_before, 0, tracks_to_move[i]) - } - - var snapshot_id = null - if (action.snapshot_id ) snapshot_id = action.snapshot_id - Object.assign(playlist, { tracks: tracks, snapshot_id: snapshot_id }) - playlists[action.key] = playlist - return Object.assign({}, core, { playlists: playlists }); - case 'LIBRARY_PLAYLISTS_LOADED': if (core.library_playlists){ var library_playlists = [...core.library_playlists, ...action.uris] diff --git a/src/js/services/lastfm/actions.js b/src/js/services/lastfm/actions.js index 90047323..9a7ba9c1 100755 --- a/src/js/services/lastfm/actions.js +++ b/src/js/services/lastfm/actions.js @@ -1,7 +1,7 @@ -var coreActions = require('../core/actions') -var uiActions = require('../ui/actions') -var helpers = require('../../helpers') +var coreActions = require('../core/actions'); +var uiActions = require('../ui/actions'); +var helpers = require('../../helpers'); /** * Send an ajax request to the LastFM API @@ -150,34 +150,9 @@ export function revokeAuthorization(){ return { type: 'LASTFM_AUTHORIZATION_REVOKED' } } -export function connect(){ - return (dispatch, getState) => { - - dispatch({ type: 'LASTFM_CONNECTING' }); - - // Authorized? Multi-purpose our connection test to get the current user - if (getState().lastfm.session){ - dispatch(getMe()); - - // Not authorized? Just use a generic lookup to test our connection - } else { - sendRequest(dispatch, getState, 'method=artist.getInfo&artist=Moby') - .then( - response => { - dispatch({ type: 'LASTFM_CONNECTED' }) - }, - error => { - dispatch({ type: 'LASTFM_DISCONNECTED' }) - } - ) - } - } -} - /** * Signed requests - * TODO **/ export function loveTrack(uri, artist, track){ @@ -218,6 +193,35 @@ export function unloveTrack(uri, artist, track){ } } +export function scrobble(track){ + return (dispatch, getState) => { + var track_name = track.name; + var artist_name = "Unknown"; + if (track.artists){ + artist_name = track.artists[0].name; + } + var artist_name = encodeURIComponent(artist_name); + + var params = 'method=track.scrobble'; + params += '&track='+track_name+'&artist='+artist_name; + params += '×tamp='+Math.floor(Date.now() / 1000); + + sendSignedRequest(dispatch, getState, params) + .then( + response => { + console.log("Scrobbled", response); + }, + error => { + dispatch(coreActions.handleException( + 'Could not scrobble track', + error, + (error.description ? error.description : null) + )); + } + ) + } +} + /** * Non-signed requests @@ -331,32 +335,3 @@ export function getTrack(track, artist_name = null, track_name = null){ } } -export function scrobble(track){ - return (dispatch, getState) => { - var track_name = track.name; - var artist_name = "Unknown"; - if (track.artists){ - artist_name = track.artists[0].name; - } - var artist_name = encodeURIComponent(artist_name); - - var params = 'method=track.scrobble'; - params += '&track='+track_name+'&artist='+artist_name; - params += '×tamp='+Math.floor(Date.now() / 1000); - - sendSignedRequest(dispatch, getState, params) - .then( - response => { - console.log("Scrobbled", response); - }, - error => { - dispatch(coreActions.handleException( - 'Could not scrobble track', - error, - (error.description ? error.description : null) - )); - } - ) - } -} - diff --git a/src/js/services/mopidy/middleware.js b/src/js/services/mopidy/middleware.js index 3a943df9..3956b381 100755 --- a/src/js/services/mopidy/middleware.js +++ b/src/js/services/mopidy/middleware.js @@ -1259,11 +1259,11 @@ const MopidyMiddleware = (function(){ type: 'PLAYLIST_KEY_UPDATED', key: action.key, new_key: response.uri - }) - hashHistory.push(global.baseURL+'playlist/'+response.uri) + }); + hashHistory.push(global.baseURL+'playlist/'+encodeURIComponent(response.uri)); } - store.dispatch(uiActions.createNotification('Saved')) + store.dispatch(uiActions.createNotification('Saved')); }) }); break @@ -1703,17 +1703,17 @@ const MopidyMiddleware = (function(){ * ====================================================================================== **/ + case 'MOPIDY_TLTRACKS': + store.dispatch({ + type: 'QUEUE_LOADED', + tracks: helpers.formatTracks(action.data) + }) + break; + case 'MOPIDY_CURRENTTLTRACK': if (action.data && action.data.track){ var track = helpers.formatTracks(action.data); - // Fire off our universal track index loader - store.dispatch({ - type: 'TRACK_LOADED', - key: track.uri, - track: track - }); - // We've got Spotify running, and it's a spotify track - go straight to the source! if (helpers.uriSource(track.uri) == 'spotify' && store.getState().spotify.enabled){ store.dispatch(spotifyActions.getTrack(track.uri)) @@ -1722,9 +1722,16 @@ const MopidyMiddleware = (function(){ } else { store.dispatch(mopidyActions.getImages('tracks',[track.uri])) } - } - next(action); + // Set our window title to the track title + helpers.setWindowTitle(track, store.getState().mopidy.play_state); + + store.dispatch({ + type: 'CURRENT_TRACK_LOADED', + current_track: track, + current_track_uri: track.uri + }); + } break; case 'MOPIDY_GET_TRACK': diff --git a/src/js/services/spotify/actions.js b/src/js/services/spotify/actions.js index f88c6e21..6f0cf7dc 100755 --- a/src/js/services/spotify/actions.js +++ b/src/js/services/spotify/actions.js @@ -232,10 +232,6 @@ export function importAuthorization(data){ **/ export function getMe(){ return (dispatch, getState) => { - - // flush out the previous store value - dispatch({ type: 'SPOTIFY_ME_LOADED', data: false }); - sendRequest(dispatch, getState, 'me' ) .then( response => { @@ -1192,7 +1188,7 @@ export function playArtistTopTracks(uri){ * ====================================================================================== **/ -export function getUser(uri){ +export function getUser(uri, and_playlists = false){ return (dispatch, getState) => { // get the user @@ -1212,7 +1208,9 @@ export function getUser(uri){ } ) - dispatch(getUserPlaylists(uri)) + if (and_playlists){ + dispatch(getUserPlaylists(uri)); + } } } @@ -1242,14 +1240,11 @@ export function getUserPlaylists(user_uri){ } dispatch({ - type: 'PLAYLISTS_LOADED', - playlists: playlists - }); - - dispatch({ - type: 'SPOTIFY_USER_PLAYLISTS_LOADED', - key: user_uri, - data: response + type: 'LOADED_MORE', + parent_type: 'user', + parent_key: user_uri, + records_type: 'playlist', + records_data: response }); }, error => { diff --git a/src/js/services/spotify/middleware.js b/src/js/services/spotify/middleware.js index 02fdbb76..26cf7189 100755 --- a/src/js/services/spotify/middleware.js +++ b/src/js/services/spotify/middleware.js @@ -415,7 +415,7 @@ const SpotifyMiddleware = (function(){ store.dispatch({ type: 'USERS_LOADED', users: [action.data] - }) + }); next(action); break; diff --git a/src/js/views/Artist.js b/src/js/views/Artist.js index ef47a929..5b24b6cb 100755 --- a/src/js/views/Artist.js +++ b/src/js/views/Artist.js @@ -78,7 +78,14 @@ class Artist extends React.Component{ } loadMore(){ - this.props.spotifyActions.getURL(this.props.artist.albums_more, 'SPOTIFY_ARTIST_ALBUMS_LOADED', this.props.params.uri ); + this.props.spotifyActions.loadMore( + this.props.artist.albums_more, + { + parent_type: 'artist', + parent_key: this.props.params.uri, + records_type: 'album' + } + ); } inLibrary(){ @@ -190,7 +197,7 @@ class Artist extends React.Component{

Albums

- this.loadMore() }/> + this.loadMore()} />
) diff --git a/src/js/views/Playlist.js b/src/js/views/Playlist.js index 026ac9fb..8c81714b 100755 --- a/src/js/views/Playlist.js +++ b/src/js/views/Playlist.js @@ -14,6 +14,7 @@ import LazyLoadListener from '../components/LazyLoadListener' import FollowButton from '../components/FollowButton' import Header from '../components/Header' import ContextMenuTrigger from '../components/ContextMenuTrigger' +import URILink from '../components/URILink' import * as helpers from '../helpers' import * as coreActions from '../services/core/actions' @@ -101,20 +102,20 @@ class Playlist extends React.Component{ // TODO: Once deletion occurs, remove playlist from global playlists list delete(){ - this.props.mopidyActions.deletePlaylist(this.props.playlist.uri ) + this.props.mopidyActions.deletePlaylist(this.props.playlist.uri); } reorderTracks(indexes, index){ - this.props.coreActions.reorderPlaylistTracks(this.props.playlist.uri, indexes, index, this.props.playlist.snapshot_id ) + this.props.coreActions.reorderPlaylistTracks(this.props.playlist.uri, indexes, index, this.props.playlist.snapshot_id); } removeTracks(tracks_indexes){ - this.props.coreActions.removeTracksFromPlaylist(this.props.playlist.uri, tracks_indexes ) + this.props.coreActions.removeTracksFromPlaylist(this.props.playlist.uri, tracks_indexes); } inLibrary(){ - var library = helpers.uriSource(this.props.params.uri)+'_library_playlists' - return (this.props[library] && this.props[library].indexOf(this.props.params.uri) > -1) + var library = helpers.uriSource(this.props.params.uri)+'_library_playlists'; + return (this.props[library] && this.props[library].indexOf(this.props.params.uri) > -1); } renderActions(){ @@ -195,8 +196,8 @@ class Playlist extends React.Component{ { this.props.playlist.description ?

: null }