Hosing out old code, standardizing item loader

This commit is contained in:
James Barnsley
2020-10-05 21:13:21 +13:00
parent 4dcb29b51d
commit 4e0a5a7f7d
16 changed files with 877 additions and 1919 deletions

File diff suppressed because it is too large Load Diff

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
// These are automatically injected to built HTML
var build = "1601711067";
var build = "1601884219";
var version = "3.52.4";
// Construct the script tag

View File

@ -321,7 +321,7 @@ export class App extends React.Component {
/>
<Route
exact
path="/discover/categories/:id"
path="/discover/categories/:uri"
component={DiscoverCategory}
/>
<Route

View File

@ -14,10 +14,10 @@ export default memo(({
{
categories.map((category) => (
<GridItem
key={category.id}
key={category.uri}
type="category"
item={category}
link={`/discover/categories/${encodeURIComponent(category.id)}`}
link={`/discover/categories/${encodeURIComponent(category.uri)}`}
/>
))
}

View File

@ -204,6 +204,12 @@ export function libraryLoaded(library) {
library,
};
}
export function unloadLibrary(uri) {
return {
type: 'UNLOAD_LIBRARY',
uri,
};
}
export function itemsLoaded(items) {
return {
type: 'ITEMS_LOADED',
@ -213,6 +219,12 @@ export function itemsLoaded(items) {
export function itemLoaded(item) {
return itemsLoaded([item]);
}
export function unloadItem(uri) {
return {
type: 'UNLOAD_ITEM',
uri,
};
}
export function tracksLoaded(tracks) {
return {

View File

@ -23,6 +23,97 @@ const mopidyActions = require('../mopidy/actions.js');
const googleActions = require('../google/actions.js');
const spotifyActions = require('../spotify/actions.js');
/**
* Ensure we have an item in our index
* If it's not there, attempt to fetch it from our cold storage
* If it's not their either, call the provided fetch()
*
* @param {*} Object { store, action, fetch, dependents}
*/
const ensureItemLoaded = ({
store,
action,
fetch,
dependents = [],
}) => {
const {
uri,
options: {
forceRefetch,
full,
},
} = action;
const {
core: {
items: {
[uri]: item,
} = {},
} = {},
} = store.getState();
// Forced refetch bypasses everything
if (forceRefetch) {
console.info(`Force-refetching "${uri}"`);
fetch();
return;
}
// Already-loaded asset; check we have all of it's dependents
if (item) {
const loadableDependents = dependents.filter((k) => item[k] && item[k].length > 0);
if (!full || (loadableDependents.length === dependents.length)) {
console.info(`"${uri}" already in index`);
loadableDependents.forEach(
(dependent) => store.dispatch(coreActions.loadItems(item[dependent])),
);
store.dispatch(uiActions.stopLoading(uri));
return;
}
}
localForage.getItem(uri).then((restoredItem) => {
if (!restoredItem) {
fetch();
return;
}
const loadableDependents = dependents.filter(
(k) => restoredItem[k] && restoredItem[k].length > 0,
);
console.info(`Restoring "${uri}" from database`);
store.dispatch(coreActions.restoreItemsFromColdStore([restoredItem]));
if (full) {
// We already have the dependents of our restored item, so restore them.
// We assume that because THIS item is in the coldstore, its dependents
// are as well.
if (dependents.length && loadableDependents.length === dependents.length) {
const dependentUris = loadableDependents.reduce(
(acc, dependent) => [...acc, ...restoredItem[dependent]],
[],
);
console.info(`Restoring ${dependentUris.length} dependents from database`);
const restoreAllDependents = dependentUris.map(
(dependentUri) => localForage.getItem(dependentUri),
);
Promise.all(restoreAllDependents).then(
(dependentItems) => {
store.dispatch(
coreActions.restoreItemsFromColdStore(
compact(dependentItems), // Squash nulls (ie items not found in coldstore)
),
);
},
);
} else {
fetch();
}
}
});
}
const CoreMiddleware = (function () {
return (store) => (next) => (action = {}) => {
const {
@ -304,7 +395,7 @@ const CoreMiddleware = (function () {
break;
case 'LOAD_TRACK': {
const fetchTrack = () => {
const fetch = () => {
switch (uriSource(action.uri)) {
case 'spotify':
store.dispatch(spotifyActions.getTrack(action.uri, action.options));
@ -319,154 +410,67 @@ const CoreMiddleware = (function () {
break;
}
};
if (action.options.forceRefetch) {
console.info(`Force-refetching "${action.uri}"`);
fetchTrack();
break;
}
if (
store.getState().core.items[action.uri]
&& store.getState().core.items[action.uri].images
) {
console.info(`${action.uri}" already in index`);
store.dispatch(uiActions.stopLoading(action.uri));
break;
}
localForage.getItem(action.uri).then((result) => {
if (result) {
console.info(`Loading "${action.uri}" from database`);
store.dispatch(coreActions.restoreItemsFromColdStore([result]));
if (!result.images) {
fetchTrack();
}
} else {
fetchTrack();
}
ensureItemLoaded({
store,
action,
fetch,
dependents: ['images'],
});
next(action);
break;
}
case 'LOAD_ALBUM':
const fetchAlbum = () => {
case 'LOAD_ALBUM': {
const fetch = () => {
switch (uriSource(action.uri)) {
case 'spotify':
store.dispatch(spotifyActions.getAlbum(action.uri, action.options));
if (spotify.me) {
store.dispatch(spotifyActions.following(action.uri));
}
break;
default:
store.dispatch(mopidyActions.getAlbum(action.uri, action.options));
break;
};
};
if (action.options.forceRefetch) {
console.info(`Force-refetching "${action.uri}"`);
fetchAlbum();
break;
}
if (
store.getState().core.items[action.uri]
&& (!action.options.full || store.getState().core.items[action.uri].tracks)
) {
console.info(`${action.uri}" already in index`);
store.dispatch(uiActions.stopLoading(action.uri));
break;
}
localForage.getItem(action.uri).then((result) => {
if (result) {
console.info(`Loading "${action.uri}" from database`);
store.dispatch(coreActions.restoreItemsFromColdStore([result]));
// We don't have the complete Album, so refetch
if (!result.tracks) {
fetchAlbum();
}
} else {
fetchAlbum();
}
ensureItemLoaded({
store,
action,
fetch,
dependents: ['tracks'],
});
next(action);
break;
}
case 'LOAD_ARTIST':
const fetchArtist = () => {
case 'LOAD_ARTIST': {
const fetch = () => {
switch (uriSource(action.uri)) {
case 'spotify':
store.dispatch(spotifyActions.getArtist(action.uri, action.options));
if (spotify.me) {
store.dispatch(spotifyActions.following(action.uri));
}
break;
default:
store.dispatch(mopidyActions.getArtist(action.uri, action.options));
break;
}
};
if (action.options.forceRefetch) {
console.info(`Force-refetching "${action.uri}"`);
fetchArtist();
break;
}
if (
store.getState().core.items[action.uri]
&& (
!action.options.full
|| (
store.getState().core.items[action.uri].tracks
&& store.getState().core.items[action.uri].albums_uris
&& store.getState().core.items[action.uri].images
)
)
) {
console.info(`${action.uri}" already in index`);
store.dispatch(coreActions.loadItems(store.getState().core.items[action.uri].albums_uris));
store.dispatch(uiActions.stopLoading(action.uri));
break;
}
localForage.getItem(action.uri).then((artist) => {
if (artist) {
console.info(`Restoring "${action.uri}" and ${artist.albums_uris ? artist.albums_uris.length : 0} albums from database`);
store.dispatch(coreActions.restoreItemsFromColdStore([artist]));
if (artist.albums_uris) {
const promises = artist.albums_uris.map((albumUri) => localForage.getItem(albumUri));
console.time('restoring')
Promise.all(promises).then(
(albums) => {
store.dispatch(coreActions.restoreItemsFromColdStore(compact(albums)));
console.timeEnd('restoring')
},
);
}
if (action.options.full && (!artist.tracks || !artist.albums_uris || !artist.images)) {
fetchArtist();
}
} else {
fetchArtist();
}
ensureItemLoaded({
store,
action,
fetch,
dependents: ['tracks', 'albums_uris'],
});
next(action);
break;
}
case 'LOAD_PLAYLIST':
const fetchPlaylist = () => {
case 'LOAD_PLAYLIST': {
const fetch = () => {
switch (uriSource(action.uri)) {
case 'spotify':
store.dispatch(spotifyActions.getPlaylist(action.uri, action.options));
@ -481,121 +485,57 @@ const CoreMiddleware = (function () {
break;
}
};
if (action.options.forceRefetch) {
console.info(`Force-refetching "${action.uri}"`);
fetchPlaylist();
break;
}
if (
store.getState().core.items[action.uri]
&& (
!action.options.full
|| store.getState().core.items[action.uri].tracks
)
) {
console.info(`${action.uri}" already in index`);
store.dispatch(uiActions.stopLoading(action.uri));
break;
}
localForage.getItem(action.uri).then((result) => {
if (result) {
console.info(`Restoring "${action.uri}" from database`);
store.dispatch(coreActions.restoreItemsFromColdStore([result]));
if (!action.options.full || !result.tracks) {
fetchPlaylist();
}
} else {
fetchPlaylist();
}
ensureItemLoaded({
store,
action,
fetch,
dependents: ['tracks'],
});
next(action);
break;
}
case 'LOAD_USER':
const fetchUser = () => {
case 'LOAD_USER': {
const fetch = () => {
switch (uriSource(action.uri)) {
case 'spotify':
store.dispatch(spotifyActions.getUser(action.uri, action.options));
if (spotify.me) {
store.dispatch(spotifyActions.following(action.uri));
}
break;
default:
// No mopidy user model
break;
}
};
if (action.options.forceRefetch) {
console.info(`Force-refetching "${action.uri}"`);
fetchUser();
break;
}
if (
store.getState().core.items[action.uri]
&& (
!action.options.full
|| store.getState().core.items[action.uri].playlists_uris
)
) {
console.info(`${action.uri}" already in index`);
store.dispatch(coreActions.loadItems(store.getState().core.items[action.uri].playlists_uris));
store.dispatch(uiActions.stopLoading(action.uri));
break;
}
localForage.getItem(action.uri).then((user) => {
if (user) {
console.info(`Restoring "${action.uri}" and ${user.playlists_uris ? user.playlists_uris.length : 0} playlists from database`);
store.dispatch(coreActions.restoreItemsFromColdStore([user]));
if (user.playlists_uris) {
const promises = user.playlists_uris.map((playlistUri) => localForage.getItem(playlistUri));
Promise.all(promises).then(
(playlists) => {
store.dispatch(coreActions.restoreItemsFromColdStore(compact(playlists)));
},
);
}
if (action.options.full && !user.playlists_uris) {
fetchUser();
}
} else {
fetchUser();
}
ensureItemLoaded({
store,
dependents: ['playlists_uris'],
action,
fetch,
});
next(action);
break;
}
case 'LOAD_USERXXX':
if (
!action.options.forceRefetch
&& store.getState().core.users[action.uri]
&& store.getState().core.users[action.uri].playlists_uris) {
console.info(`Loading "${action.uri}" from index`);
break;
}
switch (uriSource(action.uri)) {
case 'spotify':
store.dispatch(spotifyActions.getUser(action.uri, action.options));
if (spotify.me) {
store.dispatch(spotifyActions.following(action.uri));
}
break;
default:
// No Mopidy mechanism for users
break;
}
case 'LOAD_CATEGORY':
const fetch = () => {
switch (uriSource(action.uri)) {
case 'spotify':
store.dispatch(spotifyActions.getCategory(action.uri, action.options));
break;
default:
// No mopidy category model
break;
}
};
ensureItemLoaded({
store,
dependents: ['playlists_uris'],
action,
fetch,
});
next(action);
break;
@ -644,7 +584,6 @@ const CoreMiddleware = (function () {
store.dispatch(coreActions.restoreLibraryFromColdStore(library));
},
);
} else {
fetchLibrary();
}
@ -653,13 +592,19 @@ const CoreMiddleware = (function () {
next(action);
break;
case 'UNLOAD_LIBRARY': {
localForage.removeItem(action.uri);
next(action);
break;
}
case 'ADD_TO_LIBRARY': {
const library = store.getState().core.libraries[action.uri];
if (library) {
library.items_uris.push(action.item.uri);
store.dispatch(coreActions.libraryLoaded(library));
} else {
// Clear our stored library. This prevents the next call to possibly restore a stale
// Clear our stored library. This prevents the next call from possibly restoring a stale
// library listing.
localForage.removeItem(action.uri);
}
@ -706,91 +651,6 @@ const CoreMiddleware = (function () {
next(action);
break;
case 'TRACKS_LOADED':
var tracks_index = { ...core.tracks };
var artists_index = core.artists;
var albums_index = core.albums;
var tracks_loaded = [];
var artists_loaded = [];
var albums_loaded = [];
for (const raw_track of action.tracks) {
var track = formatTrack(raw_track);
if (tracks_index[track.uri] !== undefined) {
track = { ...tracks_index[track.uri], ...track };
}
if (raw_track.album) {
track.album = formatSimpleObject(raw_track.album);
if (!albums_index[raw_track.album.uri]) {
albums_loaded.push(raw_track.album);
}
}
if (raw_track.artists && raw_track.artists.length > 0) {
track.artists = [];
for (var artist of raw_track.artists) {
track.artists.push(formatSimpleObject(artist));
// Not already in our index, so let's add it
if (!artists_index[artist.uri]) {
artists_loaded.push(artist);
}
}
}
tracks_loaded.push(track);
}
action.tracks = tracks_loaded;
next(action);
break;
case 'ALBUMS_LOADED':
var albums_index = { ...core.albums };
var albums_loaded = [];
var artists_loaded = [];
var tracks_loaded = [];
for (const raw_album of action.albums) {
let album = formatAlbum(raw_album);
if (albums_index[album.uri]) {
album = { ...albums_index[album.uri], ...album };
}
if (raw_album.tracks) {
album.tracks = raw_album.tracks.map((track) => ({
...formatTrack(track),
album: formatSimpleObject(album)
}));
}
if (raw_album.artists) {
album.artists = formatSimpleObjects(raw_album.artists);
}
albums_loaded.push(album);
}
action.albums = albums_loaded;
if (artists_loaded.length > 0) {
store.dispatch(coreActions.items(artists_loaded));
}
if (tracks_loaded.length > 0) {
store.dispatch(coreActions.items(tracks_loaded));
}
store.dispatch(coreActions.updateColdStore(albums_loaded));
next(action);
break;
case 'ITEMS_LOADED':
const mergedItems = [];
action.items.forEach((item) => {
@ -814,100 +674,6 @@ const CoreMiddleware = (function () {
next(action);
break;
case 'ARTISTS_LOADED':
var artists_index = { ...core.artists };
var artists_loaded = [];
var tracks_loaded = [];
for (const raw_artist of action.artists) {
var artist = formatArtist(raw_artist);
artist = { ...artists_index[artist.uri], ...artist };
artists_loaded.push(artist);
}
store.dispatch(coreActions.updateColdStore(artists_loaded));
next({
...action,
artists: artists_loaded,
});
break;
case 'PLAYLISTS_LOADED':
var playlists_index = { ...core.playlists };
var playlists_loaded = [];
var tracks_loaded = [];
for (var playlist of action.playlists) {
playlist = formatPlaylist(playlist);
// Detect editability
switch (uriSource(playlist.uri)) {
case 'm3u':
playlist.can_edit = true;
break;
case 'spotify':
if (spotify.authorization && spotify.me) {
playlist.can_edit = (playlist.owner && playlist.owner.id == spotify.me.id);
}
}
// Already have this playlist partially in our index
if (playlists_index[playlist.uri]) {
playlist = { ...playlists_index[playlist.uri], ...playlist };
// Setup placeholder tracks_uris
if (playlist.tracks_uris === undefined) {
playlist.tracks_uris = [];
}
}
// Load our tracks
if (playlist.tracks) {
var tracks = formatTracks(playlist.tracks);
var tracks_uris = arrayOf('uri', tracks);
playlist.tracks_uris = tracks_uris;
delete playlist.tracks;
tracks_loaded = [...tracks_loaded, ...tracks];
}
// Update index
playlists_loaded.push(playlist);
}
action.playlists = playlists_loaded;
if (tracks_loaded.length > 0) {
store.dispatch(coreActions.tracksLoaded(tracks_loaded));
}
next(action);
break;
case 'USERS_LOADED':
var users_index = { ...core.users };
var users_loaded = [];
for (let user of action.users) {
user = formatUser(user);
if (users_index[user.uri]) {
user = { ...users_index[user.uri], ...user };
}
users_loaded.push(user);
}
action.users = users_loaded;
next(action);
break;
case 'USER_PLAYLISTS_LOADED':
store.dispatch(coreActions.playlistsLoaded(action.playlists));
next(action);
break;
/**
* Loaded more linked assets
* Often fired during lazy-loading or async asset grabbing.

View File

@ -97,6 +97,15 @@ export default function reducer(core = {}, action) {
},
};
case 'UNLOAD_LIBRARY': {
const libraries = { ...core.libraries };
delete libraries[action.uri];
return {
...core,
libraries,
};
}
case 'USER_PLAYLISTS_LOADED':
var users = { ...core.users };
var existing_playlists_uris = [];

View File

@ -1863,7 +1863,7 @@ const MopidyMiddleware = (function () {
uri: track.uri,
});
store.dispatch(coreActions.loadItem(track.uri));
store.dispatch(coreActions.loadItem(track.uri, { full: true }));
}
break;

View File

@ -11,6 +11,8 @@ import {
upgradeSpotifyPlaylistUris,
} from '../../util/helpers';
import {
formatCategory,
formatCategories,
formatTracks,
formatPlaylist,
formatPlaylists,
@ -424,7 +426,7 @@ export function getCategories() {
(response) => {
dispatch({
type: 'SPOTIFY_CATEGORIES_LOADED',
categories: response.categories.items,
categories: formatCategories(response.categories.items),
});
},
(error) => {
@ -437,24 +439,40 @@ export function getCategories() {
};
}
export function getCategory(id, forceRefetch = false) {
export function getCategory(uri, { forceRefetch } = {}) {
return (dispatch, getState) => {
const id = getFromUri('categoryid', uri);
let endpoint = `browse/categories/${id}`;
endpoint += `?country=${getState().spotify.country}`;
endpoint += `&locale=${getState().spotify.locale}`;
if (forceRefetch) endpoint += `&refetch=${Date.now()}`;
let plEndpoint = `browse/categories/${id}/playlists`;
plEndpoint += '?limit=50';
plEndpoint += `&country=${getState().spotify.country}`;
plEndpoint += `&locale=${getState().spotify.locale}`;
if (forceRefetch) plEndpoint += `&refetch=${Date.now()}`;
request(dispatch, getState, endpoint)
.then(
(response) => {
dispatch({
type: 'SPOTIFY_CATEGORY_LOADED',
category: {
uri: `category:${response.id}`,
playlist_uris: null,
...response,
},
});
const category = formatCategory(response);
let playlists = [];
const fetchPlaylists = (plEndpoint) => request(dispatch, getState, plEndpoint)
.then((response) => {
playlists = [...playlists, ...formatPlaylists(response.playlists.items)];
if (response.playlists.next) {
fetchPlaylists(response.playlists.next);
} else {
dispatch(coreActions.itemLoaded({
...category,
playlists_uris: arrayOf('uri', playlists),
}));
dispatch(coreActions.itemsLoaded(playlists));
}
});
fetchPlaylists(plEndpoint);
},
(error) => {
dispatch(coreActions.handleException(
@ -466,33 +484,6 @@ export function getCategory(id, forceRefetch = false) {
};
}
export function getCategoryPlaylists(id, forceRefetch = false) {
return (dispatch, getState) => {
let endpoint = `browse/categories/${id}/playlists`;
endpoint += '?limit=50';
endpoint += `&country=${getState().spotify.country}`;
endpoint += `&locale=${getState().spotify.locale}`;
if (forceRefetch) endpoint += `&refetch=${Date.now()}`;
request(dispatch, getState, endpoint)
.then(
(response) => {
dispatch({
type: 'SPOTIFY_CATEGORY_PLAYLISTS_LOADED',
uri: `category:${id}`,
playlists: response.playlists,
});
},
(error) => {
dispatch(coreActions.handleException(
'Could not load category playlists',
error,
));
},
);
};
}
export function getNewReleases(forceRefetch = false) {
return (dispatch, getState) => {
let endpoint = 'browse/new-releases';
@ -1510,134 +1501,6 @@ export function getLibraryTracksAndPlayProcessor(data) {
};
}
/**
* Get all tracks for a playlist
*
* Recursively get .next until we have all tracks
* */
export function getAllPlaylistTracks(
uri,
shuffle = false,
callback_action = null,
play_next = false,
at_position = null,
offset = 0,
) {
return (dispatch, getState) => {
if (data.callback_action == 'enqueue') {
dispatch(mopidyActions.enqueueURIs(uris, data.uri, data.play_next, data.at_position, data.offset));
} else {
dispatch(mopidyActions.playURIs(uris, data.uri));
}
dispatch(uiActions.startProcess(
'SPOTIFY_GET_ALL_PLAYLIST_TRACKS_PROCESSOR',
'Loading playlist tracks',
{
uri,
next: `playlists/${getFromUri('playlistid', uri)}/tracks?market=${getState().spotify.country}`,
shuffle,
play_next,
at_position,
offset,
callback_action,
},
));
};
}
export function getAllPlaylistTracksProcessor(data) {
return (dispatch, getState) => {
request(dispatch, getState, data.next)
.then(
(response) => {
// Check to see if we've been cancelled
if (getState().ui.processes.SPOTIFY_GET_ALL_PLAYLIST_TRACKS_PROCESSOR !== undefined) {
const processor = getState().ui.processes.SPOTIFY_GET_ALL_PLAYLIST_TRACKS_PROCESSOR;
if (processor.status == 'cancelling') {
dispatch(uiActions.processCancelled('SPOTIFY_GET_ALL_PLAYLIST_TRACKS_PROCESSOR'));
return false;
}
}
// Add on our new batch of loaded tracks
let tracks = [];
const new_tracks = [];
for (const item of response.items) {
if (item.track) {
new_tracks.push(item.track);
}
}
if (data.tracks) {
tracks = [...data.tracks, ...new_tracks];
} else {
tracks = new_tracks;
}
// We got a next link, so we've got more work to be done
if (response.next) {
dispatch(uiActions.updateProcess(
'SPOTIFY_GET_ALL_PLAYLIST_TRACKS_PROCESSOR',
`Loading ${response.total - tracks.length} playlist tracks`,
{
...data,
next: response.next,
total: response.total,
remaining: response.total - tracks.length,
},
));
dispatch(uiActions.runProcess(
'SPOTIFY_GET_ALL_PLAYLIST_TRACKS_PROCESSOR',
{
...data,
next: response.next,
tracks,
},
));
} else {
// Seeing as we now have all the playlist's tracks, add them to the playlist we have
// in our index for quicker reuse next time
dispatch(coreActions.loadedMore(
'playlist',
data.uri,
'track',
{ tracks },
));
let uris = arrayOf('uri', tracks);
if (data.shuffle) {
uris = shuffle(uris);
}
// We don't bother "finishing", we just want it "finished" immediately
// This bypasses the fade transition for a more smooth transition between two
// processes that flow together
dispatch(uiActions.removeProcess('SPOTIFY_GET_ALL_PLAYLIST_TRACKS_PROCESSOR'));
if (data.callback_action == 'enqueue') {
dispatch(mopidyActions.enqueueURIs(uris, data.uri, data.play_next, data.at_position, data.offset));
} else {
dispatch(mopidyActions.playURIs(uris, data.uri));
}
}
},
(error) => {
dispatch(coreActions.handleException(
'Could not load tracks to play playlist',
error,
));
},
);
};
}
export function addTracksToPlaylist(uri, tracks_uris) {
return (dispatch, getState) => {

View File

@ -120,109 +120,13 @@ const SpotifyMiddleware = (function () {
break;
}
// TODO: This can go
case 'SPOTIFY_ARTIST_ALBUMS_LOADED':
store.dispatch(coreActions.albumsLoaded(action.data.items));
store.dispatch({
type: 'ARTIST_ALBUMS_LOADED',
artist_uri: action.artist_uri,
albums_uris: arrayOf('uri', action.data.items),
more: action.data.next,
total: action.data.total,
});
break;
case 'SPOTIFY_USER_PLAYLISTS_LOADED':
var playlists = [];
for (var i = 0; i < action.data.items.length; i++) {
var playlist = {
...action.data.items[i],
tracks_total: action.data.items[i].tracks.total,
};
// remove our tracklist. It'll overwrite any full records otherwise
delete playlist.tracks;
playlists.push(playlist);
}
store.dispatch({
type: 'PLAYLISTS_LOADED',
playlists,
});
store.dispatch({
type: 'USER_PLAYLISTS_LOADED',
key: action.key,
uris: arrayOf('uri', playlists),
more: action.data.next,
total: action.data.total,
});
break;
/*
case 'SPOTIFY_CATEGORY_PLAYLISTS_LOADED':
var playlists = []
for (var i = 0; i < action.data.playlists.items.length; i++){
var playlist = Object.assign(
{},
action.data.playlists.items[i],
{
tracks_total: action.data.playlists.items[i].tracks.total
}
)
// remove our tracklist. It'll overwrite any full records otherwise
delete playlist.tracks
playlists.push(playlist)
}
store.dispatch({
type: 'PLAYLISTS_LOADED',
playlists: playlists
});
store.dispatch({
type: 'CATEGORY_PLAYLISTS_LOADED',
key: action.key,
uris: arrayOf('uri',playlists),
more: action.data.playlists.next,
total: action.data.playlists.total
});
break
*/
case 'SPOTIFY_CATEGORY_PLAYLISTS_LOADED':
store.dispatch(coreActions.playlistsLoaded(action.playlists.items));
action.uris = arrayOf('uri', action.playlists.items);
action.more = action.playlists.next;
action.total = action.playlists.total;
delete action.playlists;
// Upgrade our URIs
action.uris = upgradeSpotifyPlaylistUris(action.uris);
next(action);
break;
case 'SPOTIFY_CATEGORY_PLAYLISTS_LOADED_MORE':
store.dispatch({
type: 'SPOTIFY_CATEGORY_PLAYLISTS_LOADED',
uri: action.uri,
playlists: action.data.playlists,
});
break;
case 'SPOTIFY_CATEGORY_LOADED':
store.dispatch({
type: 'SPOTIFY_CATEGORIES_LOADED',
categories: [action.category],
});
case 'SPOTIFY_FLUSH_LIBRARY': {
store.dispatch(coreActions.unloadLibrary('spotify:library:artists'));
store.dispatch(coreActions.unloadLibrary('spotify:library:albums'));
store.dispatch(coreActions.unloadLibrary('spotify:library:playlists'));
store.dispatch(coreActions.unloadLibrary('spotify:library:tracks'));
break;
}
case 'SPOTIFY_CATEGORIES_LOADED':
var categories_index = { ...spotify.categories };

View File

@ -78,7 +78,7 @@ export default function reducer(spotify = {}, action) {
case 'SPOTIFY_NEW_RELEASES_LOADED':
var new_releases = [];
if (spotify.new_releases) {
new_releases = Object.assign([], spotify.new_releases);
new_releases = Object.assign([], spotify.new_releases);
}
return {
...spotify,
@ -96,7 +96,6 @@ export default function reducer(spotify = {}, action) {
};
}
return {
...spotify,
discover: [...spotify.discover, ...[action.data]],
};
@ -106,7 +105,6 @@ export default function reducer(spotify = {}, action) {
case 'SPOTIFY_RECOMMENDATIONS_LOADED':
return {
...spotify,
recommendations: {
artists_uris: action.artists_uris,
@ -117,7 +115,6 @@ export default function reducer(spotify = {}, action) {
case 'SPOTIFY_FAVORITES_LOADED':
return {
...spotify,
favorite_artists: action.artists_uris,
favorite_tracks: action.tracks_uris,
@ -127,7 +124,6 @@ export default function reducer(spotify = {}, action) {
var { autocomplete_results } = spotify;
autocomplete_results[action.field_id] = { loading: true };
return {
...spotify,
autocomplete_results,
};
@ -137,7 +133,6 @@ export default function reducer(spotify = {}, action) {
autocomplete_results[action.field_id] = action.results;
autocomplete_results[action.field_id].loading = false;
return {
...spotify,
autocomplete_results,
};
@ -148,14 +143,12 @@ export default function reducer(spotify = {}, action) {
delete autocomplete_results[action.field_id];
}
return {
...spotify,
autocomplete_results,
};
case 'SPOTIFY_GENRES_LOADED':
return {
...spotify,
genres: action.genres,
};
@ -172,69 +165,6 @@ export default function reducer(spotify = {}, action) {
}
return { ...spotify, categories };
case 'SPOTIFY_CATEGORY_PLAYLISTS_LOADED':
var categories = { ...spotify.categories };
var playlists_uris = [];
if (categories[action.uri] && categories[action.uri].playlists_uris) {
playlists_uris = categories[action.uri].playlists_uris;
}
var category = {
...categories[action.uri],
playlists_uris: [...playlists_uris, ...action.uris],
playlists_more: action.more,
playlists_total: action.total,
};
categories[action.uri] = category;
return { ...spotify, categories };
/**
* Library
* */
case 'SPOTIFY_FLUSH_LIBRARY':
return {
...spotify,
library_playlists: null,
library_playlists_loaded_all: null,
library_playlists_status: null,
library_albums: null,
library_albums_status: null,
library_albums_loaded_all: null,
library_artists: null,
library_artists_status: null,
library_artists_loaded_all: null,
library_tracks: null,
library_tracks_status: null,
library_tracks_loaded_all: null,
};
case 'SPOTIFY_LIBRARY_PLAYLISTS_LOADED':
if (spotify.library_playlists) {
var uris = [...spotify.library_playlists, ...action.uris];
} else {
var { uris } = action;
}
return { ...spotify, library_playlists: removeDuplicates(uris) };
case 'SPOTIFY_LIBRARY_ARTISTS_LOADED':
if (spotify.library_artists) {
var uris = [...spotify.library_artists, ...action.uris];
} else {
var { uris } = action;
}
return { ...spotify, library_artists: removeDuplicates(uris) };
case 'SPOTIFY_LIBRARY_ALBUMS_LOADED':
if (spotify.library_albums) {
var uris = [...spotify.library_albums, ...action.uris];
} else {
var { uris } = action;
}
return { ...spotify, library_albums: removeDuplicates(uris) };
case 'SPOTIFY_LIBRARY_TRACKS_LOADED':
case 'SPOTIFY_LIBRARY_TRACKS_LOADED_MORE':
var tracks = action.data.items;
@ -254,65 +184,6 @@ export default function reducer(spotify = {}, action) {
library_tracks_more: action.data.next,
};
case 'SPOTIFY_LIBRARY_PLAYLISTS_LOADED_ALL':
return { ...spotify, library_playlists_loaded_all: true };
case 'SPOTIFY_LIBRARY_PLAYLISTS_CLEAR':
return { ...spotify, library_playlists: [] };
case 'SPOTIFY_LIBRARY_ARTISTS_CLEAR':
return { ...spotify, library_artists: [] };
case 'SPOTIFY_LIBRARY_ALBUMS_CLEAR':
return { ...spotify, library_albums: [] };
case 'SPOTIFY_LIBRARY_ALBUM_CHECK':
var items = Object.assign([], spotify.library_albums);
var index = items.indexOf(action.key);
if (index > -1 && !action.in_library) {
items.splice(index, 1);
} else if (index < 0 && action.in_library) {
items.push(action.key);
}
return { ...spotify, library_albums: items };
case 'SPOTIFY_LIBRARY_ARTIST_CHECK':
var items = Object.assign([], spotify.library_artists);
var index = items.indexOf(action.key);
if (index > -1 && !action.in_library) {
items.splice(index, 1);
} else if (index < 0 && action.in_library) {
items.push(action.key);
}
return { ...spotify, library_artists: items };
case 'SPOTIFY_LIBRARY_PLAYLIST_CHECK':
var items = Object.assign([], spotify.library_playlists);
var index = items.indexOf(action.key);
if (index > -1 && !action.in_library) {
items.splice(index, 1);
} else if (index < 0 && action.in_library) {
items.push(action.key);
}
return { ...spotify, library_playlists: items };
case 'SPOTIFY_LIBRARY_TRACK_CHECK':
var items = Object.assign([], spotify.library_tracks);
var index = items.indexOf(action.key);
if (index > -1 && !action.in_library) {
items.splice(index, 1);
} else if (index < 0 && action.in_library) {
items.push(action.key);
}
return { ...spotify, library_tracks: items };
/**
* Searching
* */
case 'SPOTIFY_CLEAR_SEARCH_RESULTS':
return { ...spotify, search_results: {} };

View File

@ -258,6 +258,13 @@ const formatUsers = function (records = []) {
}
return formatted;
};
const formatCategories = function (records = []) {
const formatted = [];
for (const record of records) {
formatted.push(formatCategory(record));
}
return formatted;
};
const formatSimpleObjects = function (records = []) {
const formatted = [];
for (const record of records) {
@ -725,6 +732,38 @@ const formatClient = function (data) {
return client;
};
/**
* Spotify playlists category
*
* @param data obj
* @return obj
* */
const formatCategory = function (data) {
const category = {};
const fields = [
'id',
'uri',
'name',
'playlists_uris',
];
for (const field of fields) {
if (data.hasOwnProperty(field)) {
category[field] = data[field];
}
}
if (!category.uri && data.id) {
category.uri = `spotify:category:${data.id}`;
}
if (data.icons) {
category.images = formatImages(data.icons);
}
return category;
};
/**
* Format a snapcast client object into a universal format
*
@ -909,6 +948,8 @@ export {
formatTracks,
formatClient,
formatGroup,
formatCategory,
formatCategories,
collate,
collateLibrary,
};
@ -932,6 +973,8 @@ export default {
formatTracks,
formatClient,
formatGroup,
formatCategory,
formatCategories,
collate,
collateLibrary,
};

View File

@ -131,10 +131,9 @@ const getCurrentPusherConnection = function (connections, connectionid) {
* @param uri = string
* */
let uriSource = function (uri) {
if (!uri) {
return false;
}
const exploded = uri.split(':');
if (!uri) return false;
const exploded = `${uri}`.split(':');
return exploded[0];
};
/**
@ -146,7 +145,7 @@ let uriSource = function (uri) {
const uriType = function (uri) {
if (!uri) return null;
const exploded = uri.split(':');
const exploded = `${uri}`.split(':');
if (exploded[0] === 'm3u') {
return 'playlist';
@ -226,9 +225,9 @@ const sourceIcon = function (uri, source = null) {
* @param element = string, the element we wish to extract
* @param uri = string
* */
const getFromUri = function (element, uri = '') {
const exploded = uri.split(':');
const namespace = exploded[0];
const getFromUri = function (element, uri) {
if (!uri) return null;
const exploded = `${uri}`.split(':');
switch (element) {
case 'mbid':
@ -281,6 +280,12 @@ const getFromUri = function (element, uri = '') {
}
break;
case 'categoryid':
if (exploded[1] == 'category') {
return exploded[2];
}
break;
case 'seeds':
if (exploded[1] == 'discover') {
return exploded[2];

View File

@ -1,17 +1,20 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Header from '../../components/Header';
import Icon from '../../components/Icon';
import PlaylistGrid from '../../components/PlaylistGrid';
import LazyLoadListener from '../../components/LazyLoadListener';
import Loader from '../../components/Loader';
import ErrorMessage from '../../components/ErrorMessage';
import Button from '../../components/Button';
import * as uiActions from '../../services/ui/actions';
import * as coreActions from '../../services/core/actions';
import * as spotifyActions from '../../services/spotify/actions';
import { isLoading } from '../../util/helpers';
import { collate } from '../../util/format';
import { i18n } from '../../locale';
import { I18n, i18n } from '../../locale';
import {
makeItemSelector,
makeLoadingSelector,
} from '../../util/selectors';
class DiscoverCategory extends React.Component {
componentDidMount() {
@ -20,23 +23,15 @@ class DiscoverCategory extends React.Component {
}
componentDidUpdate = ({
match: {
params: {
id: prevId,
},
},
uri: prevUri,
category: prevCategory,
}) => {
const {
match: {
params: {
id,
},
},
uri,
category,
} = this.props;
if (prevId !== id) this.loadCategory();
if (prevUri !== uri) this.loadCategory();
if (!prevCategory && category) this.setWindowTitle(category);
}
@ -54,105 +49,104 @@ class DiscoverCategory extends React.Component {
loadCategory = () => {
const {
uri,
category,
match: {
params: { id },
},
spotifyActions: {
getCategory,
getCategoryPlaylists,
coreActions: {
loadItem,
},
} = this.props;
if (!category) {
getCategory(id);
}
if (!category.playlists_uris) {
getCategoryPlaylists(id);
loadItem(uri);
}
}
loadMore = () => {
refresh = () => {
const {
spotifyActions: {
getMore,
uri,
uiActions: {
hideContextMenu,
},
category: {
playlists_more,
},
match: {
params: {
id,
},
coreActions: {
loadItem,
},
} = this.props;
getMore(
playlists_more,
null,
{
type: 'SPOTIFY_CATEGORY_PLAYLISTS_LOADED_MORE',
uri: `category:${id}`,
},
);
hideContextMenu();
loadItem(uri, { forceRefetch: true });
}
render = () => {
const {
category: categoryProp,
category,
playlists,
load_queue,
loading,
uiActions,
uri,
} = this.props;
if (isLoading(load_queue, ['spotify_browse/categories/'])) {
if (loading) {
return <Loader body loading />;
}
if (!category) {
return (
<div className="view discover-categories-view">
<Header>
<Icon name="mood" type="material" />
{(categoryProp ? categoryProp.name : i18n('discover.category.category'))}
</Header>
<Loader body loading />
</div>
<ErrorMessage type="not-found" title="Not found">
<p>
<I18n path="errors.uri_not_found" uri={uri} />
</p>
</ErrorMessage>
);
}
if (!categoryProp) {
return null;
}
const category = collate(categoryProp, { playlists });
const options = (
<Button
noHover
onClick={this.refresh}
tracking={{ category: 'DiscoverCategory', action: 'Refresh' }}
>
<Icon name="refresh" />
<I18n path="actions.refresh" />
</Button>
);
return (
<div className="view discover-categories-view">
<Header uiActions={uiActions}>
<Header uiActions={uiActions} options={options}>
<Icon name="mood" type="material" />
{category.name}
</Header>
<div className="content-wrapper">
<section className="grid-wrapper">
<PlaylistGrid playlists={category.playlists} />
<PlaylistGrid playlists={playlists} />
</section>
<LazyLoadListener
loadKey={category.playlists_more}
showLoader={category.playlists_more}
loadMore={this.loadMore}
/>
</div>
</div>
);
}
}
const mapStateToProps = (state, ownProps) => ({
load_queue: state.ui.load_queue,
playlists: state.core.playlists,
category: (state.spotify.categories && state.spotify.categories[`category:${ownProps.match.params.id}`] !== undefined ? state.spotify.categories[`category:${ownProps.match.params.id}`] : false),
});
const mapStateToProps = (state, ownProps) => {
const uri = decodeURIComponent(ownProps.match.params.uri);
const loadingSelector = makeLoadingSelector([`(.*)${uri}(.*)`]);
const categorySelector = makeItemSelector(uri);
const category = categorySelector(state);
let playlists = null;
if (category && category.playlists_uris) {
const playlistsSelector = makeItemSelector(category.playlists_uris);
playlists = playlistsSelector(state);
}
return {
uri,
loading: loadingSelector(state),
playlists,
category,
};
};
const mapDispatchToProps = (dispatch) => ({
uiActions: bindActionCreators(uiActions, dispatch),
coreActions: bindActionCreators(coreActions, dispatch),
spotifyActions: bindActionCreators(spotifyActions, dispatch),
});