Track loading (current_track and next_track); Add to playlist

This commit is contained in:
James Barnsley
2020-09-09 07:54:19 +12:00
parent 9b3c0bcefe
commit 3e4fdd2c2f
14 changed files with 321 additions and 414 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -106,7 +106,7 @@
// Release details
// These are automatically injected to built HTML
var build = "1599416957";
var build = "1599594817";
var version = "3.52.4";
// Construct the script tag

View File

@ -229,6 +229,7 @@ artist:
overview:
title: Overview
top_tracks: Top tracks
albums: Albums
related_artists:
title: Related artists
more: All related artists

View File

@ -107,16 +107,17 @@ export function updateColdStore(items) {
* relevant service to load the record - all from one neat package.
* */
export function loadItems(uris, forceRefetch = false) {
export function loadItems(uris, forceRefetch = false, callbackAction = null) {
return {
type: 'LOAD_ITEMS',
uris,
forceRefetch,
callbackAction,
};
}
export function loadItem(uri, forceRefetch = false) {
return loadItems([uri], forceRefetch);
export function loadItem(uri, forceRefetch = false, callbackAction) {
return loadItems([uri], forceRefetch, callbackAction);
}
export function loadTrack(uri, forceRefetch = false) {

View File

@ -319,6 +319,7 @@ const CoreMiddleware = (function () {
type: `LOAD_${uriType(uri).toUpperCase()}`,
uri,
forceRefetch: action.forceRefetch,
callbackAction: action.callbackAction,
});
});
break;
@ -345,7 +346,10 @@ const CoreMiddleware = (function () {
fetchTrack();
break;
}
if (store.getState().core.items[action.uri]) {
if (
store.getState().core.items[action.uri]
&& store.getState().core.items[action.uri].images
) {
console.info(`Using "${action.uri}" from index`);
break;
}
@ -354,6 +358,10 @@ const CoreMiddleware = (function () {
if (result) {
console.info(`Loading "${action.uri}" from database`);
store.dispatch(coreActions.restoreItemsFromColdStore([result]));
if (!result.images) {
fetchTrack();
}
} else {
fetchTrack();
}
@ -435,19 +443,34 @@ const CoreMiddleware = (function () {
if (
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].albums_uris
&& store.getState().core.items[action.uri].images
) {
console.info(`Using "${action.uri}" from index`);
break;
}
localForage.getItem(action.uri).then((artist) => {
if (artist) {
console.info(`Restoring "${action.uri}" and ${artist.albums_uris.length} albums from database`);
const promises = artist.albums_uris.map((albumUri) => localForage.getItem(albumUri));
Promise.all(promises).then(
(albums) => store.dispatch(coreActions.restoreItemsFromColdStore(compact(albums))),
);
store.dispatch(coreActions.restoreLibraryFromColdStore(artist));
} else {
fetchArtist();
}
});
localForage.getItem(action.uri).then((result) => {
if (result) {
console.info(`Restoring "${action.uri}" from database`);
store.dispatch(coreActions.restoreItemsFromColdStore([result]));
if (!result.tracks || !result.albums || !result.images) {
if (!result.tracks || !result.albums_uris || !result.images) {
fetchArtist();
}
} else {
@ -459,7 +482,6 @@ const CoreMiddleware = (function () {
break;
case 'LOAD_PLAYLIST':
console.log(action);
const fetchPlaylist = () => {
switch (uriSource(action.uri)) {
case 'spotify':
@ -585,7 +607,7 @@ const CoreMiddleware = (function () {
localForage.getItem(action.uri).then((library) => {
if (library) {
console.info(`Restoring "${action.uri}" and ${library.items_uris.length} items from db`);
console.info(`Restoring "${action.uri}" and ${library.items_uris.length} items from database`);
const promises = library.items_uris.map((libraryItem) => localForage.getItem(libraryItem));
Promise.all(promises).then(

View File

@ -76,7 +76,10 @@ export default function reducer(core = {}, action) {
};
case 'ITEMS_LOADED':
const mergedItems = action.items.reduce((obj, item) => (obj[item.uri] = item, obj), {});
const mergedItems = action.items.reduce(
(obj, item) => (obj[item.uri] = item, obj),
{},
);
return {
...core,
items: {

View File

@ -17,6 +17,7 @@ import {
formatSimpleObject,
getTrackIcon,
formatArtists,
formatArtist,
} from '../../util/format';
import {
arrayOf,
@ -1566,55 +1567,23 @@ const MopidyMiddleware = (function () {
...response,
uri: response.uri,
type: 'playlist',
is_completely_loaded: true,
provider: 'mopidy',
tracks: (response.tracks ? response.tracks : []),
tracks_total: (response.tracks ? response.tracks.length : 0),
can_edit: true,
};
// tracks? get the full track objects
if (playlist.tracks.length > 0) {
store.dispatch({
type: 'MOPIDY_RESOLVE_PLAYLIST_TRACKS',
tracks: playlist.tracks,
key: playlist.uri,
request(store, 'library.lookup', { uris: arrayOf('uri', response.tracks) })
.then((tracksResponse) => {
const tracks = response.tracks.map((simpleTrack) => {
const fullTracks = tracksResponse[simpleTrack.uri];
return {
...simpleTrack,
...(fullTracks.length ? fullTracks[0] : {}),
};
});
playlist.tracks = tracks;
store.dispatch(coreActions.itemLoaded(playlist));
});
}
store.dispatch(coreActions.playlistLoaded(playlist));
});
break;
case 'MOPIDY_RESOLVE_PLAYLIST_TRACKS':
var tracks = Object.assign([], action.tracks);
var uris = arrayOf('uri', tracks);
request(store, 'library.lookup', { uris })
.then((response) => {
for (const uri in response) {
if (response.hasOwnProperty(uri)) {
var track = response[uri][0];
if (track) {
// find the track reference, and drop in the full track data
function getByURI(trackReference) {
return track.uri == trackReference.uri;
}
const trackReferences = tracks.filter(getByURI);
// there could be multiple instances of this track, so accommodate this
for (let j = 0; j < trackReferences.length; j++) {
const key = tracks.indexOf(trackReferences[j]);
tracks[key] = track;
}
}
}
}
store.dispatch({
type: 'PLAYLIST_TRACKS',
tracks,
key: action.key,
});
});
break;
@ -1873,36 +1842,22 @@ const MopidyMiddleware = (function () {
for (const raw_artist of response[0].artists) {
// We're only interested in the artist we asked for
if (raw_artist.uri === artist.uri) {
artist = { ...raw_artist };
artist = { ...formatArtist(raw_artist) };
}
}
// Add our tracks and albums
artist.albums_uris = arrayOf('uri', albums);
artist.tracks = response;
artist.tracks = formatTracks(response);
store.dispatch(coreActions.itemLoaded(artist));
store.dispatch(lastfmActions.getArtist(artist.uri, artist.name, artist.musicbrainz_id));
// Load supprting information from LastFM and Discogs
const existing_artist = store.getState().core.items[artist.uri];
if (existing_artist) {
if (!existing_artist.images) {
if (store.getState().spotify.enabled) {
store.dispatch(spotifyActions.getArtistImages(artist));
} else {
store.dispatch(discogsActions.getArtistImages(artist.uri, artist));
}
}
// Get biography and other stats from LastFM
if (!existing_artist.biography) {
// TODO: Move this condition into lastfmActions
if (artist.musicbrainz_id) {
store.dispatch(lastfmActions.getArtist(artist.uri, false, artist.musicbrainz_id));
} else {
store.dispatch(lastfmActions.getArtist(artist.uri, artist.name));
}
}
if (store.getState().spotify.enabled) {
store.dispatch(spotifyActions.getArtistImages(artist));
} else {
store.dispatch(discogsActions.getArtistImages(artist.uri, artist));
}
});
break;
@ -1982,24 +1937,13 @@ const MopidyMiddleware = (function () {
var track = formatTrack(action.tl_track);
if (track.uri) {
// Deliver the data we've got already
store.dispatch({
type: 'CURRENT_TRACK_LOADED',
track,
uri: track.uri,
});
// Now attempt to get supporting images
if (store.getState().core.tracks[track.uri] === undefined || store.getState().core.tracks[track.uri].images === undefined) {
// We've got Spotify running, and it's a spotify track - go straight to the source!
if (store.getState().spotify.enabled && uriSource(track.uri) == 'spotify') {
store.dispatch(spotifyActions.getTrack(track.uri));
// Some other source, rely on Mopidy backends to do their work
} else {
store.dispatch(mopidyActions.getImages([track.uri]));
}
}
store.dispatch(coreActions.loadItem(track.uri));
}
break;

View File

@ -1041,7 +1041,9 @@ export function getArtist(uri, full = false, forceRefetch = false) {
request(dispatch, getState, endpoint, 'GET', false, true)
.then(
(response) => {
dispatch(coreActions.itemLoaded(formatArtist(response)));
const artist = formatArtist(response);
dispatch(coreActions.itemLoaded(artist));
dispatch(lastfmActions.getArtist(uri, artist.name, artist.mbid));
},
);
@ -1058,8 +1060,9 @@ export function getArtist(uri, full = false, forceRefetch = false) {
} else {
dispatch(coreActions.itemLoaded({
uri,
albums,
albums_uris: arrayOf('uri', albums),
}));
dispatch(coreActions.itemsLoaded(albums));
}
});
fetchAlbums(`artists/${getFromUri('artistid', uri)}/albums?limit=50&include_groups=album,single&market=${getState().spotify.country}`);
@ -1257,18 +1260,11 @@ export function createPlaylist(name, description, is_public, is_collaborative) {
request(dispatch, getState, `users/${getState().spotify.me.id}/playlists/`, 'POST', data)
.then(
(response) => {
dispatch({
type: 'PLAYLIST_LOADED',
key: response.uri,
playlist: {
...response,
can_edit: true,
tracks: [],
tracks_more: null,
tracks_total: 0,
},
});
dispatch(coreActions.itemLoaded({
...formatPlaylist(response),
can_edit: true,
tracks: [],
}));
dispatch({
type: 'LIBRARY_PLAYLISTS_LOADED',
@ -1372,7 +1368,7 @@ export function getPlaylist(uri, forceRefetch = false, callbackAction = null) {
dispatch(coreActions.itemLoaded({
...formatPlaylist(response),
user: formatUser(response.owner),
can_edit: (getState().spotify.me && getState().spotify.me.id === response.owner.id),
tracks,
description,
}));

View File

@ -363,7 +363,7 @@ const formatArtist = function (data) {
'biography_link',
'biography_publish_date',
'related_artists',
'albums',
'albums_uris',
'tracks',
];
@ -424,7 +424,7 @@ const formatPlaylist = function (data) {
'followers',
'last_modified',
'can_edit',
'owner',
'user',
'tracks',
];
@ -466,8 +466,8 @@ const formatPlaylist = function (data) {
playlist.last_modified = data.added_at;
}
if (data.owner) {
playlist.owner = {
if (data.owner && playlist.user === undefined) {
playlist.user = {
id: data.owner.id,
uri: data.owner.uri,
name: (data.owner.display_name || data.owner.id),

View File

@ -20,9 +20,6 @@ import Loader from '../components/Loader';
import * as coreActions from '../services/core/actions';
import * as uiActions from '../services/ui/actions';
import * as mopidyActions from '../services/mopidy/actions';
import * as pusherActions from '../services/pusher/actions';
import * as lastfmActions from '../services/lastfm/actions';
import * as spotifyActions from '../services/spotify/actions';
import {
uriSource,
getFromUri,
@ -548,9 +545,6 @@ const mapDispatchToProps = (dispatch) => ({
coreActions: bindActionCreators(coreActions, dispatch),
uiActions: bindActionCreators(uiActions, dispatch),
mopidyActions: bindActionCreators(mopidyActions, dispatch),
pusherActions: bindActionCreators(pusherActions, dispatch),
lastfmActions: bindActionCreators(lastfmActions, dispatch),
spotifyActions: bindActionCreators(spotifyActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(Artist);

View File

@ -44,9 +44,9 @@ class Playlist extends React.Component {
}
componentDidMount() {
const { coreActions: { loadPlaylist }, uri } = this.props;
const { coreActions: { loadItem }, uri } = this.props;
this.setWindowTitle();
loadPlaylist(uri);
loadItem(uri);
}
componentDidUpdate = ({
@ -57,7 +57,7 @@ class Playlist extends React.Component {
uri,
playlist,
coreActions: {
loadPlaylist,
loadItem,
},
history: {
push,
@ -69,7 +69,7 @@ class Playlist extends React.Component {
}
if (uri !== prevUri) {
loadPlaylist(uri);
loadItem(uri);
}
if (!prevPlaylist && playlist) this.setWindowTitle(playlist);

View File

@ -13,26 +13,28 @@ import * as spotifyActions from '../../services/spotify/actions';
import { sourceIcon, decodeMopidyUri } from '../../util/helpers';
import { sortItems } from '../../util/arrays';
import { I18n, i18n } from '../../locale';
import { collate } from '../../util/format';
class AddToPlaylist extends React.Component {
componentDidMount = () => {
const {
spotify_library_playlists_status,
mopidy_library_playlists_status,
spotify_library_playlists,
mopidy_library_playlists,
spotify_available,
spotifyActions,
mopidyActions,
coreActions: {
loadLibrary,
},
uiActions: {
setWindowTitle,
},
} = this.props;
if ((!spotify_library_playlists_status || spotify_library_playlists_status !== 'finished') && spotify_available) {
spotifyActions.getLibraryPlaylists();
if (!spotify_library_playlists && spotify_available) {
loadLibrary('spotify:library:playlists');
}
if ((!mopidy_library_playlists_status || mopidy_library_playlists_status !== 'finished')) {
mopidyActions.getLibraryPlaylists();
if (!mopidy_library_playlists) {
loadLibrary('mopidy:library:playlists');
}
setWindowTitle(i18n('modal.add_to_playlist.title'));
@ -50,23 +52,20 @@ class AddToPlaylist extends React.Component {
window.history.back();
}
render = () =>{
const { playlists, uris, spotify_library_playlists_status } = this.props;
render = () => {
const {
uris,
items,
spotify_library,
mopidy_library,
} = this.props;
if (!playlists) return (
<div className="empty">
<I18n path="modal.add_to_playlist.no_editable_playlists" />
</div>
);
let playlists = [
...(collate(spotify_library, { items }).items),
...(collate(mopidy_library, { items }).items),
].filter((playlist) => playlist.can_edit);
let editablePlaylists = [];
for (let uri in playlists) {
if (playlists[uri].can_edit) editablePlaylists.push(playlists[uri]);
}
editablePlaylists = sortItems(editablePlaylists, 'name');
const isLoading = spotify_library_playlists_status === 'running';
playlists = sortItems(playlists, 'name');
return (
<Modal className="modal--add-to-playlist">
@ -78,34 +77,34 @@ class AddToPlaylist extends React.Component {
plural={uris.length > 1 ? 's' : ''}
/>
</h2>
{editablePlaylists.length <= 0 && (
{playlists.length ? (
<div className="list small playlists">
{playlists.map((playlist) => (
<div
className="list__item"
key={playlist.uri}
onClick={() => this.playlistSelected(playlist.uri)}
>
<Thumbnail images={playlist.images} size="small" />
<h4 className="list__item__name">{ playlist.name }</h4>
<ul className="list__item__details details">
<li><Icon type="fontawesome" className="source" name={sourceIcon(playlist.uri)} /></li>
<li>
{playlist.tracks_total && (
<span className="mid_grey-text">
{`${playlist.tracks_total} tracks`}
</span>
)}
</li>
</ul>
</div>
))}
</div>
) : (
<div className="no-results">
<I18n path="modal.add_to_playlist.no_playlists" />
</div>
)}
<div className="list small playlists">
{editablePlaylists.map((playlist) => (
<div
className="list__item"
key={playlist.uri}
onClick={() => this.playlistSelected(playlist.uri)}
>
<Thumbnail images={playlist.images} size="small" />
<h4 className="list__item__name">{ playlist.name }</h4>
<ul className="list__item__details details">
<li><Icon type="fontawesome" className="source" name={sourceIcon(playlist.uri)} /></li>
<li>
{playlist.tracks_total && (
<span className="mid_grey-text">
{`${playlist.tracks_total} tracks`}
</span>
)}
</li>
</ul>
</div>
))}
</div>
{isLoading && <Loader body lazy loading />}
</Modal>
);
}
@ -114,11 +113,10 @@ class AddToPlaylist extends React.Component {
const mapStateToProps = (state, ownProps) => ({
uris: (ownProps.match.params.uris ? decodeURIComponent(ownProps.match.params.uris).split(',') : []),
mopidy_uri_schemes: state.mopidy.uri_schemes,
mopidy_library_playlists: state.mopidy.library_playlists,
mopidy_library_playlists_status: (state.ui.processes.MOPIDY_LIBRARY_PLAYLISTS_PROCESSOR !== undefined ? state.ui.processes.MOPIDY_LIBRARY_PLAYLISTS_PROCESSOR.status : null),
items: state.core.items,
mopidy_library: state.core.libraries['mopidy:library:playlists'] || { items_uris: [] },
spotify_library: state.core.libraries['spotify:library:playlists'] || { items_uris: [] },
spotify_available: state.spotify.access_token,
spotify_library_playlists: state.spotify.library_playlists,
spotify_library_playlists_status: (state.ui.processes.SPOTIFY_GET_LIBRARY_PLAYLISTS_PROCESSOR !== undefined ? state.ui.processes.SPOTIFY_GET_LIBRARY_PLAYLISTS_PROCESSOR.status : null),
load_queue: state.ui.load_queue,
me_id: (state.spotify.me ? state.spotify.me.id : false),
playlists: state.core.playlists,