Upgrading process calls; Tracks library now works like aother asset libraries (huge libraries may kill react-persist?)

This commit is contained in:
James Barnsley
2020-10-23 23:11:24 +13:00
parent eca594e9e5
commit 5724f102cf
19 changed files with 1571 additions and 1029 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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 = "1603178804"; var build = "1603447492";
var version = "3.53.0"; var version = "3.53.0";
// Construct the script tag // Construct the script tag

View File

@ -32,12 +32,12 @@ export default memo((props) => {
const { const {
body, body,
loading, loading,
finished = false, finished,
mini, mini,
lazy, lazy,
white, white,
className = '', className = '',
progress = null, progress,
} = props; } = props;
if (!loading && !finished) { if (!loading && !finished) {
@ -85,7 +85,7 @@ export default memo((props) => {
); );
} }
if (progress) { if (progress !== undefined) {
classNameString += ' loader--progress'; classNameString += ' loader--progress';
return ( return (
<div className={classNameString}> <div className={classNameString}>

View File

@ -38,6 +38,7 @@ actions:
confirm: Are you sure? confirm: Are you sure?
delete: Delete delete: Delete
remove: Remove remove: Remove
cancel: Cancel
common: common:
popularity: Popularity popularity: Popularity
name: Name name: Name
@ -117,6 +118,7 @@ services:
title: Mopidy title: Mopidy
local: Local local: Local
adding_uris: 'Adding %{count} URI(s)' adding_uris: 'Adding %{count} URI(s)'
adding_uris_broadcast: '%{username} is adding %{count} URI(s)'
searching: 'Searching %{provider} %{type}' searching: 'Searching %{provider} %{type}'
searching_providers: 'Searching %{count} Mopidy providers' searching_providers: 'Searching %{count} Mopidy providers'
loading_albums: 'Loading %{count} local albums' loading_albums: 'Loading %{count} local albums'

View File

@ -82,6 +82,10 @@ const ensureLoaded = ({
if (item) { if (item) {
if (getMissingDependents(item).length === 0) { if (getMissingDependents(item).length === 0) {
console.info(`"${uri}" and all dependents already in index`); console.info(`"${uri}" and all dependents already in index`);
// TODO
// When an item is in the index, we assume the dependents are loaded as well, which in the
// case of local artists is not true. Ie a local artist's albums are not auto-loaded.
store.dispatch(uiActions.stopLoading(uri)); store.dispatch(uiActions.stopLoading(uri));
coreActions.loadItems(getDependentUris(item)); coreActions.loadItems(getDependentUris(item));
return; return;
@ -373,6 +377,7 @@ const CoreMiddleware = (function () {
switch (uriSource(key)) { switch (uriSource(key)) {
case 'spotify': case 'spotify':
console.log("GETTING", key)
store.dispatch(spotifyActions.getPlaylist(key)); store.dispatch(spotifyActions.getPlaylist(key));
break; break;
case 'm3u': case 'm3u':

View File

@ -26,9 +26,7 @@ const GoogleMiddleware = (function () {
return (store) => (next) => (action) => { return (store) => (next) => (action) => {
switch (action.type) { switch (action.type) {
case 'GOOGLE_GET_LIBRARY_ALBUMS': { case 'GOOGLE_GET_LIBRARY_ALBUMS': {
store.dispatch( store.dispatch(uiActions.startProcess(action.type, { notification: false }) );
uiActions.updateProcess(action.type, { notification: false }),
);
request( request(
store, store,
@ -38,7 +36,7 @@ const GoogleMiddleware = (function () {
const allUris = arrayOf('uri', browseResponse); const allUris = arrayOf('uri', browseResponse);
store.dispatch(uiActions.updateProcess( store.dispatch(uiActions.updateProcess(
'GOOGLE_GET_LIBRARY_ALBUMS', action.type,
{ {
remaining: allUris.length, remaining: allUris.length,
total: allUris.length, total: allUris.length,
@ -83,7 +81,7 @@ const GoogleMiddleware = (function () {
store.dispatch(uiActions.processFinished(action.type)); store.dispatch(uiActions.processFinished(action.type));
store.dispatch(coreActions.libraryLoaded({ store.dispatch(coreActions.libraryLoaded({
uri: 'google:library:albums', uri: 'google:library:albums',
items_uris: arrayOf('uri', allUris), items_uris: arrayOf('uri', browseResponse),
})); }));
} }
}; };
@ -95,7 +93,7 @@ const GoogleMiddleware = (function () {
} }
case 'GOOGLE_GET_LIBRARY_ARTISTS': { case 'GOOGLE_GET_LIBRARY_ARTISTS': {
store.dispatch(uiActions.updateProcess(action.type, { notification: false })); store.dispatch(uiActions.startProcess(action.type, { notification: false }));
request( request(
store, store,
@ -149,7 +147,7 @@ const GoogleMiddleware = (function () {
store.dispatch(uiActions.processFinished(action.type)); store.dispatch(uiActions.processFinished(action.type));
store.dispatch(coreActions.libraryLoaded({ store.dispatch(coreActions.libraryLoaded({
uri: 'google:library:artists', uri: 'google:library:artists',
items_uris: arrayOf('uri', allUris), items_uris: arrayOf('uri', browseResponse),
})); }));
} }
}; };

View File

@ -473,9 +473,17 @@ export function getLibraryArtists(uri = null) {
}; };
} }
export function clearLibraryArtists() { export function getLibraryAlbums(uri = null) {
return { return {
type: 'MOPIDY_CLEAR_LIBRARY_ARTISTS', type: 'MOPIDY_GET_LIBRARY_ALBUMS',
uri,
};
}
export function getLibraryTracks(uri = null) {
return {
type: 'MOPIDY_GET_LIBRARY_TRACKS',
uri,
}; };
} }
@ -495,39 +503,6 @@ export function getAlbum(uri, options) {
}; };
} }
export function getAlbums(uris, processor = null) {
return {
type: 'MOPIDY_GET_ALBUMS',
uris,
processor,
};
}
export function getLibraryAlbums(uri = null) {
return {
type: 'MOPIDY_GET_LIBRARY_ALBUMS',
uri,
};
}
export function clearLibraryAlbums() {
return {
type: 'MOPIDY_CLEAR_LIBRARY_ALBUMS',
};
}
export function runProcessor(processor) {
return {
type: processor,
};
}
export function cancelProcessor(processor) {
return {
type: `${processor}_CANCEL`,
};
}
export function view_getRandomTracks(limit = 100) { export function view_getRandomTracks(limit = 100) {
return { return {
type: 'VIEW__GET_RANDOM_TRACKS', type: 'VIEW__GET_RANDOM_TRACKS',
@ -535,11 +510,6 @@ export function view_getRandomTracks(limit = 100) {
}; };
} }
/**
* Searching
* */
export function clearSearchResults() { export function clearSearchResults() {
return { return {
type: 'MOPIDY_CLEAR_SEARCH_RESULTS', type: 'MOPIDY_CLEAR_SEARCH_RESULTS',
@ -555,11 +525,6 @@ export function getSearchResults(query, limit = 100, uri_schemes) {
}; };
} }
/**
* Other general actions
* */
export function getQueueHistory() { export function getQueueHistory() {
return { return {
type: 'MOPIDY_GET_QUEUE_HISTORY', type: 'MOPIDY_GET_QUEUE_HISTORY',

View File

@ -2,7 +2,7 @@
import ReactGA from 'react-ga'; import ReactGA from 'react-ga';
import Mopidy from 'mopidy'; import Mopidy from 'mopidy';
import { sha256 } from 'js-sha256'; import { sha256 } from 'js-sha256';
import { sampleSize, compact } from 'lodash'; import { sampleSize, compact, chunk } from 'lodash';
import { i18n } from '../../locale'; import { i18n } from '../../locale';
import { import {
generateGuid, generateGuid,
@ -257,7 +257,8 @@ const MopidyMiddleware = (function () {
method = 'library.search', method = 'library.search',
data, data,
} = queue.shift(); } = queue.shift();
const processor = store.getState().ui.processes.MOPIDY_GET_SEARCH_RESULTS; const processKey = 'MOPIDY_GET_SEARCH_RESULTS';
const processor = store.getState().ui.processes[processKey];
if (processor && processor.status === 'cancelling') { if (processor && processor.status === 'cancelling') {
store.dispatch(uiActions.processCancelled('MOPIDY_GET_SEARCH_RESULTS')); store.dispatch(uiActions.processCancelled('MOPIDY_GET_SEARCH_RESULTS'));
@ -265,7 +266,7 @@ const MopidyMiddleware = (function () {
} }
store.dispatch(uiActions.updateProcess( store.dispatch(uiActions.updateProcess(
'MOPIDY_GET_SEARCH_RESULTS', processKey,
i18n( i18n(
'services.mopidy.searching', 'services.mopidy.searching',
{ {
@ -369,9 +370,7 @@ const MopidyMiddleware = (function () {
if (queue.length) { if (queue.length) {
processSearchQueue(store, queue); processSearchQueue(store, queue);
} else { } else {
store.dispatch(uiActions.processFinished( store.dispatch(uiActions.processFinished(processKey));
'MOPIDY_GET_SEARCH_RESULTS',
));
} }
}, },
); );
@ -862,11 +861,42 @@ const MopidyMiddleware = (function () {
break; break;
} }
let remaining = action.uris.length;
let at_position = action.at_position;
// Uris are to go immediately after the currently-playing track (which could be paused)
if (action.play_next) {
const {
current_track,
queue,
} = store.getState().core;
if (current_track) {
for (let i = 0; i < queue.length; i += 1) {
if (queue[i].tlid === current_track.tlid) {
at_position = i + 1;
break;
}
}
}
}
store.dispatch(uiActions.startProcess(
action.type,
{
content: i18n('services.mopidy.adding_uris', { count: remaining }),
remaining,
total: remaining,
},
));
store.dispatch(pusherActions.deliverBroadcast( store.dispatch(pusherActions.deliverBroadcast(
'notification', 'notification',
{ {
notification: { notification: {
content: `${store.getState().pusher.username} is adding ${action.uris.length} URIs to queue`, content: i18n(
'services.mopidy.adding_uris_broadcast',
{ username: store.getState().pusher.username, count: remaining },
),
icon: ( icon: (
store.getState().core.current_track store.getState().core.current_track
? getTrackIcon(store.getState().core.current_track, store.getState().core) ? getTrackIcon(store.getState().core.current_track, store.getState().core)
@ -876,125 +906,65 @@ const MopidyMiddleware = (function () {
}, },
)); ));
const uris = Object.assign([], action.uris); // Prepare our URIS into batches. This allows performance gain of several URIs at once
const batches = []; // (which is a blocking request), but not so many that it locks the Mopidy server.
const batch_size = 5; // It allows a window of opportunity for other requests to complete before we proceed to
while (uris.length > 0) { // the next batch.
batches.push({ const batchSize = 5;
uris: uris.splice(0, batch_size), const batches = chunk(action.uris, batchSize);
at_position: action.at_position,
play_next: action.play_next,
offset: action.offset + (batch_size * batches.length),
from_uri: action.from_uri,
});
}
next({ // This is our process iterator
...action, const run = () => {
batches, const processor = store.getState().ui.processes[action.type];
}); if (processor && processor.status === 'cancelling') {
store.dispatch(uiActions.processCancelled(action.type));
store.dispatch(uiActions.startProcess( return;
'MOPIDY_ENQUEUE_URIS_PROCESSOR',
i18n('services.mopidy.adding_uris', { count: action.uris.length }),
{
batches,
remaining: action.uris.length,
total: action.uris.length,
},
));
break;
}
case 'MOPIDY_ENQUEUE_URIS_PROCESSOR': {
const last_run = store.getState().ui.processes.MOPIDY_ENQUEUE_URIS_PROCESSOR;
if (last_run && last_run.status == 'cancelling') {
store.dispatch(uiActions.processCancelled('MOPIDY_ENQUEUE_URIS_PROCESSOR'));
return;
} if (action.data.batches && action.data.batches.length > 0) {
var batches = Object.assign([], action.data.batches);
var batch = batches[0];
let total_uris = 0;
for (var i = 0; i < batches.length; i++) {
total_uris += batches[i].uris.length;
}
batches.shift();
store.dispatch(uiActions.updateProcess(
'MOPIDY_ENQUEUE_URIS_PROCESSOR',
`Adding ${total_uris} URI(s)`,
{
remaining: total_uris,
},
));
// no batches means we're done here
} else {
store.dispatch(uiActions.processFinished('MOPIDY_ENQUEUE_URIS_PROCESSOR'));
break;
}
const {
current_track,
queue,
} = store.getState().core;
let current_track_index = -1;
if (current_track) {
for (var i = 0; i < queue.length; i++) {
if (queue[i].tlid == current_track.tlid) {
current_track_index = i;
break;
}
}
}
var params = { uris: batch.uris };
// Play this batch next
if (batch.play_next) {
// Make sure we're playing something first
if (current_track_index > -1) {
params.at_position = current_track_index + batch.offset + 1;
// Default to top of queue if we're not playing
} else {
params.at_position = 0 + batch.offset;
} }
// A specific position has been defined const uris = batches.shift();
// NOTE: This is likely to be wrong as the original action is unaware of batches or other client requests
} else if (batch.at_position) {
params.at_position = batch.at_position + batch.offset;
}
request(store, 'tracklist.add', params) request(store, 'tracklist.add', { uris, at_position })
.then( .then(
(response) => { (response) => {
// add metadata to queue const tlids = response.map((track) => track.tlid);
const tlids = []; store.dispatch(pusherActions.addQueueMetadata(tlids, action.from_uri));
for (let i = 0; i < response.length; i++) {
tlids.push(response[i].tlid);
}
store.dispatch(pusherActions.addQueueMetadata(tlids, batch.from_uri));
// Re-run the batch checker in 100ms. This allows a small window for other // Re-run the batch checker in 100ms. This allows a small window for other server
// server requests before our next batch. It's a little crude but it means the server isn't // requests before our next batch. A little crude but it means the server isn't
// locked until we're completely done. // locked until we're completely done.
setTimeout( setTimeout(
() => { () => {
store.dispatch(uiActions.runProcess(action.type, { batches })); if (!batches.length) {
}, store.dispatch(uiActions.processFinished(action.type));
100, } else {
); remaining -= uris.length;
}, if (at_position !== null) at_position += uris.length;
(error) => { store.dispatch(
store.dispatch(coreActions.handleException( uiActions.updateProcess(
`Mopidy: ${error.message ? error.message : 'Adding tracks failed'}`, action.type,
error, {
)); remaining,
}, content: i18n('services.mopidy.adding_uris', { count: remaining }),
); },
),
);
run();
}
},
100,
);
},
(error) => {
store.dispatch(uiActions.processFinished(action.type));
store.dispatch(coreActions.handleException(
`Mopidy: ${error.message || 'Adding tracks failed'}`,
error,
));
},
);
};
run(); // Kick off the loop
break; break;
} }
@ -1724,14 +1694,8 @@ const MopidyMiddleware = (function () {
* My Music libraries * My Music libraries
*/ */
case 'MOPIDY_GET_LIBRARY_ARTISTS': case 'MOPIDY_GET_LIBRARY_ARTISTS':
store.dispatch( store.dispatch(uiActions.startProcess(action.type, { notification: false }));
uiActions.startProcess(
'MOPIDY_GET_LIBRARY_ARTISTS',
{
notification: false,
},
),
);
request(store, 'library.browse', { uri: store.getState().mopidy.library_artists_uri }) request(store, 'library.browse', { uri: store.getState().mopidy.library_artists_uri })
.then((raw_response) => { .then((raw_response) => {
if (raw_response.length <= 0) return; if (raw_response.length <= 0) return;
@ -1748,19 +1712,12 @@ const MopidyMiddleware = (function () {
uri: 'mopidy:library:artists', uri: 'mopidy:library:artists',
items_uris: arrayOf('uri', response), items_uris: arrayOf('uri', response),
})); }));
store.dispatch(uiActions.processFinished('MOPIDY_GET_LIBRARY_ARTISTS')); store.dispatch(uiActions.processFinished(action.type));
}); });
break; break;
case 'MOPIDY_GET_LIBRARY_PLAYLISTS': { case 'MOPIDY_GET_LIBRARY_PLAYLISTS': {
store.dispatch( store.dispatch(uiActions.startProcess(action.type, { notification: false }));
uiActions.startProcess(
'MOPIDY_GET_LIBRARY_PLAYLISTS',
{
notification: false,
},
),
);
request(store, 'playlists.asList') request(store, 'playlists.asList')
.then((listResponse) => { .then((listResponse) => {
@ -1799,7 +1756,7 @@ const MopidyMiddleware = (function () {
store.dispatch( store.dispatch(
uiActions.updateProcess( uiActions.updateProcess(
'MOPIDY_GET_LIBRARY_PLAYLISTS', action.type,
{ {
remaining: playlist_uris.length - index - 1, remaining: playlist_uris.length - index - 1,
}, },
@ -1812,35 +1769,28 @@ const MopidyMiddleware = (function () {
uri: 'mopidy:library:playlists', uri: 'mopidy:library:playlists',
items_uris: arrayOf('uri', libraryPlaylists), items_uris: arrayOf('uri', libraryPlaylists),
})); }));
store.dispatch(uiActions.processFinished('MOPIDY_GET_LIBRARY_PLAYLISTS')); store.dispatch(uiActions.processFinished(action.type));
} }
}); });
}); });
} else { } else {
store.dispatch(uiActions.stopLoading('mopidy:library:playlists')); store.dispatch(uiActions.stopLoading('mopidy:library:playlists'));
store.dispatch(uiActions.processFinished('MOPIDY_GET_LIBRARY_PLAYLISTS')); store.dispatch(uiActions.processFinished(action.type));
} }
}); });
break; break;
} }
case 'MOPIDY_GET_LIBRARY_ALBUMS': case 'MOPIDY_GET_LIBRARY_ALBUMS':
store.dispatch( store.dispatch(uiActions.startProcess(action.type, { notification: false }));
uiActions.startProcess(
'MOPIDY_GET_LIBRARY_ALBUMS',
{
notification: false,
},
),
);
request(store, 'library.browse', { uri: store.getState().mopidy.library_albums_uri }) request(store, 'library.browse', { uri: store.getState().mopidy.library_albums_uri })
.then((response) => { .then((browseResponse) => {
const uris = arrayOf('uri', response); const uris = arrayOf('uri', browseResponse);
store.dispatch( store.dispatch(
uiActions.updateProcess( uiActions.updateProcess(
'MOPIDY_GET_LIBRARY_ALBUMS', action.type,
{ {
total: uris.length, total: uris.length,
remaining: uris.length, remaining: uris.length,
@ -1849,29 +1799,82 @@ const MopidyMiddleware = (function () {
); );
request(store, 'library.lookup', { uris }) request(store, 'library.lookup', { uris })
.then((response) => { .then((lookupResponse) => {
const libraryAlbums = indexToArray(response).map((tracks) => ({ const libraryAlbums = indexToArray(lookupResponse).map((tracks) => ({
artists: tracks[0].artists ? formatArtists(tracks[0].artists) : null, artists: tracks[0].artists ? formatArtists(tracks[0].artists) : null,
tracks: formatTracks(tracks), tracks: formatTracks(tracks),
last_modified: tracks[0].last_modified, last_modified: tracks[0].last_modified,
...formatAlbum(tracks[0].album), ...formatAlbum(tracks[0].album),
})); }));
console.log('LOADED', response);
store.dispatch(coreActions.itemsLoaded(libraryAlbums)); store.dispatch(coreActions.itemsLoaded(libraryAlbums));
store.dispatch(coreActions.libraryLoaded({ store.dispatch(coreActions.libraryLoaded({
uri: 'mopidy:library:albums', uri: 'mopidy:library:albums',
items_uris: arrayOf('uri', libraryAlbums), items_uris: arrayOf('uri', libraryAlbums),
})); }));
store.dispatch(uiActions.processFinished('MOPIDY_GET_LIBRARY_ALBUMS')); store.dispatch(uiActions.processFinished(action.type));
}); });
}); });
break; break;
// This action is irrelevant to us, pass it on to the next middleware case 'MOPIDY_GET_LIBRARY_TRACKS':
store.dispatch(uiActions.startProcess(action.type, { notification: false }));
request(store, 'library.browse', { uri: 'local:directory?type=track' })
.then((browseResponse) => {
const allUris = arrayOf('uri', browseResponse);
store.dispatch(uiActions.updateProcess(
action.type,
{
remaining: allUris.length,
total: allUris.length,
},
));
const run = () => {
if (allUris.length) {
const uris = allUris.splice(0, 100);
const processor = store.getState().ui.processes[action.type];
if (processor && processor.status === 'cancelling') {
store.dispatch(uiActions.processCancelled(action.type));
store.dispatch(uiActions.stopLoading('mopidy:library:tracks'));
return;
}
store.dispatch(uiActions.updateProcess(action.type, { remaining: allUris.length }));
request(store, 'library.lookup', { uris })
.then(
(lookupResponse) => {
const libraryItems = compact(
indexToArray(lookupResponse).map(
(results) => (results.length ? formatTrack(results[0]) : null),
),
);
if (libraryItems.length) {
store.dispatch(coreActions.itemsLoaded(libraryItems));
}
run();
},
);
} else {
store.dispatch(uiActions.processFinished(action.type));
store.dispatch(coreActions.libraryLoaded({
uri: 'mopidy:library:tracks',
items_uris: arrayOf('uri', browseResponse),
}));
}
};
run();
});
break;
default: default:
return next(action); next(action);
break;
} }
}; };
}()); }());

View File

@ -1,6 +1,7 @@
import ReactGA from 'react-ga'; import ReactGA from 'react-ga';
import { uriType, generateGuid } from '../../util/helpers'; import { uriType, generateGuid } from '../../util/helpers';
import { trackEvent } from '../../components/Trackable';
const coreActions = require('../core/actions'); const coreActions = require('../core/actions');
const uiActions = require('../ui/actions'); const uiActions = require('../ui/actions');
@ -532,15 +533,14 @@ const PusherMiddleware = (function () {
/** /**
* Commands * Commands
**/ * */
case 'PUSHER_GET_COMMANDS': {
case 'PUSHER_GET_COMMANDS':
request(store, 'get_commands') request(store, 'get_commands')
.then( .then(
(response) => { (response) => {
store.dispatch(pusherActions.commandsUpdated(response.commands)); store.dispatch(pusherActions.commandsUpdated(response.commands));
}, },
(error) => { () => {
// We're not too worried about capturing errors here // We're not too worried about capturing errors here
// It's also likely to fail where UI has been updated but // It's also likely to fail where UI has been updated but
// server hasn't been restarted yet. // server hasn't been restarted yet.
@ -549,24 +549,25 @@ const PusherMiddleware = (function () {
); );
next(action); next(action);
break; break;
}
case 'PUSHER_SET_COMMAND': case 'PUSHER_SET_COMMAND': {
var commands = { ...pusher.commands }; const commands = {
...pusher.commands,
if (commands[action.command.id]) { [action.command.id]: {
var command = { ...commands[action.command.id], ...action.command }; ...(pusher.commands[action.command.id] || {}),
} else { ...action.command,
var { command } = action; },
} };
commands[action.command.id] = command;
store.dispatch(pusherActions.setCommands(commands)); store.dispatch(pusherActions.setCommands(commands));
break; break;
}
case 'PUSHER_SET_COMMANDS': case 'PUSHER_SET_COMMANDS': {
request(store, 'set_commands', { commands: action.commands }) request(store, 'set_commands', { commands: action.commands })
.then( .then(
(response) => { () => {
// No action required, the change will be broadcast // No action required, the change will be broadcast
}, },
(error) => { (error) => {
@ -579,14 +580,15 @@ const PusherMiddleware = (function () {
next(action); next(action);
break; break;
}
case 'PUSHER_REMOVE_COMMAND': case 'PUSHER_REMOVE_COMMAND': {
var commands_index = { ...pusher.commands }; const commands_index = { ...pusher.commands };
delete commands_index[action.id]; delete commands_index[action.id];
request(store, 'set_commands', { commands: commands_index }) request(store, 'set_commands', { commands: commands_index })
.then( .then(
(response) => { () => {
// No action required, the change will be broadcast // No action required, the change will be broadcast
}, },
(error) => { (error) => {
@ -599,32 +601,34 @@ const PusherMiddleware = (function () {
next(action); next(action);
break; break;
}
case 'PUSHER_RUN_COMMAND': case 'PUSHER_RUN_COMMAND': {
var command = { ...pusher.commands[action.id] }; const notification_key = `command_${action.id}`;
var notification_key = `command_${action.id}`;
if (action.notify) { if (action.notify) {
store.dispatch(uiActions.startProcess(notification_key, 'Running command')); store.dispatch(uiActions.startProcess(notification_key, 'Running command'));
} }
request(store, 'run_command', { id: action.id }) request(store, 'run_command', { id: action.id })
.then((response) => { .then(() => {
console.log('Command response', response);
store.dispatch(uiActions.removeProcess(notification_key)); store.dispatch(uiActions.removeProcess(notification_key));
if (action.notify) { if (action.notify) {
store.dispatch(uiActions.createNotification({ key: notification_key, level: 'warning', content: 'Command sent' })); store.dispatch(uiActions.createNotification({
key: notification_key,
content: 'Command sent',
}));
} }
}, },
(error) => { (error) => {
store.dispatch(uiActions.removeProcess(notification_key)); store.dispatch(uiActions.removeProcess(notification_key));
store.dispatch(coreActions.handleException( store.dispatch(coreActions.handleException(
'Could not run command', 'Could not run command',
error, error,
)); ));
}); });
break; break;
}
/** /**
@ -654,26 +658,26 @@ const PusherMiddleware = (function () {
break; break;
case 'PUSHER_START_RADIO': case 'PUSHER_START_RADIO':
case 'PUSHER_UPDATE_RADIO': case 'PUSHER_UPDATE_RADIO': {
if (store.getState().ui.allow_reporting) { trackEvent({ category: 'Pusher', action: 'Start radio', label: action.uris.join() });
ReactGA.event({ category: 'Pusher', action: 'Start radio', label: action.uris.join() });
}
// start our UI process notification store.dispatch(
if (action.type == 'PUSHER_UPDATE_RADIO') { uiActions.startProcess(
store.dispatch(uiActions.startProcess('PUSHER_RADIO_PROCESS', 'Updating radio')); action.type,
} else { {
store.dispatch(uiActions.startProcess('PUSHER_RADIO_PROCESS', 'Starting radio')); content: `${action.type === 'PUSHER_UPDATE_RADIO' ? 'Updating' : 'Starting'} radio`,
} },
),
);
var data = { const data = {
reset: (action.type == 'PUSHER_START_RADIO'), reset: (action.type === 'PUSHER_START_RADIO'),
seed_artists: [], seed_artists: [],
seed_genres: [], seed_genres: [],
seed_tracks: [], seed_tracks: [],
}; };
for (let i = 0; i < action.uris.length; i++) { for (let i = 0; i < action.uris.length; i += 1) {
switch (uriType(action.uris[i])) { switch (uriType(action.uris[i])) {
case 'artist': case 'artist':
data.seed_artists.push(action.uris[i]); data.seed_artists.push(action.uris[i]);
@ -684,15 +688,16 @@ const PusherMiddleware = (function () {
case 'genre': case 'genre':
data.seed_genres.push(action.uris[i]); data.seed_genres.push(action.uris[i]);
break; break;
default:
break;
} }
} }
if (action.type == 'PUSHER_START_RADIO') { if (action.type === 'PUSHER_START_RADIO') {
store.dispatch(pusherActions.deliverBroadcast( store.dispatch(pusherActions.deliverBroadcast(
'notification', 'notification',
{ {
notification: { notification: {
level: 'warning',
content: `${pusher.username} is starting radio mode`, content: `${pusher.username} is starting radio mode`,
}, },
}, },
@ -702,14 +707,14 @@ const PusherMiddleware = (function () {
request(store, 'change_radio', data) request(store, 'change_radio', data)
.then( .then(
(response) => { (response) => {
store.dispatch(uiActions.processFinished('PUSHER_RADIO_PROCESS')); store.dispatch(uiActions.processFinished(action.type));
if (response.status == 0) { if (response.status === 0) {
store.dispatch(uiActions.createNotification({ content: response.message, level: 'error' })); store.dispatch(uiActions.createNotification({ content: response.message, level: 'error' }));
} }
store.dispatch(pusherActions.radioChanged(response.radio)); store.dispatch(pusherActions.radioChanged(response.radio));
}, },
(error) => { (error) => {
store.dispatch(uiActions.processFinished('PUSHER_RADIO_PROCESS')); store.dispatch(uiActions.processFinished(action.type));
store.dispatch(coreActions.handleException( store.dispatch(coreActions.handleException(
'Could not change radio', 'Could not change radio',
error, error,
@ -717,25 +722,22 @@ const PusherMiddleware = (function () {
}, },
); );
break; break;
}
case 'PUSHER_STOP_RADIO': case 'PUSHER_STOP_RADIO': {
store.dispatch(uiActions.createNotification({ content: 'Stopping radio' })); store.dispatch(uiActions.createNotification({ content: 'Stopping radio' }));
trackEvent({ category: 'Pusher', action: 'Stop radio' });
if (store.getState().ui.allow_reporting) {
ReactGA.event({ category: 'Pusher', action: 'Stop radio' });
}
store.dispatch(pusherActions.deliverBroadcast( store.dispatch(pusherActions.deliverBroadcast(
'notification', 'notification',
{ {
notification: { notification: {
level: 'warning',
content: `${pusher.username} stopped radio mode`, content: `${pusher.username} stopped radio mode`,
}, },
}, },
)); ));
var data = { const data = {
seed_artists: [], seed_artists: [],
seed_genres: [], seed_genres: [],
seed_tracks: [], seed_tracks: [],
@ -753,29 +755,18 @@ const PusherMiddleware = (function () {
}, },
); );
break; break;
}
/**
* Notifications and alerts
* */
case 'PUSHER_BROWSER_NOTIFICATION': case 'PUSHER_BROWSER_NOTIFICATION':
store.dispatch(uiActions.createBrowserNotification(action)); store.dispatch(uiActions.createBrowserNotification(action));
break; break;
case 'PUSHER_NOTIFICATION': case 'PUSHER_NOTIFICATION':
var data = { store.dispatch(uiActions.createNotification({
...action, type: action.notification_type, ...action, type: action.notification_type,
}; }));
store.dispatch(uiActions.createNotification(data));
break; break;
/**
* Server actions
* */
case 'PUSHER_RELOAD': case 'PUSHER_RELOAD':
// Hard reload. This doesn't strictly clear the cache, but our compiler's // Hard reload. This doesn't strictly clear the cache, but our compiler's
// cache buster should handle that // cache buster should handle that
@ -812,12 +803,11 @@ const PusherMiddleware = (function () {
next(action); next(action);
break; break;
case 'PUSHER_CONFIG': case 'PUSHER_CONFIG': {
// Set default country/locale (unless we've already been configured) // Set default country/locale (unless we've already been configured)
var { spotify } = store.getState(); const { spotify } = store.getState();
var spotify_updated = false; const spotify_updates = {};
var spotify_updates = {}; let spotify_updated = false;
if (!spotify.country && action.config.country) { if (!spotify.country && action.config.country) {
spotify_updates.country = action.config.country; spotify_updates.country = action.config.country;
@ -848,6 +838,7 @@ const PusherMiddleware = (function () {
next(action); next(action);
break; break;
}
case 'PUSHER_DEBUG': case 'PUSHER_DEBUG':
request(store, action.message.method, action.message.data) request(store, action.message.method, action.message.data)
@ -867,13 +858,9 @@ const PusherMiddleware = (function () {
case 'PUSHER_ERROR': case 'PUSHER_ERROR':
store.dispatch(uiActions.createNotification(action.message, 'bad')); store.dispatch(uiActions.createNotification(action.message, 'bad'));
if (store.getState().ui.allow_reporting) { trackEvent({ category: 'Pusher', action: 'Error', label: action.message });
ReactGA.event({ category: 'Pusher', action: 'Error', label: action.message });
}
break; break;
// This action is irrelevant to us, pass it on to the next middleware
default: default:
return next(action); return next(action);
} }

View File

@ -342,7 +342,7 @@ export function getTrack(uri) {
); );
}; };
} }
/*
export function getLibraryTracks() { export function getLibraryTracks() {
return (dispatch, getState) => { return (dispatch, getState) => {
request(dispatch, getState, 'me/tracks?limit=50') request(dispatch, getState, 'me/tracks?limit=50')
@ -362,6 +362,7 @@ export function getLibraryTracks() {
); );
}; };
} }
*/
export function getFeaturedPlaylists(forceRefetch = false) { export function getFeaturedPlaylists(forceRefetch = false) {
return (dispatch, getState) => { return (dispatch, getState) => {
@ -564,8 +565,9 @@ export function getMore(url, core_action = null, custom_action = null, extra_dat
} }
export function getSearchResults({ type, term }, limit = 50, offset = 0) { export function getSearchResults({ type, term }, limit = 50, offset = 0) {
const processKey = 'SPOTIFY_GET_SEARCH_RESULTS';
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(uiActions.startProcess('SPOTIFY_GET_SEARCH_RESULTS_PROCESSOR', 'Searching Spotify')); dispatch(uiActions.startProcess(processKey, 'Searching Spotify'));
let typeString = type.replace(/s+$/, ''); let typeString = type.replace(/s+$/, '');
if (typeString === 'all') { if (typeString === 'all') {
@ -617,7 +619,7 @@ export function getSearchResults({ type, term }, limit = 50, offset = 0) {
)); ));
} }
dispatch(uiActions.processFinished('SPOTIFY_GET_SEARCH_RESULTS_PROCESSOR')); dispatch(uiActions.processFinished(processKey));
}, },
(error) => { (error) => {
dispatch(coreActions.handleException( dispatch(coreActions.handleException(
@ -1320,7 +1322,7 @@ export function savePlaylist(uri, name, description, is_public, is_collaborative
}; };
} }
export function getPlaylist(uri, { full, forceRefetch, callbackAction }) { export function getPlaylist(uri, { full, forceRefetch, callbackAction } = {}) {
return (dispatch, getState) => { return (dispatch, getState) => {
let endpoint = `playlists/${getFromUri('playlistid', uri)}`; let endpoint = `playlists/${getFromUri('playlistid', uri)}`;
endpoint += `?market=${getState().spotify.country}`; endpoint += `?market=${getState().spotify.country}`;
@ -1566,24 +1568,18 @@ export function reorderPlaylistTracks(uri, range_start, range_length, insert_bef
* ====================================================================================== * ======================================================================================
* */ * */
export function flushLibrary() {
return {
type: 'SPOTIFY_FLUSH_LIBRARY',
};
}
export function getLibraryPlaylists(forceRefetch) { export function getLibraryPlaylists(forceRefetch) {
return (dispatch, getState) => { return (dispatch, getState) => {
const processKey = 'SPOTIFY_GET_LIBRARY_PLAYLISTS'; const processKey = 'SPOTIFY_GET_LIBRARY_PLAYLISTS';
dispatch(uiActions.updateProcess(processKey, { notification: false })); dispatch(uiActions.startProcess(processKey, { notification: false }));
let libraryItems = []; let libraryItems = [];
const fetchLibraryPlaylists = (endpoint) => request(dispatch, getState, endpoint) const fetch = (endpoint) => request(dispatch, getState, endpoint)
.then((response) => { .then((response) => {
const processor = getState().ui.processes[processKey]; const processor = getState().ui.processes[processKey];
if (processor && processor.status === 'cancelling') { if (processor && processor.status === 'cancelling') {
dispatch(uiActions.stopLoading('spotify:library:playlists'));
dispatch(uiActions.processCancelled(processKey)); dispatch(uiActions.processCancelled(processKey));
dispatch(uiActions.stopLoading('spotify:library:albums'));
return; return;
} }
@ -1601,7 +1597,7 @@ export function getLibraryPlaylists(forceRefetch) {
); );
libraryItems = [...libraryItems, ...items]; libraryItems = [...libraryItems, ...items];
if (response.next) { if (response.next) {
fetchLibraryPlaylists(`${response.next}${forceRefetch ? `&refetch=${Date.now()}` : ''}`); fetch(`${response.next}${forceRefetch ? `&refetch=${Date.now()}` : ''}`);
} else { } else {
dispatch(uiActions.processFinished(processKey)); dispatch(uiActions.processFinished(processKey));
dispatch(coreActions.itemsLoaded(libraryItems)); dispatch(coreActions.itemsLoaded(libraryItems));
@ -1612,17 +1608,17 @@ export function getLibraryPlaylists(forceRefetch) {
} }
}); });
fetchLibraryPlaylists(`me/playlists?limit=50${forceRefetch ? `&refetch=${Date.now()}` : ''}`); fetch(`me/playlists?limit=50${forceRefetch ? `&refetch=${Date.now()}` : ''}`);
}; };
} }
export function getLibraryAlbums(forceRefetch) { export function getLibraryAlbums(forceRefetch) {
return (dispatch, getState) => { return (dispatch, getState) => {
const processKey = 'SPOTIFY_GET_LIBRARY_ALBUMS'; const processKey = 'SPOTIFY_GET_LIBRARY_ALBUMS';
dispatch(uiActions.updateProcess(processKey, { notification: false })); dispatch(uiActions.startProcess(processKey, { notification: false }));
let libraryItems = []; let libraryItems = [];
const fetchLibraryAlbums = (endpoint) => request(dispatch, getState, endpoint) const fetch = (endpoint) => request(dispatch, getState, endpoint)
.then((response) => { .then((response) => {
const processor = getState().ui.processes[processKey]; const processor = getState().ui.processes[processKey];
if (processor && processor.status === 'cancelling') { if (processor && processor.status === 'cancelling') {
@ -1645,7 +1641,7 @@ export function getLibraryAlbums(forceRefetch) {
libraryItems = [...libraryItems, ...items]; libraryItems = [...libraryItems, ...items];
if (response.next) { if (response.next) {
fetchLibraryAlbums(`${response.next}${forceRefetch ? `&refetch=${Date.now()}` : ''}`); fetch(`${response.next}${forceRefetch ? `&refetch=${Date.now()}` : ''}`);
} else { } else {
dispatch(uiActions.processFinished(processKey)); dispatch(uiActions.processFinished(processKey));
dispatch(coreActions.itemsLoaded(libraryItems)); dispatch(coreActions.itemsLoaded(libraryItems));
@ -1656,17 +1652,17 @@ export function getLibraryAlbums(forceRefetch) {
} }
}); });
fetchLibraryAlbums(`me/albums?limit=50${forceRefetch ? `&refetch=${Date.now()}` : ''}`); fetch(`me/albums?limit=50${forceRefetch ? `&refetch=${Date.now()}` : ''}`);
}; };
} }
export function getLibraryArtists(forceRefetch) { export function getLibraryArtists(forceRefetch) {
return (dispatch, getState) => { return (dispatch, getState) => {
const processKey = 'SPOTIFY_GET_LIBRARY_ARTISTS'; const processKey = 'SPOTIFY_GET_LIBRARY_ARTISTS';
dispatch(uiActions.updateProcess(processKey, { notification: false })); dispatch(uiActions.startProcess(processKey, { notification: false }));
let libraryItems = []; let libraryItems = [];
const fetchLibraryArtists = (endpoint) => request(dispatch, getState, endpoint) const fetch = (endpoint) => request(dispatch, getState, endpoint)
.then((response) => { .then((response) => {
const processor = getState().ui.processes[processKey]; const processor = getState().ui.processes[processKey];
if (processor && processor.status === 'cancelling') { if (processor && processor.status === 'cancelling') {
@ -1677,7 +1673,10 @@ export function getLibraryArtists(forceRefetch) {
dispatch(uiActions.updateProcess( dispatch(uiActions.updateProcess(
processKey, processKey,
{ total: response.artists.total, remaining: response.artists.total - libraryItems.length }, {
total: response.artists.total,
remaining: response.artists.total - libraryItems.length,
},
)); ));
const items = response.artists.items.map( const items = response.artists.items.map(
@ -1688,7 +1687,7 @@ export function getLibraryArtists(forceRefetch) {
); );
libraryItems = [...libraryItems, ...items]; libraryItems = [...libraryItems, ...items];
if (response.next) { if (response.next) {
fetchLibraryArtists(`${response.next}${forceRefetch ? `&refetch=${Date.now()}` : ''}`); fetch(`${response.next}${forceRefetch ? `&refetch=${Date.now()}` : ''}`);
} else { } else {
dispatch(uiActions.processFinished(processKey)); dispatch(uiActions.processFinished(processKey));
dispatch(coreActions.itemsLoaded(libraryItems)); dispatch(coreActions.itemsLoaded(libraryItems));
@ -1699,6 +1698,49 @@ export function getLibraryArtists(forceRefetch) {
} }
}); });
fetchLibraryArtists(`me/following?type=artist&limit=50${forceRefetch ? `&refetch=${Date.now()}` : ''}`); fetch(`me/following?type=artist&limit=50${forceRefetch ? `&refetch=${Date.now()}` : ''}`);
};
}
export function getLibraryTracks(forceRefetch) {
return (dispatch, getState) => {
const processKey = 'SPOTIFY_GET_LIBRARY_TRACKS';
dispatch(uiActions.startProcess(processKey, { notification: false }));
let libraryItems = [];
const fetch = (endpoint) => request(dispatch, getState, endpoint)
.then((response) => {
const processor = getState().ui.processes[processKey];
if (processor && processor.status === 'cancelling') {
dispatch(uiActions.processCancelled(processKey));
dispatch(uiActions.stopLoading('spotify:library:tracks'));
return;
}
dispatch(uiActions.updateProcess(
processKey,
{ total: response.total, remaining: response.total - libraryItems.length },
));
const items = response.items.map(
(item) => ({
...formatTrack(item),
in_library: true,
}),
);
libraryItems = [...libraryItems, ...items];
if (response.next) {
fetch(`${response.next}${forceRefetch ? `&refetch=${Date.now()}` : ''}`);
} else {
dispatch(uiActions.processFinished(processKey));
dispatch(coreActions.itemsLoaded(libraryItems));
dispatch(coreActions.libraryLoaded({
uri: 'spotify:library:tracks',
items_uris: arrayOf('uri', libraryItems),
}));
}
});
fetch(`me/tracks?limit=50${forceRefetch ? `&refetch=${Date.now()}` : ''}`);
}; };
} }

View File

@ -298,10 +298,10 @@ export function runProcess(key, data = {}) {
}; };
} }
export function cancelProcess(key) { export function cancelProcess(keys) {
return { return {
type: 'CANCEL_PROCESS', type: 'CANCEL_PROCESS',
key, keys: Array.isArray(keys) ? keys : [keys],
}; };
} }

View File

@ -148,9 +148,16 @@ export default function reducer(ui = {}, action) {
case 'CANCEL_PROCESS': { case 'CANCEL_PROCESS': {
const processes = { ...(ui.processes ? ui.processes : {}) }; const processes = { ...(ui.processes ? ui.processes : {}) };
if (processes[action.key]) { action.keys.forEach((key) => {
processes[action.key] = { ...processes[action.key], status: 'cancelling' }; if (processes[key]) {
} processes[key] = {
...processes[key],
status: 'cancelling',
total: undefined,
remaining: undefined,
};
}
});
return { ...ui, processes }; return { ...ui, processes };
} }

View File

@ -56,15 +56,17 @@ const makeSearchResultsSelector = (term, type) => createSelector(
const makeProcessProgressSelector = (keys) => createSelector( const makeProcessProgressSelector = (keys) => createSelector(
[getProcesses], [getProcesses],
(processes) => { (processes) => {
const selectedProcesses = keys.map((key) => processes[key] || {}); const selectedProcesses = keys
.map((key) => processes[key] || {})
.filter((i) => i.status === 'running');
let total = 0; let total = 0;
let remaining = 0; let remaining = 0;
selectedProcesses.forEach((process) => { selectedProcesses.forEach((process) => {
if (process.total) total += process.total; if (process.total) total += process.total;
if (process.remaining) remaining += process.remaining; if (process.remaining) remaining += process.remaining;
}); });
console.log({ total, remaining }) return total && remaining ? ((total - remaining) / total).toFixed(4) : 1;
return total && remaining ? ((total - remaining) / total).toFixed(4) : 0;
}, },
); );

View File

@ -20,10 +20,15 @@ import { i18n, I18n } from '../../locale';
import Loader from '../../components/Loader'; import Loader from '../../components/Loader';
import { import {
makeLibrarySelector, makeLibrarySelector,
makeLoadingSelector,
makeProcessProgressSelector, makeProcessProgressSelector,
} from '../../util/selectors'; } from '../../util/selectors';
const processKeys = [
'MOPIDY_GET_LIBRARY_ALBUMS',
'SPOTIFY_GET_LIBRARY_ALBUMS',
'GOOGLE_GET_LIBRARY_ALBUMS',
];
class LibraryAlbums extends React.Component { class LibraryAlbums extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -68,7 +73,7 @@ class LibraryAlbums extends React.Component {
} }
} }
onRefresh = () => { refresh = () => {
const { uiActions: { hideContextMenu } } = this.props; const { uiActions: { hideContextMenu } } = this.props;
hideContextMenu(); hideContextMenu();
@ -77,6 +82,13 @@ class LibraryAlbums extends React.Component {
this.getSpotifyLibrary(true); this.getSpotifyLibrary(true);
} }
cancelRefresh = () => {
const { uiActions: { hideContextMenu, cancelProcess } } = this.props;
hideContextMenu();
cancelProcess(processKeys);
}
getMopidyLibrary = (forceRefetch = false) => { getMopidyLibrary = (forceRefetch = false) => {
const { const {
source, source,
@ -175,7 +187,6 @@ class LibraryAlbums extends React.Component {
sort, sort,
sort_reverse, sort_reverse,
view, view,
loading,
loading_progress, loading_progress,
} = this.props; } = this.props;
const { const {
@ -184,9 +195,8 @@ class LibraryAlbums extends React.Component {
} = this.state; } = this.state;
let { albums } = this.props; let { albums } = this.props;
if (loading) { if (loading_progress < 1) {
console.log(loading_progress); return <Loader body loading progress={loading_progress} />;
return <Loader body loading progress={loading_progress} />
} }
if (sort) { if (sort) {
@ -245,11 +255,13 @@ class LibraryAlbums extends React.Component {
source, source,
sort_reverse, sort_reverse,
uiActions, uiActions,
loading_progress,
} = this.props; } = this.props;
const { const {
filter, filter,
per_page, per_page,
} = this.state; } = this.state;
const loading = loading_progress < 1;
const source_options = [ const source_options = [
{ {
@ -349,11 +361,11 @@ class LibraryAlbums extends React.Component {
<Button <Button
noHover noHover
discrete discrete
onClick={this.onRefresh} onClick={loading ? this.cancelRefresh : this.refresh}
tracking={{ category: 'LibraryAlbums', action: 'Refresh' }} tracking={{ category: 'LibraryAlbums', action: 'Refresh' }}
> >
<Icon name="refresh" /> {loading ? <Icon name="close" /> : <Icon name="refresh" /> }
<I18n path="actions.refresh" /> {loading ? <I18n path="actions.cancel" /> : <I18n path="actions.refresh" /> }
</Button> </Button>
</div> </div>
); );
@ -372,21 +384,15 @@ class LibraryAlbums extends React.Component {
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
const source = state.ui.library_albums_source ? state.ui.library_albums_source : 'all'; const source = state.ui.library_albums_source ? state.ui.library_albums_source : 'all';
const loadingSelector = makeLoadingSelector(['(.*):library:albums']);
const libraryUris = []; const libraryUris = [];
if (source === 'all' || source === 'local') libraryUris.push('mopidy:library:albums'); if (source === 'all' || source === 'local') libraryUris.push('mopidy:library:albums');
if (source === 'all' || source === 'spotify') libraryUris.push('spotify:library:albums'); if (source === 'all' || source === 'spotify') libraryUris.push('spotify:library:albums');
if (source === 'all' || source === 'google') libraryUris.push('google:library:albums'); if (source === 'all' || source === 'google') libraryUris.push('google:library:albums');
const librarySelector = makeLibrarySelector(libraryUris); const librarySelector = makeLibrarySelector(libraryUris);
const processProgressSelector = makeProcessProgressSelector([ const processProgressSelector = makeProcessProgressSelector(processKeys);
'MOPIDY_GET_LIBRARY_ALBUMS',
'SPOTIFY_GET_LIBRARY_ALBUMS',
'GOOGLE_GET_LIBRARY_ALBUMS',
]);
return { return {
loading: loadingSelector(state),
loading_progress: processProgressSelector(state), loading_progress: processProgressSelector(state),
mopidy_uri_schemes: state.mopidy.uri_schemes, mopidy_uri_schemes: state.mopidy.uri_schemes,
albums: librarySelector(state), albums: librarySelector(state),

View File

@ -21,6 +21,12 @@ import {
makeProcessProgressSelector, makeProcessProgressSelector,
} from '../../util/selectors'; } from '../../util/selectors';
const processKeys = [
'MOPIDY_GET_LIBRARY_ARTISTS',
'SPOTIFY_GET_LIBRARY_ARTISTS',
'GOOGLE_GET_LIBRARY_ARTISTS',
];
class LibraryArtists extends React.Component { class LibraryArtists extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -63,7 +69,7 @@ class LibraryArtists extends React.Component {
} }
} }
onRefresh = () => { refresh = () => {
const { uiActions: { hideContextMenu } } = this.props; const { uiActions: { hideContextMenu } } = this.props;
hideContextMenu(); hideContextMenu();
@ -72,6 +78,13 @@ class LibraryArtists extends React.Component {
this.getSpotifyLibrary(true); this.getSpotifyLibrary(true);
} }
cancelRefresh = () => {
const { uiActions: { hideContextMenu, cancelProcess } } = this.props;
hideContextMenu();
cancelProcess(processKeys);
}
setSort = (value) => { setSort = (value) => {
const { sort, sort_reverse, uiActions: { set } } = this.props; const { sort, sort_reverse, uiActions: { set } } = this.props;
let reverse = false; let reverse = false;
@ -151,7 +164,6 @@ class LibraryArtists extends React.Component {
sort, sort,
sort_reverse, sort_reverse,
view, view,
loading,
loading_progress, loading_progress,
} = this.props; } = this.props;
const { const {
@ -160,7 +172,7 @@ class LibraryArtists extends React.Component {
} = this.state; } = this.state;
let { artists } = this.props; let { artists } = this.props;
if (loading) { if (loading_progress < 1) {
return <Loader body loading progress={loading_progress} /> return <Loader body loading progress={loading_progress} />
} }
@ -215,7 +227,9 @@ class LibraryArtists extends React.Component {
const { const {
spotify_available, spotify_available,
google_available, google_available,
loading_progress,
} = this.props; } = this.props;
const loading = loading_progress < 1;
const source_options = [ const source_options = [
{ {
@ -307,11 +321,11 @@ class LibraryArtists extends React.Component {
<Button <Button
noHover noHover
discrete discrete
onClick={this.onRefresh} onClick={loading ? this.cancelRefresh : this.refresh}
tracking={{ category: 'LibraryArtists', action: 'Refresh' }} tracking={{ category: 'LibraryArtists', action: 'Refresh' }}
> >
<Icon name="refresh" /> {loading ? <Icon name="close" /> : <Icon name="refresh" /> }
<I18n path="actions.refresh" /> {loading ? <I18n path="actions.cancel" /> : <I18n path="actions.refresh" /> }
</Button> </Button>
</span> </span>
); );
@ -330,18 +344,13 @@ class LibraryArtists extends React.Component {
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
const source = state.ui.library_artists_source || 'all'; const source = state.ui.library_artists_source || 'all';
const loadingSelector = makeLoadingSelector(['(.*):library:artists']);
const libraryUris = []; const libraryUris = [];
if (source === 'all' || source === 'local') libraryUris.push('mopidy:library:artists'); if (source === 'all' || source === 'local') libraryUris.push('mopidy:library:artists');
if (source === 'all' || source === 'spotify') libraryUris.push('spotify:library:artists'); if (source === 'all' || source === 'spotify') libraryUris.push('spotify:library:artists');
if (source === 'all' || source === 'google') libraryUris.push('google:library:artists'); if (source === 'all' || source === 'google') libraryUris.push('google:library:artists');
const librarySelector = makeLibrarySelector(libraryUris); const librarySelector = makeLibrarySelector(libraryUris);
const processProgressSelector = makeProcessProgressSelector([ const processProgressSelector = makeProcessProgressSelector(processKeys);
'MOPIDY_GET_LIBRARY_ARTISTS',
'SPOTIFY_GET_LIBRARY_ARTISTS',
'GOOGLE_GET_LIBRARY_ARTISTS',
]);
return { return {
loading_progress: processProgressSelector(state), loading_progress: processProgressSelector(state),
@ -349,7 +358,6 @@ const mapStateToProps = (state) => {
google_available: (state.mopidy.uri_schemes && state.mopidy.uri_schemes.includes('gmusic:')), google_available: (state.mopidy.uri_schemes && state.mopidy.uri_schemes.includes('gmusic:')),
spotify_available: (state.spotify.access_token), spotify_available: (state.spotify.access_token),
artists: librarySelector(state), artists: librarySelector(state),
loading: loadingSelector(state),
source, source,
sort: (state.ui.library_artists_sort ? state.ui.library_artists_sort : null), sort: (state.ui.library_artists_sort ? state.ui.library_artists_sort : null),
sort_reverse: (state.ui.library_artists_sort_reverse ? state.ui.library_artists_sort_reverse : false), sort_reverse: (state.ui.library_artists_sort_reverse ? state.ui.library_artists_sort_reverse : false),

View File

@ -16,10 +16,17 @@ import * as mopidyActions from '../../services/mopidy/actions';
import * as spotifyActions from '../../services/spotify/actions'; import * as spotifyActions from '../../services/spotify/actions';
import { applyFilter, removeDuplicates, sortItems } from '../../util/arrays'; import { applyFilter, removeDuplicates, sortItems } from '../../util/arrays';
import { I18n, i18n } from '../../locale'; import { I18n, i18n } from '../../locale';
import { collate } from '../../util/format';
import { isLoading } from '../../util/helpers';
import Loader from '../../components/Loader'; import Loader from '../../components/Loader';
import { makeLibrarySelector, makeLoadingSelector } from '../../util/selectors'; import {
makeLibrarySelector,
makeLoadingSelector,
makeProcessProgressSelector,
} from '../../util/selectors';
const processKeys = [
'MOPIDY_GET_LIBRARY_PLAYLISTS',
'SPOTIFY_GET_LIBRARY_PLAYLISTS',
];
class LibraryPlaylists extends React.Component { class LibraryPlaylists extends React.Component {
constructor(props) { constructor(props) {
@ -55,7 +62,7 @@ class LibraryPlaylists extends React.Component {
this.getSpotifyLibrary(); this.getSpotifyLibrary();
} }
onRefresh = () => { refresh = () => {
const { uiActions: { hideContextMenu } } = this.props; const { uiActions: { hideContextMenu } } = this.props;
hideContextMenu(); hideContextMenu();
@ -63,6 +70,13 @@ class LibraryPlaylists extends React.Component {
this.getSpotifyLibrary(true); this.getSpotifyLibrary(true);
} }
cancelRefresh = () => {
const { uiActions: { hideContextMenu, cancelProcess } } = this.props;
hideContextMenu();
cancelProcess(processKeys);
}
getMopidyLibrary = (forceRefetch = false) => { getMopidyLibrary = (forceRefetch = false) => {
const { const {
source, source,
@ -137,7 +151,7 @@ class LibraryPlaylists extends React.Component {
sort, sort,
sort_reverse, sort_reverse,
view, view,
loading, loading_progress,
playlists: playlistsProp, playlists: playlistsProp,
} = this.props; } = this.props;
const { const {
@ -145,8 +159,8 @@ class LibraryPlaylists extends React.Component {
limit, limit,
} = this.state; } = this.state;
if (loading) { if (loading_progress < 1) {
return <Loader body loading />; return <Loader body loading progress={loading_progress} />;
} }
let playlists = [...playlistsProp]; let playlists = [...playlistsProp];
@ -206,10 +220,12 @@ class LibraryPlaylists extends React.Component {
sort, sort,
sort_reverse, sort_reverse,
view, view,
loading_progress,
} = this.props; } = this.props;
const { const {
filter, filter,
} = this.state; } = this.state;
const loading = loading_progress < 1;
const source_options = [ const source_options = [
{ {
@ -306,11 +322,11 @@ class LibraryPlaylists extends React.Component {
<Button <Button
noHover noHover
discrete discrete
onClick={this.onRefresh} onClick={loading ? this.cancelRefresh : this.refresh}
tracking={{ category: 'LibraryAlbums', action: 'Refresh' }} tracking={{ category: 'LibraryAlbums', action: 'Refresh' }}
> >
<Icon name="refresh" /> {loading ? <Icon name="close" /> : <Icon name="refresh" /> }
<I18n path="actions.refresh" /> {loading ? <I18n path="actions.cancel" /> : <I18n path="actions.refresh" /> }
</Button> </Button>
<Button <Button
to="/playlist/create" to="/playlist/create"
@ -338,20 +354,20 @@ class LibraryPlaylists extends React.Component {
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
const source = state.ui.library_playlists_source || 'all'; const source = state.ui.library_playlists_source || 'all';
const loadingSelector = makeLoadingSelector(['(.*):library:playlists']);
const libraryUris = []; const libraryUris = [];
if (source === 'all' || source === 'local') libraryUris.push('mopidy:library:playlists'); if (source === 'all' || source === 'local') libraryUris.push('mopidy:library:playlists');
if (source === 'all' || source === 'spotify') libraryUris.push('spotify:library:playlists'); if (source === 'all' || source === 'spotify') libraryUris.push('spotify:library:playlists');
if (source === 'all' || source === 'google') libraryUris.push('google:library:playlists'); if (source === 'all' || source === 'google') libraryUris.push('google:library:playlists');
const librarySelector = makeLibrarySelector(libraryUris); const librarySelector = makeLibrarySelector(libraryUris);
const processProgressSelector = makeProcessProgressSelector(processKeys);
return { return {
slim_mode: state.ui.slim_mode, slim_mode: state.ui.slim_mode,
mopidy_uri_schemes: state.mopidy.uri_schemes, mopidy_uri_schemes: state.mopidy.uri_schemes,
spotify_available: state.spotify.access_token, spotify_available: state.spotify.access_token,
playlists: librarySelector(state), playlists: librarySelector(state),
loading: loadingSelector(state), loading_progress: processProgressSelector(state),
source, source,
me_id: (state.spotify.me ? state.spotify.me.id : false), me_id: (state.spotify.me ? state.spotify.me.id : false),
view: state.ui.library_playlists_view, view: state.ui.library_playlists_view,

View File

@ -2,116 +2,336 @@
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 Loader from '../../components/Loader';
import TrackList from '../../components/TrackList'; import TrackList from '../../components/TrackList';
import Header from '../../components/Header'; import Header from '../../components/Header';
import DropdownField from '../../components/Fields/DropdownField';
import FilterField from '../../components/Fields/FilterField';
import LazyLoadListener from '../../components/LazyLoadListener'; import LazyLoadListener from '../../components/LazyLoadListener';
import Icon from '../../components/Icon'; import Icon from '../../components/Icon';
import * as coreActions from '../../services/core/actions';
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 { isLoading } from '../../util/helpers'; import { sortItems, applyFilter } from '../../util/arrays';
import { I18n, i18n } from '../../locale';
import Button from '../../components/Button'; import Button from '../../components/Button';
import { i18n, I18n } from '../../locale';
import Loader from '../../components/Loader';
import {
makeLibrarySelector,
makeProcessProgressSelector,
} from '../../util/selectors';
const processKeys = [
'MOPIDY_GET_LIBRARY_TRACKS',
'SPOTIFY_GET_LIBRARY_TRACKS',
];
class LibraryTracks extends React.Component { class LibraryTracks extends React.Component {
componentDidMount() { constructor(props) {
this.props.uiActions.setWindowTitle(i18n('library.tracks.title')); super(props);
if (!this.props.spotify_available) { this.state = {
this.props.uiActions.createNotification({ filter: '',
level: 'warning', limit: 50,
content: i18n('errors.enable_first', { provider: i18n('services.spotify.title') }), per_page: 50,
};
}
componentDidMount() {
const {
location: {
state = {},
},
uiActions: {
setWindowTitle,
},
} = this.props;
// Restore any limit defined in our location state
if (state.limit) {
this.setState({
limit: state.limit,
}); });
} else if (this.props.library_tracks === undefined) { }
this.props.spotifyActions.getLibraryTracks();
setWindowTitle(i18n('library.tracks.title'));
this.getMopidyLibrary();
this.getSpotifyLibrary();
}
componentDidUpdate = ({ source: prevSource }) => {
const { source } = this.props;
if (source !== prevSource) {
this.getMopidyLibrary();
this.getSpotifyLibrary();
} }
} }
loadMore() { refresh = () => {
this.props.spotifyActions.getMore( const { uiActions: { hideContextMenu } } = this.props;
this.props.library_tracks_more,
null, hideContextMenu();
{ this.getMopidyLibrary(true);
type: 'SPOTIFY_LIBRARY_TRACKS_LOADED_MORE', this.getSpotifyLibrary(true);
}
cancelRefresh = () => {
const { uiActions: { hideContextMenu, cancelProcess } } = this.props;
hideContextMenu();
cancelProcess(processKeys);
}
getMopidyLibrary = (forceRefetch = false) => {
const {
source,
coreActions: {
loadLibrary,
}, },
} = this.props;
if (source !== 'local' && source !== 'all') return;
loadLibrary('mopidy:library:tracks', { forceRefetch });
};
getSpotifyLibrary = (forceRefetch = false) => {
const {
source,
spotify_available,
coreActions: {
loadLibrary,
},
} = this.props;
if (!spotify_available) return;
if (source !== 'spotify' && source !== 'all') return;
loadLibrary('spotify:library:tracks', { forceRefetch });
};
handleContextMenu = (e, item) => {
const {
uiActions: {
showContextMenu,
},
} = this.props;
showContextMenu({
e,
context: 'album',
uris: [item.uri],
items: [item],
});
}
loadMore = () => {
const {
limit,
per_page,
} = this.state;
const {
location: {
state,
},
history,
} = this.props;
const new_limit = limit + per_page;
this.setState({ limit: new_limit });
history.replace({ state: { ...state, limit: new_limit } });
}
setSort = (value) => {
const {
sort,
sort_reverse,
uiActions: {
set,
},
} = this.props;
let reverse = false;
if (sort === value) reverse = !sort_reverse;
set({
library_tracks_sort_reverse: reverse,
library_tracks_sort: value,
});
}
renderView = () => {
const {
sort,
sort_reverse,
loading_progress,
} = this.props;
const {
limit,
filter,
} = this.state;
let { tracks } = this.props;
if (loading_progress < 1) {
return <Loader body loading progress={loading_progress} />;
}
if (sort) {
tracks = sortItems(tracks, sort, sort_reverse);
}
if (filter && filter !== '') {
tracks = applyFilter('name', filter, tracks);
}
// Apply our lazy-load-rendering
const total_tracks = tracks.length;
tracks = tracks.slice(0, limit);
return (
<section className="content-wrapper">
<TrackList tracks={tracks} />
<LazyLoadListener
loadKey={total_tracks > limit ? limit : total_tracks}
showLoader={limit < total_tracks}
loadMore={this.loadMore}
/>
</section>
); );
} }
playAll() { render = () => {
this.props.spotifyActions.getLibraryTracksAndPlay(); const {
} spotify_available,
sort,
source,
sort_reverse,
uiActions,
loading_progress,
} = this.props;
const {
filter,
per_page,
} = this.state;
const loading = loading_progress < 1;
render() { const source_options = [
// Note trailing "?" makes sure our context menu in_library checks doesn't interfere {
if (isLoading(this.props.load_queue, ['spotify_me/tracks?'])) { value: 'all',
return ( label: i18n('fields.filters.all'),
<div className="view library-tracks-view"> },
<Header icon="music" title={i18n('library.tracks.title')} /> {
<Loader body loading /> value: 'local',
</div> label: i18n('services.mopidy.local'),
); },
];
if (spotify_available) {
source_options.push({
value: 'spotify',
label: i18n('services.spotify.title'),
});
} }
const tracks = []; const sort_options = [
if (this.props.library_tracks && this.props.tracks) { {
for (let i = 0; i < this.props.library_tracks.length; i++) { value: null,
const uri = this.props.library_tracks[i]; label: i18n('fields.filters.as_loaded'),
if (this.props.tracks.hasOwnProperty(uri)) { },
tracks.push(this.props.tracks[uri]); {
} value: 'name',
} label: i18n('fields.filters.name'),
} },
{
value: 'artists.first.name',
label: i18n('fields.filters.artist'),
},
{
value: 'last_modified',
label: i18n('fields.filters.updated'),
},
{
value: 'tracks_uris.length',
label: i18n('fields.filters.tracks'),
},
{
value: 'uri',
label: i18n('fields.filters.source'),
},
];
const options = ( const options = (
<div className="header__options__wrapper"> <div className="header__options__wrapper">
<FilterField
initialValue={filter}
handleChange={(value) => this.setState({ filter: value, limit: per_page })}
onSubmit={() => uiActions.hideContextMenu()}
/>
<DropdownField
icon="swap_vert"
name={i18n('fields.sort')}
value={sort}
valueAsLabel
options={sort_options}
selected_icon={sort ? (sort_reverse ? 'keyboard_arrow_up' : 'keyboard_arrow_down') : null}
handleChange={(val) => { this.setSort(val); uiActions.hideContextMenu(); }}
/>
<DropdownField
icon="cloud"
name={i18n('fields.source')}
value={source}
valueAsLabel
options={source_options}
handleChange={(val) => { uiActions.set({ library_tracks_source: val }); uiActions.hideContextMenu(); }}
/>
<Button <Button
onClick={(e) => this.playAll(e)}
tracking={{ category: 'LibraryTracks', action: 'Play' }}
noHover noHover
discrete discrete
onClick={loading ? this.cancelRefresh : this.refresh}
tracking={{ category: 'LibraryTracks', action: 'Refresh' }}
> >
<Icon name="play_circle_filled" /> {loading ? <Icon name="close" /> : <Icon name="refresh" /> }
<I18n path="actions.play_all" /> {loading ? <I18n path="actions.cancel" /> : <I18n path="actions.refresh" /> }
</Button> </Button>
</div> </div>
); );
return ( return (
<div className="view library-tracks-view"> <div className="view library-tracks-view">
<Header options={options} uiActions={this.props.uiActions}> <Header options={options} uiActions={uiActions}>
<Icon name="music_note" type="material" /> <Icon name="album" type="material" />
<I18n path="library.tracks.title" /> <I18n path="library.tracks.title" />
</Header> </Header>
<section className="content-wrapper"> {this.renderView()}
<TrackList tracks={tracks} />
<LazyLoadListener
loadKey={this.props.library_tracks_more}
showLoader={this.props.library_tracks_more}
loadMore={() => this.loadMore()}
/>
</section>
</div> </div>
); );
} }
} }
const mapStateToProps = (state) => {
const source = state.ui.library_tracks_source ? state.ui.library_tracks_source : 'all';
/** const libraryUris = [];
* Export our component if (source === 'all' || source === 'local') libraryUris.push('mopidy:library:tracks');
* if (source === 'all' || source === 'spotify') libraryUris.push('spotify:library:tracks');
* We also integrate our global store, using connect() const librarySelector = makeLibrarySelector(libraryUris);
* */ const processProgressSelector = makeProcessProgressSelector(processKeys);
const mapStateToProps = (state, ownProps) => ({ return {
load_queue: state.ui.load_queue, loading_progress: processProgressSelector(state),
tracks: state.core.tracks, mopidy_uri_schemes: state.mopidy.uri_schemes,
spotify_available: state.spotify.access_token, tracks: librarySelector(state),
library_tracks: state.spotify.library_tracks, spotify_available: state.spotify.access_token,
library_tracks_more: state.spotify.library_tracks_more, view: state.ui.library_tracks_view,
}); source,
sort: state.ui.library_tracks_sort,
sort_reverse: state.ui.library_tracks_sort_reverse,
};
};
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
coreActions: bindActionCreators(coreActions, 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),