Playlists stored in universal store, indexes stored for each view

This commit is contained in:
James Barnsley
2017-01-13 13:17:47 +13:00
parent 701ca67b5a
commit 8b718c0a33
14 changed files with 264 additions and 157 deletions

5
src/js/bootstrap.js vendored
View File

@ -65,6 +65,11 @@ var initialState = {
playlists: [],
tracks: []
},
library_artists: [],
artists: [],
library_albums: [],
albums: [],
library_playlists: [],
playlists: [],
notifications: []
}

View File

@ -38,7 +38,7 @@ export default class GridItem extends React.Component{
<div className="name">{ item.name }</div>
<div className="secondary">
{ item.artists ? <ArtistSentence artists={ item.artists } /> : null }
{ item.type == 'playlist' && item.tracks ? item.tracks.total+' tracks' : null }
{ item.type == 'playlist' && item.tracks_total ? item.tracks_total+' tracks' : null }
{ item.followers ? item.followers.total.toLocaleString()+' followers' : null }
{ item.type == 'playlist' && item.can_edit ? <FontAwesome name="edit" /> : null }
</div>

View File

@ -245,7 +245,7 @@ class TrackList extends React.Component{
}
render(){
if( !this.state.tracks ) return null
if( !this.state.tracks || Object.prototype.toString.call(this.state.tracks) !== '[object Array]' ) return null
let self = this;
return (

View File

@ -93,6 +93,28 @@ export let getTrackIcon = function( track = false ){
}
/**
* Get a track's icon
* @param track object
* @return string
**/
export let flattenTracks = function( tracks ){
var flattened = []
for( var i = 0; i < tracks.length; i++ ){
flattened.push( Object.assign(
{},
tracks[i].track,
{
added_by: tracks[i].added_by,
added_at: tracks[i].added_at
}
))
}
return flattened
}
/**

View File

@ -12,7 +12,7 @@ const localstorageMiddleware = (function(){
// append our state to a global variable. This gives us access to debug the store at any point
window._store = store
//console.log(action)
console.log(action)
switch( action.type ){

View File

@ -157,47 +157,47 @@ export function deletePlaylist( uri ){
}
}
export function getPlaylists(){
return { type: 'MOPIDY_PLAYLISTS' }
export function getLibraryPlaylists(){
return { type: 'MOPIDY_GET_LIBRARY_PLAYLISTS' }
}
export function getPlaylist( uri ){
return {
type: 'MOPIDY_PLAYLIST',
type: 'MOPIDY_GET_PLAYLIST',
data: { uri: uri }
}
}
export function getDirectory( uri ){
return {
type: 'MOPIDY_DIRECTORY',
type: 'MOPIDY_GET_DIRECTORY',
data: { uri: uri }
}
}
export function getAlbum( uri ){
return {
type: 'MOPIDY_ALBUM',
type: 'MOPIDY_GET_ALBUM',
data: { uri: uri }
}
}
export function getArtist( uri ){
return {
type: 'MOPIDY_ARTIST',
type: 'MOPIDY_GET_ARTIST',
data: { uri: uri }
}
}
export function getArtists(){
return {
type: 'MOPIDY_ARTISTS'
type: 'MOPIDY_GET_ARTISTS'
}
}
export function getAlbums(){
return {
type: 'MOPIDY_ALBUMS'
type: 'MOPIDY_GET_ALBUMS'
}
}

View File

@ -282,29 +282,47 @@ const MopidyMiddleware = (function(){
})
break;
case 'MOPIDY_PLAYLISTS':
/**
* =============================================================== PLAYLIST(S) ==========
* ======================================================================================
**/
case 'MOPIDY_GET_LIBRARY_PLAYLISTS':
instruct( socket, store, 'playlists.asList' )
.then( response => {
for( var i = 0; i < response.length; i++ ){
if( response[i].uri.startsWith('m3u:')) response[i] = Object.assign({}, response[i], { can_edit: true })
}
store.dispatch({ type: 'MOPIDY_PLAYLISTS_LOADED', data: response });
// TODO: dispatch an action to fetch extra data for each playlist
// this will then trigger an PLAYLIST_REFRESHED action to update a specific store item
// drop in our URI list
var playlist_uris = helpers.asURIs(response)
store.dispatch({ type: 'LIBRARY_PLAYLISTS_LOADED', uris: playlist_uris });
// get the full playlist objects
for (var i = 0; i < response.length; i++ ){
instruct( socket, store, 'playlists.lookup', { uri: response[i].uri })
.then( response => {
var playlist = Object.assign(
{},
{
images: []
},
{
can_edit: (response.uri.startsWith('m3u:'))
},
response,
{
tracks: ( response.tracks ? response.tracks : [] ),
tracks_total: ( response.tracks ? response.tracks.length : 0 )
}
)
store.dispatch({ type: 'PLAYLIST_LOADED', playlist: playlist })
})
}
})
break;
case 'MOPIDY_CREATE_PLAYLIST':
instruct( socket, store, 'playlists.create', { name: action.name, uri_scheme: action.scheme })
.then( response => {
// re-load our global playlists
store.dispatch({ type: 'MOPIDY_PLAYLISTS' });
});
break
case 'MOPIDY_PLAYLIST':
case 'MOPIDY_GET_PLAYLIST':
store.dispatch({ type: 'MOPIDY_PLAYLIST_LOADED', data: false });
instruct( socket, store, 'playlists.lookup', action.data )
.then( response => {
@ -321,18 +339,15 @@ const MopidyMiddleware = (function(){
)
// tracks? get the full track objects
if( playlist.tracks.length > 0 ) store.dispatch({ type: 'MOPIDY_PLAYLIST_TRACKS', tracks: playlist.tracks })
if( playlist.tracks.length > 0 ) store.dispatch({ type: 'MOPIDY_RESOLVE_PLAYLIST_TRACKS', tracks: playlist.tracks })
store.dispatch({ type: 'MOPIDY_PLAYLIST_LOADED', data: playlist })
store.dispatch({ type: 'PLAYLIST_LOADED', playlist: playlist })
})
break;
case 'MOPIDY_PLAYLIST_TRACKS':
case 'MOPIDY_RESOLVE_PLAYLIST_TRACKS':
var tracks = Object.assign([], action.tracks)
var uris = [];
for( var i = 0; i < tracks.length; i++ ){
uris.push( tracks[i].uri );
}
var uris = helpers.asURIs(tracks)
instruct( socket, store, 'library.lookup', { uris: uris } )
.then( response => {
@ -357,7 +372,7 @@ const MopidyMiddleware = (function(){
}
}
store.dispatch({ type: 'PLAYLIST_TRACKS_LOADED', tracks: tracks })
store.dispatch({ type: 'PLAYLIST_TRACKS_LOADED', playlist_uri: action.playlist_uri, tracks: tracks })
})
break
@ -460,14 +475,37 @@ const MopidyMiddleware = (function(){
});
break
case 'MOPIDY_CREATE_PLAYLIST':
instruct( socket, store, 'playlists.create', { name: action.name, uri_scheme: action.scheme })
.then( response => {
// re-load our global playlists
//store.dispatch({ type: 'MOPIDY_GET_PLAYLISTS' });
});
break
case 'MOPIDY_DELETE_PLAYLIST':
instruct( socket, store, 'playlists.delete', { uri: action.uri })
.then( response => {
// re-load our global playlists
store.dispatch({ type: 'MOPIDY_PLAYLISTS' });
// store.dispatch({ type: 'MOPIDY_PLAYLISTS' });
});
break
/**
* =============================================================== ALBUM(S) =============
* ======================================================================================
**/
case 'MOPIDY_ALBUMS':
store.dispatch({ type: 'MOPIDY_ALBUMS_LOADED', data: false });
instruct( socket, store, 'library.browse', { uri: 'local:directory?type=album' } )
.then( response => {
store.dispatch({ type: 'MOPIDY_ALBUMS_LOADED', data: response });
})
break;
case 'MOPIDY_ALBUM':
//store.dispatch({ type: 'MOPIDY_ALBUM_LOADED', data: false });
@ -526,6 +564,20 @@ const MopidyMiddleware = (function(){
})
})
break;
/**
* =============================================================== ARTIST(S) ============
* ======================================================================================
**/
case 'MOPIDY_ARTISTS':
store.dispatch({ type: 'MOPIDY_ARTISTS_LOADED', data: false });
instruct( socket, store, 'library.browse', { uri: 'local:directory?type=artist' } )
.then( response => {
store.dispatch({ type: 'MOPIDY_ARTISTS_LOADED', data: response });
})
break;
case 'MOPIDY_ARTIST':
store.dispatch({ type: 'MOPIDY_ARTIST_LOADED', data: false });
@ -560,6 +612,12 @@ const MopidyMiddleware = (function(){
store.dispatch({ type: 'MOPIDY_ARTIST_LOADED', data: artist });
})
break;
/**
* =============================================================== LOCAL ================
* ======================================================================================
**/
case 'MOPIDY_DIRECTORY':
store.dispatch({ type: 'MOPIDY_DIRECTORY_LOADED', data: false });
@ -569,22 +627,6 @@ const MopidyMiddleware = (function(){
})
break;
case 'MOPIDY_ARTISTS':
store.dispatch({ type: 'MOPIDY_ARTISTS_LOADED', data: false });
instruct( socket, store, 'library.browse', { uri: 'local:directory?type=artist' } )
.then( response => {
store.dispatch({ type: 'MOPIDY_ARTISTS_LOADED', data: response });
})
break;
case 'MOPIDY_ALBUMS':
store.dispatch({ type: 'MOPIDY_ALBUMS_LOADED', data: false });
instruct( socket, store, 'library.browse', { uri: 'local:directory?type=album' } )
.then( response => {
store.dispatch({ type: 'MOPIDY_ALBUMS_LOADED', data: response });
})
break;
// This action is irrelevant to us, pass it on to the next middleware
default:
return next(action);

View File

@ -262,9 +262,18 @@ export function getFeaturedPlaylists(){
sendRequest( dispatch, getState, 'browse/featured-playlists?timestamp='+timestamp+'&country='+getState().spotify.country+'&limit=50&locale='+getState().spotify.locale )
.then( response => {
for (var i = 0; i < response.playlists.items.length; i++){
dispatch({
type: 'PLAYLIST_LOADED',
playlist: response.playlists.items[i]
});
}
dispatch({
type: 'SPOTIFY_FEATURED_PLAYLISTS_LOADED',
data: response
data: {
message: response.message,
playlists: helpers.asURIs(response.playlists.items)
}
});
});
}
@ -290,28 +299,41 @@ export function getCategory( id ){
dispatch({ type: 'SPOTIFY_CATEGORY_LOADED', data: false });
sendRequest( dispatch, getState, 'browse/categories/'+id+'?country='+getState().spotify.country+'&locale='+getState().spotify.locale )
.then( response => {
dispatch({
type: 'SPOTIFY_CATEGORY_LOADED',
data: response
});
});
}
}
export function getCategoryPlaylists( id ){
return (dispatch, getState) => {
dispatch({ type: 'SPOTIFY_CATEGORY_PLAYLISTS_LOADED', data: false });
sendRequest( dispatch, getState, 'browse/categories/'+id+'/playlists?limit=50&country='+getState().spotify.country+'&locale='+getState().spotify.locale )
.then( response => {
dispatch({
type: 'SPOTIFY_CATEGORY_PLAYLISTS_LOADED',
data: response.playlists
});
$.when(
sendRequest( dispatch, getState, 'browse/categories/'+id+'?country='+getState().spotify.country+'&locale='+getState().spotify.locale ),
sendRequest( dispatch, getState, 'browse/categories/'+id+'/playlists?limit=50&country='+getState().spotify.country+'&locale='+getState().spotify.locale )
).then( ( category_response, playlists_response ) => {
var playlists = []
for (var i = 0; i < playlists_response.playlists.items.length; i++){
playlists.push(
Object.assign(
{},
playlists_response.playlists.items[i],
{
tracks: false,
tracks_total: playlists_response.playlists.items[i].tracks.total
}
)
)
}
var category = Object.assign(
{},
category_response,
{
items: false,
playlists: playlists
}
)
dispatch({
type: 'SPOTIFY_CATEGORY_LOADED',
data: category
});
});
}
}
@ -330,13 +352,14 @@ export function getNewReleases(){
}
}
export function getURL( url, action_name ){
export function getURL( url, action_name, uri = false ){
return (dispatch, getState) => {
sendRequest( dispatch, getState, url )
.then( response => {
dispatch({
type: action_name,
data: response
data: response,
uri: uri
});
});
}
@ -774,15 +797,24 @@ export function savePlaylist( uri, name, is_public ){
export function getPlaylist( uri ){
return (dispatch, getState) => {
// flush out the previous store value
dispatch({ type: 'SPOTIFY_PLAYLIST_LOADED', data: false });
// get the main playlist object
sendRequest( dispatch, getState, 'users/'+ helpers.getFromUri('userid',uri) +'/playlists/'+ helpers.getFromUri('playlistid',uri) +'?market='+getState().spotify.country )
.then( response => {
var playlist = Object.assign(
{},
response,
{
can_edit: (getState().spotify.me && response.owner.id == getState().spotify.me.id),
tracks: helpers.flattenTracks(response.tracks.items),
tracks_more: response.tracks.next,
tracks_total: response.tracks.total
}
)
dispatch({
type: 'SPOTIFY_PLAYLIST_LOADED',
data: response
type: 'PLAYLIST_LOADED',
playlist: playlist
})
})
}
@ -801,15 +833,28 @@ function loadNextPlaylistsBatch( dispatch, getState, playlists, lastResponse ){
// used to define what we can add tracks to
if( getState().spotify.authorized ){
for( var i = 0; i < playlists.length; i++ ){
if( playlists[i].owner.id == getState().spotify.me.id ){
playlists[i] = Object.assign({}, playlists[i], { can_edit: true })
}
var playlist = Object.assign(
{},
playlists[i],
{
can_edit: (getState().spotify.me && playlists[i].owner.id == getState().spotify.me.id),
tracks: helpers.flattenTracks(playlists[i].tracks.items),
tracks_more: response.tracks.next,
tracks_total: response.tracks.total
}
)
dispatch({
type: 'PLAYLIST_LOADED',
playlist: playlist
});
}
}
dispatch({
type: 'SPOTIFY_LIBRARY_PLAYLISTS_LOADED',
data: playlists
type: 'LIBRARY_PLAYLISTS_LOADED',
uris: helpers.asURIs(playlists)
});
}
}

View File

@ -231,46 +231,21 @@ export default function reducer(ui = {}, action){
if( !action.data ) return Object.assign({}, ui, { playlist: false })
return Object.assign({}, ui, { playlist: action.data })
case 'SPOTIFY_PLAYLIST_LOADED':
if( !action.data ) return Object.assign({}, ui, { playlist: false })
case 'PLAYLIST_LOADED_MORE_TRACKS':
var tracks = []
for( var i = 0; i < action.data.tracks.items.length; i++ ){
tracks.push( Object.assign(
{},
action.data.tracks.items[i].track,
{
added_by: action.data.tracks.items[i].added_by,
added_at: action.data.tracks.items[i].added_at
}
))
}
var playlists = ui.playlists
var playlist = Object.assign(
{},
playlists[action.uri],
{
tracks: [...playlists[action.uri].tracks, ...helpers.flattenTracks(action.data.items)],
tracks_more: action.data.next,
tracks_total: action.data.total
}
)
var playlist = Object.assign({}, action.data, {
tracks: tracks,
tracks_more: action.data.tracks.next,
tracks_total: action.data.tracks.total
})
return Object.assign({}, ui, { playlist: playlist });
case 'SPOTIFY_PLAYLIST_LOADED_MORE':
var tracks = []
for( var i = 0; i < action.data.items.length; i++ ){
tracks.push( Object.assign(
{},
action.data.items[i].track,
{
added_by: action.data.items[i].added_by,
added_at: action.data.items[i].added_at
}
))
}
var playlist = Object.assign({}, ui.playlist, {
tracks: [...ui.playlist.tracks, ...tracks],
tracks_more: action.data.next
})
return Object.assign({}, ui, { playlist: playlist });
playlists[action.uri] = playlist
return Object.assign({}, ui, { playlists: playlists });
case 'PLAYLIST_TRACKS_REMOVED':
var tracks = Object.assign([], ui.playlist.tracks)
@ -316,14 +291,25 @@ export default function reducer(ui = {}, action){
* Library Playlists
**/
case 'MOPIDY_PLAYLISTS_LOADED':
case 'SPOTIFY_LIBRARY_PLAYLISTS_LOADED':
if( !action.data ) return ui
var playlists = [...ui.playlists, ...action.data]
playlists = helpers.mergeDuplicates(playlists, 'uri')
helpers.sortItems(playlists, 'name')
case 'PLAYLIST_LOADED':
case 'PLAYLIST_UPDATED':
var playlists = ui.playlists
var playlist = Object.assign({}, action.playlist)
// if we already have one in our list, fetch it and update it
if (playlists[action.playlist.uri]){
playlist = Object.assign({}, playlists[action.playlist.uri], action.playlist)
}
playlists[action.playlist.uri] = playlist
return Object.assign({}, ui, { playlists: playlists });
case 'LIBRARY_PLAYLISTS_LOADED':
var library_playlists = []
if (ui.library_playlists) library_playlists = ui.library_playlists
return Object.assign({}, ui, {
playlists: playlists
library_playlists: [...library_playlists, ...action.uris]
});

View File

@ -53,7 +53,7 @@ class App extends React.Component{
// mopidy comes online
if( !this.props.mopidy_connected && nextProps.mopidy_connected ){
this.props.mopidyActions.getPlaylists();
this.props.mopidyActions.getLibraryPlaylists();
}
// spotify authorized

View File

@ -55,7 +55,7 @@ class Playlist extends React.Component{
loadMore(){
if( !this.props.playlist.tracks_more ) return
this.props.spotifyActions.getURL( this.props.playlist.tracks_more, 'SPOTIFY_PLAYLIST_LOADED_MORE' );
this.props.spotifyActions.getURL( this.props.playlist.tracks_more, 'PLAYLIST_LOADED_MORE_TRACKS', this.props.playlist.uri );
}
play(){
@ -90,19 +90,6 @@ class Playlist extends React.Component{
this.props.uiActions.removeTracksFromPlaylist( this.props.playlist.uri, tracks_indexes )
}
isEditable(){
if( helpers.uriSource( this.props.playlist.uri ) == 'spotify' ){
if( !this.props.spotify_authorized ) return false
return ( this.props.playlist &&
this.props.playlist.owner &&
this.props.playlist.owner.id == this.props.spotify_userid
)
}else{
return true
}
}
renderExtraButtons(){
switch( helpers.uriSource( this.props.playlist.uri ) ){
@ -115,7 +102,7 @@ class Playlist extends React.Component{
)
case 'spotify':
if( this.isEditable() ){
if( this.props.playlist.can_edit ){
return (
<span>
<button className="large tertiary" onClick={ e => this.props.uiActions.openModal('edit_playlist', { uri: this.props.playlist.uri, name: this.props.playlist.name, is_public: this.props.playlist.public }) }>Edit</button>
@ -167,7 +154,7 @@ class Playlist extends React.Component{
</div>
<section className="list-wrapper">
{ this.props.playlist.tracks ? <TrackList context={ this.isEditable() ? 'editable-playlist' : 'playlist'} tracks={ this.props.playlist.tracks } removeTracks={ tracks_indexes => this.removeTracks(tracks_indexes) } reorderTracks={ (indexes, index) => this.reorderTracks(indexes, index) } /> : null }
{ this.props.playlist.tracks ? <TrackList context={ this.props.playlist.can_edit ? 'editable-playlist' : 'playlist'} tracks={ this.props.playlist.tracks } removeTracks={ tracks_indexes => this.removeTracks(tracks_indexes) } reorderTracks={ (indexes, index) => this.reorderTracks(indexes, index) } /> : null }
<LazyLoadListener loadMore={ () => this.loadMore() }/>
</section>
@ -186,7 +173,7 @@ class Playlist extends React.Component{
const mapStateToProps = (state, ownProps) => {
return {
playlist: state.ui.playlist,
playlist: state.ui.playlists[ownProps.params.uri],
mopidy_connected: state.mopidy.connected,
spotify_authorized: state.spotify.authorized,
spotify_userid: state.spotify.me.id

View File

@ -17,13 +17,11 @@ class DiscoverCategory extends React.Component{
componentDidMount(){
this.props.spotifyActions.getCategory( this.props.params.id );
this.props.spotifyActions.getCategoryPlaylists( this.props.params.id );
}
componentWillReceiveProps( nextProps ){
if( nextProps.params.id != this.props.params.id ){
this.props.spotifyActions.getCategory( nextProps.params.id );
this.props.spotifyActions.getCategoryPlaylists( nextProps.params.id );
}
}
@ -39,7 +37,7 @@ class DiscoverCategory extends React.Component{
<div className="view discover-categories-view">
<Header icon="grid" title={this.props.spotify.category.name} />
<section className="grid-wrapper">
{ this.props.spotify.category_playlists ? <PlaylistGrid playlists={this.props.spotify.category_playlists.items} /> : null }
{ this.props.spotify.category.playlists ? <PlaylistGrid playlists={this.props.spotify.category.playlists} /> : null }
</section>
<LazyLoadListener loadMore={ () => this.loadMore() }/>
</div>

View File

@ -20,11 +20,22 @@ class DiscoverFeatured extends React.Component{
}
render(){
var playlists = []
if (this.props.featured_playlists){
for (var i = 0; i < this.props.featured_playlists.playlists.length; i++){
var uri = this.props.featured_playlists.playlists[i]
if (this.props.playlists.hasOwnProperty(uri)){
playlists.push(this.props.playlists[uri])
}
}
}
return (
<div className="view discover-featured-view">
<Header icon="star" title="Featured playlists" />
<section className="grid-wrapper">
{ this.props.spotify.featured_playlists ? <PlaylistGrid playlists={this.props.spotify.featured_playlists.playlists.items} /> : null }
{ playlists ? <PlaylistGrid playlists={playlists} /> : null }
</section>
</div>
);
@ -39,7 +50,10 @@ class DiscoverFeatured extends React.Component{
**/
const mapStateToProps = (state, ownProps) => {
return state;
return {
featured_playlists: state.spotify.featured_playlists,
playlists: state.ui.playlists
}
}
const mapDispatchToProps = (dispatch) => {

View File

@ -33,9 +33,16 @@ class LibraryPlaylists extends React.Component{
}
renderView(){
if( !this.props.playlists ) return null
if( !this.props.library_playlists || !this.props.playlists ) return null
var playlists = []
for (var i = 0; i < this.props.library_playlists.length; i++){
var uri = this.props.library_playlists[i]
if (this.props.playlists.hasOwnProperty(uri)){
playlists.push(this.props.playlists[uri])
}
}
var playlists = Object.assign([], this.props.playlists)
if( this.props.sort ){
playlists = helpers.sortItems(playlists, this.props.sort, this.props.sort_reverse)
}
@ -141,6 +148,7 @@ const mapStateToProps = (state, ownProps) => {
view: state.ui.library_playlists_view,
sort: state.ui.library_playlists_sort,
sort_reverse: state.ui.library_playlists_sort_reverse,
library_playlists: state.ui.library_playlists,
playlists: state.ui.playlists
}
}