Using core.items to contain all loaded assets

This commit is contained in:
James Barnsley
2020-09-03 12:40:38 +12:00
parent 53be543dd3
commit 6020781be0
10 changed files with 270 additions and 528 deletions

View File

@ -167,6 +167,13 @@ export function loadUserPlaylists(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 itemLoaded(item) {
return {
type: 'ITEM_LOADED',
item,
};
}
export function trackLoaded(track) {
return tracksLoaded([track]);
}
@ -177,9 +184,6 @@ export function tracksLoaded(tracks) {
};
}
export function artistLoaded(artist) {
return artistsLoaded([artist]);
}
export function artistsLoaded(artists) {
return {
type: 'ARTISTS_LOADED',

View File

@ -399,7 +399,6 @@ const CoreMiddleware = (function () {
if (action.item) {
store.dispatch({
type: 'RESTORED_FROM_COLD_STORE',
item_type: uriType(action.item.uri),
item: action.item,
});
}
@ -618,40 +617,27 @@ const CoreMiddleware = (function () {
next(action);
break;
case 'ITEM_LOADED':
const mergedItem = {
...core.items[action.item.uri] || {},
...action.item,
};
store.dispatch(coreActions.updateColdStore([mergedItem]));
next({
...action,
item: mergedItem,
});
break;
case 'ARTISTS_LOADED':
var artists_index = { ...core.artists };
var artists_loaded = [];
var tracks_loaded = [];
for (const raw_artist of action.artists) {
console.log(raw_artist);
var artist = formatArtist(raw_artist);
// Already have an artist in the index
if (artists_index[artist.uri]) {
// And we've already got some images, make sure we merge the arrays,
// rather than overwriting
if (artists_index[artist.uri].images && artist.images) {
const existing_images = artists_index[artist.uri].images;
let are_new_images = true;
// loop all extisting images to make sure we're not adding one that
// we already have
for (const existing_image of existing_images) {
// We only need to check one size, the formatter should insist on consistency
// Note that we depend on having a one-item array of images provided per action
if (existing_image.huge == artist.images[0].huge) {
are_new_images = false;
}
}
// Only if they're new images should we merge them in
if (are_new_images) {
artist.images = Object.assign([], [...existing_images, ...artist.images]);
}
}
artist = { ...artists_index[artist.uri], ...artist };
}
artist = { ...artists_index[artist.uri], ...artist };
/*
// Migrate nested tracks objects into references to our tracks index
if (raw_artist.tracks) {
@ -665,10 +651,11 @@ const CoreMiddleware = (function () {
}
action.artists = artists_loaded;
/*
if (tracks_loaded.length > 0) {
store.dispatch(coreActions.tracksLoaded(tracks_loaded));
}
*/
store.dispatch(coreActions.updateColdStore(artists_loaded));

View File

@ -66,6 +66,10 @@ export default function reducer(core = {}, action) {
* and appended to their relevant index.
* */
case 'ITEM_LOADED':
return { ...core, items: { ...core.items, [action.item.uri]: action.item } };
case 'TRACKS_LOADED':
var tracks = { ...core.tracks };
for (const track of action.tracks) {
@ -141,44 +145,16 @@ export default function reducer(core = {}, action) {
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;
}
return {
...core,
items: {
...core.items,
[action.item.uri]: {
...core.items[action.item.uri],
...action.item,
},
},
};
/**
* Remove an item from an index

View File

@ -211,9 +211,6 @@ export function getTrack(uri) {
dispatch(coreActions.trackLoaded(merged_track));
}
},
(error) => {
console.info(`LastFM: No results for track '${track_name}' by '${artist_name}'`);
},
);
};
}
@ -231,21 +228,16 @@ export function getArtist(uri, artist, mbid = false) {
.then(
(response) => {
if (response.artist) {
const artist = {
dispatch(coreActions.artistLoaded({
uri,
mbid: response.artist.mbid,
biography: response.artist.bio.content,
biography_publish_date: response.artist.bio.published,
biography_link: response.artist.bio.links.link.href,
listeners: parseInt(response.artist.stats.listeners),
};
dispatch(coreActions.artistLoaded(artist));
}));
}
},
(error) => {
console.info(`LastFM: No results for artist '${artist}'`);
},
);
};
}
@ -284,9 +276,6 @@ export function getAlbum(uri, artist, album, mbid = false) {
dispatch(coreActions.albumLoaded(album));
}
},
(error) => {
console.info(`LastFM: No results for album '${album}'`);
},
);
};
}

View File

@ -15,6 +15,7 @@ import {
formatPlaylist,
formatUser,
formatAlbum,
formatArtist,
formatArtists,
formatAlbums,
} from '../../util/format';
@ -1078,104 +1079,54 @@ export function getGenres() {
* */
export function getArtist(uri, full = false) {
return (dispatch, getState) => {
// Start with an empty object
// As each requests completes, they'll add to this object
let artist = {};
// We need our artist, obviously
const requests = [
request(dispatch, getState, `artists/${getFromUri('artistid', uri)}`, 'GET', false, true)
.then(
(response) => {
artist = { ...artist, ...response };
},
),
];
request(dispatch, getState, `artists/${getFromUri('artistid', uri)}`, 'GET', false, true)
.then(
(response) => {
dispatch(coreActions.itemLoaded(formatArtist(response)));
},
);
// Do we want a full artist, with all supporting material?
if (full) {
requests.push(
request(dispatch, getState, `artists/${getFromUri('artistid', uri)}/top-tracks?country=${getState().spotify.country}`)
.then(
(response) => {
artist = { ...artist, tracks: formatTracks(response.tracks) };
},
),
);
requests.push(
request(dispatch, getState, `artists/${getFromUri('artistid', uri)}/related-artists`)
.then(
(response) => {
artist = { ...artist, related_artists: formatArtists(response.artists) };
},
),
);
}
$.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')));
}
// Get all their albums
if (full) {
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}`);
}
});
};
}
export function getArtists(uris) {
return (dispatch, getState) => {
// now get all the artists for this album (full objects)
let ids = '';
for (let i = 0; i < uris.length; i++) {
if (ids != '') ids += ',';
ids += getFromUri('artistid', uris[i]);
}
request(dispatch, getState, `artists/?ids=${ids}`)
.then(
(response) => {
for (var i = i; i < response.length; i++) {
const artist = response;
for (var i = 0; i < artist.albums.length; i++) {
dispatch({
type: 'ALBUM_LOADED',
album: artist.albums[i],
});
}
artist.albums_uris = arrayOf('uri', artist.albums);
artist.albums_more = artist.albums.next;
dispatch(coreActions.artistLoaded(artist));
// All albums (gets all pages, may take some time to iterate them all)
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.itemLoaded({
uri,
albums,
}));
}
},
(error) => {
dispatch(coreActions.handleException(
'Could not load artists',
error,
));
},
);
});
fetchAlbums(`artists/${getFromUri('artistid', uri)}/albums?limit=50&include_groups=album,single&market=${getState().spotify.country}`);
// Get top tracks
request(dispatch, getState, `artists/${getFromUri('artistid', uri)}/top-tracks?country=${getState().spotify.country}`)
.then(
(response) => {
dispatch(coreActions.itemLoaded({
uri,
tracks: formatTracks(response.tracks),
}));
},
);
// Related artists
request(dispatch, getState, `artists/${getFromUri('artistid', uri)}/related-artists`)
.then(
(response) => {
dispatch(coreActions.itemLoaded({
uri,
related_artists: formatArtists(response.artists),
}));
},
);
}
};
}
@ -1202,7 +1153,6 @@ export function getArtistImages(artist) {
};
}
export function playArtistTopTracks(uri) {
return (dispatch, getState) => {
const { artists } = getState().core;
@ -1220,12 +1170,6 @@ export function playArtistTopTracks(uri) {
const uris = arrayOf('uri', response.tracks);
dispatch(mopidyActions.playURIs(uris, uri));
},
(error) => {
dispatch(coreActions.handleException(
'Could not play artist\'s top tracks',
error,
));
},
);
}
};
@ -1306,47 +1250,28 @@ export function getAlbum(uri) {
request(dispatch, getState, `albums/${getFromUri('albumid', uri)}`)
.then(
(response) => {
// dispatch our loaded artists (simple objects)
dispatch(coreActions.artistsLoaded(response.artists));
const tracks = Object.assign([], response.tracks.items);
const album = {
dispatch(coreActions.itemLoaded({
...formatAlbum(response),
artists_uris: arrayOf('uri', response.artists),
tracks_more: response.tracks.next,
tracks_total: response.tracks.total,
};
album.tracks = tracks.map((track) => ({
...track,
album: {
name: album.name,
uri: album.uri,
},
tracks: formatTracks(response.tracks.items),
}));
dispatch(coreActions.albumLoaded(album));
let tracks = formatTracks(response.tracks.items);
const fetchTracks = (endpoint) => request(dispatch, getState, endpoint)
.then((response) => {
tracks = [...tracks, ...formatTracks(response.items)];
if (response.next) {
fetchTracks(response.next);
} else {
dispatch(coreActions.itemLoaded({
uri,
tracks,
}));
}
});
// now get all the artists for this album (full objects)
// we do this to get the artist artwork
const artist_ids = [];
for (var i = 0; i < response.artists.length; i++) {
artist_ids.push(getFromUri('artistid', response.artists[i].uri));
if (response.tracks.next) {
fetchTracks(response.tracks.next);
}
// get all album artists as full objects
request(dispatch, getState, `artists/?ids=${artist_ids}`)
.then(
(response) => {
dispatch(coreActions.artistsLoaded(response.artists));
},
(error) => {
dispatch(coreActions.handleException(
'Could not load album\'s artists',
error,
));
},
);
},
(error) => {
dispatch(coreActions.handleException(

View File

@ -41,6 +41,7 @@ let state = {
playlists: {},
users: {},
tracks: {},
items: {},
http_streaming_enabled: false,
http_streaming_cachebuster: null,
http_streaming_url: `http://${window.location.hostname}:8000/mopidy`,
@ -151,6 +152,7 @@ const corePersistConfig = {
storage: localForage,
debug: window.test_mode,
blacklist: [
'items',
'albums',
'artists',
'playlists',

View File

@ -84,7 +84,7 @@ export class Album extends React.Component {
}
// We have just received our full album or our album artists
if ((!prevAlbum && album) || (!prevAlbum.artists && album.artists)) {
if ((!prevAlbum && album)) {
if (album.artists && album.wiki === undefined) {
getAlbum(album.uri, album.artists[0].name, album.name);
}
@ -216,7 +216,7 @@ export class Album extends React.Component {
<li>
{i18n(
'specs.tracks',
{ count: album.tracks_total || album.tracks.length },
{ count: album.tracks.length },
)}
</li>
) : null}
@ -295,10 +295,7 @@ const mapStateToProps = (state, ownProps) => {
load_queue: state.ui.load_queue,
tracks: state.core.tracks,
artists: state.core.artists,
album:
state.core.albums && state.core.albums[uri] !== undefined
? state.core.albums[uri]
: false,
album: (state.core.items[uri] || null),
albums: state.core.albums,
spotify_library_albums: state.spotify.library_albums,
local_library_albums: state.mopidy.library_albums,

View File

@ -146,29 +146,26 @@ class Artist extends React.Component {
const {
uri,
uiActions,
artist: artistProp,
artists,
albums,
tracks,
artist: {
tracks = [],
related_artists = [],
} = {},
sort,
sort_reverse,
filter,
} = this.props;
const artist = collate(
artistProp,
{
artists,
albums,
tracks,
},
);
let {
artist: {
albums = [],
} = {},
} = this.props;
if (sort && artist.albums) {
artist.albums = sortItems(artist.albums, sort, sort_reverse);
if (sort && albums) {
albums = sortItems(albums, sort, sort_reverse);
}
if (filter && artist.albums) {
artist.albums = applyFilter('type', filter, artist.albums);
if (filter && albums) {
albums = applyFilter('type', filter, albums);
}
const sort_options = [
@ -211,21 +208,21 @@ class Artist extends React.Component {
return (
<div className="body overview">
<div className={`top-tracks col col--w${artist.related_artists && artist.related_artists.length > 0 ? '70' : '100'}`}>
{artist.tracks && <h4><I18n path="artist.overview.top_tracks" /></h4>}
<div className={`top-tracks col col--w${related_artists && related_artists.length > 0 ? '70' : '100'}`}>
{tracks && <h4><I18n path="artist.overview.top_tracks" /></h4>}
<div className="list-wrapper">
<TrackList className="artist-track-list" uri={artist.uri} tracks={artist.tracks ? artist.tracks.splice(0, 10) : []} />
<TrackList className="artist-track-list" uri={uri} tracks={tracks ? tracks.slice(0, 10) : []} />
</div>
</div>
<div className="col col--w5" />
{artist.related_artists && artist.related_artists.length > 0 && (
{related_artists && related_artists.length > 0 && (
<div className="col col--w25 related-artists">
<h4><I18n path="artist.overview.related_artists.title" /></h4>
<div className="list-wrapper">
<RelatedArtists
artists={artist.related_artists.slice(0, 6)}
artists={related_artists.slice(0, 6)}
uiActions={uiActions}
/>
</div>
@ -240,7 +237,7 @@ class Artist extends React.Component {
<div className="cf" />
{artist.albums && (
{albums && (
<div className="albums">
<h4>
<div><I18n path="artist.overview.albums" /></div>
@ -275,12 +272,7 @@ class Artist extends React.Component {
</h4>
<section className="grid-wrapper no-top-padding">
<AlbumGrid albums={artist.albums} />
<LazyLoadListener
loadKey={artist.albums_more}
showLoader={artist.albums_more}
loadMore={this.loadMore}
/>
<AlbumGrid albums={albums} />
</section>
</div>
)}
@ -289,31 +281,12 @@ class Artist extends React.Component {
}
renderTracks = () => {
const {
artist: artistProp,
artists,
tracks,
} = this.props;
const artist = collate(
artistProp,
{
artists,
tracks,
},
);
const is_loading_tracks = (
!artist.tracks_uris
|| (artist.tracks_uris && !artist.tracks)
|| (artist.tracks_uris.length !== artist.tracks.length)
);
const { artist: { uri, tracks } } = this.props;
return (
<div className="body related-artists">
<section className="list-wrapper no-top-padding">
<TrackList className="artist-track-list" uri={artist.uri} tracks={artist.tracks} />
<LazyLoadListener showLoader={is_loading_tracks} />
<TrackList className="artist-track-list" uri={uri} tracks={tracks} />
</section>
</div>
);
@ -559,7 +532,7 @@ const mapStateToProps = (state, ownProps) => {
theme: state.ui.theme,
slim_mode: state.ui.slim_mode,
load_queue: state.ui.load_queue,
artist: (state.core.artists[uri] !== undefined ? state.core.artists[uri] : false),
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,