Using localForage and restoring from coldstore

This commit is contained in:
James Barnsley
2020-09-02 08:12:18 +12:00
parent 1d77cad28c
commit 53be543dd3
21 changed files with 4886 additions and 842 deletions

View File

@ -5,46 +5,6 @@ import { uriSource } from '../../util/helpers';
const spotifyActions = require('../../services/spotify/actions');
const mopidyActions = require('../../services/mopidy/actions');
export function getBroadcasts() {
return (dispatch, getState) => {
const config = {
method: 'GET',
timeout: 15000,
};
// Fetch the "iris_broadcasts.json" file from Gist (or "_test" for test mode)
if (getState().ui.test_mode) {
config.url = 'https://gist.githubusercontent.com/jaedb/cb3a5ee6909632abb2e0fe66d4c8c311/raw';
} else {
config.url = 'https://gist.githubusercontent.com/jaedb/b677dccf80daf3ccb2ef12e96e495677/raw';
}
$.ajax(config).then(
(response) => {
dispatch({
type: 'BROADCASTS_LOADED',
broadcasts: JSON.parse(response),
});
},
(xhr, status, error) => {
dispatch(
handleException(
'Could not fetch broadcasts from GitHub',
{
config,
xhr,
status,
error,
},
null,
false,
),
);
},
);
};
}
export function startSearch(search_type, query, only_mopidy = false) {
return {
type: 'SEARCH_STARTED',
@ -111,6 +71,27 @@ export function cachebustHttpStream() {
};
}
export function clearStorage() {
return {
type: 'CLEAR_STORAGE',
};
}
export function restoreFromColdStore(item) {
return {
type: 'RESTORE_FROM_COLD_STORE',
item,
};
}
export function updateColdStore(items) {
return {
type: 'UPDATE_COLD_STORE',
items,
};
}
/**
* Record getters
*

View File

@ -1,6 +1,7 @@
import React from 'react';
import ReactGA from 'react-ga';
import localForage from 'localforage';
import { arrayOf } from '../../util/arrays';
import URILink from '../../components/URILink';
import { uriSource, upgradeSpotifyPlaylistUris, uriType } from '../../util/helpers';
@ -12,6 +13,7 @@ import {
formatArtist,
formatPlaylist,
formatUser,
formatSimpleObjects,
} from '../../util/format';
import { handleException } from './actions';
@ -21,9 +23,6 @@ const mopidyActions = require('../mopidy/actions.js');
const spotifyActions = require('../spotify/actions.js');
const CoreMiddleware = (function () {
/**
* The actual middleware inteceptor
* */
return (store) => (next) => (action = {}) => {
const {
core,
@ -32,6 +31,12 @@ const CoreMiddleware = (function () {
spotify,
} = store.getState();
window.localForage = localForage;
// Attach store to window variable. This enables debug tools to directly call on
// store.getState()
window._store = store;
switch (action.type) {
case 'HANDLE_EXCEPTION':
const state = store.getState();
@ -343,6 +348,8 @@ const CoreMiddleware = (function () {
break;
case 'LOAD_ALBUM':
// Load from Redux store
if (
!action.force_reload
&& store.getState().core.albums[action.uri]
@ -351,46 +358,84 @@ const CoreMiddleware = (function () {
break;
}
switch (uriSource(action.uri)) {
case 'spotify':
store.dispatch(spotifyActions.getAlbum(action.uri));
// Try our cold storage
localForage.getItem(action.uri).then((result) => {
if (spotify.me) {
store.dispatch(spotifyActions.following(action.uri));
if (result) {
console.info(`Loading "${action.uri}" from database`);
store.dispatch(coreActions.restoreFromColdStore(result));
} else {
switch (uriSource(action.uri)) {
case 'spotify':
store.dispatch(spotifyActions.getAlbum(action.uri));
if (spotify.me) {
store.dispatch(spotifyActions.following(action.uri));
}
break;
default:
store.dispatch(mopidyActions.getAlbum(action.uri));
break;
}
break;
default:
store.dispatch(mopidyActions.getAlbum(action.uri));
break;
}
};
});
next(action);
break;
// TODO: Relocate this
case 'UPDATE_COLD_STORE':
console.log(action);
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.item) {
store.dispatch({
type: 'RESTORED_FROM_COLD_STORE',
item_type: uriType(action.item.uri),
item: action.item,
});
}
break;
case 'LOAD_ARTIST':
if (
!action.force_reload
&& store.getState().core.artists[action.uri]
&& store.getState().core.artists[action.uri].albums_uris
&& store.getState().core.artists[action.uri].tracks_uris) {
&& store.getState().core.artists[action.uri].albums
&& store.getState().core.artists[action.uri].tracks) {
console.info(`Loading "${action.uri}" from index`);
break;
}
switch (uriSource(action.uri)) {
case 'spotify':
store.dispatch(spotifyActions.getArtist(action.uri, true));
// Try our cold storage
localForage.getItem(action.uri).then((result) => {
if (result) {
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 (spotify.me) {
store.dispatch(spotifyActions.following(action.uri));
if (spotify.me) {
store.dispatch(spotifyActions.following(action.uri));
}
break;
default:
store.dispatch(mopidyActions.getArtist(action.uri));
break;
}
break;
default:
store.dispatch(mopidyActions.getArtist(action.uri));
break;
}
}
});
next(action);
break;
@ -529,13 +574,6 @@ const CoreMiddleware = (function () {
action.tracks = tracks_loaded;
if (artists_loaded.length > 0) {
store.dispatch(coreActions.artistsLoaded(artists_loaded));
}
if (albums_loaded.length > 0) {
store.dispatch(coreActions.albumsLoaded(albums_loaded));
}
next(action);
break;
@ -553,20 +591,14 @@ const CoreMiddleware = (function () {
}
if (raw_album.tracks) {
album.tracks_uris = [];
for (var track of raw_album.tracks) {
if (!track.album) {
track.album = formatSimpleObject(album);
}
album.tracks_uris.push(track.uri);
tracks_loaded.push(track);
}
album.tracks = raw_album.tracks.map((track) => ({
...formatTrack(track),
album: formatSimpleObject(album)
}));
}
if (raw_album.artists) {
album.artists_uris = arrayOf('uri', raw_album.artists);
artists_loaded = [...artists_loaded, ...raw_album.artists];
album.artists = formatSimpleObjects(raw_album.artists);
}
albums_loaded.push(album);
@ -581,6 +613,8 @@ const CoreMiddleware = (function () {
store.dispatch(coreActions.tracksLoaded(tracks_loaded));
}
store.dispatch(coreActions.updateColdStore(albums_loaded));
next(action);
break;
@ -618,14 +652,14 @@ const CoreMiddleware = (function () {
artist = { ...artists_index[artist.uri], ...artist };
}
/*
// Migrate nested tracks objects into references to our tracks index
if (raw_artist.tracks) {
var tracks = formatTracks(raw_artist.tracks);
var tracks_uris = arrayOf('uri', tracks);
artist.tracks_uris = tracks_uris;
tracks_loaded = [...tracks_loaded, ...tracks];
}
}*/
artists_loaded.push(artist);
}
@ -636,6 +670,8 @@ const CoreMiddleware = (function () {
store.dispatch(coreActions.tracksLoaded(tracks_loaded));
}
store.dispatch(coreActions.updateColdStore(artists_loaded));
next(action);
break;

View File

@ -140,6 +140,46 @@ export default function reducer(core = {}, action) {
return { ...core, users };
case 'RESTORED_FROM_COLD_STORE':
switch (action.item_type) {
case 'artist':
return {
...core,
artists: {
...core.artists,
[action.item.uri]: {
...core.artists[action.item.uri],
...action.item,
},
},
};
case 'album':
return {
...core,
albums: {
...core.albums,
[action.item.uri]: {
...core.albums[action.item.uri],
...action.item,
},
},
};
case 'playlist':
return {
...core,
playlists: {
...core.playlists,
[action.item.uri]: {
...core.albums[action.item.uri],
...action.item,
},
},
};
default:
return core;
}
/**
* Remove an item from an index
* */
@ -294,12 +334,6 @@ export default function reducer(core = {}, action) {
},
};
case 'UPDATE_PINNED':
return {
...core,
pinned: action.pinned,
};
default:
return core;
}

View File

@ -0,0 +1,6 @@
export function restoreFromColdStore(item) {
return {
type: 'RESTORE_FROM_COLD_STORE',
item,
};
}

View File

@ -264,7 +264,7 @@ const localstorageMiddleware = (function () {
},
);
break;
/*
case 'SUPPRESS_BROADCAST':
var ui = storage.get('ui');
if (ui.suppressed_broadcasts !== undefined) {
@ -282,6 +282,7 @@ const localstorageMiddleware = (function () {
},
);
break;
*/
/**
* Experimental saving of stores to localStorage

View File

@ -15,6 +15,8 @@ import {
formatPlaylist,
formatUser,
formatAlbum,
formatArtists,
formatAlbums,
} from '../../util/format';
import URILink from '../../components/URILink';
import { i18n } from '../../locale';
@ -1078,20 +1080,14 @@ export function getArtist(uri, full = false) {
return (dispatch, getState) => {
// Start with an empty object
// As each requests completes, they'll add to this object
const artist = {};
let artist = {};
// We need our artist, obviously
const requests = [
request(dispatch, getState, `artists/${getFromUri('artistid', uri)}`, 'GET', false, true)
.then(
(response) => {
Object.assign(artist, response);
},
(error) => {
dispatch(coreActions.handleException(
'Could not load artist',
error,
));
artist = { ...artist, ...response };
},
),
];
@ -1102,13 +1098,7 @@ export function getArtist(uri, full = false) {
request(dispatch, getState, `artists/${getFromUri('artistid', uri)}/top-tracks?country=${getState().spotify.country}`)
.then(
(response) => {
Object.assign(artist, response);
},
(error) => {
dispatch(coreActions.handleException(
'Could not load artist\'s top tracks',
error,
));
artist = { ...artist, tracks: formatTracks(response.tracks) };
},
),
);
@ -1117,47 +1107,38 @@ export function getArtist(uri, full = false) {
request(dispatch, getState, `artists/${getFromUri('artistid', uri)}/related-artists`)
.then(
(response) => {
dispatch(coreActions.artistsLoaded(response.artists));
Object.assign(artist, { related_artists_uris: arrayOf('uri', response.artists) });
},
(error) => {
dispatch(coreActions.handleException(
'Could not load artist\'s related artists',
error,
));
artist = { ...artist, related_artists: formatArtists(response.artists) };
},
),
);
}
// Run our requests
$.when.apply($, requests).then(() => {
dispatch(coreActions.artistLoaded(artist));
// Get supporting biography and imagery
if (artist.musicbrainz_id) {
dispatch(lastfmActions.getArtist(artist.uri, false, artist.musicbrainz_id));
} else {
dispatch(lastfmActions.getArtist(artist.uri, artist.name.replace('&', 'and')));
}
dispatch(coreActions.artistLoaded(artist));
// Now go get our artist albums
// Get all their albums
if (full) {
request(dispatch, getState, `artists/${getFromUri('artistid', uri)}/albums?market=${getState().spotify.country}`)
.then(
(response) => {
dispatch({
type: 'SPOTIFY_ARTIST_ALBUMS_LOADED',
artist_uri: uri,
data: response,
});
},
(error) => {
dispatch(coreActions.handleException(
'Could not load artist\'s albums',
error,
));
},
);
let albums = [];
const fetchAlbums = (endpoint) => request(dispatch, getState, endpoint)
.then((response) => {
albums = [...albums, ...formatAlbums(response.items)];
if (response.next) {
fetchAlbums(response.next);
} else {
dispatch(coreActions.artistLoaded({
uri: artist.uri,
albums,
}));
}
});
fetchAlbums(`artists/${getFromUri('artistid', uri)}/albums?market=${getState().spotify.country}`);
}
});
};
@ -1333,21 +1314,18 @@ export function getAlbum(uri) {
const album = {
...formatAlbum(response),
artists_uris: arrayOf('uri', response.artists),
tracks_uris: arrayOf('uri', tracks),
tracks_more: response.tracks.next,
tracks_total: response.tracks.total,
};
// add our album to all the tracks
for (var i = 0; i < tracks.length; i++) {
tracks[i].album = {
album.tracks = tracks.map((track) => ({
...track,
album: {
name: album.name,
uri: album.uri,
};
}
},
}));
dispatch(coreActions.albumLoaded(album));
dispatch(coreActions.tracksLoaded(tracks));
// now get all the artists for this album (full objects)
// we do this to get the artist artwork

View File

@ -1,5 +1,45 @@
import { generateGuid } from '../../util/helpers';
import { handleException } from '../core/actions';
export function getBroadcasts() {
return (dispatch, getState) => {
const config = {
method: 'GET',
timeout: 15000,
};
// Fetch the "iris_broadcasts.json" file from Gist (or "_test" for test mode)
if (getState().ui.test_mode) {
config.url = 'https://gist.githubusercontent.com/jaedb/cb3a5ee6909632abb2e0fe66d4c8c311/raw';
} else {
config.url = 'https://gist.githubusercontent.com/jaedb/b677dccf80daf3ccb2ef12e96e495677/raw';
}
$.ajax(config).then(
(response) => {
dispatch({
type: 'BROADCASTS_LOADED',
broadcasts: JSON.parse(response),
});
},
(xhr, status, error) => {
dispatch(
handleException(
'Could not fetch broadcasts from GitHub',
{
config,
xhr,
status,
error,
},
null,
false,
),
);
},
);
};
}
export function setWindowTitle(title = null, play_state = null) {
return {

View File

@ -197,6 +197,8 @@ export default function reducer(ui = {}, action) {
}
return { ...ui, processes };
case 'SUPPRESS_BROADCAST':
return { ...ui, suppressed_broadcasts: [...(ui.suppressed_broadcasts || []), action.key] };
default:
return ui;