Moving tracks into items (need to consider performance knock-ons)

This commit is contained in:
James Barnsley
2020-09-06 21:13:53 +12:00
parent 1bc7f2b372
commit 60326d9de6
13 changed files with 739 additions and 876 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -106,7 +106,7 @@
// Release details // Release details
// These are automatically injected to built HTML // These are automatically injected to built HTML
var build = "1599289916"; var build = "1599362472";
var version = "3.52.4"; var version = "3.52.4";
// Construct the script tag // Construct the script tag

View File

@ -333,22 +333,30 @@ class PlaybackControls extends React.Component {
} }
} }
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => {
snapcast_enabled: state.pusher.config.snapcast_enabled, const {
current_track: (state.core.current_track && state.core.tracks[state.core.current_track.uri] !== undefined ? state.core.tracks[state.core.current_track.uri] : null), items,
next_track: (state.core.next_track_uri && state.core.tracks[state.core.next_track_uri] !== undefined ? state.core.tracks[state.core.next_track_uri] : null), next_track_uri,
radio_enabled: (!!(state.ui.radio && state.ui.radio.enabled)), current_track,
play_state: state.mopidy.play_state, } = state.core;
time_position: state.mopidy.time_position,
consume: state.mopidy.consume, return {
repeat: state.mopidy.repeat, snapcast_enabled: state.pusher.config.snapcast_enabled,
random: state.mopidy.random, current_track: current_track ? items[current_track.uri] || current_track : null,
volume: state.mopidy.volume, next_track: items[next_track_uri],
mute: state.mopidy.mute, radio_enabled: (!!(state.ui.radio && state.ui.radio.enabled)),
sidebar_open: state.ui.sidebar_open, play_state: state.mopidy.play_state,
slim_mode: state.ui.slim_mode, time_position: state.mopidy.time_position,
touch_enabled: state.ui.playback_controls_touch_enabled, consume: state.mopidy.consume,
}); repeat: state.mopidy.repeat,
random: state.mopidy.random,
volume: state.mopidy.volume,
mute: state.mopidy.mute,
sidebar_open: state.ui.sidebar_open,
slim_mode: state.ui.slim_mode,
touch_enabled: state.ui.playback_controls_touch_enabled,
};
};
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
coreActions: bindActionCreators(coreActions, dispatch), coreActions: bindActionCreators(coreActions, dispatch),

View File

