Upgrading other playlist views to use playlist providers selector

This commit is contained in:
James Barnsley
2021-05-05 21:20:00 +12:00
parent e186e51730
commit a26611261d
11 changed files with 520 additions and 435 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

@ -115,7 +115,7 @@
// Release details // Release details
// These are automatically injected to built HTML // These are automatically injected to built HTML
var build = "1619945300"; var build = "1620206341";
var version = "3.57.6"; var version = "3.57.6";
// Construct the script tag // Construct the script tag

View File

@ -15,7 +15,6 @@ import {
import { import {
arrayOf, arrayOf,
sortItems, sortItems,
indexToArray,
} from '../util/arrays'; } from '../util/arrays';
import Link from './Link'; import Link from './Link';
import Icon from './Icon'; import Icon from './Icon';
@ -28,8 +27,18 @@ import * as pusherActions from '../services/pusher/actions';
import * as mopidyActions from '../services/mopidy/actions'; import * as mopidyActions from '../services/mopidy/actions';
import * as lastfmActions from '../services/lastfm/actions'; import * as lastfmActions from '../services/lastfm/actions';
import * as spotifyActions from '../services/spotify/actions'; import * as spotifyActions from '../services/spotify/actions';
import { I18n, i18n } from '../locale'; import { I18n } from '../locale';
import { collate, encodeUri } from '../util/format'; import { encodeUri } from '../util/format';
import {
makeProcessProgressSelector,
makeProvidersSelector,
makeLibrarySelector,
} from '../util/selectors';
const processKeys = [
'MOPIDY_GET_LIBRARY_PLAYLISTS',
'SPOTIFY_GET_LIBRARY_PLAYLISTS',
];
class ContextMenu extends React.Component { class ContextMenu extends React.Component {
constructor(props) { constructor(props) {
@ -750,28 +759,21 @@ class ContextMenu extends React.Component {
setSubmenu = (name) => { setSubmenu = (name) => {
const { submenu } = this.state; const { submenu } = this.state;
if (submenu !== name && name === 'add-to-playlist') {
this.getLibraries();
}
this.setState({ submenu: name });
}
getLibraries = () => {
const { const {
spotify_available, providers,
spotify_library_playlists: {
items_uris: spotify_library,
},
mopidy_library_playlists: {
items_uris: mopidy_library,
},
coreActions: { coreActions: {
loadLibrary, loadLibrary,
}, },
} = this.props; } = this.props;
if (submenu !== name && name === 'add-to-playlist') { providers.forEach((provider) => loadLibrary(provider.uri, 'playlists'));
if (spotify_available && !spotify_library.length) {
loadLibrary('spotify:library:playlists', 'playlists');
}
if (!mopidy_library.length) {
loadLibrary('mopidy:library:playlists', 'playlists');
}
}
this.setState({ submenu: name });
} }
closeSubmenu = () => this.setState({ submenu: null }); closeSubmenu = () => this.setState({ submenu: null });
@ -779,20 +781,13 @@ class ContextMenu extends React.Component {
renderSubmenu = () => { renderSubmenu = () => {
const { submenu } = this.state; const { submenu } = this.state;
const { const {
items, playlists: allPlaylists,
spotify_library_playlists, loading_progress,
mopidy_library_playlists,
} = this.props; } = this.props;
let list = null; let list = null;
const isLoading = false;
if (submenu === 'add-to-playlist') { if (submenu === 'add-to-playlist') {
let playlists = [ let playlists = compact(allPlaylists.map((playlist) => {
...(collate(spotify_library_playlists, { items }).items || []),
...(collate(mopidy_library_playlists, { items }).items || []),
];
playlists = compact(playlists.map((playlist) => {
if (!playlist.can_edit) return null; if (!playlist.can_edit) return null;
return { return {
...playlist, ...playlist,
@ -847,7 +842,7 @@ class ContextMenu extends React.Component {
</span> </span>
</a> </a>
</div> </div>
{isLoading ? ( {loading_progress ? (
<div className="context-menu__item context-menu__item--loader"> <div className="context-menu__item context-menu__item--loader">
<Loader className="context-menu__item" mini loading /> <Loader className="context-menu__item" mini loading />
</div> </div>
@ -1345,13 +1340,23 @@ class ContextMenu extends React.Component {
} }
} }
const librarySelector = makeLibrarySelector('playlists');
const providersSelector = makeProvidersSelector('playlists');
const processProgressSelector = makeProcessProgressSelector(processKeys);
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
providers: providersSelector(state),
playlists: librarySelector(state, 'playlists'),
loading_progress: processProgressSelector(state),
menu: state.ui.context_menu, menu: state.ui.context_menu,
load_queue: state.ui.load_queue, load_queue: state.ui.load_queue,
processes: state.ui.processes, processes: state.ui.processes,
current_track: state.core.current_track, current_track: state.core.current_track,
current_tracklist: state.core.current_tracklist, current_tracklist: state.core.current_tracklist,
queue_metadata: state.core.queue_metadata, queue_metadata: state.core.queue_metadata,
pinned: state.pusher.pinned,
lastfm_authorized: state.lastfm.authorization,
// TODO: Delete all of the below
spotify_available: state.spotify.access_token, spotify_available: state.spotify.access_token,
spotify_library_playlists: state.core.libraries['spotify:library:playlists'] || { items_uris: [] }, spotify_library_playlists: state.core.libraries['spotify:library:playlists'] || { items_uris: [] },
spotify_library_artists: state.core.libraries['spotify:library:artists'] || { items_uris: [] }, spotify_library_artists: state.core.libraries['spotify:library:artists'] || { items_uris: [] },
@ -1361,8 +1366,6 @@ const mapStateToProps = (state) => ({
mopidy_library_artists: state.core.libraries['mopidy:library:artists'] || { items_uris: [] }, mopidy_library_artists: state.core.libraries['mopidy:library:artists'] || { items_uris: [] },
mopidy_library_albums: state.core.libraries['mopidy:library:albums'] || { items_uris: [] }, mopidy_library_albums: state.core.libraries['mopidy:library:albums'] || { items_uris: [] },
items: state.core.items, items: state.core.items,
pinned: state.pusher.pinned,
lastfm_authorized: state.lastfm.authorization,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({

View File

@ -149,6 +149,8 @@ services:
unlove: Unlove unlove: Unlove
youtube: youtube:
title: YouTube title: YouTube
tidal:
title: Tidal
## ##
# Components # Components

View File

@ -437,8 +437,11 @@ export function deletePlaylist(uri) {
}; };
} }
export function getLibraryPlaylists() { export function getLibraryPlaylists(uri) {
return { type: 'MOPIDY_GET_LIBRARY_PLAYLISTS' }; return {
type: 'MOPIDY_GET_LIBRARY_PLAYLISTS',
uri,
};
} }
export function getPlaylist(uri, options) { export function getPlaylist(uri, options) {

View File

@ -1668,9 +1668,19 @@ const MopidyMiddleware = (function () {
subdirectories.push(formatArtist({ ...item, loading: true })); subdirectories.push(formatArtist({ ...item, loading: true }));
subdirectoryImagesToLoad.push(item.uri); subdirectoryImagesToLoad.push(item.uri);
} else if (item.__model__ === 'Ref' && item.type === 'playlist') { } else if (item.__model__ === 'Ref' && item.type === 'playlist') {
console.debug('playlist', { item }) // Tidal moods and genres incorrectly marked as Playlist type
subdirectories.push(formatPlaylist({ ...item, loading: true })); if (
subdirectoryImagesToLoad.push(item.uri); item.uri.indexOf('tidal:mood') > -1 ||
item.uri.indexOf('tidal:genre') > -1
) {
subdirectories.push({
...item,
type: 'directory',
});
} else {
subdirectories.push(formatPlaylist({ ...item, loading: true }));
subdirectoryImagesToLoad.push(item.uri);
}
} else { } else {
subdirectories.push(item); subdirectories.push(item);
} }
@ -1770,7 +1780,7 @@ const MopidyMiddleware = (function () {
if (!raw_response.length) { if (!raw_response.length) {
store.dispatch(coreActions.libraryLoaded({ store.dispatch(coreActions.libraryLoaded({
uri: 'mopidy:library:artists', uri: 'mopidy:library:artists',
type: action.uriType, type: 'artists',
items_uris: [], items_uris: [],
})); }));
store.dispatch(uiActions.processFinished(action.type)); store.dispatch(uiActions.processFinished(action.type));
@ -1796,70 +1806,134 @@ const MopidyMiddleware = (function () {
case 'MOPIDY_GET_LIBRARY_PLAYLISTS': { case 'MOPIDY_GET_LIBRARY_PLAYLISTS': {
store.dispatch(uiActions.startProcess(action.type, { notification: false })); store.dispatch(uiActions.startProcess(action.type, { notification: false }));
request(store, 'playlists.asList') // Built-in playlist support works differently to other providers
.then((listResponse) => { if (action.uri === 'm3u:playlists') {
// Remove any Spotify playlists. These will be handled by our Spotify API request(store, 'playlists.asList')
const playlist_uris = arrayOf('uri', listResponse).filter( .then((listResponse) => {
(playlistUri) => uriSource(playlistUri) !== 'spotify', const libraryPlaylists = [];
); const playlist_uris = arrayOf('uri', listResponse).filter(
const libraryPlaylists = []; (pUri) => (pUri.indexOf('m3u') > -1),
);
store.dispatch(
uiActions.updateProcess(
action.type,
{
total: playlist_uris.length,
remaining: playlist_uris.length,
},
),
);
store.dispatch( if (playlist_uris.length) {
uiActions.updateProcess( playlist_uris.forEach((uri, index) => {
action.type, request(store, 'playlists.lookup', { uri })
{ .then((response) => {
total: playlist_uris.length, if (response) {
remaining: playlist_uris.length, libraryPlaylists.push(
}, formatPlaylist({
), name: response.name,
); uri: response.uri,
can_edit: uriSource(response.uri) === 'm3u',
last_modified: response.last_modified,
// By not including actual tracks they will be fetched when needed. We don't
// want these simple tracks because they don't contain duration, artist, etc.
tracks_total: response.tracks ? response.tracks.length : null,
}),
);
}
if (playlist_uris.length) { store.dispatch(
playlist_uris.forEach((uri, index) => { uiActions.updateProcess(
request(store, 'playlists.lookup', { uri }) action.type,
.then((response) => { {
libraryPlaylists.push( remaining: playlist_uris.length - index - 1,
formatPlaylist({ },
name: response.name, ),
uri: response.uri, );
can_edit: uriSource(response.uri) === 'm3u',
last_modified: response.last_modified,
// By not including actual tracks they will be fetched when needed. We don't
// want these simple tracks because they don't contain duration, artist, etc.
tracks_total: response.tracks ? response.tracks.length : null,
}),
);
store.dispatch( if (index === playlist_uris.length - 1) {
uiActions.updateProcess( store.dispatch(coreActions.itemsLoaded(libraryPlaylists));
action.type, store.dispatch(coreActions.libraryLoaded({
{ uri: action.uri,
remaining: playlist_uris.length - index - 1, type: 'playlists',
}, items_uris: arrayOf('uri', libraryPlaylists),
), }));
); store.dispatch(uiActions.processFinished(action.type));
}
});
});
} else {
store.dispatch(coreActions.libraryLoaded({
uri: action.uri,
type: 'playlists',
items_uris: [],
}));
store.dispatch(uiActions.stopLoading('mopidy:library:playlists'));
store.dispatch(uiActions.processFinished(action.type));
}
});
} else {
request(store, 'library.browse', { uri: action.uri })
.then((browseResponse) => {
const libraryPlaylists = [];
if (index === playlist_uris.length - 1) { store.dispatch(
store.dispatch(coreActions.itemsLoaded(libraryPlaylists)); uiActions.updateProcess(
store.dispatch(coreActions.libraryLoaded({ action.type,
uri: 'mopidy:library:playlists', {
type: action.uriType, total: browseResponse.length,
items_uris: arrayOf('uri', libraryPlaylists), remaining: browseResponse.length,
})); },
store.dispatch(uiActions.processFinished(action.type)); ),
} );
});
}); if (browseResponse.length) {
} else { browseResponse.forEach((playlist, index) => {
store.dispatch(coreActions.libraryLoaded({ request(store, 'library.lookup', { uris: [playlist.uri] })
uri: action.uri, .then((response) => {
type: 'playlists', if (response) {
items_uris: [], libraryPlaylists.push(
})); formatPlaylist({
store.dispatch(uiActions.stopLoading('mopidy:library:playlists')); name: playlist.name,
store.dispatch(uiActions.processFinished(action.type)); uri: playlist.uri,
} can_edit: uriSource(playlist.uri) === 'm3u',
}); last_modified: playlist.last_modified,
tracks: formatTracks(response[playlist.uri]),
}),
);
}
store.dispatch(
uiActions.updateProcess(
action.type,
{
remaining: browseResponse.length - index - 1,
},
),
);
if (index === browseResponse.length - 1) {
store.dispatch(coreActions.itemsLoaded(libraryPlaylists));
store.dispatch(coreActions.libraryLoaded({
uri: action.uri,
type: 'playlists',
items_uris: arrayOf('uri', libraryPlaylists),
}));
store.dispatch(uiActions.processFinished(action.type));
}
});
});
} else {
store.dispatch(coreActions.libraryLoaded({
uri: action.uri,
type: 'playlists',
items_uris: [],
}));
store.dispatch(uiActions.stopLoading('mopidy:library:playlists'));
store.dispatch(uiActions.processFinished(action.type));
}
});
}
break; break;
} }
@ -1868,7 +1942,6 @@ const MopidyMiddleware = (function () {
request(store, 'library.browse', { uri: action.uri }) request(store, 'library.browse', { uri: action.uri })
.then((browseResponse) => { .then((browseResponse) => {
console.debug({ action, browseResponse })
const allUris = arrayOf('uri', browseResponse); const allUris = arrayOf('uri', browseResponse);
store.dispatch(uiActions.updateProcess( store.dispatch(uiActions.updateProcess(

View File

@ -99,6 +99,23 @@ const makeProcessProgressSelector = (keys) => createSelector(
); );
const providers = { const providers = {
playlists: [
{
scheme: 'm3u:',
uri: 'm3u:playlists',
title: i18n('services.mopidy.local'),
},
{
scheme: 'spotify:',
uri: 'spotify:library:playlists',
title: i18n('services.spotify.title'),
},
{
scheme: 'tidal:',
uri: 'tidal:my_playlists',
title: i18n('services.tidal.title'),
},
],
albums: [ albums: [
{ {
scheme: 'local:', scheme: 'local:',
@ -115,6 +132,11 @@ const providers = {
uri: 'spotify:library:albums', uri: 'spotify:library:albums',
title: i18n('services.spotify.title'), title: i18n('services.spotify.title'),
}, },
{
scheme: 'tidal:',
uri: 'tidal:my_albums',
title: i18n('services.tidal.title'),
},
{ {
scheme: 'ytmusic:', scheme: 'ytmusic:',
uri: 'ytmusic:album', uri: 'ytmusic:album',
@ -137,6 +159,11 @@ const providers = {
uri: 'spotify:library:artists', uri: 'spotify:library:artists',
title: i18n('services.spotify.title'), title: i18n('services.spotify.title'),
}, },
{
scheme: 'tidal:',
uri: 'tidal:my_artists',
title: i18n('services.tidal.title'),
},
{ {
scheme: 'ytmusic:', scheme: 'ytmusic:',
uri: 'ytmusic:artist', uri: 'ytmusic:artist',

View File

@ -18,6 +18,7 @@ import Loader from '../../components/Loader';
import { import {
makeLibrarySelector, makeLibrarySelector,
makeProcessProgressSelector, makeProcessProgressSelector,
makeProvidersSelector,
getLibrarySource, getLibrarySource,
} from '../../util/selectors'; } from '../../util/selectors';
@ -44,16 +45,14 @@ class LibraryPlaylists extends React.Component {
setWindowTitle(i18n('library.playlists.title')); setWindowTitle(i18n('library.playlists.title'));
this.getMopidyLibrary(); this.getLibraries();
this.getSpotifyLibrary();
} }
refresh = () => { refresh = () => {
const { uiActions: { hideContextMenu } } = this.props; const { uiActions: { hideContextMenu } } = this.props;
hideContextMenu(); hideContextMenu();
this.getMopidyLibrary(true); this.getLibraries(true);
this.getSpotifyLibrary(true);
} }
cancelRefresh = () => { cancelRefresh = () => {
@ -63,46 +62,35 @@ class LibraryPlaylists extends React.Component {
cancelProcess(processKeys); cancelProcess(processKeys);
} }
getMopidyLibrary = (forceRefetch = false) => {
const {
source,
coreActions: {
loadLibrary,
},
} = this.props;
if (source !== 'local' && source !== 'all') return;
loadLibrary('mopidy:library:playlists', 'playlists', { forceRefetch });
};
getSpotifyLibrary = (forceRefetch = false) => {
const {
source,
spotify_available,
coreActions: {
loadLibrary,
},
} = this.props;
if (!spotify_available) return;
if (source !== 'spotify' && source !== 'all') return;
loadLibrary('spotify:library:playlists', 'playlists', { forceRefetch });
};
componentDidUpdate = ({ source: prevSource }) => { componentDidUpdate = ({ source: prevSource }) => {
const { source } = this.props; const { source } = this.props;
if (source !== prevSource) { if (source !== prevSource) {
this.getMopidyLibrary(); this.getLibraries();
this.getSpotifyLibrary();
} }
} }
getLibraries = (forceRefetch = false) => {
const {
source,
providers,
coreActions: {
loadLibrary,
},
} = this.props;
let uris = [];
if (source === 'all') {
uris = providers.map((p) => p.uri);
} else {
uris.push(source);
}
uris.forEach((uri) => loadLibrary(uri, 'playlists', { forceRefetch }));
};
setSort(value) { setSort(value) {
let reverse = false; let reverse = false;
if (this.props.sort == value) reverse = !this.props.sort_reverse; if (this.props.sort === value) reverse = !this.props.sort_reverse;
const data = { const data = {
library_playlists_sort_reverse: reverse, library_playlists_sort_reverse: reverse,
@ -169,7 +157,7 @@ class LibraryPlaylists extends React.Component {
render = () => { render = () => {
const { const {
uiActions, uiActions,
spotify_available, providers,
source, source,
sort, sort,
sort_reverse, sort_reverse,
@ -180,24 +168,6 @@ class LibraryPlaylists extends React.Component {
filter, filter,
} = this.state; } = this.state;
const source_options = [
{
value: 'all',
label: i18n('fields.filters.all'),
},
{
value: 'local',
label: i18n('services.mopidy.local'),
},
];
if (spotify_available) {
source_options.push({
value: 'spotify',
label: i18n('services.spotify.title'),
});
}
const view_options = [ const view_options = [
{ {
value: 'thumbnails', value: 'thumbnails',
@ -269,7 +239,13 @@ class LibraryPlaylists extends React.Component {
name={i18n('fields.source')} name={i18n('fields.source')}
valueAsLabel valueAsLabel
value={source} value={source}
options={source_options} options={[
{
value: 'all',
label: i18n('fields.filters.all'),
},
...providers.map((p) => ({ value: p.uri, label: p.title })),
]}
handleChange={(value) => { uiActions.set({ library_playlists_source: value }); uiActions.hideContextMenu(); }} handleChange={(value) => { uiActions.set({ library_playlists_source: value }); uiActions.hideContextMenu(); }}
/> />
<Button <Button
@ -307,6 +283,7 @@ class LibraryPlaylists extends React.Component {
const librarySelector = makeLibrarySelector('playlists'); const librarySelector = makeLibrarySelector('playlists');
const processProgressSelector = makeProcessProgressSelector(processKeys); const processProgressSelector = makeProcessProgressSelector(processKeys);
const providersSelector = makeProvidersSelector('playlists');
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
const { const {
spotify: { spotify: {
@ -318,8 +295,7 @@ const mapStateToProps = (state) => {
return { return {
slim_mode: state.ui.slim_mode, slim_mode: state.ui.slim_mode,
mopidy_uri_schemes: state.mopidy.uri_schemes, providers: providersSelector(state),
spotify_available: state.spotify.access_token,
playlists: librarySelector(state, 'playlists'), playlists: librarySelector(state, 'playlists'),
loading_progress: processProgressSelector(state), loading_progress: processProgressSelector(state),
source: getLibrarySource(state, 'playlists'), source: getLibrarySource(state, 'playlists'),

View File

@ -12,8 +12,12 @@ import * as spotifyActions from '../../services/spotify/actions';
import { sourceIcon } from '../../util/helpers'; import { sourceIcon } from '../../util/helpers';
import { sortItems } from '../../util/arrays'; import { sortItems } from '../../util/arrays';
import { I18n, i18n } from '../../locale'; import { I18n, i18n } from '../../locale';
import { collate, decodeUri } from '../../util/format'; import { decodeUri } from '../../util/format';
import { makeProcessProgressSelector } from '../../util/selectors'; import {
makeProcessProgressSelector,
makeProvidersSelector,
makeLibrarySelector,
} from '../../util/selectors';
const processKeys = [ const processKeys = [
'MOPIDY_GET_LIBRARY_PLAYLISTS', 'MOPIDY_GET_LIBRARY_PLAYLISTS',
@ -23,32 +27,26 @@ const processKeys = [
class AddToPlaylist extends React.Component { class AddToPlaylist extends React.Component {
componentDidMount = () => { componentDidMount = () => {
const { const {
spotify_library: {
items_uris: spotify_library,
},
mopidy_library: {
items_uris: mopidy_library,
},
spotify_available,
coreActions: {
loadLibrary,
},
uiActions: { uiActions: {
setWindowTitle, setWindowTitle,
}, },
} = this.props; } = this.props;
if (!spotify_library.length && spotify_available) { this.getLibraries();
loadLibrary('spotify:library:playlists', 'playlists');
}
if (!mopidy_library.length) {
loadLibrary('mopidy:library:playlists', 'playlists');
}
setWindowTitle(i18n('modal.add_to_playlist.title')); setWindowTitle(i18n('modal.add_to_playlist.title'));
} }
getLibraries = () => {
const {
providers,
coreActions: {
loadLibrary,
},
} = this.props;
providers.forEach((provider) => loadLibrary(provider.uri, 'playlists'));
};
playlistSelected = (playlist_uri) => { playlistSelected = (playlist_uri) => {
const { const {
coreActions: { coreActions: {
@ -62,9 +60,7 @@ class AddToPlaylist extends React.Component {
renderList = () => { renderList = () => {
const { const {
items, playlists: allPlaylists,
spotify_library = { items_uris: [] },
mopidy_library = { items_uris: [] },
loading_progress, loading_progress,
} = this.props; } = this.props;
@ -72,11 +68,7 @@ class AddToPlaylist extends React.Component {
return <Loader body loading progress={loading_progress} />; return <Loader body loading progress={loading_progress} />;
} }
let playlists = [ let playlists = allPlaylists.filter((playlist) => playlist.can_edit);
...(collate(spotify_library, { items }).items),
...(collate(mopidy_library, { items }).items),
].filter((playlist) => playlist.can_edit);
playlists = sortItems(playlists, 'name'); playlists = sortItems(playlists, 'name');
if (playlists.length > 0) { if (playlists.length > 0) {
@ -128,6 +120,9 @@ class AddToPlaylist extends React.Component {
} }
} }
const librarySelector = makeLibrarySelector('playlists');
const processProgressSelector = makeProcessProgressSelector(processKeys);
const providersSelector = makeProvidersSelector('playlists');
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {
const { const {
spotify: { spotify: {
@ -136,29 +131,18 @@ const mapStateToProps = (state, ownProps) => {
} = {}, } = {},
access_token: spotify_available, access_token: spotify_available,
}, },
core: {
playlists,
items,
libraries,
},
mopidy: {
uri_schemes: mopidy_uri_schemes,
},
} = state; } = state;
const processProgressSelector = makeProcessProgressSelector(processKeys);
const unencodedUris = ownProps.match.params?.uris.split(','); const unencodedUris = ownProps.match.params?.uris;
const uris = unencodedUris ? unencodedUris.map((uri) => decodeUri(uri)) : []; const uris = unencodedUris ? decodeUri(unencodedUris).split(',') : [];
return { return {
uris, uris,
mopidy_uri_schemes, providers: providersSelector(state),
items, playlists: librarySelector(state, 'playlists'),
mopidy_library: libraries['mopidy:library:playlists'] || { items_uris: [] },
spotify_library: libraries['spotify:library:playlists'] || { items_uris: [] },
spotify_available, spotify_available,
loading_progress: processProgressSelector(state), loading_progress: processProgressSelector(state),
me_id, me_id,
playlists,
}; };
}; };