URI format for libraries, but own core.libraries for better control

This commit is contained in:
James Barnsley
2020-09-06 13:53:22 +12:00
parent f6f5e37aa9
commit 1bc7f2b372
21 changed files with 1231 additions and 1346 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
// These are automatically injected to built HTML
var build = "1599193622";
var build = "1599289916";
var version = "3.52.4";
// Construct the script tag

View File

@ -138,7 +138,6 @@ export class App extends React.Component {
}
componentDidMount() {
localForage.keys().then((r) => console.log(r))
const {
history,
snapcast_enabled,

View File

@ -29,6 +29,7 @@ import * as mopidyActions from '../services/mopidy/actions';
import * as lastfmActions from '../services/lastfm/actions';
import * as spotifyActions from '../services/spotify/actions';
import { I18n, i18n } from '../locale';
import { collate } from '../util/format';
class ContextMenu extends React.Component {
constructor(props) {
@ -181,11 +182,11 @@ class ContextMenu extends React.Component {
switch (uriType(uri)) {
case 'artist':
return (spotify_library_artists && spotify_library_artists.indexOf(uri) > -1);
return (spotify_library_artists.items_uris.indexOf(uri) > -1);
case 'album':
return (spotify_library_albums && spotify_library_albums.indexOf(uri) > -1);
return (spotify_library_albums.items_uris.indexOf(uri) > -1);
case 'playlist':
return (spotify_library_playlists && spotify_library_playlists.indexOf(uri) > -1);
return (spotify_library_playlists.items_uris.indexOf(uri) > -1);
case 'track':
return (spotify_library_tracks && spotify_library_tracks.indexOf(uri) > -1);
default:
@ -742,14 +743,19 @@ class ContextMenu extends React.Component {
const { submenu } = this.state;
const {
spotify_available,
spotify_library_playlists_loaded_all,
mopidy_library_playlists_loaded_all,
spotifyActions,
mopidyActions,
spotify_library_playlists: {
items_uris: spotify_library,
},
mopidy_library_playlists: {
items_uris: mopidy_library,
},
coreActions: {
loadLibrary,
},
} = this.props;
if (submenu !== name && name === 'add-to-playlist') {
if (spotify_available && !spotify_library_playlists_loaded_all) spotifyActions.getLibraryPlaylists();
if (!mopidy_library_playlists_loaded_all) mopidyActions.getLibraryPlaylists();
if (spotify_available && !spotify_library.length) loadLibrary('spotify:library:playlists');
if (!mopidy_library.length) loadLibrary('mopidy:library:playlists');
}
this.setState({ submenu: name });
@ -760,57 +766,57 @@ class ContextMenu extends React.Component {
renderSubmenu = () => {
const { submenu } = this.state;
const {
playlists: playlistsIndex,
processes,
items,
spotify_library_playlists,
mopidy_library_playlists,
} = this.props;
let list = null;
let isLoading = false;
if (submenu === 'add-to-playlist') {
if (processes.SPOTIFY_GET_LIBRARY_PLAYLISTS_PROCESSOR && processes.SPOTIFY_GET_LIBRARY_PLAYLISTS_PROCESSOR.status === 'running') {
isLoading = true;
} else {
let playlists = indexToArray(playlistsIndex);
playlists = compact(playlists.map((playlist) => {
if (!playlist.can_edit) return null;
return {
...playlist,
is_pinned: this.isPinned(playlist),
};
}));
playlists = sortItems(playlists, 'name');
playlists = sortItems(playlists, 'is_pinned');
let playlists = [
...(collate(spotify_library_playlists, { items }).items || []),
...(collate(mopidy_library_playlists, { items }).items || []),
];
playlists = compact(playlists.map((playlist) => {
if (!playlist.can_edit) return null;
return {
...playlist,
is_pinned: this.isPinned(playlist),
};
}));
playlists = sortItems(playlists, 'name');
playlists = sortItems(playlists, 'is_pinned');
list = (
<span className="context-menu__item">
<span className="context-menu__item mid_grey-text">
<span className="context-menu__item__link context-menu__item__link--inactive">
<I18n path="context_menu.add_to_playlist.no_playlists" />
</span>
list = (
<span className="context-menu__item">
<span className="context-menu__item mid_grey-text">
<span className="context-menu__item__link context-menu__item__link--inactive">
<I18n path="context_menu.add_to_playlist.no_playlists" />
</span>
</span>
);
if (playlists.length) {
list = playlists.map((playlist) => {
</span>
);
if (playlists.length) {
list = playlists.map((playlist) => {
// Allows us to css target last-child
const ElementTag = playlist.is_pinned ? 'em' : 'span';
// Allows us to css target last-child
const ElementTag = playlist.is_pinned ? 'em' : 'span';
return (
<ElementTag className="context-menu__item" key={playlist.uri}>
<a
className="context-menu__item__link"
onClick={(e) => this.addTracksToPlaylist(e, playlist.uri)}
>
<span className="context-menu__item__label">
{playlist.name}
</span>
</a>
</ElementTag>
);
});
}
return (
<ElementTag className="context-menu__item" key={playlist.uri}>
<a
className="context-menu__item__link"
onClick={(e) => this.addTracksToPlaylist(e, playlist.uri)}
>
<span className="context-menu__item__label">
{playlist.name}
</span>
</a>
</ElementTag>
);
});
}
}
@ -1305,17 +1311,16 @@ const mapStateToProps = (state) => ({
current_tracklist: state.core.current_tracklist,
queue_metadata: state.core.queue_metadata,
spotify_available: state.spotify.access_token,
spotify_library_playlists: state.spotify.library_playlists,
spotify_library_playlists_loaded_all: state.spotify.library_playlists_loaded_all,
spotify_library_artists: state.spotify.library_artists,
spotify_library_albums: state.spotify.library_albums,
spotify_library_playlists: state.core.items['spotify:library:playlists'] || { items_uris: [] },
spotify_library_artists: state.core.items['spotify:library:artists'] || { items_uris: [] },
spotify_library_albums: state.core.items['spotify:library:albums'] || { items_uris: [] },
spotify_library_tracks: state.spotify.library_tracks,
mopidy_library_playlists: state.mopidy.library_playlists,
mopidy_library_playlists_loaded_all: state.mopidy.library_playlists_loaded_all,
mopidy_library_artists: state.mopidy.library_artists,
mopidy_library_albums: state.mopidy.library_albums,
mopidy_library_playlists: state.core.items['mopidy:library:playlists'] || { items_uris: [] },
mopidy_library_artists: state.core.items['mopidy:library:artists'] || { items_uris: [] },
mopidy_library_albums: state.core.items['mopidy:library:albums'] || { items_uris: [] },
playlists: state.core.playlists,
tracks: state.core.tracks,
items: state.core.items,
pinned: state.pusher.pinned,
lastfm_authorized: state.lastfm.authorization,
});

View File

@ -33,28 +33,36 @@ class Hotkeys extends React.Component {
allow_reporting,
} = this.props;
let { volume } = this.props;
const key = e.key.toLowerCase();
let { key } = e;
const {
target,
altKey,
ctrlKey,
metaKey,
shiftKey,
preventDefault,
} = e;
key = key.toLowerCase();
// Ignore text input fields
if (
(e.target.nodeName === 'INPUT' && (e.target.type === 'text' || e.target.type === 'number')) ||
e.target.nodeName === 'TEXTAREA' ||
(e.target.nodeName === 'BUTTON' && key === ' ')) {
(target.nodeName === 'INPUT' && (target.type === 'text' || target.type === 'number')) ||
target.nodeName === 'TEXTAREA' ||
(target.nodeName === 'BUTTON' && key === ' ')) {
return;
}
// Ignore when there are any key modifiers. This enables us to avoid interfering
// with browser- and OS-default functions.
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) {
if (altKey || ctrlKey || metaKey || shiftKey) {
return;
}
let prevent = false;
switch (key) {
case ' ':
case 'p': // Super-useful once you get used to it. This negates the issue where interactive elements
// are in focus (ie slider) and <space> is reserved for that field's interactivity.
if (play_state == 'playing') {
if (play_state === 'playing') {
mopidyActions.pause();
uiActions.createNotification({ content: 'pause', type: 'shortcut' });
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Pause' });
@ -63,13 +71,13 @@ class Hotkeys extends React.Component {
uiActions.createNotification({ content: 'play_arrow', type: 'shortcut' });
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Play' });
}
prevent = true;
preventDefault();
break;
case 's': // Stop
mopidyActions.stop();
uiActions.createNotification({ content: 'stop', type: 'shortcut' });
prevent = true;
preventDefault();
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Stop' });
break;
@ -80,14 +88,14 @@ class Hotkeys extends React.Component {
}
mopidyActions.setTimePosition(new_position);
uiActions.createNotification({ content: 'fast_rewind', type: 'shortcut' });
prevent = true;
preventDefault();
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Rewind' });
break;
case 'f': // Seek forwards 30 sec
mopidyActions.setTimePosition(play_time_position + 30000);
uiActions.createNotification({ content: 'fast_forward', type: 'shortcut' });
prevent = true;
preventDefault();
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Fastforward' });
break;
@ -95,7 +103,7 @@ class Hotkeys extends React.Component {
case '<': // Previous track
mopidyActions.previous();
uiActions.createNotification({ content: 'skip_previous', type: 'shortcut' });
prevent = true;
preventDefault();
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Previous' });
break;
@ -103,7 +111,7 @@ class Hotkeys extends React.Component {
case '>': // Next track
mopidyActions.next();
uiActions.createNotification({ content: 'skip_next', type: 'shortcut' });
prevent = true;
preventDefault();
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Next' });
break;
@ -120,7 +128,7 @@ class Hotkeys extends React.Component {
}
uiActions.createNotification({ content: 'volume_up', type: 'shortcut' });
}
prevent = true;
preventDefault();
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Volume up' });
break;
@ -136,7 +144,7 @@ class Hotkeys extends React.Component {
}
}
uiActions.createNotification({ content: 'volume_down', type: 'shortcut' });
prevent = true;
preventDefault();
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Volume down' });
break;
@ -150,46 +158,42 @@ class Hotkeys extends React.Component {
uiActions.createNotification({ content: 'volume_off', type: 'shortcut' });
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Mute' });
}
prevent = true;
preventDefault();
break;
case 'escape': // Cancel current action/context
if (dragging) {
uiActions.dragEnd();
prevent = true;
preventDefault();
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Dragging' });
} else if ($('body').hasClass('modal-open')) {
window.history.back();
prevent = true;
preventDefault();
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Modal' });
}
break;
case '1': // Navigation: Queue
history.push('/queue');
prevent = true;
preventDefault();
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Queue' });
break;
case '2': // Navigation: Search
history.push('/search');
prevent = true;
preventDefault();
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Search' });
break;
case '3': // Navigation: Kiosk mode
history.push('/kiosk-mode');
prevent = true;
preventDefault();
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Kiosk mode' });
break;
default:
break;
}
if (prevent) {
e.preventDefault();
}
}
render() {

View File

@ -32,6 +32,15 @@ class TrackList extends React.Component {
}
handleKeyDown(e) {
const {
key,
altKey,
ctrlKey,
metaKey,
shiftKey,
preventDefault,
} = e;
// When we're focussed on certian elements, don't fire any shortcuts
// Typically form inputs
const ignoreNodes = ['INPUT', 'TEXTAREA'];
@ -41,20 +50,18 @@ class TrackList extends React.Component {
// Ignore when there are any key modifiers. This enables us to avoid interfering
// with browser- and OS-default functions.
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) {
if (altKey || ctrlKey || metaKey || shiftKey) {
return;
}
const tracks_keys = this.digestTracksKeys();
let prevent = false;
switch (e.key.toLowerCase()) {
switch (key.toLowerCase()) {
case 'enter':
if (tracks_keys && tracks_keys.length > 0) {
this.playTracks();
}
prevent = true;
preventDefault();
break;
case 'backspace':
@ -62,7 +69,7 @@ class TrackList extends React.Component {
if (tracks_keys && tracks_keys.length > 0) {
this.removeTracks();
}
prevent = true;
preventDefault();
break;
case 'a':
@ -71,14 +78,9 @@ class TrackList extends React.Component {
all_tracks.push(this.buildTrackKey(this.props.tracks[i], i));
}
this.props.uiActions.setSelectedTracks(all_tracks);
prevent = true;
preventDefault();
break;
}
if (prevent) {
e.preventDefault();
return false;
}
}
handleDrag(e, track_key) {

View File

@ -77,13 +77,20 @@ export function clearStorage() {
};
}
export function restoreFromColdStore(items) {
export function restoreItemsFromColdStore(items) {
return {
type: 'RESTORE_FROM_COLD_STORE',
type: 'RESTORE_ITEMS_FROM_COLD_STORE',
items,
};
}
export function restoreLibraryFromColdStore(library) {
return {
type: 'RESTORE_LIBRARY_FROM_COLD_STORE',
library,
};
}
export function updateColdStore(items) {
return {
type: 'UPDATE_COLD_STORE',
@ -175,29 +182,31 @@ export function loadLibrary(uri, force_reload = false) {
* We've got a loaded record, now we just need to plug it in to our state and stores.
* */
export function libraryLoaded(library) {
return {
type: 'LIBRARY_LOADED',
library,
};
}
export function itemsLoaded(items) {
return {
type: 'ITEMS_LOADED',
items,
};
}
export function itemLoaded(item) {
return {
type: 'ITEM_LOADED',
item,
};
return itemsLoaded([item]);
}
export function trackLoaded(track) {
return tracksLoaded([track]);
}
export function tracksLoaded(tracks) {
return {
type: 'TRACKS_LOADED',
tracks,
};
}
export function trackLoaded(track) {
return tracksLoaded([track]);
}
export function artistsLoaded(artists) {
return {
@ -206,35 +215,35 @@ export function artistsLoaded(artists) {
};
}
export function albumLoaded(album) {
return albumsLoaded([album]);
}
export function albumsLoaded(albums) {
return {
type: 'ALBUMS_LOADED',
albums,
};
}
export function playlistLoaded(playlist) {
return playlistsLoaded([playlist]);
export function albumLoaded(album) {
return albumsLoaded([album]);
}
export function playlistsLoaded(playlists) {
return {
type: 'PLAYLISTS_LOADED',
playlists,
};
}
export function userLoaded(user) {
return usersLoaded([user]);
export function playlistLoaded(playlist) {
return playlistsLoaded([playlist]);
}
export function usersLoaded(users) {
return {
type: 'USERS_LOADED',
users,
};
}
export function userLoaded(user) {
return usersLoaded([user]);
}
export function userPlaylistsLoaded(uri, playlists, more = null, total = null) {
return {
type: 'USER_PLAYLISTS_LOADED',

View File

@ -1,7 +1,7 @@
import React from 'react';
import ReactGA from 'react-ga';
import localForage from 'localforage';
import { compact } from 'lodash';
import { arrayOf } from '../../util/arrays';
import URILink from '../../components/URILink';
import { uriSource, upgradeSpotifyPlaylistUris, uriType, titleCase } from '../../util/helpers';
@ -366,12 +366,13 @@ const CoreMiddleware = (function () {
};
if (action.force_reload) {
console.info(`Force-reloading "${action.uri}"`);
fetchAlbum();
break;
}
if (
store.getState().core.items[action.uri] &&
store.getState().core.items[action.uri].tracks
store.getState().core.items[action.uri]
&& store.getState().core.items[action.uri].tracks
) {
console.info(`Using "${action.uri}" from index`);
break;
@ -380,8 +381,7 @@ const CoreMiddleware = (function () {
localForage.getItem(action.uri).then((result) => {
if (result) {
console.info(`Loading "${action.uri}" from database`);
store.dispatch(coreActions.restoreFromColdStore([result]));
console.log(result);
store.dispatch(coreActions.restoreItemsFromColdStore([result]));
// We don't have the complete Album, so refetch
if (!result.tracks) {
@ -395,54 +395,48 @@ const CoreMiddleware = (function () {
next(action);
break;
// TODO: Relocate this
case 'UPDATE_COLD_STORE':
if (action.items) {
action.items.map((item) => {
localForage.getItem(item.uri).then((result) => {
localForage.setItem(item.uri, { ...(result || {}), ...item });
});
});
}
break;
case 'RESTORE_FROM_COLD_STORE':
if (action.items) {
store.dispatch({
type: 'RESTORED_FROM_COLD_STORE',
items: action.items,
});
}
break;
case 'LOAD_ARTIST':
const fetchArtist = () => {
switch (uriSource(action.uri)) {
case 'spotify':
store.dispatch(spotifyActions.getArtist(action.uri, true));
if (spotify.me) {
store.dispatch(spotifyActions.following(action.uri));
}
break;
default:
store.dispatch(mopidyActions.getArtist(action.uri));
break;
}
};
if (action.force_reload) {
console.info(`Force-reloading "${action.uri}"`);
fetchArtist();
break;
}
if (
!action.force_reload
&& store.getState().core.artists[action.uri]
&& store.getState().core.artists[action.uri].albums
&& store.getState().core.artists[action.uri].tracks) {
console.info(`Loading "${action.uri}" from index`);
store.getState().core.items[action.uri]
&& store.getState().core.items[action.uri].tracks
&& store.getState().core.items[action.uri].albums
&& store.getState().core.items[action.uri].images
) {
console.info(`Using "${action.uri}" from index`);
break;
}
// Try our cold storage
localForage.getItem(action.uri).then((result) => {
if (result && !action.force_reload) {
console.info(`Loading "${action.uri}" from database`);
store.dispatch(coreActions.restoreFromColdStore([result]));
} else {
switch (uriSource(action.uri)) {
case 'spotify':
store.dispatch(spotifyActions.getArtist(action.uri, true));
if (result) {
console.info(`Restoring "${action.uri}" from database`);
store.dispatch(coreActions.restoreItemsFromColdStore([result]));
if (spotify.me) {
store.dispatch(spotifyActions.following(action.uri));
}
break;
default:
store.dispatch(mopidyActions.getArtist(action.uri));
break;
if (!result.tracks || !result.albums || !result.images) {
fetchArtist();
}
} else {
fetchArtist();
}
});
@ -467,10 +461,14 @@ const CoreMiddleware = (function () {
};
if (action.force_reload) {
console.info(`Force-reloading "${action.uri}"`);
fetchPlaylist();
break;
}
if (store.getState().core.items[action.uri]) {
if (
store.getState().core.items[action.uri]
&& store.getState().core.items[action.uri].tracks
) {
console.info(`Using "${action.uri}" from index`);
break;
}
@ -478,7 +476,11 @@ const CoreMiddleware = (function () {
localForage.getItem(action.uri).then((result) => {
if (result) {
console.info(`Restoring "${action.uri}" from database`);
store.dispatch(coreActions.restoreFromColdStore([result]));
store.dispatch(coreActions.restoreItemsFromColdStore([result]));
if (!result.tracks) {
fetchPlaylist();
}
} else {
fetchPlaylist();
}
@ -540,27 +542,28 @@ const CoreMiddleware = (function () {
switch (uriSource(action.uri)) {
case 'spotify':
store.dispatch(
spotifyActions[`getLibrary${titleCase(uriType(action.uri))}`](action.uri),
spotifyActions[`getLibrary${titleCase(uriType(action.uri))}`](),
);
break;
case 'google':
store.dispatch(
googleActions[`getLibrary${titleCase(uriType(action.uri))}`](action.uri),
googleActions[`getLibrary${titleCase(uriType(action.uri))}`](),
);
break;
default:
store.dispatch(
mopidyActions[`getLibrary${titleCase(uriType(action.uri))}`](action.uri),
mopidyActions[`getLibrary${titleCase(uriType(action.uri))}`](),
);
break;
}
};
if (action.force_reload) {
console.info(`Force-reloading "${action.uri}"`);
fetchLibrary();
break;
}
if (store.getState().core.items[action.uri]) {
if (store.getState().core.libraries[action.uri]) {
console.info(`Using "${action.uri}" from index`);
break;
}
@ -569,13 +572,12 @@ const CoreMiddleware = (function () {
if (library) {
console.info(`Restoring "${action.uri}" and ${library.items_uris.length} items from db`);
var promises = library.items_uris.map(function(item) { return localForage.getItem(item); });
Promise.all(promises).then(function(libraryItems) {
store.dispatch(coreActions.restoreFromColdStore(libraryItems));
});
const promises = library.items_uris.map((libraryItem) => localForage.getItem(libraryItem));
Promise.all(promises).then(
(libraryItems) => store.dispatch(coreActions.restoreItemsFromColdStore(compact(libraryItems))),
);
//store.dispatch(coreActions.loadItems(result.items_uris));
store.dispatch(coreActions.restoreFromColdStore([library]));
store.dispatch(coreActions.restoreLibraryFromColdStore(library));
} else {
fetchLibrary();
}
@ -687,23 +689,22 @@ const CoreMiddleware = (function () {
next(action);
break;
// TODO: Flatten ITEM_LOADED to become an alias for ITEMS_LOADED (prefer bulk)
case 'ITEMS_LOADED':
action.items.forEach((item) => store.dispatch(coreActions.itemLoaded(item)));
break;
case 'ITEM_LOADED':
const mergedItem = {
...core.items[action.item.uri] || {},
...action.item,
};
store.dispatch(coreActions.updateColdStore([mergedItem]));
const mergedItems = action.items.map((item) => ({
...core.items[item.uri] || {},
...item,
}));
store.dispatch(coreActions.updateColdStore(mergedItems));
next({
...action,
item: mergedItem,
items: mergedItems,
});
break;
case 'LIBRARY_LOADED':
store.dispatch(coreActions.updateColdStore([action.library]));
break;
case 'ARTISTS_LOADED':
var artists_index = { ...core.artists };
var artists_loaded = [];
@ -753,7 +754,7 @@ const CoreMiddleware = (function () {
case 'spotify':
if (spotify.authorization && spotify.me) {
playlist.can_edit = (playlist.owner.id == spotify.me.id);
playlist.can_edit = (playlist.owner && playlist.owner.id == spotify.me.id);
}
}
@ -911,6 +912,17 @@ const CoreMiddleware = (function () {
next(action);
break;
// TODO: Relocate this
case 'UPDATE_COLD_STORE':
if (action.items) {
action.items.map((item) => {
localForage.getItem(item.uri).then((result) => {
localForage.setItem(item.uri, { ...(result || {}), ...item });
});
});
}
break;
// This action is irrelevant to us, pass it on to the next middleware
default:
return next(action);

View File

@ -75,6 +75,26 @@ export default function reducer(core = {}, action) {
},
};
case 'ITEMS_LOADED':
const mergedItems = action.items.reduce((obj, item) => (obj[item.uri] = item, obj), {});
return {
...core,
items: {
...core.items,
...mergedItems,
},
};
case 'LIBRARY_LOADED':
console.log(action);
return {
...core,
libraries: {
...core.libraries,
[action.library.uri]: action.library,
},
};
case 'TRACKS_LOADED':
var tracks = { ...core.tracks };
for (const track of action.tracks) {
@ -148,8 +168,18 @@ export default function reducer(core = {}, action) {
users[action.uri] = user;
return { ...core, users };
case 'RESTORE_LIBRARY_FROM_COLD_STORE':
const { libraries } = core;
libraries[action.library.uri] = {
...(libraries[action.library.uri] || {}),
...action.library,
};
return {
...core,
libraries,
};
case 'RESTORED_FROM_COLD_STORE':
case 'RESTORE_ITEMS_FROM_COLD_STORE':
const { items } = core;
action.items.forEach((item) => {
items[item.uri] = {

View File

@ -228,7 +228,7 @@ export function getArtist(uri, artist, mbid = false) {
.then(
(response) => {
if (response.artist) {
dispatch(coreActions.artistLoaded({
dispatch(coreActions.itemLoaded({
uri,
mbid: response.artist.mbid,
biography: response.artist.bio.content,
@ -255,7 +255,7 @@ export function getAlbum(uri, artist, album, mbid = false) {
.then(
(response) => {
if (response.album) {
const existing_album = getState().core.albums[uri];
const existing_album = getState().core.items[uri];
const album = {
uri,
images: response.album.image,
@ -273,7 +273,7 @@ export function getAlbum(uri, artist, album, mbid = false) {
delete album.images;
}
dispatch(coreActions.albumLoaded(album));
dispatch(coreActions.itemLoaded(album));
}
},
);

View File

@ -652,7 +652,7 @@ const MopidyMiddleware = (function () {
* */
case 'MOPIDY_PLAY_PLAYLIST':
var playlist = store.getState().core.playlists[action.uri];
var playlist = store.getState().core.items[action.uri];
// We have the playlist loaded already, and we've got at least 1 track to start playing
if (playlist && playlist.tracks_uris && playlist.tracks_uris.length > 0) {
@ -700,7 +700,7 @@ const MopidyMiddleware = (function () {
case 'MOPIDY_ENQUEUE_PLAYLIST':
var playlist = store.getState().core.playlists[action.uri];
var playlist = store.getState().core.items[action.uri];
// We have the playlist loaded already, and we've got at least 1 track to start playing
if (playlist && playlist.tracks_uris && playlist.tracks_uris.length > 0) {
@ -1598,41 +1598,6 @@ const MopidyMiddleware = (function () {
* ======================================================================================
* */
case 'MOPIDY_GET_LIBRARY_PLAYLISTS':
request(store, 'playlists.asList')
.then((response) => {
// Remove any Spotify playlists. These will be handled by our Spotify API
const playlist_uris = arrayOf('uri', response).filter(
(uri) => uriSource(uri) !== 'spotify',
);
const libraryPlaylists = [];
// get the full playlist objects
playlist_uris.forEach((uri, index) => {
request(store, 'playlists.lookup', { uri })
.then((response) => {
libraryPlaylists.push({
type: 'playlist',
name: response.name,
uri: response.uri,
source: uriSource(response.uri),
provider: 'mopidy',
last_modified: response.last_modified,
tracks: response.tracks,
});
if (index === playlist_uris.length - 1) {
store.dispatch(coreActions.itemsLoaded(libraryPlaylists));
store.dispatch(coreActions.itemLoaded({
uri: 'mopidy:library:playlists',
items_uris: arrayOf('uri', libraryPlaylists),
}));
}
});
});
});
break;
case 'MOPIDY_GET_PLAYLIST':
request(store, 'playlists.lookup', action.data)
@ -1766,7 +1731,7 @@ const MopidyMiddleware = (function () {
// Overwrite our playlist with the response to our save
// This is essential to get the updated URI from Mopidy
const playlist = {
...store.getState().core.playlists[action.key],
...store.getState().core.items[action.key],
uri: response.uri,
name: response.name,
};
@ -1858,28 +1823,6 @@ const MopidyMiddleware = (function () {
* */
case 'MOPIDY_GET_LIBRARY_ALBUMS':
request(store, 'library.browse', { uri: store.getState().mopidy.library_albums_uri })
.then((response) => {
const uris = arrayOf('uri', response);
request(store, 'library.lookup', { uris })
.then((response) => {
const libraryAlbums = indexToArray(response).map((tracks) => ({
source: 'local',
artists: tracks[0].artists || null,
tracks,
last_modified: tracks[0].last_modified,
...tracks[0].album,
}));
store.dispatch(coreActions.itemsLoaded(libraryAlbums));
store.dispatch(coreActions.itemLoaded({
uri: 'mopidy:library:albums',
items_uris: arrayOf('uri', libraryAlbums),
}));
});
});
break;
case 'MOPIDY_GET_ALBUM':
request(store, 'library.lookup', { uris: [action.uri] })
@ -1911,7 +1854,8 @@ const MopidyMiddleware = (function () {
* =============================================================== ARTIST(S) ============
* ======================================================================================
**/
case 'MOPIDY_GET_LIBRARY_ARTISTS':
case 'MOPIDY_GET_LIBRARY_ARTISTSXXXXXXXXXXXX':
const uri = store.getState().mopidy.library_artists_uri;
request(store, 'library.browse', { uri })
.then((response) => {
@ -1919,8 +1863,6 @@ const MopidyMiddleware = (function () {
const uris = [];
for (let i = 0; i < response.length; i++) {
// Convert local URI to actual artist URI
// See https://github.com/mopidy/mopidy-local-sqlite/issues/39
response[i].uri = response[i].uri.replace('local:directory?albumartist=', '').replace('local:directory?artist=', '');
uris.push(response[i].uri);
}
@ -1934,62 +1876,6 @@ const MopidyMiddleware = (function () {
});
break;
/**
* TODO: Fetch and process library artists
*
* We can't get specific artist artwork from Mopidy. Perhaps we fetch additional
* artist metadata via LastFM? Their API limits will make this quite slow.
case 'MOPIDY_GET_LIBRARY_ARTISTS':
var last_run = store.getState().ui.processes.MOPIDY_LIBRARY_ARTISTS_PROCESSOR
if (!last_run){
request(store, 'library.browse', { uri: 'local:directory?type=artist' } )
.then(response => {
if (response.length <= 0) return;
var uris = arrayOf('uri',response);
store.dispatch({
type: 'MOPIDY_LIBRARY_ARTISTS_LOADED',
uris: uris
});
// Start our process to load the full album objects
store.dispatch(uiActions.startProcess('MOPIDY_LIBRARY_ARTISTS_PROCESSOR','Loading '+uris.length+' local artists', {uris: uris}));
})
} else if (last_run.status == 'cancelled'){
store.dispatch(uiActions.resumeProcess('MOPIDY_LIBRARY_ARTISTS_PROCESSOR'))
} else if (last_run.status == 'finished'){
// TODO: do we want to force a refresh?
}
break;
case 'MOPIDY_LIBRARY_ARTISTS_PROCESSOR':
if (store.getState().ui.processes['MOPIDY_LIBRARY_ARTISTS_PROCESSOR'] !== undefined){
var processor = store.getState().ui.processes['MOPIDY_LIBRARY_ARTISTS_PROCESSOR'];
if (processor.status == 'cancelling'){
store.dispatch(uiActions.processCancelled('MOPIDY_LIBRARY_ARTISTS_PROCESSOR'));
return false;
}
}
var uris = Object.assign([], action.data.uris)
var uris_to_load = uris.splice(0,50)
if (uris_to_load.length > 0){
store.dispatch(uiActions.updateProcess('MOPIDY_LIBRARY_ARTISTS_PROCESSOR', 'Loading '+uris.length+' local artists', {uris: uris}));
store.dispatch(mopidyActions.getArtists(uris_to_load, {name: 'MOPIDY_LIBRARY_ARTISTS_PROCESSOR', data: {uris: uris}}));
} else {
store.dispatch(uiActions.processFinished('MOPIDY_LIBRARY_ARTISTS_PROCESSOR'));
}
break;
* */
case 'MOPIDY_GET_ARTIST':
request(store, 'library.lookup', { uris: [action.uri] })
.then((_response) => {
@ -2016,10 +1902,9 @@ const MopidyMiddleware = (function () {
}
}
if (albums) {
store.dispatch(coreActions.albumsLoaded(albums));
store.dispatch(coreActions.itemsLoaded(albums));
}
// Start with an empty artist object
let artist = {
uri: action.uri,
provider: 'mopidy',
@ -2035,13 +1920,13 @@ const MopidyMiddleware = (function () {
}
// Add our tracks and albums
artist.albums_uris = arrayOf('uri', albums),
artist.tracks = response;
artist.albums_uris = arrayOf('uri', albums);
artist.tracks = response;
store.dispatch(coreActions.artistLoaded(artist));
store.dispatch(coreActions.itemLoaded(artist));
// Load supprting information from LastFM and Discogs
const existing_artist = store.getState().core.artists[artist.uri];
const existing_artist = store.getState().core.items[artist.uri];
if (existing_artist) {
if (!existing_artist.images) {
if (store.getState().spotify.enabled) {
@ -2049,7 +1934,7 @@ const MopidyMiddleware = (function () {
} else {
store.dispatch(discogsActions.getArtistImages(artist.uri, artist));
}
}
}
// Get biography and other stats from LastFM
if (!existing_artist.biography) {
@ -2380,6 +2265,88 @@ const MopidyMiddleware = (function () {
next(action);
break;
/**
* My Music libraries
*/
case 'MOPIDY_GET_LIBRARY_ARTISTS':
request(store, 'library.browse', { uri: store.getState().mopidy.library_artists_uri })
.then((raw_response) => {
if (raw_response.length <= 0) return;
// Convert local URI to actual artist URI
// See https://github.com/mopidy/mopidy-local-sqlite/issues/39
const response = raw_response.map((artist) => ({
...artist,
uri: artist.uri.replace('local:directory?albumartist=', '').replace('local:directory?artist=', '')
}));
store.dispatch(coreActions.itemsLoaded(response));
store.dispatch(coreActions.libraryLoaded({
uri: 'mopidy:library:artists',
items_uris: arrayOf('uri', response),
}));
});
break;
case 'MOPIDY_GET_LIBRARY_PLAYLISTS':
request(store, 'playlists.asList')
.then((response) => {
// Remove any Spotify playlists. These will be handled by our Spotify API
const playlist_uris = arrayOf('uri', response).filter(
(uri) => uriSource(uri) !== 'spotify',
);
const libraryPlaylists = [];
// get the full playlist objects
playlist_uris.forEach((uri, index) => {
request(store, 'playlists.lookup', { uri })
.then((response) => {
libraryPlaylists.push({
type: 'playlist',
name: response.name,
uri: response.uri,
source: uriSource(response.uri),
provider: 'mopidy',
last_modified: response.last_modified,
tracks: response.tracks,
});
if (index === playlist_uris.length - 1) {
store.dispatch(coreActions.itemsLoaded(libraryPlaylists));
store.dispatch(coreActions.libraryLoaded({
uri: 'mopidy:library:playlists',
items_uris: arrayOf('uri', libraryPlaylists),
}));
}
});
});
});
break;
case 'MOPIDY_GET_LIBRARY_ALBUMS':
request(store, 'library.browse', { uri: store.getState().mopidy.library_albums_uri })
.then((response) => {
const uris = arrayOf('uri', response);
request(store, 'library.lookup', { uris })
.then((response) => {
const libraryAlbums = indexToArray(response).map((tracks) => ({
source: 'local',
artists: tracks[0].artists || null,
tracks,
last_modified: tracks[0].last_modified,
...tracks[0].album,
}));
store.dispatch(coreActions.itemsLoaded(libraryAlbums));
store.dispatch(coreActions.libraryLoaded({
uri: 'mopidy:library:albums',
items_uris: arrayOf('uri', libraryAlbums),
}));
});
});
break;
// This action is irrelevant to us, pass it on to the next middleware
default:
return next(action);

View File

@ -19,6 +19,7 @@ import {
formatArtists,
formatAlbums,
formatPlaylists,
formatImages,
} from '../../util/format';
import URILink from '../../components/URILink';
import { i18n } from '../../locale';
@ -391,7 +392,6 @@ export function getFeaturedPlaylists() {
const playlists = [];
for (let i = 0; i < response.playlists.items.length; i++) {
playlists.push({
...response.playlists.items[i],
is_completely_loaded: false,
can_edit: (getState().spotify.me && response.playlists.items[i].owner.id == getState().spotify.me.id),
@ -891,38 +891,6 @@ export function resolveRadioSeeds(radio) {
* */
/**
* Get my favorites
*
* @param uri string
* */
export function getFavorites(limit = 50, term = 'long_term') {
return (dispatch, getState) => {
dispatch({ type: 'SPOTIFY_FAVORITES_LOADED', artists: [], tracks: [] });
$.when(
request(dispatch, getState, `me/top/artists?limit=${limit}&time_range=${term}`),
request(dispatch, getState, `me/top/tracks?limit=${limit}&time_range=${term}`),
).then(
(artists_response, tracks_response) => {
dispatch({
type: 'SPOTIFY_FAVORITES_LOADED',
artists: artists_response.items,
tracks: tracks_response.items,
});
},
(artists_error, tracks_error) => {
dispatch(coreActions.handleException(
'Could not load favorites',
{ ...artists_error, ...tracks_error },
));
},
);
};
}
/**
* Get our recommendations
* This is based off our 'favorites' and then we use those as seeds
@ -993,7 +961,7 @@ export function getRecommendations(uris = [], limit = 20, tunabilities = null) {
// is not one of the seeds
if (!artists_uris.includes(artist.uri) && !artists_ids.includes(artist.id)) {
artists_uris.push(artist.uri);
dispatch(getArtist(artist.uri));
dispatch(getArtist(artist.uri, false));
}
}
}
@ -1136,21 +1104,14 @@ export function getArtistImages(artist) {
return (dispatch, getState) => {
request(dispatch, getState, `search?q=${artist.name}&type=artist`)
.then(response => {
console.log('images', response);
if (response.artists.items.length > 0) {
const updatedArtist = {
dispatch(coreActions.itemLoaded({
uri: artist.uri,
images: response.artists.items[0].images,
}
dispatch(coreActions.artistLoaded(updatedArtist));
images: [formatImages(response.artists.items[0].images)],
}));
}
},
error => {
dispatch(coreActions.handleException(
'Could not load artists',
error,
));
},
);
});
};
}
@ -1724,22 +1685,18 @@ export function flushLibrary() {
};
}
/**
* Playlists
* */
export function getLibraryPlaylists() {
return (dispatch, getState) => {
let libraryPlaylists = [];
const fetchLibraryPlaylists = (endpoint) => request(dispatch, getState, endpoint)
.then((response) => {
console.log('spot', response);
libraryPlaylists = [...libraryPlaylists, ...formatPlaylists(response.items)];
if (response.next) {
fetchLibraryPlaylists(response.next);
} else {
dispatch(coreActions.itemsLoaded(libraryPlaylists));
dispatch(coreActions.itemLoaded({
dispatch(coreActions.libraryLoaded({
uri: 'spotify:library:playlists',
items_uris: arrayOf('uri', libraryPlaylists),
}));
@ -1760,7 +1717,7 @@ export function getLibraryAlbums() {
fetchLibraryAlbums(response.next);
} else {
dispatch(coreActions.itemsLoaded(libraryAlbums));
dispatch(coreActions.itemLoaded({
dispatch(coreActions.libraryLoaded({
uri: 'spotify:library:albums',
items_uris: arrayOf('uri', libraryAlbums),
}));
@ -1771,70 +1728,23 @@ export function getLibraryAlbums() {
};
}
/**
* Artists
* */
export function getLibraryArtists() {
return (dispatch, getState) => {
const last_run = getState().ui.processes.SPOTIFY_GET_LIBRARY_ARTISTS_PROCESSOR;
let libraryArtists = [];
const fetchLibraryArtists = (endpoint) => request(dispatch, getState, endpoint)
.then((response) => {
libraryArtists = [...libraryArtists, ...formatArtists(response.artists.items)];
if (response.next) {
fetchLibraryArtists(response.next);
} else {
dispatch(coreActions.itemsLoaded(libraryArtists));
dispatch(coreActions.libraryLoaded({
uri: 'spotify:library:artists',
items_uris: arrayOf('uri', libraryArtists),
}));
}
});
if (!last_run) {
dispatch(uiActions.startProcess('SPOTIFY_GET_LIBRARY_ARTISTS_PROCESSOR', 'Loading Spotify artists', { next: 'me/following?type=artist&limit=50' }));
} else if (last_run.status === 'cancelled') {
dispatch(uiActions.resumeProcess('SPOTIFY_GET_LIBRARY_ARTISTS_PROCESSOR'));
// We've already finished, but the status has been flushed
} else if (last_run.status === 'finished' && !getState().spotify.library_artists_loaded_all) {
dispatch(uiActions.startProcess('SPOTIFY_GET_LIBRARY_ARTISTS_PROCESSOR', 'Loading Spotify artists', { next: 'me/following?type=artist&limit=50' }));
}
};
}
export function getLibraryArtistsProcessor(data) {
return (dispatch, getState) => {
request(dispatch, getState, data.next)
.then(
(response) => {
dispatch({
type: 'SPOTIFY_LIBRARY_ARTISTS_LOADED',
artists: response.artists.items,
});
// We got a next link, so we've got more work to be done
if (response.artists.next) {
const { total } = response.artists;
const loaded = getState().spotify.library_artists.length;
const remaining = total - loaded;
dispatch(uiActions.updateProcess(
'SPOTIFY_GET_LIBRARY_ARTISTS_PROCESSOR',
`Loading ${remaining} Spotify artists`,
{
next: response.artists.next,
total: response.artists.total,
remaining,
},
));
dispatch(uiActions.runProcess('SPOTIFY_GET_LIBRARY_ARTISTS_PROCESSOR', { next: response.artists.next }));
} else {
dispatch(uiActions.processFinished('SPOTIFY_GET_LIBRARY_ARTISTS_PROCESSOR'));
}
},
() => {
dispatch(uiActions.processFinished(
'SPOTIFY_GET_LIBRARY_ARTISTS_PROCESSOR',
{
content: i18n(
'errors.could_not_load_library',
{
name: i18n('library.artists.title'),
provider: i18n('services.spotify.title'),
},
),
level: 'error',
},
));
},
);
fetchLibraryArtists('me/following?type=artist&limit=50');
};
}

View File

@ -42,6 +42,7 @@ let state = {
users: {},
tracks: {},
items: {},
libraries: {},
http_streaming_enabled: false,
http_streaming_cachebuster: null,
http_streaming_url: `http://${window.location.hostname}:8000/mopidy`,
@ -143,7 +144,10 @@ state = migration(state);
const rootPersistConfig = {
key: 'root',
storage: localForage,
blacklist: ['ui', 'core'],
blacklist: [
'ui',
'core',
],
debug: window.test_mode,
};
@ -158,13 +162,23 @@ const corePersistConfig = {
'playlists',
'users',
'tracks',
'libraries', // We manually hydrate this, so we can handle the rehydration of library items
],
};
const uiPersistConfig = {
key: 'ui',
storage: localForage,
blacklist: ['load_queue', 'notifications'],
blacklist: [
'load_queue',
'notifications',
'context_menu',
'current_track_transition',
'dragger',
'selected_tracks',
'sidebar_open',
'window_focus',
],
debug: window.test_mode,
};

View File

@ -1,4 +1,4 @@
import { compact } from 'lodash';
import {
isObject,
upgradeSpotifyPlaylistUri,
@ -438,6 +438,15 @@ const formatPlaylist = function (data) {
if (playlist.images && !playlist.images.formatted) {
playlist.images = formatImages(playlist.images);
}
if (playlist.tracks) {
if (Array.isArray(playlist.tracks)) {
playlist.tracks = formatTracks(playlist.tracks);
} else if (playlist.tracks.items) {
playlist.tracks = formatTracks(playlist.tracks.items);
} else {
playlist.tracks = null;
}
}
if (data.last_modified_date && playlist.last_modified === undefined) {
playlist.last_modified = data.last_modified_date;
@ -461,9 +470,8 @@ const formatPlaylist = function (data) {
playlist.owner = {
id: data.owner.id,
uri: data.owner.uri,
name: (data.owner.display_name ? data.owner.display_name : null),
name: (data.owner.display_name || data.owner.id),
};
playlist.user_uri = data.owner.uri;
}
// Spotify upgraded their playlists URI to remove user component (Sept 2018)
@ -737,6 +745,10 @@ const formatGroup = function (data) {
};
const collateLibrary = (uris, itemsIndex) => {
return compact(uris.map((uri) => itemsIndex[uri]));
};
/**
* Collate an object with external references into a fully self-contained object
* We merge *_uris references (ie tracks_uris) into the main object
@ -888,6 +900,7 @@ export {
formatClient,
formatGroup,
collate,
collateLibrary,
};
export default {
@ -910,4 +923,5 @@ export default {
formatClient,
formatGroup,
collate,
collateLibrary,
};

View File

@ -190,7 +190,7 @@ const uriType = function (uri) {
}
return exploded[1];
default:
return '';
return exploded[1];
}
};

View File

@ -138,27 +138,25 @@ class Artist extends React.Component {
inLibrary = () => {
const { uri } = this.props;
const libraryName = `${uriSource(uri)}_library_artists`;
const { [libraryName]: library = [] } = this.props;
return library.indexOf(uri) > -1;
const { [libraryName]: { items_uris } } = this.props;
return items_uris.indexOf(uri) > -1;
}
renderOverview = () => {
const {
uri,
uiActions,
artist: {
tracks = [],
related_artists = [],
} = {},
artist,
items,
sort,
sort_reverse,
filter,
} = this.props;
let {
artist: {
albums = [],
} = {},
} = this.props;
tracks,
related_artists,
albums,
} = collate(artist, { albums: items });
if (sort && albums) {
albums = sortItems(albums, sort, sort_reverse);
@ -317,7 +315,7 @@ class Artist extends React.Component {
},
);
const thumbnails = artist.images && artist.images.map(
const thumbnails = artist.images && Array.isArray(artist.images) && artist.images.map(
(image) => {
if (!image.huge) return null;
return (
@ -532,11 +530,10 @@ const mapStateToProps = (state, ownProps) => {
theme: state.ui.theme,
slim_mode: state.ui.slim_mode,
load_queue: state.ui.load_queue,
artist: (state.core.items[uri] !== undefined ? state.core.items[uri] : false),
tracks: state.core.tracks,
artists: state.core.artists,
spotify_library_artists: state.spotify.library_artists,
local_library_artists: state.mopidy.library_artists,
artist: (state.core.items[uri] || null),
items: state.core.items,
spotify_library_artists: state.core.items['spotify:library:artists'] || { items_uris: [] },
mopidy_library_artists: state.core.items['mopidy:library:artists'] || { items_uris: [] },
albums: (state.core.albums ? state.core.albums : []),
filter: (state.ui.artist_albums_filter ? state.ui.artist_albums_filter : null),
sort: (state.ui.artist_albums_sort ? state.ui.artist_albums_sort : null),

View File

@ -318,13 +318,13 @@ class Playlist extends React.Component {
<Icon type="fontawesome" name={sourceIcon(playlist.uri)} />
</li>
)}
{playlist.user_uri && (
{playlist.user && (
<li>
<URILink
type="user"
uri={playlist.user_uri}
uri={playlist.user.uri}
>
{playlist.user ? playlist.user.name : getFromUri('userid', playlist.user_uri)}
{playlist.user.name}
</URILink>
</li>
)}
@ -349,16 +349,20 @@ class Playlist extends React.Component {
{this.renderActions()}
<section className="list-wrapper">
<TrackList
uri={playlist.uri}
className="playlist-track-list"
track_context={context}
tracks={playlist.tracks}
removeTracks={this.removeTracks}
reorderTracks={this.reorderTracks}
/>
</section>
{isLoading(load_queue, [`spotify_playlists/${playlist_id}/tracks`]) ? (
<Loader body loading />
) : (
<section className="list-wrapper">
<TrackList
uri={playlist.uri}
className="playlist-track-list"
track_context={context}
tracks={playlist.tracks}
removeTracks={this.removeTracks}
reorderTracks={this.reorderTracks}
/>
</section>
)}
</div>
);
}

View File

@ -18,7 +18,7 @@ import {
uriSource,
} from '../../util/helpers';
import { sortItems, applyFilter } from '../../util/arrays';
import { collate } from '../../util/format';
import { collateLibrary, collate } from '../../util/format';
import { i18n, I18n } from '../../locale';
class LibraryAlbums extends React.Component {
@ -355,9 +355,9 @@ const mapStateToProps = (state) => ({
mopidy_uri_schemes: state.mopidy.uri_schemes,
load_queue: state.ui.load_queue,
items: state.core.items,
mopidy_library: state.core.items['mopidy:library:albums'] || { items_uris: [] },
spotify_library: state.core.items['spotify:library:albums'] || { items_uris: [] },
google_library: state.core.items['google:library:albums'] || { items_uris: [] },
mopidy_library: state.core.libraries['mopidy:library:albums'] || { items_uris: [] },
spotify_library: state.core.libraries['spotify:library:albums'] || { items_uris: [] },
google_library: state.core.libraries['google:library:albums'] || { items_uris: [] },
google_available: (state.mopidy.uri_schemes && state.mopidy.uri_schemes.includes('gmusic:')),
spotify_available: state.spotify.access_token,
view: state.ui.library_albums_view,

View File

@ -10,14 +10,13 @@ import FilterField from '../../components/Fields/FilterField';
import LazyLoadListener from '../../components/LazyLoadListener';
import Icon from '../../components/Icon';
import * as uiActions from '../../services/ui/actions';
import * as mopidyActions from '../../services/mopidy/actions';
import * as spotifyActions from '../../services/spotify/actions';
import * as googleActions from '../../services/google/actions';
import * as coreActions from '../../services/core/actions';
import {
uriSource,
} from '../../util/helpers';
import { sortItems, applyFilter } from '../../util/arrays';
import { I18n, i18n } from '../../locale';
import { collate, collateLibrary } from '../../util/format';
class LibraryArtists extends React.Component {
constructor(props) {
@ -65,51 +64,44 @@ class LibraryArtists extends React.Component {
getMopidyLibrary = () => {
const {
source,
mopidy_library_artists,
mopidyActions: {
getLibraryArtists,
coreActions: {
loadLibrary,
},
} = this.props;
if (source !== 'local' && source !== 'all') return;
if (mopidy_library_artists) return;
getLibraryArtists();
loadLibrary('mopidy:library:artists');
};
getGoogleLibrary = () => {
const {
source,
google_available,
google_library_artists,
googleActions: {
getLibraryArtists,
coreActions: {
loadLibrary,
},
} = this.props;
if (!google_available) return;
if (source !== 'google' && source !== 'all') return;
if (google_library_artists) return;
getLibraryArtists();
loadLibrary('google:library:artists');
};
getSpotifyLibrary = () => {
const {
source,
spotify_available,
spotify_library_artists_status,
spotifyActions: {
getLibraryArtists,
coreActions: {
loadLibrary,
},
} = this.props;
if (!spotify_available) return;
if (source !== 'spotify' && source !== 'all') return;
if (spotify_library_artists_status === 'finished') return;
if (spotify_library_artists_status === 'started') return;
getLibraryArtists();
loadLibrary('spotify:library:artists');
};
handleContextMenu(e, item) {
@ -144,70 +136,41 @@ class LibraryArtists extends React.Component {
this.props.uiActions.set(data);
}
renderView() {
let artists = [];
renderView = () => {
const {
spotify_library,
google_library,
mopidy_library,
items,
source,
sort,
sort_reverse,
view,
} = this.props;
const {
limit,
filter,
} = this.state;
// Mopidy library items
if (this.props.mopidy_library_artists && (this.props.source == 'all' || this.props.source == 'local')) {
for (uri of this.props.mopidy_library_artists) {
// Construct item placeholder. This is used as Mopidy needs to
// lookup ref objects to get the full object which can take some time
var source = uriSource(uri);
var artist = {
uri,
source,
};
let artists = [
...(source === 'all' || source === 'spotify' ? collate(spotify_library, { items }).items : []),
...(source === 'all' || source === 'google' ? collate(google_library, { items }).items : []),
...(source === 'all' || source === 'local' ? collate(mopidy_library, { items }).items : []),
];
if (this.props.artists.hasOwnProperty(uri)) {
artist = this.props.artists[uri];
}
artists.push(artist);
}
if (sort) {
artists = sortItems(artists, sort, sort_reverse);
}
// Google library items
if (this.props.google_library_artists && (this.props.source == 'all' || this.props.source == 'google')) {
for (uri of this.props.google_library_artists) {
// Construct item placeholder. This is used as Mopidy needs to
// lookup ref objects to get the full object which can take some time
var source = uriSource(uri);
var artist = {
uri,
source,
};
if (this.props.artists.hasOwnProperty(uri)) {
artist = this.props.artists[uri];
}
artists.push(artist);
}
}
// Spotify library items
if (this.props.spotify_library_artists && (this.props.source == 'all' || this.props.source == 'spotify')) {
for (let i = 0; i < this.props.spotify_library_artists.length; i++) {
var uri = this.props.spotify_library_artists[i];
if (this.props.artists.hasOwnProperty(uri)) {
artists.push(this.props.artists[uri]);
}
}
}
if (this.props.sort) {
artists = sortItems(artists, this.props.sort, this.props.sort_reverse);
}
if (this.state.filter !== '') {
artists = applyFilter('name', this.state.filter, artists);
if (filter !== '') {
artists = applyFilter('name', filter, artists);
}
// Apply our lazy-load-rendering
const total_artists = artists.length;
artists = artists.slice(0, this.state.limit);
artists = artists.slice(0, limit);
if (this.props.view == 'list') {
if (view === 'list') {
return (
<section className="content-wrapper">
<List
@ -220,8 +183,8 @@ class LibraryArtists extends React.Component {
link_prefix="/artist/"
/>
<LazyLoadListener
loadKey={total_artists > this.state.limit ? this.state.limit : total_artists}
showLoader={this.state.limit < total_artists}
loadKey={total_artists > limit ? limit : total_artists}
showLoader={limit < total_artists}
loadMore={() => this.loadMore()}
/>
</section>
@ -234,15 +197,20 @@ class LibraryArtists extends React.Component {
artists={artists}
/>
<LazyLoadListener
loadKey={total_artists > this.state.limit ? this.state.limit : total_artists}
showLoader={this.state.limit < total_artists}
loadKey={total_artists > limit ? limit : total_artists}
showLoader={limit < total_artists}
loadMore={() => this.loadMore()}
/>
</section>
);
}
render() {
render = () => {
const {
spotify_available,
google_available,
} = this.props;
const source_options = [
{
value: 'all',
@ -254,14 +222,14 @@ class LibraryArtists extends React.Component {
},
];
if (this.props.spotify_available) {
if (spotify_available) {
source_options.push({
value: 'spotify',
label: i18n('services.spotify.title'),
});
}
if (this.props.google_available) {
if (google_available) {
source_options.push({
value: 'google',
label: i18n('services.google.title'),
@ -347,13 +315,12 @@ class LibraryArtists extends React.Component {
const mapStateToProps = (state) => ({
mopidy_uri_schemes: state.mopidy.uri_schemes,
mopidy_library_artists: state.mopidy.library_artists,
google_available: (state.mopidy.uri_schemes && state.mopidy.uri_schemes.includes('gmusic:')),
google_library_artists: state.google.library_artists,
spotify_available: state.spotify.access_token,
spotify_library_artists: state.spotify.library_artists,
spotify_library_artists_status: (state.ui.processes.SPOTIFY_GET_LIBRARY_ARTISTS_PROCESSOR !== undefined ? state.ui.processes.SPOTIFY_GET_LIBRARY_ARTISTS_PROCESSOR.status : null),
artists: state.core.artists,
items: state.core.items,
mopidy_library: state.core.libraries['mopidy:library:artists'] || { items_uris: [] },
spotify_library: state.core.libraries['spotify:library:artists'] || { items_uris: [] },
google_library: state.core.libraries['google:library:artists'] || { items_uris: [] },
source: (state.ui.library_artists_source ? state.ui.library_artists_source : 'all'),
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),
@ -362,9 +329,7 @@ const mapStateToProps = (state) => ({
const mapDispatchToProps = (dispatch) => ({
uiActions: bindActionCreators(uiActions, dispatch),
mopidyActions: bindActionCreators(mopidyActions, dispatch),
spotifyActions: bindActionCreators(spotifyActions, dispatch),
googleActions: bindActionCreators(googleActions, dispatch),
coreActions: bindActionCreators(coreActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(LibraryArtists);

View File

@ -304,8 +304,8 @@ const mapStateToProps = (state) => {
mopidy_uri_schemes: state.mopidy.uri_schemes,
spotify_available: state.spotify.access_token,
items: state.core.items,
mopidy_library: state.core.items['mopidy:library:playlists'] || { items_uris: [] },
spotify_library: state.core.items['spotify:library:playlists'] || { items_uris: [] },
mopidy_library: state.core.libraries['mopidy:library:playlists'] || { items_uris: [] },
spotify_library: state.core.libraries['spotify:library:playlists'] || { items_uris: [] },
load_queue: state.ui.load_queue,
me_id: (state.spotify.me ? state.spotify.me.id : false),
view: state.ui.library_playlists_view,