@ -107,71 +107,74 @@ export function updateColdStore(items) {
* relevant service to load the record - all from one neat package. * relevant service to load the record - all from one neat package.
* */ * */
export function loadItems(uris, force_reload = false) { export function loadItems(uris, forceReload = false) {
return { return {
type: 'LOAD_ITEMS', type: 'LOAD_ITEMS',
uris, uris,
force_reload, forceReload,
}; };
} }
export function loadItem(uri, force_reload = false) { export function loadItem(uri, forceReload = false) {
return loadItems([uri], force_reload); return loadItems([uri], forceReload);
} }
export function loadTrack(uri, force_reload = false) { export function loadTrack(uri, forceReload = false) {
return { return {
type: 'LOAD_TRACK', type: 'LOAD_TRACK',
uri, uri,
force_reload, forceReload,
}; };
} }
export function loadAlbum(uri, force_reload = false) { export function loadAlbum(uri, forceReload = false, callbackAction) {
return { return {
type: 'LOAD_ALBUM', type: 'LOAD_ALBUM',
uri, uri,
force_reload, forceReload,
callbackAction,
}; };
} }
export function loadArtist(uri, force_reload = false) { export function loadArtist(uri, forceReload = false, callbackAction) {
return { return {
type: 'LOAD_ARTIST', type: 'LOAD_ARTIST',
uri, uri,
force_reload, forceReload,
callbackAction,
}; };
} }
export function loadPlaylist(uri, force_reload = false) { export function loadPlaylist(uri, forceReload = false, callbackAction) {
return { return {
type: 'LOAD_PLAYLIST', type: 'LOAD_PLAYLIST',
uri, uri,
force_reload, forceReload,
callbackAction,
}; };
} }
export function loadUser(uri, force_reload = false) { export function loadUser(uri, forceReload = false) {
return { return {
type: 'LOAD_USER', type: 'LOAD_USER',
uri, uri,
force_reload, forceReload,
}; };
} }
export function loadUserPlaylists(uri, force_reload = false) { export function loadUserPlaylists(uri, forceReload = false) {
return { return {
type: 'LOAD_USER_PLAYLISTS', type: 'LOAD_USER_PLAYLISTS',
uri, uri,
force_reload, forceReload,
}; };
} }
export function loadLibrary(uri, force_reload = false) { export function loadLibrary(uri, forceReload = false) {
return { return {
type: 'LOAD_LIBRARY', type: 'LOAD_LIBRARY',
uri, uri,
force_reload, forceReload,
}; };
} }

View File

@ -318,35 +318,50 @@ const CoreMiddleware = (function () {
store.dispatch({ store.dispatch({
type: `LOAD_${uriType(uri).toUpperCase()}`, type: `LOAD_${uriType(uri).toUpperCase()}`,
uri, uri,
force_reload: action.force_reload, forceReload: action.forceReload,
}); });
}); });
break; break;
case 'LOAD_TRACK': case 'LOAD_TRACK': {
if ( const fetchTrack = () => {
!action.force_reload switch (uriSource(action.uri)) {
&& store.getState().core.tracks[action.uri]) { case 'spotify':
console.info(`Loading "${action.uri}" from index`); store.dispatch(spotifyActions.getTrack(action.uri));
if (spotify.me) {
store.dispatch(spotifyActions.following(action.uri));
}
break;
default:
store.dispatch(mopidyActions.getTrack(action.uri));
break;
}
};
if (action.forceReload) {
console.info(`Force-reloading "${action.uri}"`);
fetchTrack();
break;
}
if (store.getState().core.items[action.uri]) {
console.info(`Using "${action.uri}" from index`);
break; break;
} }
switch (uriSource(action.uri)) { localForage.getItem(action.uri).then((result) => {
case 'spotify': if (result) {
store.dispatch(spotifyActions.getTrack(action.uri)); console.info(`Loading "${action.uri}" from database`);
store.dispatch(coreActions.restoreItemsFromColdStore([result]));
if (spotify.me) { } else {
store.dispatch(spotifyActions.following(action.uri)); fetchTrack();
} }
break; });
default:
store.dispatch(mopidyActions.getTrack(action.uri));
break;
}
next(action); next(action);
break; break;
}
case 'LOAD_ALBUM': case 'LOAD_ALBUM':
const fetchAlbum = () => { const fetchAlbum = () => {
@ -365,7 +380,7 @@ const CoreMiddleware = (function () {
}; };
}; };
if (action.force_reload) { if (action.forceReload) {
console.info(`Force-reloading "${action.uri}"`); console.info(`Force-reloading "${action.uri}"`);
fetchAlbum(); fetchAlbum();
break; break;
@ -412,7 +427,7 @@ const CoreMiddleware = (function () {
} }
}; };
if (action.force_reload) { if (action.forceReload) {
console.info(`Force-reloading "${action.uri}"`); console.info(`Force-reloading "${action.uri}"`);
fetchArtist(); fetchArtist();
break; break;
@ -460,7 +475,7 @@ const CoreMiddleware = (function () {
} }
}; };
if (action.force_reload) { if (action.forceReload) {
console.info(`Force-reloading "${action.uri}"`); console.info(`Force-reloading "${action.uri}"`);
fetchPlaylist(); fetchPlaylist();
break; break;
@ -477,7 +492,6 @@ const CoreMiddleware = (function () {
if (result) { if (result) {
console.info(`Restoring "${action.uri}" from database`); console.info(`Restoring "${action.uri}" from database`);
store.dispatch(coreActions.restoreItemsFromColdStore([result])); store.dispatch(coreActions.restoreItemsFromColdStore([result]));
if (!result.tracks) { if (!result.tracks) {
fetchPlaylist(); fetchPlaylist();
} }
@ -491,7 +505,7 @@ const CoreMiddleware = (function () {
case 'LOAD_USER': case 'LOAD_USER':
if ( if (
!action.force_reload !action.forceReload
&& store.getState().core.users[action.uri] && store.getState().core.users[action.uri]
&& store.getState().core.users[action.uri].playlists_uris) { && store.getState().core.users[action.uri].playlists_uris) {
console.info(`Loading "${action.uri}" from index`); console.info(`Loading "${action.uri}" from index`);
@ -517,7 +531,7 @@ const CoreMiddleware = (function () {
case 'LOAD_USER_PLAYLISTS': case 'LOAD_USER_PLAYLISTS':
if ( if (
!action.force_reload !action.forceReload
&& store.getState().core.users[action.uri] && store.getState().core.users[action.uri]
&& store.getState().core.users[action.uri].playlists_uris) { && store.getState().core.users[action.uri].playlists_uris) {
console.info(`Loading "${action.uri}" playlists from index`); console.info(`Loading "${action.uri}" playlists from index`);
@ -558,7 +572,7 @@ const CoreMiddleware = (function () {
} }
}; };
if (action.force_reload) { if (action.forceReload) {
console.info(`Force-reloading "${action.uri}"`); console.info(`Force-reloading "${action.uri}"`);
fetchLibrary(); fetchLibrary();
break; break;
@ -592,11 +606,15 @@ const CoreMiddleware = (function () {
* These modify our asset indexes, which are used globally * These modify our asset indexes, which are used globally
* */ * */
case 'CURRENT_TRACK_LOADED': case 'CURRENT_TRACK_LOADED': {
store.dispatch(coreActions.trackLoaded(action.track)); const track = formatTrack(action.track);
action.track = formatTrack(action.track); store.dispatch(coreActions.itemLoaded(track));
next(action); next({
...action,
track,
});
break; break;
}
case 'QUEUE_LOADED': case 'QUEUE_LOADED':
store.dispatch(coreActions.tracksLoaded(action.tracks)); store.dispatch(coreActions.tracksLoaded(action.tracks));

View File

@ -651,109 +651,65 @@ const MopidyMiddleware = (function () {
* Advanced playback events * Advanced playback events
* */ * */
case 'MOPIDY_PLAY_PLAYLIST': case 'MOPIDY_PLAY_PLAYLIST': {
var playlist = store.getState().core.items[action.uri]; const playlist = store.getState().core.items[action.uri];
if (playlist && playlist.tracks) {
// We have the playlist loaded already, and we've got at least 1 track to start playing store.dispatch(
if (playlist && playlist.tracks_uris && playlist.tracks_uris.length > 0) { mopidyActions.playURIs(
// We've got all of the tracks, so just play those; no further action required arrayOf('uri', playlist.tracks),
if (playlist.tracks_total == playlist.tracks_uris.length) { action.uri,
store.dispatch(mopidyActions.playURIs(playlist.tracks_uris, action.uri, action.shuffle)); action.shuffle,
break; ),
} );
// Spotify-provided playlists need to be handled by the Spotify service
if (playlist.provider == 'spotify') {
store.dispatch(spotifyActions.getAllPlaylistTracks(action.uri, action.shuffle, 'play'));
break;
}
// It's a Spotify playlist that we haven't loaded yet, so Spotify HTTP API needs to go get it
} else if (uriSource(action.uri) == 'spotify' && store.getState().spotify.enabled) {
store.dispatch(spotifyActions.getAllPlaylistTracks(action.uri, action.shuffle, 'play'));
break; break;
} }
store.dispatch(
// Not in index, and Spotify HTTP not enabled, so just play it as-is coreActions.loadPlaylist(
// Fetch the playlist tracks via backend and add each track by URI action.uri,
request(store, 'playlists.lookup', { uri: action.uri }) false,
.then( {
(response) => { name: 'play',
if (!response || response.tracks === undefined || !response.tracks) { shuffle: action.shuffle,
store.dispatch(uiActions.createNotification({ content: 'Failed to load playlist tracks', level: 'error' }));
} else {
let tracks_uris = arrayOf('uri', response.tracks);
if (action.shuffle) {
tracks_uris = shuffle(tracks_uris);
}
store.dispatch(mopidyActions.playURIs(tracks_uris, action.uri));
}
}, },
(error) => { ),
store.dispatch(coreActions.handleException( );
`Mopidy: ${error.message ? error.message : 'Lookup failed'}`,
error,
));
},
);
break; break;
}
case 'MOPIDY_ENQUEUE_PLAYLIST': case 'MOPIDY_ENQUEUE_PLAYLIST': {
const playlist = store.getState().core.items[action.uri];
var playlist = store.getState().core.items[action.uri]; if (playlist && playlist.tracks) {
store.dispatch(
// We have the playlist loaded already, and we've got at least 1 track to start playing mopidyActions.enqueueURIs(
if (playlist && playlist.tracks_uris && playlist.tracks_uris.length > 0) { arrayOf('uri', playlist.tracks),
// We've got all of the tracks, so just play those; no further action required action.uri,
if (playlist.tracks_total == playlist.tracks_uris.length) { action.shuffle,
let tracks_uris = Object.assign([], playlist.tracks_uris); ),
if (action.shuffle) { );
tracks_uris = shuffle(tracks_uris);
}
store.dispatch(mopidyActions.enqueueURIs(tracks_uris, action.uri, action.play_next, action.at_position, action.offset));
break;
}
// Spotify-provided playlists need to be handled by the Spotify service
if (playlist.provider == 'spotify') {
store.dispatch(spotifyActions.getAllPlaylistTracks(action.uri, action.shuffle, 'enqueue', action.play_next));
break;
}
// It's a Spotify playlist that we haven't loaded yet, so Spotify HTTP API needs to go get it
} else if (uriSource(action.uri) == 'spotify' && store.getState().spotify.enabled) {
store.dispatch(spotifyActions.getAllPlaylistTracks(action.uri, action.shuffle, 'enqueue', action.play_next));
break; break;
} }
store.dispatch(
// Not in index, and Spotify HTTP not enabled, so just play it as-is coreActions.loadPlaylist(
// Fetch the playlist tracks via backend and add each track by URI action.uri,
request(store, 'playlists.lookup', { uri: action.uri }) false,
.then( {
(response) => { name: 'enqueue',
if (response.tracks === undefined) { shuffle: action.shuffle,
store.dispatch(uiActions.createNotification({ content: 'Failed to load playlist tracks', level: 'error' })); play_next: action.play_next,
} else { at_position: action.at_position,
let tracks_uris = arrayOf('uri', response.tracks); offset: action.offset,
if (action.shuffle) {
tracks_uris = shuffle(tracks_uris);
}
store.dispatch(mopidyActions.enqueueURIs(tracks_uris, action.uri, action.play_next, action.at_position, action.offset));
}
}, },
(error) => { ),
store.dispatch(coreActions.handleException( );
`Mopidy: ${error.message ? error.message : 'Lookup failed'}`,
error,
));
},
);
break; break;
}
case 'MOPIDY_ENQUEUE_URIS': case 'MOPIDY_ENQUEUE_URIS': {
if (!action.uris || action.uris.length <= 0) { if (!action.uris || action.uris.length <= 0) {
this.props.uiActions.createNotification({ content: 'No URIs to enqueue', level: 'warning' }); this.props.uiActions.createNotification({
content: 'No URIs to enqueue',
level: 'warning',
});
break; break;
} }
@ -762,15 +718,18 @@ const MopidyMiddleware = (function () {
{ {
notification: { notification: {
content: `${store.getState().pusher.username} is adding ${action.uris.length} URIs to queue`, content: `${store.getState().pusher.username} is adding ${action.uris.length} URIs to queue`,
icon: (store.getState().core.current_track ? getTrackIcon(store.getState().core.current_track, store.getState().core) : false), icon: (
store.getState().core.current_track
? getTrackIcon(store.getState().core.current_track, store.getState().core)
: false
),
}, },
}, },
)); ));
// split into batches const uris = Object.assign([], action.uris);
var uris = Object.assign([], action.uris); const batches = [];
var batches = []; const batch_size = 5;
var batch_size = 5;
while (uris.length > 0) { while (uris.length > 0) {
batches.push({ batches.push({
uris: uris.splice(0, batch_size), uris: uris.splice(0, batch_size),
@ -781,11 +740,11 @@ const MopidyMiddleware = (function () {
}); });
} }
// pass this modified action to the reducer (and other middleware) next({
action.batches = batches; ...action,
next(action); batches,
});
// start our processor
store.dispatch(uiActions.startProcess( store.dispatch(uiActions.startProcess(
'MOPIDY_ENQUEUE_URIS_PROCESSOR', 'MOPIDY_ENQUEUE_URIS_PROCESSOR',
i18n('services.mopidy.adding_uris', { count: action.uris.length }), i18n('services.mopidy.adding_uris', { count: action.uris.length }),
@ -796,17 +755,14 @@ const MopidyMiddleware = (function () {
}, },
)); ));
break; break;
}
case 'MOPIDY_ENQUEUE_URIS_PROCESSOR': case 'MOPIDY_ENQUEUE_URIS_PROCESSOR': {
const last_run = store.getState().ui.processes.MOPIDY_ENQUEUE_URIS_PROCESSOR;
var last_run = store.getState().ui.processes.MOPIDY_ENQUEUE_URIS_PROCESSOR;
// Cancelling
if (last_run && last_run.status == 'cancelling') { if (last_run && last_run.status == 'cancelling') {
store.dispatch(uiActions.processCancelled('MOPIDY_ENQUEUE_URIS_PROCESSOR')); store.dispatch(uiActions.processCancelled('MOPIDY_ENQUEUE_URIS_PROCESSOR'));
return; return;
// make sure we have some uris in the queue
} if (action.data.batches && action.data.batches.length > 0) { } if (action.data.batches && action.data.batches.length > 0) {
var batches = Object.assign([], action.data.batches); var batches = Object.assign([], action.data.batches);
var batch = batches[0]; var batch = batches[0];
@ -829,9 +785,11 @@ const MopidyMiddleware = (function () {
break; break;
} }
var { current_track } = store.getState().core; const {
var { queue } = store.getState().core; current_track,
var current_track_index = -1; queue,
} = store.getState().core;
let current_track_index = -1;
if (current_track) { if (current_track) {
for (var i = 0; i < queue.length; i++) { for (var i = 0; i < queue.length; i++) {
@ -888,8 +846,8 @@ const MopidyMiddleware = (function () {
)); ));
}, },
); );
break; break;
}
case 'MOPIDY_PLAY_URIS': case 'MOPIDY_PLAY_URIS':
const { from_uri } = action; const { from_uri } = action;
@ -2039,7 +1997,7 @@ const MopidyMiddleware = (function () {
// Some other source, rely on Mopidy backends to do their work // Some other source, rely on Mopidy backends to do their work
} else { } else {
store.dispatch(mopidyActions.getImages('tracks', [track.uri])); store.dispatch(mopidyActions.getImages([track.uri]));
} }
} }
} }
@ -2060,17 +2018,7 @@ const MopidyMiddleware = (function () {
uri: track.uri, uri: track.uri,
}); });
// We don't have the track (including images) already in our index store.dispatch(coreActions.loadItem(track.uri));
if (store.getState().core.tracks[track.uri] === undefined || store.getState().core.tracks[track.uri].images === undefined) {
// We've got Spotify running, and it's a spotify track - go straight to the source!
if (store.getState().spotify.enabled && uriSource(track.uri) == 'spotify') {
store.dispatch(spotifyActions.getTrack(track.uri));
// Some other source, rely on Mopidy backends to do their work
} else {
store.dispatch(mopidyActions.getImages('tracks', [track.uri]));
}
}
} }
} }
}, },
@ -2088,7 +2036,7 @@ const MopidyMiddleware = (function () {
store.dispatch(coreActions.tracksLoaded(tracks)); store.dispatch(coreActions.tracksLoaded(tracks));
if (action.get_images) { if (action.get_images) {
store.dispatch(mopidyActions.getImages('tracks', arrayOf('uri', tracks))); store.dispatch(mopidyActions.getImages(arrayOf('uri', tracks)));
} }
}, },
(error) => { (error) => {

View File

@ -946,7 +946,7 @@ export function getRecommendations(uris = [], limit = 20, tunabilities = null) {
request(dispatch, getState, endpoint) request(dispatch, getState, endpoint)
.then( .then(
(response) => { (response) => {
const tracks = Object.assign([], response.tracks); const tracks = Object.assign([], formatTracks(response.tracks));
// We only get simple artist objects, so we need to // We only get simple artist objects, so we need to
// get the full object. We'll add URIs to our recommendations // get the full object. We'll add URIs to our recommendations
@ -983,11 +983,11 @@ export function getRecommendations(uris = [], limit = 20, tunabilities = null) {
} }
if (albums.length > 0) { if (albums.length > 0) {
dispatch(coreActions.albumsLoaded(albums)); dispatch(coreActions.itemsLoaded(albums));
} }
if (tracks.length > 0) { if (tracks.length > 0) {
dispatch(coreActions.tracksLoaded(tracks)); dispatch(coreActions.itemsLoaded(tracks));
} }
dispatch({ dispatch({
@ -1104,7 +1104,6 @@ export function getArtistImages(artist) {
return (dispatch, getState) => { return (dispatch, getState) => {
request(dispatch, getState, `search?q=${artist.name}&type=artist`) request(dispatch, getState, `search?q=${artist.name}&type=artist`)
.then(response => { .then(response => {
console.log('images', response);
if (response.artists.items.length > 0) { if (response.artists.items.length > 0) {
dispatch(coreActions.itemLoaded({ dispatch(coreActions.itemLoaded({
uri: artist.uri, uri: artist.uri,
@ -1117,22 +1116,27 @@ export function getArtistImages(artist) {
export function playArtistTopTracks(uri) { export function playArtistTopTracks(uri) {
return (dispatch, getState) => { return (dispatch, getState) => {
const { artists } = getState().core; const {
items: {
[uri]: artist,
},
} = getState().core;
// Do we have this artist (and their tracks) in our index already? // Do we have this artist (and their tracks) in our index already?
if (typeof (artists[uri]) !== 'undefined' && typeof (artists[uri].tracks) !== 'undefined') { if (artist && artist.tracks) {
const uris = arrayOf('uri', artists[uri].tracks); const uris = arrayOf('uri', artist.tracks);
dispatch(mopidyActions.playURIs(uris, uri)); dispatch(mopidyActions.playURIs(uris, uri));
// We need to load the artist's top tracks first
} else { } else {
request(dispatch, getState, `artists/${getFromUri('artistid', uri)}/top-tracks?country=${getState().spotify.country}`) request(
.then( dispatch,
(response) => { getState,
const uris = arrayOf('uri', response.tracks); `artists/${getFromUri('artistid', uri)}/top-tracks?country=${getState().spotify.country}`,
dispatch(mopidyActions.playURIs(uris, uri)); ).then(
}, (response) => {
); const uris = arrayOf('uri', response.tracks);
dispatch(mopidyActions.playURIs(uris, uri));
},
);
} }
}; };
} }
@ -1359,9 +1363,8 @@ export function savePlaylist(uri, name, description, is_public, is_collaborative
}; };
} }
export function getPlaylist(uri) { export function getPlaylist(uri, callbackAction = null) {
return (dispatch, getState) => { return (dispatch, getState) => {
// get the main playlist object
request(dispatch, getState, `playlists/${getFromUri('playlistid', uri)}?market=${getState().spotify.country}`) request(dispatch, getState, `playlists/${getFromUri('playlistid', uri)}?market=${getState().spotify.country}`)
.then( .then(
(response) => { (response) => {
@ -1393,6 +1396,29 @@ export function getPlaylist(uri) {
uri, uri,
tracks, tracks,
})); }));
if (callbackAction) {
switch (callbackAction.name) {
case 'enqueue':
dispatch(mopidyActions.enqueueURIs(
arrayOf('uri', tracks),
uri,
callbackAction.play_next,
callbackAction.at_position,
callbackAction.offset,
));
break;
case 'play':
dispatch(mopidyActions.playURIs(
arrayOf('uri', tracks),
uri,
callbackAction.shuffle,
));
break;
default:
break;
}
}
} }
}); });
@ -1498,8 +1524,26 @@ export function getLibraryTracksAndPlayProcessor(data) {
* Recursively get .next until we have all tracks * Recursively get .next until we have all tracks
* */ * */
export function getAllPlaylistTracks(uri, shuffle = false, callback_action = null, play_next = false, at_position = null, offset = 0) { export function getAllPlaylistTracks(
uri,
shuffle = false,
callback_action = null,
play_next = false,
at_position = null,
offset = 0,
) {
return (dispatch, getState) => { return (dispatch, getState) => {
if (data.callback_action == 'enqueue') {
dispatch(mopidyActions.enqueueURIs(uris, data.uri, data.play_next, data.at_position, data.offset));
} else {
dispatch(mopidyActions.playURIs(uris, data.uri));
}
dispatch(uiActions.startProcess( dispatch(uiActions.startProcess(
'SPOTIFY_GET_ALL_PLAYLIST_TRACKS_PROCESSOR', 'SPOTIFY_GET_ALL_PLAYLIST_TRACKS_PROCESSOR',
'Loading playlist tracks', 'Loading playlist tracks',
@ -1690,7 +1734,6 @@ export function getLibraryPlaylists() {
let libraryPlaylists = []; let libraryPlaylists = [];
const fetchLibraryPlaylists = (endpoint) => request(dispatch, getState, endpoint) const fetchLibraryPlaylists = (endpoint) => request(dispatch, getState, endpoint)
.then((response) => { .then((response) => {
console.log('spot', response);
libraryPlaylists = [...libraryPlaylists, ...formatPlaylists(response.items)]; libraryPlaylists = [...libraryPlaylists, ...formatPlaylists(response.items)];
if (response.next) { if (response.next) {
fetchLibraryPlaylists(response.next); fetchLibraryPlaylists(response.next);

View File

@ -172,6 +172,7 @@ const uiPersistConfig = {
blacklist: [ blacklist: [
'load_queue', 'load_queue',
'notifications', 'notifications',
'processes',
'context_menu', 'context_menu',
'current_track_transition', 'current_track_transition',
'dragger', 'dragger',

View File

@ -12,9 +12,9 @@ const indexToArray = (index, keys) => {
if (!index) return []; if (!index) return [];
if (keys) { if (keys) {
return keys.map((key) => index[key]); return compact(keys.map((key) => index[key]));
} }
return Object.keys(index).map((key) => index[key]); return compact(Object.keys(index).map((key) => index[key]));
}; };
/** /**

View File

@ -31,7 +31,7 @@ import {
titleCase, titleCase,
} from '../util/helpers'; } from '../util/helpers';
import { collate } from '../util/format'; import { collate } from '../util/format';
import { sortItems, applyFilter } from '../util/arrays'; import { sortItems, applyFilter, arrayOf } from '../util/arrays';
import { i18n, I18n } from '../locale'; import { i18n, I18n } from '../locale';
import Button from '../components/Button'; import Button from '../components/Button';
import { trackEvent } from '../components/Trackable'; import { trackEvent } from '../components/Trackable';
@ -102,12 +102,14 @@ class Artist extends React.Component {
const { const {
artist: { artist: {
uri, uri,
tracks_uris, tracks,
albums_uris, albums_uris,
}, },
mopidyActions: { playURIs }, mopidyActions: {
playURIs,
},
} = this.props; } = this.props;
playURIs(tracks_uris || albums_uris, uri); playURIs(arrayOf('uri', tracks) || albums_uris, uri);
} }
setWindowTitle = (artist = this.props.artist) => { setWindowTitle = (artist = this.props.artist) => {
@ -532,8 +534,8 @@ const mapStateToProps = (state, ownProps) => {
load_queue: state.ui.load_queue, load_queue: state.ui.load_queue,
artist: (state.core.items[uri] || null), artist: (state.core.items[uri] || null),
items: state.core.items, items: state.core.items,
spotify_library_artists: state.core.items['spotify:library:artists'] || { items_uris: [] }, spotify_library_artists: state.core.libraries['spotify:library:artists'] || { items_uris: [] },
mopidy_library_artists: state.core.items['mopidy:library:artists'] || { items_uris: [] }, mopidy_library_artists: state.core.libraries['mopidy:library:artists'] || { items_uris: [] },
albums: (state.core.albums ? state.core.albums : []), albums: (state.core.albums ? state.core.albums : []),
filter: (state.ui.artist_albums_filter ? state.ui.artist_albums_filter : null), filter: (state.ui.artist_albums_filter ? state.ui.artist_albums_filter : null),
sort: (state.ui.artist_albums_sort ? state.ui.artist_albums_sort : null), sort: (state.ui.artist_albums_sort ? state.ui.artist_albums_sort : null),

View File

@ -22,6 +22,7 @@ import {
} from '../util/helpers'; } from '../util/helpers';
import { i18n, I18n } from '../locale'; import { i18n, I18n } from '../locale';
import Button from '../components/Button'; import Button from '../components/Button';
import { indexToArray } from '../util/arrays';
const Artwork = ({ const Artwork = ({
image, image,
@ -79,28 +80,13 @@ class Queue extends React.Component {
}) => { }) => {
const { const {
coreActions: { coreActions: {
loadAlbum, loadItem,
loadArtist,
loadPlaylist,
}, },
added_from_uri, added_from_uri,
} = this.props; } = this.props;
if (added_from_uri && added_from_uri !== prev_added_from_uri) { if (added_from_uri && added_from_uri !== prev_added_from_uri) {
const item_type = uriType(added_from_uri); loadItem(added_from_uri);
switch (item_type) {
case 'album':
loadAlbum(added_from_uri);
break;
case 'artist':
loadArtist(added_from_uri);
break;
case 'playlist':
loadPlaylist(added_from_uri);
break;
default:
break;
}
} }
} }
@ -120,7 +106,12 @@ class Queue extends React.Component {
} }
removeTracks = (track_indexes) => { removeTracks = (track_indexes) => {
const { queue_tracks, mopidyActions } = this.props; const {
queue_tracks,
mopidyActions: {
removeTracks: doRemoveTracks,
},
} = this.props;
const tlids = []; const tlids = [];
for (let i = 0; i < track_indexes.length; i++) { for (let i = 0; i < track_indexes.length; i++) {
const track = queue_tracks[track_indexes[i]]; const track = queue_tracks[track_indexes[i]];
@ -130,7 +121,7 @@ class Queue extends React.Component {
} }
if (tlids.length > 0) { if (tlids.length > 0) {
mopidyActions.removeTracks(tlids); doRemoveTracks(tlids);
} }
} }
@ -150,61 +141,51 @@ class Queue extends React.Component {
} }
renderAddedFrom = () => { renderAddedFrom = () => {
const { added_from_uri } = this.props; const {
items,
added_from_uri,
} = this.props;
if (!added_from_uri) return null; if (!added_from_uri) return null;
const uri_type = uriType(added_from_uri); const uri_type = uriType(added_from_uri);
const items = []; let addedFromItems = [];
// Radio nests it's seed URIs in an encoded URI format // Radio nests it's seed URIs in an encoded URI format
switch (uri_type){ switch (uri_type){
case 'radio': case 'radio':
const radio_seeds = getFromUri('seeds', added_from_uri); addedFromItems = indexToArray(items, getFromUri('seeds', added_from_uri));
for (let seed of radio_seeds) {
let item_type = uriType(seed);
let item_library = this.props[`${item_type}s`];
if (item_library && item_library[seed]) {
items.push(item_library[seed]);
}
}
break; break;
case 'search': case 'search':
items.push({ addedFromItems = [{
uri: added_from_uri, uri: added_from_uri,
name: `"${getFromUri('searchterm', added_from_uri)}" search`, name: `"${getFromUri('searchterm', added_from_uri)}" search`,
}) }];
break; break;
default: default:
const item_library = this.props[`${uri_type}s`]; addedFromItems = indexToArray(items, [added_from_uri]);
if (item_library && item_library[added_from_uri]) {
items.push(item_library[added_from_uri]);
}
break; break;
} }
if (items.length <= 0) return null; if (!addedFromItems.length) return null;
return ( return (
<div className="current-track__added-from"> <div className="current-track__added-from">
{items[0].images && ( {addedFromItems[0].images && (
<URILink <URILink
uri={items[0].uri} uri={addedFromItems[0].uri}
className="current-track__added-from__thumbnail" className="current-track__added-from__thumbnail"
> >
<Thumbnail <Thumbnail
images={items[0].images} images={addedFromItems[0].images}
size="small" size="small"
circle={uriType(items[0].uri) === 'artist'} circle={uriType(addedFromItems[0].uri) === 'artist'}
type="artist" type="artist"
/> />
</URILink> </URILink>
)} )}
<div className="current-track__added-from__text"> <div className="current-track__added-from__text">
{'Playing from '} {'Playing from '}
<LinksSentence items={items} /> <LinksSentence items={addedFromItems} />
{uri_type === 'radio' && ( {uri_type === 'radio' && (
<span className="flag flag--blue"> <span className="flag flag--blue">
{i18n('now_playing.current_track.radio')} {i18n('now_playing.current_track.radio')}
@ -221,6 +202,12 @@ class Queue extends React.Component {
queue_tracks, queue_tracks,
theme, theme,
current_track_uri, current_track_uri,
spotify_enabled,
uiActions,
mopidyActions: {
clearTracklist,
shuffleTracklist,
},
} = this.props; } = this.props;
const { limit } = this.state; const { limit } = this.state;
const total_queue_tracks = queue_tracks.length; const total_queue_tracks = queue_tracks.length;
@ -235,7 +222,7 @@ class Queue extends React.Component {
const options = ( const options = (
<span> <span>
{this.props.spotify_enabled && ( {spotify_enabled && (
<Button noHover discrete to="/queue/radio"> <Button noHover discrete to="/queue/radio">
<Icon name="radio" /> <Icon name="radio" />
<I18n path="now_playing.context_actions.radio" /> <I18n path="now_playing.context_actions.radio" />
@ -254,7 +241,7 @@ class Queue extends React.Component {
return ( return (
<div className="view queue-view preserve-3d"> <div className="view queue-view preserve-3d">
<Header options={options} uiActions={this.props.uiActions}> <Header options={options} uiActions={uiActions}>
<Icon name="play_arrow" type="material" /> <Icon name="play_arrow" type="material" />
<I18n path="now_playing.title" /> <I18n path="now_playing.title" />
</Header> </Header>
@ -293,7 +280,7 @@ class Queue extends React.Component {
<li><Dater type="total-time" data={queue_tracks} /></li> <li><Dater type="total-time" data={queue_tracks} /></li>
{queue_tracks.length > 0 && ( {queue_tracks.length > 0 && (
<li> <li>
<a onClick={this.props.mopidyActions.shuffleTracklist}> <a onClick={shuffleTracklist}>
<Icon name="shuffle" /> <Icon name="shuffle" />
<I18n path="now_playing.current_track.shuffle" /> <I18n path="now_playing.current_track.shuffle" />
</a> </a>
@ -301,7 +288,7 @@ class Queue extends React.Component {
)} )}
{queue_tracks.length > 0 && ( {queue_tracks.length > 0 && (
<li> <li>
<a onClick={this.props.mopidyActions.clearTracklist}> <a onClick={clearTracklist}>
<Icon name="delete_sweep" /> <Icon name="delete_sweep" />
<I18n path="now_playing.current_track.clear" /> <I18n path="now_playing.current_track.clear" />
</a> </a>
@ -341,12 +328,17 @@ class Queue extends React.Component {
} }
} }
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state) => {
let { current_track } = state.core; let { current_track } = state.core;
const {
items,
queue,
queue_metadata,
} = state.core;
const queue_tracks = []; const queue_tracks = [];
if (state.core.queue && state.core.tracks) { if (queue && items) {
for (const queue_track of state.core.queue) { for (const queue_track of queue) {
let track = { let track = {
...queue_track, ...queue_track,
playing: current_track && current_track.tlid == queue_track.tlid, playing: current_track && current_track.tlid == queue_track.tlid,
@ -355,18 +347,18 @@ const mapStateToProps = (state, ownProps) => {
// If we have the track in our index, merge it in. // If we have the track in our index, merge it in.
// We prioritise queue track over index track as queue has unique data, like which track // We prioritise queue track over index track as queue has unique data, like which track
// is playing and tlids. // is playing and tlids.
if (state.core.tracks.hasOwnProperty(track.uri)) { if (items[track.uri]) {
track = { track = {
...state.core.tracks[track.uri], ...items[track.uri],
...track, ...track,
}; };
} }
// Now merge in our queue metadata // Now merge in our queue metadata
if (state.core.queue_metadata[`tlid_${track.tlid}`] !== undefined) { if (queue_metadata[`tlid_${track.tlid}`] !== undefined) {
track = { track = {
...track, ...track,
...state.core.queue_metadata[`tlid_${track.tlid}`], ...queue_metadata[`tlid_${track.tlid}`],
}; };
} }
@ -389,6 +381,7 @@ const mapStateToProps = (state, ownProps) => {
albums: state.core.albums, albums: state.core.albums,
playlists: state.core.playlists, playlists: state.core.playlists,
tracks: state.core.tracks, tracks: state.core.tracks,
items,
queue_tracks, queue_tracks,
current_track_uri: state.core.current_track_uri, current_track_uri: state.core.current_track_uri,
current_track, current_track,

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { compact } from 'lodash';
import InputRange from 'react-input-range'; import InputRange from 'react-input-range';
import AlbumGrid from '../../components/AlbumGrid'; import AlbumGrid from '../../components/AlbumGrid';
import TrackList from '../../components/TrackList'; import TrackList from '../../components/TrackList';
@ -16,6 +16,7 @@ import Icon from '../../components/Icon';
import * as uiActions from '../../services/ui/actions'; import * as uiActions from '../../services/ui/actions';
import * as mopidyActions from '../../services/mopidy/actions'; import * as mopidyActions from '../../services/mopidy/actions';
import * as spotifyActions from '../../services/spotify/actions'; import * as spotifyActions from '../../services/spotify/actions';
import * as coreActions from '../../services/core/actions';
import { import {
isLoading, isLoading,
getFromUri, getFromUri,
@ -195,10 +196,10 @@ class Discover extends React.Component {
uiActions: { uiActions: {
showContextMenu, showContextMenu,
}, },
tracks: tracksProp, items: itemsProp,
} = this.props; } = this.props;
const tracks = indexToArray(tracksProp, tracks_uris); const tracks = indexToArray(itemsProp, tracks_uris);
showContextMenu({ showContextMenu({
e, e,
@ -211,9 +212,8 @@ class Discover extends React.Component {
handleURLSeeds = () => { handleURLSeeds = () => {
const { const {
spotifyActions: { coreActions: {
getArtist, loadItems,
getTrack,
}, },
match: { match: {
params: { params: {
@ -226,19 +226,7 @@ class Discover extends React.Component {
// We'd need to do this if our URL has been encoded so the whole URL can become // We'd need to do this if our URL has been encoded so the whole URL can become
// it's own URI (eg iris:discover:spotify_artist_1234) where we can't use ":" // it's own URI (eg iris:discover:spotify_artist_1234) where we can't use ":"
const seeds = seedsProp.split('_').join(':').split(','); const seeds = seedsProp.split('_').join(':').split(',');
loadItems(seeds);
for (let i = 0; i < seeds.length; i++) {
switch (uriType(seeds[i])) {
case 'artist':
getArtist(seeds[i]);
break;
case 'track':
getTrack(seeds[i]);
break;
default:
break;
}
}
this.setState( this.setState(
{ seeds }, { seeds },
@ -251,7 +239,7 @@ class Discover extends React.Component {
seeds, seeds,
} = this.state; } = this.state;
var uri = 'iris:discover'; let uri = 'iris:discover';
if (seeds) { if (seeds) {
uri += ':'; uri += ':';
for (var i = 0; i < seeds.length; i++) { for (var i = 0; i < seeds.length; i++) {
@ -325,8 +313,7 @@ class Discover extends React.Component {
renderSeeds = () => { renderSeeds = () => {
const { const {
tracks, items,
artists,
} = this.props; } = this.props;
const { const {
seeds, seeds,
@ -338,34 +325,22 @@ class Discover extends React.Component {
const uri = seeds[i]; const uri = seeds[i];
switch (uriType(uri)) { switch (uriType(uri)) {
case 'track':
if (typeof (tracks[uri]) !== 'undefined') {
seeds_objects.push(tracks[uri]);
} else {
seeds_objects.push({
name: 'Loading...',
uri,
});
}
break;
case 'artist':
if (typeof (artists[uri]) !== 'undefined') {
seeds_objects.push(artists[uri]);
} else {
seeds_objects.push({
name: 'Loading...',
uri,
});
}
break;
case 'genre': case 'genre':
var name = getFromUri('genreid', uri); const name = getFromUri('genreid', uri);
seeds_objects.push({ seeds_objects.push({
name: (name.charAt(0).toUpperCase() + name.slice(1)).replace('-', ' '), name: (name.charAt(0).toUpperCase() + name.slice(1)).replace('-', ' '),
uri, uri,
}); });
break; break;
default: default:
if (typeof (items[uri]) !== 'undefined') {
seeds_objects.push(items[uri]);
} else {
seeds_objects.push({
name: 'Loading...',
uri,
});
}
break; break;
} }
} }
@ -475,44 +450,16 @@ class Discover extends React.Component {
albums_uris, albums_uris,
artists_uris, artists_uris,
} = {}, } = {},
tracks: tracksProp, items: itemsProp,
artists: artistsProp,
albums: albumsProp,
} = this.props; } = this.props;
if (!albums_uris === undefined || artists_uris === undefined) { if (!albums_uris === undefined || artists_uris === undefined) {
return <div className="content-wrapper recommendations-results" />; return <div className="content-wrapper recommendations-results" />;
} }
const tracks = []; const tracks = compact(tracks_uris.map((uri) => itemsProp[uri]));
if (tracks_uris && tracksProp) { const artists = compact(artists_uris.map((uri) => itemsProp[uri]));
for (var i = 0; i < tracks_uris.length; i++) { const albums = compact(albums_uris.map((uri) => itemsProp[uri]));
var uri = tracks_uris[i];
if (tracksProp.hasOwnProperty(uri)) {
tracks.push(tracksProp[uri]);
}
}
}
const artists = [];
if (artists_uris && artistsProp) {
for (var i = 0; i < artists_uris.length; i++) {
var uri = artists_uris[i];
if (artistsProp.hasOwnProperty(uri)) {
artists.push(artistsProp[uri]);
}
}
}
const albums = [];
if (albums_uris && albumsProp) {
for (var i = 0; i < albums_uris.length; i++) {
var uri = albums_uris[i];
if (albumsProp.hasOwnProperty(uri)) {
albums.push(albumsProp[uri]);
}
}
}
// Complete records not yet in our index // Complete records not yet in our index
if (tracks.length <= 0 && artists.length <= 0 && albums.length <= 0) { if (tracks.length <= 0 && artists.length <= 0 && albums.length <= 0) {
@ -640,9 +587,8 @@ class Discover extends React.Component {
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
theme: state.ui.theme, theme: state.ui.theme,
albums: state.core.albums,
artists: state.core.artists,
tracks: state.core.tracks, tracks: state.core.tracks,
items: state.core.items,
genres: (state.core.genres ? state.core.genres : []), genres: (state.core.genres ? state.core.genres : []),
authorized: state.spotify.authorization, authorized: state.spotify.authorization,
load_queue: state.ui.load_queue, load_queue: state.ui.load_queue,
@ -654,6 +600,7 @@ const mapDispatchToProps = (dispatch) => ({
uiActions: bindActionCreators(uiActions, dispatch), uiActions: bindActionCreators(uiActions, dispatch),
mopidyActions: bindActionCreators(mopidyActions, dispatch), mopidyActions: bindActionCreators(mopidyActions, dispatch),
spotifyActions: bindActionCreators(spotifyActions, dispatch), spotifyActions: bindActionCreators(spotifyActions, dispatch),
coreActions: bindActionCreators(coreActions, dispatch),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(Discover); export default connect(mapStateToProps, mapDispatchToProps)(Discover);