Merge branch 'develop' into master

This commit is contained in:
James Barnsley
2018-11-23 07:59:47 +13:00
committed by GitHub
152 changed files with 13129 additions and 9363 deletions

View File

@ -631,12 +631,28 @@ const CoreMiddleware = (function(){
// Already have an artist in the index
if (artists_index[artist.uri]){
// Don't replace existing images, instead add them as supplementary
// this is to prevent LastFM overwriting Spotify images
if (artists_index[artist.uri].images){
artist.images_additional = artist.images;
delete artist.images;
}
// And we've already got some images, make sure we merge the arrays,
// rather than overwriting
if (artists_index[artist.uri].images && artist.images){
var existing_images = artists_index[artist.uri].images;
var are_new_images = true;
// loop all extisting images to make sure we're not adding one that
// we already have
for (var 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 = Object.assign({}, artists_index[artist.uri], artist);
}
@ -652,7 +668,6 @@ const CoreMiddleware = (function(){
artists_loaded.push(artist);
};
action.artists = artists_loaded;
if (tracks_loaded.length > 0){
@ -759,32 +774,23 @@ const CoreMiddleware = (function(){
var records = action.records_data;
}
// TODO: To avoid double-looping, we could
// run a single loop to extract both the formatted record
// and the URI
switch (action.records_type){
case 'track':
records = helpers.formatTracks(records);
break;
case 'artist':
records = helpers.formatArtists(records);
break;
case 'album':
records = helpers.formatAlbums(records);
break;
case 'playlist':
records = helpers.formatPlaylists(records);
break;
case 'user':
records = helpers.formatUsers(records);
break;
// Pre-emptively format tracks
// Providers give us tracks in all kinds of structures, so this cleans things first
if (action.records_type == 'track'){
records = helpers.formatTracks(records);
}
var records_type_plural = action.records_type+'s';
var records_index = {};
var records_uris = helpers.arrayOf('uri', records);
// Append our records_uris array with our new records
// If we're a list of playlists, we need to manually filter Spotify's new URI structure
// Really poor form because they haven't updated it everywhere, yet
if (action.records_type == 'playlist'){
records_uris = helpers.upgradeSpotifyPlaylistUris(records_uris);
}
// Append our parent object's reference to these records
var uris = records_uris;
if (parent[records_type_plural+'_uris'] !== undefined){
uris = [...parent[records_type_plural+'_uris'], ...uris];
@ -808,9 +814,6 @@ const CoreMiddleware = (function(){
records_action[records_type_plural] = records;
store.dispatch(records_action);
//console.log(parent_action);
//console.log(records_action);
next(action);
break;

View File

@ -1,7 +1,7 @@
var coreActions = require('../core/actions')
var uiActions = require('../ui/actions')
var helpers = require('../../helpers')
var coreActions = require('../core/actions');
var uiActions = require('../ui/actions');
var helpers = require('../../helpers');
export function set(data){
return {
@ -172,7 +172,7 @@ export function getTrackLyrics(uri, path){
});
var lyrics_html = lyrics.html();
lyrics_html = lyrics_html.replace(/(\[)/g, '<span class="grey-text">[');
lyrics_html = lyrics_html.replace(/(\[)/g, '<span class="mid_grey-text">[');
lyrics_html = lyrics_html.replace(/(\])/g, ']</span>');
dispatch(coreActions.trackLoaded({
@ -202,7 +202,7 @@ export function getTrackLyrics(uri, path){
export function findTrackLyrics(track){
return (dispatch, getState) => {
var query = '';
query += track.artists[0].name+' ';
query += track.name;

View File

@ -0,0 +1,31 @@
export function set(data){
return {
type: 'GOOGLE_SET',
data: data
}
}
export function getLibraryArtists(){
return {
type: 'GOOGLE_GET_LIBRARY_ARTISTS'
}
}
export function clearLibraryArtists(){
return {
type: 'GOOGLE_CLEAR_LIBRARY_ARTISTS'
}
}
export function getLibraryAlbums(){
return {
'type': 'GOOGLE_GET_LIBRARY_ALBUMS'
}
}
export function clearLibraryAlbums(){
return {
type: 'GOOGLE_CLEAR_LIBRARY_ALBUMS'
}
}

View File

@ -0,0 +1,145 @@
import ReactGA from 'react-ga'
var helpers = require('../../helpers');
var coreActions = require('../core/actions');
var mopidyActions = require('../mopidy/actions');
var uiActions = require('../ui/actions');
const GoogleMiddleware = (function(){
// A Google request is an alias of the Mopidy request
const request = (store, method, params = null, response_callback = null, error_callback = null) => {
store.dispatch(
mopidyActions.request(
method,
params,
response_callback,
error_callback
)
);
return;
}
return store => next => action => {
var google = store.getState().google;
switch(action.type){
case 'GOOGLE_GET_LIBRARY_ALBUMS':
var last_run = store.getState().ui.processes.GOOGLE_LIBRARY_ALBUMS_PROCESSOR
if (!last_run){
request(
store,
'library.browse',
{
uri: 'gmusic:album'
},
response => {
if (response.length <= 0){
return;
}
var uris = helpers.arrayOf('uri',response);
store.dispatch({
type: 'GOOGLE_LIBRARY_ALBUMS_LOADED',
uris: uris
});
// Start our process to load the full album objects
store.dispatch(uiActions.startProcess(
'GOOGLE_LIBRARY_ALBUMS_PROCESSOR',
'Loading '+uris.length+' Google albums',
{
uris: uris,
total: uris.length,
remaining: uris.length
}
));
}
);
} else if (last_run.status == 'cancelled'){
store.dispatch(uiActions.resumeProcess('GOOGLE_LIBRARY_ALBUMS_PROCESSOR'))
} else if (last_run.status == 'finished'){
// TODO: do we want to force a refresh?
}
break;
case 'GOOGLE_LIBRARY_ALBUMS_PROCESSOR':
if (store.getState().ui.processes['GOOGLE_LIBRARY_ALBUMS_PROCESSOR'] !== undefined){
var processor = store.getState().ui.processes['GOOGLE_LIBRARY_ALBUMS_PROCESSOR']
if (processor.status == 'cancelling'){
store.dispatch(uiActions.processCancelled('GOOGLE_LIBRARY_ALBUMS_PROCESSOR'))
return false
}
}
var uris = Object.assign([], action.data.uris);
var uris_to_load = uris.splice(0,50);
if (uris_to_load.length > 0){
store.dispatch(uiActions.updateProcess(
'GOOGLE_LIBRARY_ALBUMS_PROCESSOR',
'Loading '+uris.length+' Google albums',
{
uris: uris,
remaining: uris.length
}
));
store.dispatch(mopidyActions.getAlbums(uris_to_load, {name: 'GOOGLE_LIBRARY_ALBUMS_PROCESSOR', data: {uris: uris}}))
} else {
store.dispatch(uiActions.processFinishing('GOOGLE_LIBRARY_ALBUMS_PROCESSOR'));
}
break
case 'GOOGLE_GET_LIBRARY_ARTISTS':
request(
store,
'library.browse',
{
uri: 'gmusic:artist'
},
response => {
if (response.length <= 0){
return
}
var uris = [];
for (var i = 0; i < response.length; i++){
// Convert local URI to actual artist URI
// See https://github.com/mopidy/mopidy-local-sqlite/issues/39
response[i].uri = response[i].uri.replace('local:directory?albumartist=','');
uris.push(response[i].uri);
}
store.dispatch(coreActions.artistsLoaded(response));
store.dispatch({
type: 'GOOGLE_LIBRARY_ARTISTS_LOADED',
uris: uris
});
},
error => {
store.dispatch(coreActions.handleException(
'Could not load Google artists',
error
));
}
);
break;
// This action is irrelevant to us, pass it on to the next middleware
default:
return next(action);
}
}
})();
export default GoogleMiddleware

View File

@ -0,0 +1,38 @@
import * as helpers from '../../helpers';
export default function reducer(google = {}, action){
switch (action.type){
case 'GOOGLE_SET':
return Object.assign({}, google, action.data);
case 'GOOGLE_LIBRARY_ARTISTS_LOADED':
if (google.library_artists){
var uris = [...google.library_artists,...action.uris];
} else {
var uris = action.uris;
}
return Object.assign({}, google, { library_artists: helpers.removeDuplicates(uris) });
case 'GOOGLE_CLEAR_LIBRARY_ARTISTS':
return Object.assign({}, google, { library_artists: null });
case 'GOOGLE_LIBRARY_ALBUMS_LOADED':
if (google.library_albums){
var uris = [...google.library_albums,...action.uris];
} else {
var uris = action.uris;
}
return Object.assign({}, google, { library_albums: helpers.removeDuplicates(uris) });
case 'GOOGLE_CLEAR_LIBRARY_ALBUMS':
return Object.assign({}, google, { library_albums: null });
default:
return google
}
}

View File

@ -240,11 +240,11 @@ const localstorageMiddleware = (function(){
);
break;
case 'SNAPCAST_CLIENT_COMMANDS_UPDATED':
case 'SNAPCAST_COMMANDS_UPDATED':
helpers.setStorage(
'snapcast',
{
client_commands: action.client_commands
commands: action.commands
}
);
break;

View File

@ -8,7 +8,7 @@ export function set(data){
}
}
export function request(method, params = null, response_callback = null, error_callback = null){
export function request(method, params = {}, response_callback = null, error_callback = null){
return {
type: 'MOPIDY_REQUEST',
method: method,
@ -30,18 +30,6 @@ export function connecting(){
}
}
export function upgrading(){
return {
type: 'MOPIDY_UPGRADING'
}
}
export function restarting(){
return {
type: 'MOPIDY_RESTARTING'
}
}
export function disconnect(){
return {
type: 'MOPIDY_DISCONNECT'
@ -56,6 +44,42 @@ export function debug(call, value){
}
}
export function restartStarted(){
return {
type: 'MOPIDY_RESTART_STARTED'
}
}
export function restartFinished(){
return {
type: 'MOPIDY_RESTART_FINISHED'
}
}
export function upgradeStarted(){
return {
type: 'MOPIDY_UPGRADE_STARTED'
}
}
export function upgradeFinished(){
return {
type: 'MOPIDY_UPGRADE_FINISHED'
}
}
export function localScanStarted(){
return {
type: 'MOPIDY_LOCAL_SCAN_STARTED'
}
}
export function localScanFinished(){
return {
type: 'MOPIDY_LOCAL_SCAN_FINISHED'
}
}
/**
* Core play actions
**/
@ -357,9 +381,10 @@ export function getTrack(uri){
}
}
export function getLibraryArtists(){
export function getLibraryArtists(uri = null){
return {
type: 'MOPIDY_GET_LIBRARY_ARTISTS'
type: 'MOPIDY_GET_LIBRARY_ARTISTS',
uri: uri
}
}
@ -399,9 +424,10 @@ export function getAlbums(uris, processor = null){
}
}
export function getLibraryAlbums(){
export function getLibraryAlbums(uri = null){
return {
type: 'MOPIDY_GET_LIBRARY_ALBUMS'
type: 'MOPIDY_GET_LIBRARY_ALBUMS',
uri: uri
}
}

View File

@ -1,16 +1,17 @@
import ReactGA from 'react-ga'
import Mopidy from 'mopidy'
import ReactGA from 'react-ga';
import Mopidy from 'mopidy';
import md5 from 'md5';
import { hashHistory } from 'react-router'
import * as helpers from '../../helpers'
import { hashHistory } from 'react-router';
import * as helpers from '../../helpers';
var mopidyActions = require('./actions.js')
var coreActions = require('../core/actions.js')
var uiActions = require('../ui/actions.js')
var spotifyActions = require('../spotify/actions.js')
var pusherActions = require('../pusher/actions.js')
var lastfmActions = require('../lastfm/actions.js')
var mopidyActions = require('./actions.js');
var coreActions = require('../core/actions.js');
var uiActions = require('../ui/actions.js');
var spotifyActions = require('../spotify/actions.js');
var pusherActions = require('../pusher/actions.js');
var googleActions = require('../google/actions.js');
var lastfmActions = require('../lastfm/actions.js');
const MopidyMiddleware = (function(){
@ -45,15 +46,15 @@ const MopidyMiddleware = (function(){
store.dispatch(mopidyActions.getTimePosition());
store.dispatch(mopidyActions.getUriSchemes());
// every 1000s update our play position (when playing)
// Every 1s update our play position (when playing)
progress_interval = setInterval(() => {
if (store.getState().mopidy.play_state == 'playing'){
// every 10s get real position from server
if (progress_interval_counter % 5 == 0){
// Every 10s get real position from server, provided we're in-focus
if (progress_interval_counter % 5 == 0 && store.getState().ui.window_focus === true){
store.dispatch(mopidyActions.getTimePosition());
// otherwise we just assume to add 1000ms every 1000ms of play time
// Otherwise we just assume to add 1000ms every 1000ms of play time
} else {
store.dispatch(mopidyActions.timePosition(store.getState().mopidy.time_position + 1000));
}
@ -95,7 +96,10 @@ const MopidyMiddleware = (function(){
case 'event:trackPlaybackEnded':
store.dispatch(mopidyActions.clearCurrentTrack());
store.dispatch(mopidyActions.getTimePosition());
store.dispatch({
type: 'MOPIDY_TIME_POSITION',
time_position: 0
});
break;
case 'event:trackPlaybackStarted':
@ -227,6 +231,7 @@ const MopidyMiddleware = (function(){
var hashed_hostname = md5(window.location.hostname);
ReactGA.event({ category: 'Mopidy', action: 'Connected', label: hashed_hostname });
}
store.dispatch(uiActions.createNotification({content: 'Mopidy connected'}));
next(action);
break;
@ -236,6 +241,10 @@ const MopidyMiddleware = (function(){
store.dispatch({ type: 'MOPIDY_DISCONNECTED' })
break;
case 'MOPIDY_DISCONNECTED':
store.dispatch(uiActions.createNotification({type: 'bad', content: 'Mopidy disconnected'}));
break;
case 'MOPIDY_DEBUG':
request(socket, store, action.call, action.value )
.then(response => {
@ -243,6 +252,21 @@ const MopidyMiddleware = (function(){
})
break;
case 'SET_WINDOW_FOCUS':
// Focus has just been regained
if (action.window_focus === true){
store.dispatch(mopidyActions.getPlayState());
store.dispatch(mopidyActions.getVolume());
store.dispatch(mopidyActions.getMute());
store.dispatch(mopidyActions.getConsume());
store.dispatch(mopidyActions.getRandom());
store.dispatch(mopidyActions.getRepeat());
store.dispatch(mopidyActions.getCurrentTrack());
store.dispatch(mopidyActions.getTimePosition());
}
break;
case 'MOPIDY_REQUEST':
request(socket, store, action.method, action.params)
.then(
@ -530,21 +554,8 @@ const MopidyMiddleware = (function(){
}
// Enable Iris providers when the backend is available
if (uri_schemes.includes('spotify:')){
store.dispatch({
type: 'SPOTIFY_SET',
data: {
enabled: true
}
});
} else {
store.dispatch({
type: 'SPOTIFY_SET',
data: {
enabled: false
}
});
}
store.dispatch(spotifyActions.set({enabled: uri_schemes.includes('spotify:')}));
store.dispatch(googleActions.set({enabled: uri_schemes.includes('gmusic:')}));
// If we haven't customised our search schemes, add all to search
if (store.getState().ui.uri_schemes_search_enabled === undefined){
@ -770,12 +781,19 @@ const MopidyMiddleware = (function(){
store.dispatch(mopidyActions.clearTracklist());
}
var first_uri = action.uris[0];
// Shuffle/random mode
if (store.getState().mopidy.random){
var first_uri_index = Math.floor(Math.random() * action.uris.length);
} else {
var first_uri_index = 0;
}
var first_uri = action.uris[first_uri_index];
// add our first track
request(socket, store, 'tracklist.add', { uri: first_uri, at_position: 0 })
.then(
response => {
// play it (only if we got a successful lookup)
if (response.length > 0){
store.dispatch(mopidyActions.changeTrack(response[0].tlid));
@ -792,11 +810,13 @@ const MopidyMiddleware = (function(){
));
}
// add the rest of our uris (if any)
action.uris.shift();
// Remove our first_uri as we've already added it
action.uris.splice(first_uri_index, 1);
// And add the rest of our uris (if any)
if (action.uris.length > 0){
// wait 100ms so the server can trigger track_changed etc
// Wait a moment so the server can trigger track_changed etc
// this means our UI feels snappier as the first track shows up quickly
setTimeout(
function(){
@ -1715,7 +1735,7 @@ const MopidyMiddleware = (function(){
var last_run = store.getState().ui.processes.MOPIDY_LIBRARY_ALBUMS_PROCESSOR
if (!last_run){
request(socket, store, 'library.browse', { uri: store.getState().mopidy.library_albums_uri } )
request(socket, store, 'library.browse', { uri: store.getState().mopidy.library_albums_uri })
.then(response => {
if (response.length <= 0) return
@ -2021,9 +2041,9 @@ const MopidyMiddleware = (function(){
var existing_artist = store.getState().core.artists[artist.uri];
if (existing_artist && !existing_artist.images){
if (artist.musicbrainz_id){
store.dispatch(lastfmActions.getArtist(artist.uri, false, artist.musicbrainz_id))
store.dispatch(lastfmActions.getArtist(artist.uri, false, artist.musicbrainz_id));
} else {
store.dispatch(lastfmActions.getArtist(artist.uri, artist.name))
store.dispatch(lastfmActions.getArtist(artist.uri, artist.name));
}
}
})
@ -2242,8 +2262,10 @@ const MopidyMiddleware = (function(){
for (var item of response){
if (item.type === "track"){
tracks_uris.push(item.uri);
tracks_uris = helpers.sortItems(tracks_uris, 'name');
} else {
subdirectories.push(item);
subdirectories = helpers.sortItems(subdirectories, 'name');
}
}

View File

@ -1,24 +1,32 @@
import * as helpers from '../../helpers'
import * as helpers from '../../helpers';
export default function reducer(mopidy = {}, action){
switch (action.type){
case 'MOPIDY_SET':
return Object.assign({}, mopidy, action.data);
case 'MOPIDY_CONNECT':
case 'MOPIDY_CONNECTING':
return Object.assign({}, mopidy, { connected: false, connecting: true });
case 'MOPIDY_RESTARTING':
return Object.assign({}, mopidy, { restarting: true });
case 'MOPIDY_UPGRADING':
return Object.assign({}, mopidy, { upgrading: true });
return Object.assign({}, mopidy, {
connected: false,
connecting: true
});
case 'MOPIDY_CONNECTED':
return Object.assign({}, mopidy, { connected: true, connecting: false, restarting: false, upgrading: false });
return Object.assign({}, mopidy, {
connected: true,
connecting: false,
restart_running: false,
upgrade_running: false
});
case 'MOPIDY_DISCONNECTED':
return Object.assign({}, mopidy, { connected: false, connecting: false });
return Object.assign({}, mopidy, {
connected: false,
connecting: false
});
case 'MOPIDY_CHANGE_TRACK':
return Object.assign({}, mopidy, {
@ -30,8 +38,23 @@ export default function reducer(mopidy = {}, action){
uri_schemes: action.uri_schemes
});
case 'MOPIDY_SET':
return Object.assign({}, mopidy, action.data);
case 'MOPIDY_RESTART_STARTED':
return Object.assign({}, mopidy, { restart_running: true });
case 'MOPIDY_RESTART_FINISHED':
return Object.assign({}, mopidy, { restart_running: false });
case 'MOPIDY_UPGRADE_STARTED':
return Object.assign({}, mopidy, { upgrade_running: true });
case 'MOPIDY_UPGRADE_FINISHED':
return Object.assign({}, mopidy, { upgrade_running: false });
case 'MOPIDY_LOCAL_SCAN_STARTED':
return Object.assign({}, mopidy, { local_scan_running: true });
case 'MOPIDY_LOCAL_SCAN_FINISHED':
return Object.assign({}, mopidy, { local_scan_running: false });
/**

View File

@ -48,6 +48,12 @@ export function restart(){
}
}
export function localScan(){
return {
type: 'PUSHER_LOCAL_SCAN'
}
}
export function getConnections(){
return {
type: 'PUSHER_GET_CONNECTIONS'
@ -192,97 +198,42 @@ export function addQueueMetadata(tlids = [], from_uri = null){
}
}
/**
* Snapcast actions
* TODO: Figure out how to cleanly split this out to it's own service
* but still share the pusher middleware connection
* Commands (buttons)
**/
export function getSnapcast(){
return {
type: 'PUSHER_GET_SNAPCAST',
data: {
method: 'Server.GetStatus'
}
}
export function getCommands(){
return {
type: 'PUSHER_GET_COMMANDS'
}
}
export function snapcastServerLoaded(server){
return {
type: 'PUSHER_SNAPCAST_SERVER_LOADED',
server: server
}
export function setCommand(command){
return {
type: 'PUSHER_SET_COMMAND',
command: command
}
}
export function setSnapcastClientName(id, name){
return {
type: 'PUSHER_SET_SNAPCAST_CLIENT_NAME',
id: id,
name: name
}
export function removeCommand(id){
return {
type: 'PUSHER_REMOVE_COMMAND',
id: id
}
}
export function setSnapcastClientMute(id, mute){
return {
type: 'PUSHER_SET_SNAPCAST_CLIENT_MUTE',
id: id,
mute: mute
}
}
export function setSnapcastClientVolume(id, percent){
return {
type: 'PUSHER_SET_SNAPCAST_CLIENT_VOLUME',
id: id,
percent: percent
}
}
export function setSnapcastClientLatency(id, latency){
return {
type: 'PUSHER_SET_SNAPCAST_CLIENT_LATENCY',
id: id,
latency: latency
}
}
export function setSnapcastClientGroup(id, group_id){
return {
type: 'PUSHER_SET_SNAPCAST_CLIENT_GROUP',
id: id,
group_id: group_id
}
}
export function deleteSnapcastClient(id){
return {
type: 'PUSHER_DELETE_SNAPCAST_CLIENT',
id: id
}
}
export function setSnapcastGroupStream(id, stream_id){
return {
type: 'PUSHER_SET_SNAPCAST_GROUP_STREAM',
id: id,
stream_id: stream_id
}
}
export function setSnapcastGroupMute(id, mute){
return {
type: 'PUSHER_SET_SNAPCAST_GROUP_MUTE',
id: id,
mute: mute
}
}
export function setSnapcastGroupVolume(id, percent, old_percent = 0){
return {
type: 'PUSHER_SET_SNAPCAST_GROUP_VOLUME',
id: id,
percent: percent,
old_percent: old_percent
}
export function sendCommand(id, notify = false){
return {
type: 'PUSHER_SEND_COMMAND',
id: id,
notify: notify
}
}
export function commandsUpdated(commands){
return {
type: 'PUSHER_COMMANDS_UPDATED',
commands: commands
}
}

View File

@ -1,14 +1,15 @@
import ReactGA from 'react-ga'
import ReactGA from 'react-ga';
var helpers = require('../../helpers')
var coreActions = require('../core/actions')
var uiActions = require('../ui/actions')
var mopidyActions = require('../mopidy/actions')
var pusherActions = require('./actions')
var lastfmActions = require('../lastfm/actions')
var geniusActions = require('../genius/actions')
var spotifyActions = require('../spotify/actions')
var helpers = require('../../helpers');
var coreActions = require('../core/actions');
var uiActions = require('../ui/actions');
var mopidyActions = require('../mopidy/actions');
var pusherActions = require('./actions');
var lastfmActions = require('../lastfm/actions');
var geniusActions = require('../genius/actions');
var spotifyActions = require('../spotify/actions');
var snapcastActions = require('../snapcast/actions');
const PusherMiddleware = (function(){
@ -108,18 +109,61 @@ const PusherMiddleware = (function(){
case 'radio_stopped':
store.dispatch(pusherActions.radioStopped());
break;
case 'commands_changed':
store.dispatch(pusherActions.commandsUpdated(message.params.commands));
break;
case 'reload':
window.location.reload(true);
break;
case 'upgrading':
// Local scan
case 'local_scan_started':
store.dispatch(uiActions.updateProcess('local_scan', 'Scanning local library'));
break;
case 'local_scan_finished':
store.dispatch(uiActions.processFinished('local_scan'));
store.dispatch(uiActions.createNotification({key: 'local_scan', type: 'info', content: 'Local scan finished', description: message.params.output}));
break;
case 'local_scan_error':
store.dispatch(uiActions.processFinished('local_scan'));
store.dispatch(uiActions.createNotification({key: 'local_scan', type: 'bad', content: 'Local scan failed'}));
break;
// Upgrade
case 'upgrade_started':
store.dispatch(mopidyActions.upgrading());
store.dispatch(uiActions.createNotification({content: 'Upgrading...', type: 'info'}));
store.dispatch(uiActions.updateProcess('upgrade', 'Upgrading'));
break;
case 'restarting':
store.dispatch(mopidyActions.restarting());
store.dispatch(uiActions.createNotification({content: 'Restarting...', type: 'info'}));
case 'upgrade_finished':
store.dispatch(uiActions.updateProcess('upgrade', 'Restarting to complete upgrade'));
break;
case 'upgrade_error':
store.dispatch(uiActions.processFinished('upgrade'));
store.dispatch(uiActions.createNotification({type: 'bad', content: 'Upgrade failed'}));
break;
// Restart
case 'restart_started':
store.dispatch(uiActions.processFinished('upgrade'));
store.dispatch(uiActions.createNotification({type: 'info', content: 'Restarting server...'}));
break;
// Test
case 'test_started':
store.dispatch(uiActions.updateProcess('test', 'Running test'));
break;
case 'test_finished':
store.dispatch(uiActions.processFinished('test'));
store.dispatch(uiActions.createNotification({type: 'info', content: 'Test finished', description: message.params.output}));
break;
case 'test_error':
store.dispatch(uiActions.processFinished('test'));
store.dispatch(uiActions.createNotification({type: 'bad', content: 'Test failed'}));
break;
}
// Pass snapcast events to the Snapcast service
if (message.method.startsWith('snapcast_')){
store.dispatch(snapcastActions.eventReceived(message));
}
}
}
@ -168,6 +212,8 @@ const PusherMiddleware = (function(){
}
return store => next => action => {
var pusher = store.getState().pusher;
switch(action.type){
case 'PUSHER_CONNECT':
@ -227,6 +273,7 @@ const PusherMiddleware = (function(){
store.dispatch(pusherActions.getConfig());
store.dispatch(pusherActions.getRadio());
store.dispatch(pusherActions.getCommands());
store.dispatch(pusherActions.getQueueMetadata());
// Give things a few moments to setup before we check for version.
@ -236,7 +283,7 @@ const PusherMiddleware = (function(){
function(){
store.dispatch(pusherActions.getVersion());
},
2000
500
);
next(action);
break;
@ -303,7 +350,7 @@ const PusherMiddleware = (function(){
request(store, 'add_queue_metadata', {
tlids: action.tlids,
added_from: action.from_uri,
added_by: store.getState().pusher.username
added_by: pusher.username
})
break;
@ -387,6 +434,112 @@ const PusherMiddleware = (function(){
return next(action);
break;
/**
* Commands
**/
case 'PUSHER_GET_COMMANDS':
request(store, 'get_commands')
.then(
response => {
store.dispatch(pusherActions.commandsUpdated(response.commands));
},
error => {
// We're not too worried about capturing errors here
// It's also likely to fail where UI has been updated but
// server hasn't been restarted yet.
}
);
next(action);
break
case 'PUSHER_SET_COMMAND':
var commands_index = Object.assign({}, pusher.commands);
if (commands_index[action.command.id]){
var command = Object.assign({}, commands_index[action.command.id], action.command);
} else {
var command = action.command;
}
commands_index[action.command.id] = command;
request(store, 'set_commands', {commands: commands_index})
.then(
response => {
// No action required, the change will be broadcast
},
error => {
store.dispatch(coreActions.handleException(
'Could not set commands',
error
));
}
);
next(action);
break
case 'PUSHER_REMOVE_COMMAND':
var commands_index = Object.assign({}, pusher.commands);
delete commands_index[action.id];
request(store, 'set_commands', {commands: commands_index})
.then(
response => {
// No action required, the change will be broadcast
},
error => {
store.dispatch(coreActions.handleException(
'Could not remove command',
error
));
}
);
next(action);
break
case 'PUSHER_SEND_COMMAND':
var command = Object.assign({}, pusher.commands[action.id]);
var notification_key = 'command_'+action.id;
if (action.notify){
store.dispatch(uiActions.startProcess(notification_key, 'Sending command'));
}
try {
var ajax_settings = JSON.parse(command.command);
} catch(error){
store.dispatch(uiActions.createNotification({key: notification_key, type: 'bad', content: 'Command failed', description: error}));
break;
}
// Handle success and failure
ajax_settings.success = function(response){
console.log("Command sent, response was:",response);
if (action.notify){
store.dispatch(uiActions.processFinished(notification_key));
store.dispatch(uiActions.createNotification({key: notification_key, type: 'info', content: 'Command sent'}));
}
}
ajax_settings.error = function(xhr, status, error){
console.error("Command failed, response was:",xhr,error);
store.dispatch(uiActions.processFinished(notification_key));
store.dispatch(uiActions.createNotification({key: notification_key, type: 'bad', content: 'Command failed', description: xhr.status+": "+error}));
}
// Actually send the request
$.ajax(ajax_settings);
break
/**
* Radio
**/
case 'PUSHER_GET_RADIO':
request(store, 'get_radio')
.then(
@ -445,7 +598,7 @@ const PusherMiddleware = (function(){
{
notification: {
type: 'info',
content: store.getState().pusher.username + ' is starting radio mode'
content: pusher.username + ' is starting radio mode'
}
}
));
@ -482,7 +635,7 @@ const PusherMiddleware = (function(){
{
notification: {
type: 'info',
content: store.getState().pusher.username + ' stopped radio mode'
content: pusher.username + ' stopped radio mode'
}
}
));
@ -506,6 +659,11 @@ const PusherMiddleware = (function(){
);
break
/**
* Notifications and alerts
**/
case 'PUSHER_BROWSER_NOTIFICATION':
store.dispatch(uiActions.createBrowserNotification(action))
break
@ -520,6 +678,11 @@ const PusherMiddleware = (function(){
store.dispatch(uiActions.createNotification(data));
break
/**
* Server actions
**/
case 'PUSHER_RELOAD':
// Hard reload. This doesn't strictly clear the cache, but our compiler's
// cache buster should handle that
@ -527,15 +690,7 @@ const PusherMiddleware = (function(){
break
case 'PUSHER_RESTART':
request(store, 'restart')
.then(
response => {
store.dispatch(mopidyActions.restarting());
},
error => {
store.dispatch(uiActions.createNotification({content: error.message, description: (error.description ? error.description : null), type: 'bad'}));
}
);
request(store, 'restart');
next(action);
break
@ -543,15 +698,18 @@ const PusherMiddleware = (function(){
if (store.getState().ui.allow_reporting){
ReactGA.event({ category: 'Pusher', action: 'Upgrade', label: '' });
}
request(store, 'upgrade')
.then(
response => {
store.dispatch(mopidyActions.upgrading());
},
error => {
store.dispatch(uiActions.createNotification({content: error.message, description: (error.description ? error.description : null), type: 'bad'}));
}
);
request(store, 'upgrade');
break;
case 'PUSHER_LOCAL_SCAN':
if (store.getState().ui.allow_reporting){
ReactGA.event({ category: 'Pusher', action: 'Local scan', label: '' });
}
request(store, 'local_scan');
break;
case 'PUSHER_TEST':
request(store, 'test');
break;
case 'PUSHER_VERSION':

View File

@ -54,30 +54,8 @@ export default function reducer(pusher = {}, action){
case 'PUSHER_CONFIG':
return Object.assign({}, pusher, { config: action.config });
case 'PUSHER_SNAPCAST':
return Object.assign({}, pusher, {
snapcast_streams: action.snapcast_streams,
snapcast_groups: action.snapcast_groups,
snapcast_clients: action.snapcast_clients
});
case 'PUSHER_SNAPCAST_GROUP_UPDATED':
var snapcast_groups = Object.assign({}, pusher.snapcast_groups);
var group = snapcast_groups[action.key];
snapcast_groups[action.key] = Object.assign({}, group, action.group);
return Object.assign({}, pusher, { snapcast_groups: snapcast_groups });
case 'PUSHER_SNAPCAST_CLIENT_UPDATED':
var snapcast_clients = Object.assign({}, pusher.snapcast_clients);
var client = snapcast_clients[action.key];
client.config = Object.assign({}, client.config, action.client.config);
snapcast_clients[action.key] = client;
return Object.assign({}, pusher, { snapcast_clients: snapcast_clients });
case 'PUSHER_SNAPCAST_CLIENT_REMOVED':
var snapcast_clients = Object.assign({}, pusher.snapcast_clients);
delete snapcast_clients[action.key];
return Object.assign({}, pusher, { snapcast_clients: snapcast_clients });
case 'PUSHER_COMMANDS_UPDATED':
return Object.assign({}, pusher, { commands: action.commands });
default:
return pusher

View File

@ -37,29 +37,6 @@ export function setClientLatency(id, latency){
}
}
export function setClientCommand(id, command){
return {
type: 'SNAPCAST_SET_CLIENT_COMMAND',
id: id,
command: command
}
}
export function sendClientCommand(id, command){
return {
type: 'SNAPCAST_SEND_CLIENT_COMMAND',
id: id,
command: command
}
}
export function clientCommandsUpdated(client_commands){
return {
type: 'SNAPCAST_CLIENT_COMMANDS_UPDATED',
client_commands: client_commands
}
}
export function setClientGroup(id, group_id){
return {
type: 'SNAPCAST_SET_CLIENT_GROUP',
@ -100,6 +77,14 @@ export function setGroupVolume(id, percent, old_percent = 0){
}
}
export function eventReceived(message){
return {
type: 'SNAPCAST_EVENT_RECEIVED',
method: message.method,
params: message.params
}
}

View File

@ -13,7 +13,7 @@ const SnapcastMiddleware = (function(){
const request = (store, params = null, response_callback = null, error_callback = null) => {
store.dispatch(
pusherActions.request(
'snapcast_instruct',
'snapcast',
params,
response_callback,
error_callback
@ -228,37 +228,6 @@ const SnapcastMiddleware = (function(){
);
break
case 'SNAPCAST_SET_CLIENT_COMMAND':
var client_commands_index = Object.assign({}, snapcast.client_commands);
if (client_commands_index[action.id]){
var client_command = Object.assign({}, client_commands_index[action.id], action.command);
} else {
var client_command = action.command;
}
client_commands_index[action.id] = client_command;
store.dispatch(snapcastActions.clientCommandsUpdated(client_commands_index));
next(action);
break
case 'SNAPCAST_SEND_CLIENT_COMMAND':
// Prepare our command
// We try and make it cross-origin compatible
var command = JSON.parse(action.command);
command.crossDomain = true;
command.dataType = "jsonp";
command.success = function(response){
store.dispatch(uiActions.createNotification({type: 'info', content: 'Command sent', description: command.url}));
}
// Actually send the request
$.ajax(command);
break
case 'SNAPCAST_SET_CLIENT_GROUP':
var group = snapcast.groups[action.group_id];
@ -423,6 +392,77 @@ const SnapcastMiddleware = (function(){
}
break;
case 'SNAPCAST_EVENT_RECEIVED':
// Drop the prefix
action.method = action.method.replace('snapcast_','');
switch (action.method){
case 'Client.OnConnect':
store.dispatch(snapcastActions.clientLoaded(
{
id: action.params.client.id,
name: action.params.client.name,
volume: action.params.client.volume.percent,
mute: action.params.client.volume.muted,
connected: action.params.client.connected
}
));
break;
case 'Client.OnDisconnect':
store.dispatch(snapcastActions.clientLoaded(
{
id: action.params.client.id,
connected: action.params.client.connected
}
));
break;
case 'Client.OnVolumeChanged':
store.dispatch(snapcastActions.clientLoaded(
{
id: action.params.id,
mute: action.params.volume.muted,
volume: action.params.volume.percent
}
));
break;
case 'Client.OnLatencyChanged':
store.dispatch(snapcastActions.clientLoaded(
{
id: action.params.id,
latency: action.params.latency
}
));
break;
case 'Client.OnNameChanged':
store.dispatch(snapcastActions.clientLoaded(
{
id: action.params.id,
name: action.params.name
}
));
break;
case 'Group.OnMute':
store.dispatch(snapcastActions.groupLoaded(
{
id: action.params.id,
mute: action.params.mute
}
));
break;
case 'Server.OnUpdate':
store.dispatch(snapcastActions.serverLoaded(action.params));
break;
}
break;
// This action is irrelevant to us, pass it on to the next middleware
default:
return next(action);

View File

@ -6,9 +6,6 @@ export default function reducer(snapcast = {}, action){
var server = Object.assign({}, action.server);
return Object.assign({}, snapcast, { server: server });
case 'SNAPCAST_CLIENT_COMMANDS_UPDATED':
return Object.assign({}, snapcast, { client_commands: action.client_commands });
case 'SNAPCAST_CLIENTS_LOADED':
if (action.flush){
var clients = {};

View File

@ -15,10 +15,16 @@ var helpers = require('../../helpers')
* @param data mixed = request payload
* @return Promise
**/
const sendRequest = (dispatch, getState, endpoint, method = 'GET', data = false) => {
const request = (dispatch, getState, endpoint, method = 'GET', data = false) => {
// Add reference to loader queue
// We do this straight away so that even if we're refreshing the token, it still registers as
// loading said endpoint
var loader_key = helpers.generateGuid()
dispatch(uiActions.startLoading(loader_key, 'spotify_'+endpoint));
return new Promise((resolve, reject) => {
getToken(dispatch, getState )
getToken(dispatch, getState)
.then(
response => {
@ -49,10 +55,6 @@ const sendRequest = (dispatch, getState, endpoint, method = 'GET', data = false)
}
}
// add reference to loader queue
var loader_key = helpers.generateGuid()
dispatch(uiActions.startLoading(loader_key, 'spotify_'+endpoint))
$.ajax(config).then(
response => {
dispatch(uiActions.stopLoading(loader_key))
@ -105,21 +107,45 @@ function getToken(dispatch, getState){
}
// token is expiring/expired, so go get a new one and resolve that
refreshToken(dispatch, getState)
.then(
response => {
resolve(response.access_token)
},
error => {
reject(error)
}
);
// TODO: Detect whether we already have a pending refresh, in which case we
// need to wait until it's done, and then return that
// We've already got a refresh in progress
if (getState().ui.load_queue.spotify_refresh_token !== undefined){
console.log("Already refreshing token, we'll wait 1000ms and try again");
// Re-check the queue periodically to see if it's finished yet
// TODO: Look at properly hooking up with the ajax finish event
setTimeout(
() => {
// Return myself for a re-check
return getToken(dispatch, getState);
},
1000
);
} else {
refreshToken(dispatch, getState)
.then(
response => {
resolve(response.access_token)
},
error => {
reject(error)
}
);
}
});
}
function refreshToken(dispatch, getState){
return new Promise((resolve, reject) => {
// add reference to loader queue
var loader_key = helpers.generateGuid();
dispatch(uiActions.startLoading(loader_key, 'spotify_refresh_token'));
// Fully-authorized, so we can use the local Spotify credentials
if (getState().spotify.authorization){
var config = {
@ -132,6 +158,8 @@ function refreshToken(dispatch, getState){
$.ajax(config)
.then(
response => {
dispatch(uiActions.stopLoading(loader_key));
response.token_expiry = new Date().getTime() + (response.expires_in * 1000 )
response.source = 'spotify'
dispatch({
@ -141,6 +169,8 @@ function refreshToken(dispatch, getState){
resolve(response)
},
(xhr, status, error) => {
dispatch(uiActions.stopLoading(loader_key));
reject({
config: config,
xhr: xhr,
@ -149,7 +179,9 @@ function refreshToken(dispatch, getState){
});
}
);
// Server-side authorized (with limited scope) so we need to refresh
// using the Mopidy-Spotify credentials
} else {
var config = {
@ -162,6 +194,8 @@ function refreshToken(dispatch, getState){
$.ajax(config)
.then(
(response, status, xhr) => {
dispatch(uiActions.stopLoading(loader_key));
if (response.error){
reject({
config: config,
@ -184,6 +218,8 @@ function refreshToken(dispatch, getState){
},
(xhr, status, error) => {
dispatch(uiActions.stopLoading(loader_key));
reject({
config: config,
xhr: xhr,
@ -252,7 +288,7 @@ export function importAuthorization(authorization){
**/
export function getMe(){
return (dispatch, getState) => {
sendRequest(dispatch, getState, 'me' )
request(dispatch, getState, 'me' )
.then(
response => {
dispatch({
@ -278,7 +314,7 @@ export function getMe(){
**/
export function getTrack(uri){
return (dispatch, getState) => {
sendRequest(dispatch, getState, 'tracks/'+ helpers.getFromUri('trackid', uri))
request(dispatch, getState, 'tracks/'+ helpers.getFromUri('trackid', uri))
.then(
response => {
dispatch(coreActions.trackLoaded(response));
@ -295,7 +331,7 @@ export function getTrack(uri){
export function getLibraryTracks(){
return (dispatch, getState) => {
sendRequest(dispatch, getState, 'me/tracks?limit=50')
request(dispatch, getState, 'me/tracks?limit=50')
.then(
response => {
dispatch({
@ -334,7 +370,7 @@ export function getFeaturedPlaylists(){
var timestamp = year+'-'+month+'-'+day+'T'+hour+':'+min+':'+sec;
sendRequest(dispatch, getState, 'browse/featured-playlists?limit=50&country='+getState().spotify.country+'&locale='+getState().spotify.locale+'timestamp='+timestamp)
request(dispatch, getState, 'browse/featured-playlists?limit=50&country='+getState().spotify.country+'&locale='+getState().spotify.locale+'timestamp='+timestamp)
.then(
response => {
var playlists = []
@ -363,7 +399,7 @@ export function getFeaturedPlaylists(){
type: 'SPOTIFY_FEATURED_PLAYLISTS_LOADED',
data: {
message: response.message,
playlists: helpers.upgradePlaylistsUris(helpers.arrayOf('uri',response.playlists.items))
playlists: helpers.upgradeSpotifyPlaylistUris(helpers.arrayOf('uri',response.playlists.items))
}
});
},
@ -379,7 +415,7 @@ export function getFeaturedPlaylists(){
export function getCategories(){
return (dispatch, getState) => {
sendRequest(dispatch, getState, 'browse/categories?limit=50&country='+getState().spotify.country+'&locale='+getState().spotify.locale )
request(dispatch, getState, 'browse/categories?limit=50&country='+getState().spotify.country+'&locale='+getState().spotify.locale )
.then(
response => {
dispatch({
@ -399,7 +435,7 @@ export function getCategories(){
export function getCategory(id){
return (dispatch, getState) => {
sendRequest(dispatch, getState, 'browse/categories/'+id+'?country='+getState().spotify.country+'&locale='+getState().spotify.locale )
request(dispatch, getState, 'browse/categories/'+id+'?country='+getState().spotify.country+'&locale='+getState().spotify.locale )
.then(
response => {
dispatch({
@ -425,7 +461,7 @@ export function getCategory(id){
export function getCategoryPlaylists(id){
return (dispatch, getState) => {
sendRequest(dispatch, getState, 'browse/categories/'+id+'/playlists?limit=50&country='+getState().spotify.country+'&locale='+getState().spotify.locale )
request(dispatch, getState, 'browse/categories/'+id+'/playlists?limit=50&country='+getState().spotify.country+'&locale='+getState().spotify.locale )
.then(
response => {
dispatch({
@ -446,7 +482,7 @@ export function getCategoryPlaylists(id){
export function getNewReleases(){
return (dispatch, getState) => {
sendRequest(dispatch, getState, 'browse/new-releases?country='+getState().spotify.country+'&limit=50' )
request(dispatch, getState, 'browse/new-releases?country='+getState().spotify.country+'&limit=50' )
.then(
response => {
dispatch({
@ -466,7 +502,7 @@ export function getNewReleases(){
export function getURL(url, action_name, key = false){
return (dispatch, getState) => {
sendRequest(dispatch, getState, url )
request(dispatch, getState, url )
.then(
response => {
dispatch({
@ -487,7 +523,7 @@ export function getURL(url, action_name, key = false){
export function getMore(url, core_action = null, custom_action = null){
return (dispatch, getState) => {
sendRequest(dispatch, getState, url)
request(dispatch, getState, url)
.then(
response => {
if (core_action){
@ -538,7 +574,7 @@ export function getSearchResults(type, query, limit = 50, offset = 0){
url += '&limit='+limit
url += '&offset='+offset
sendRequest(dispatch, getState, url )
request(dispatch, getState, url )
.then(
response => {
if (response.tracks !== undefined){
@ -628,7 +664,7 @@ export function getAutocompleteResults(field_id, query, types = ['album','artist
endpoint += '&type='+types.join(',')
endpoint += '&country='+getState().spotify.country
sendRequest(dispatch, getState, endpoint)
request(dispatch, getState, endpoint)
.then(
response => {
var genres = []
@ -740,7 +776,7 @@ export function following(uri, method = 'GET'){
break;
}
sendRequest(dispatch, getState, endpoint, method, data)
request(dispatch, getState, endpoint, method, data)
.then(
response => {
if (response ) is_following = response
@ -777,7 +813,7 @@ export function resolveRadioSeeds(radio){
artist_ids += helpers.getFromUri('artistid', radio.seed_artists[i])
}
sendRequest(dispatch, getState, 'artists?ids='+ artist_ids )
request(dispatch, getState, 'artists?ids='+ artist_ids )
.then(
response => {
if (response && response.artists){
@ -805,7 +841,7 @@ export function resolveRadioSeeds(radio){
track_ids += helpers.getFromUri('trackid', radio.seed_tracks[i])
}
sendRequest(dispatch, getState, 'tracks?ids='+ track_ids )
request(dispatch, getState, 'tracks?ids='+ track_ids )
.then(
response => {
dispatch({
@ -842,8 +878,8 @@ export function getFavorites(limit = 50, term = 'long_term'){
dispatch({type: 'SPOTIFY_FAVORITES_LOADED', artists: [], tracks: []})
$.when(
sendRequest(dispatch, getState, 'me/top/artists?limit='+limit+'&time_range='+term),
sendRequest(dispatch, getState, 'me/top/tracks?limit='+limit+'&time_range='+term)
request(dispatch, getState, 'me/top/artists?limit='+limit+'&time_range='+term),
request(dispatch, getState, 'me/top/tracks?limit='+limit+'&time_range='+term)
).then(
(artists_response, tracks_response) => {
@ -918,7 +954,7 @@ export function getRecommendations(uris = [], limit = 20, tunabilities = null){
}
}
sendRequest(dispatch, getState, endpoint)
request(dispatch, getState, endpoint)
.then(
response => {
var tracks = Object.assign([], response.tracks);
@ -991,7 +1027,7 @@ export function getRecommendations(uris = [], limit = 20, tunabilities = null){
**/
export function getGenres(){
return (dispatch, getState) => {
sendRequest(dispatch, getState, 'recommendations/available-genre-seeds')
request(dispatch, getState, 'recommendations/available-genre-seeds')
.then(
response => {
dispatch({
@ -1032,7 +1068,7 @@ export function getArtist(uri, full = false){
// We need our artist, obviously
var requests = [
sendRequest(dispatch, getState, 'artists/'+ helpers.getFromUri('artistid', uri) )
request(dispatch, getState, 'artists/'+ helpers.getFromUri('artistid', uri) )
.then(
response => {
Object.assign(artist, response);
@ -1050,7 +1086,7 @@ export function getArtist(uri, full = false){
if (full){
requests.push(
sendRequest(dispatch, getState, 'artists/'+ helpers.getFromUri('artistid', uri) +'/top-tracks?country='+getState().spotify.country )
request(dispatch, getState, 'artists/'+ helpers.getFromUri('artistid', uri) +'/top-tracks?country='+getState().spotify.country )
.then(
response => {
Object.assign(artist, response);
@ -1065,7 +1101,7 @@ export function getArtist(uri, full = false){
);
requests.push(
sendRequest(dispatch, getState, 'artists/'+ helpers.getFromUri('artistid', uri) +'/related-artists' )
request(dispatch, getState, 'artists/'+ helpers.getFromUri('artistid', uri) +'/related-artists' )
.then(
response => {
dispatch(coreActions.artistsLoaded(response.artists));
@ -1094,7 +1130,7 @@ export function getArtist(uri, full = false){
// Now go get our artist albums
if (full){
sendRequest(dispatch, getState, 'artists/'+ helpers.getFromUri('artistid', uri) +'/albums?market='+getState().spotify.country )
request(dispatch, getState, 'artists/'+ helpers.getFromUri('artistid', uri) +'/albums?market='+getState().spotify.country )
.then(
response => {
dispatch({
@ -1125,7 +1161,7 @@ export function getArtists(uris){
ids += helpers.getFromUri('artistid', uris[i] );
}
sendRequest(dispatch, getState, 'artists/?ids='+ids )
request(dispatch, getState, 'artists/?ids='+ids )
.then(
response => {
for (var i = i; i < response.length; i++){
@ -1163,7 +1199,7 @@ export function playArtistTopTracks(uri){
// We need to load the artist's top tracks first
} else {
sendRequest(dispatch, getState, 'artists/'+ helpers.getFromUri('artistid', uri) +'/top-tracks?country='+getState().spotify.country )
request(dispatch, getState, 'artists/'+ helpers.getFromUri('artistid', uri) +'/top-tracks?country='+getState().spotify.country )
.then(
response => {
const uris = helpers.arrayOf('uri',response.tracks)
@ -1189,7 +1225,7 @@ export function playArtistTopTracks(uri){
export function getUser(uri){
return (dispatch, getState) => {
sendRequest(dispatch, getState, 'users/'+ helpers.getFromUri('userid',uri))
request(dispatch, getState, 'users/'+ helpers.getFromUri('userid',uri))
.then(
response => {
dispatch(coreActions.userLoaded(helpers.formatUser(response)));
@ -1208,7 +1244,7 @@ export function getUserPlaylists(uri){
return (dispatch, getState) => {
// get the first page of playlists
sendRequest(dispatch, getState, 'users/'+ helpers.getFromUri('userid', uri) +'/playlists?limit=40' )
request(dispatch, getState, 'users/'+ helpers.getFromUri('userid', uri) +'/playlists?limit=40' )
.then(
response => {
var playlists = [];
@ -1260,7 +1296,7 @@ export function getAlbum(uri){
return (dispatch, getState) => {
// get the album
sendRequest(dispatch, getState, 'albums/'+ helpers.getFromUri('albumid', uri) )
request(dispatch, getState, 'albums/'+ helpers.getFromUri('albumid', uri) )
.then(
response => {
@ -1299,7 +1335,7 @@ export function getAlbum(uri){
}
// get all album artists as full objects
sendRequest(dispatch, getState, 'artists/?ids='+artist_ids )
request(dispatch, getState, 'artists/?ids='+artist_ids )
.then(
response => {
dispatch(coreActions.artistsLoaded(response.artists));
@ -1328,7 +1364,7 @@ export function toggleAlbumInLibrary(uri, method){
if (method == 'DELETE' ) var new_state = 0
return (dispatch, getState) => {
sendRequest(dispatch, getState, 'me/albums?ids='+ helpers.getFromUri('albumid',uri), method )
request(dispatch, getState, 'me/albums?ids='+ helpers.getFromUri('albumid',uri), method )
.then(
response => {
dispatch({
@ -1366,7 +1402,7 @@ export function createPlaylist(name, description, is_public, is_collaborative){
collaborative: is_collaborative
}
sendRequest(dispatch, getState, 'users/'+ getState().spotify.me.id +'/playlists/', 'POST', data)
request(dispatch, getState, 'users/'+ getState().spotify.me.id +'/playlists/', 'POST', data)
.then(
response => {
dispatch({
@ -1411,7 +1447,7 @@ export function savePlaylist(uri, name, description, is_public, is_collaborative
}
// Update the playlist fields
sendRequest(
request(
dispatch, getState, 'users/'+ getState().spotify.me.id +'/playlists/'+ helpers.getFromUri('playlistid',uri), 'PUT', data)
.then(
response => {
@ -1419,7 +1455,7 @@ export function savePlaylist(uri, name, description, is_public, is_collaborative
// Save the image
if (image){
sendRequest(dispatch, getState, 'users/'+ getState().spotify.me.id +'/playlists/'+ helpers.getFromUri('playlistid',uri)+'/images', 'PUT', image)
request(dispatch, getState, 'users/'+ getState().spotify.me.id +'/playlists/'+ helpers.getFromUri('playlistid',uri)+'/images', 'PUT', image)
.then(
response => {
@ -1470,7 +1506,7 @@ export function getPlaylist(uri){
return (dispatch, getState) => {
// get the main playlist object
sendRequest(dispatch, getState, 'playlists/'+ helpers.getFromUri('playlistid',uri) +'?market='+getState().spotify.country )
request(dispatch, getState, 'playlists/'+ helpers.getFromUri('playlistid',uri) +'?market='+getState().spotify.country )
.then(
response => {
@ -1533,7 +1569,7 @@ export function getLibraryTracksAndPlay(uri){
export function getLibraryTracksAndPlayProcessor(data){
return (dispatch, getState) => {
sendRequest(dispatch, getState, data.next)
request(dispatch, getState, data.next)
.then(
response => {
@ -1614,7 +1650,7 @@ export function getPlaylistTracksAndPlay(uri, shuffle){
export function getPlaylistTracksAndPlayProcessor(data){
return (dispatch, getState) => {
sendRequest(dispatch, getState, data.next)
request(dispatch, getState, data.next)
.then(
response => {
@ -1685,7 +1721,7 @@ export function toggleFollowingPlaylist(uri, method){
if (method == 'DELETE' ) var new_state = 0
return (dispatch, getState) => {
sendRequest(dispatch, getState, 'playlists/'+ helpers.getFromUri('playlistid',uri) + '/followers', method )
request(dispatch, getState, 'playlists/'+ helpers.getFromUri('playlistid',uri) + '/followers', method )
.then(
response => {
dispatch({
@ -1706,7 +1742,7 @@ export function toggleFollowingPlaylist(uri, method){
export function addTracksToPlaylist(uri, tracks_uris){
return (dispatch, getState) => {
sendRequest(dispatch, getState, 'playlists/'+ helpers.getFromUri('playlistid',uri) + '/tracks', 'POST', { uris: tracks_uris } )
request(dispatch, getState, 'playlists/'+ helpers.getFromUri('playlistid',uri) + '/tracks', 'POST', { uris: tracks_uris } )
.then(
response => {
dispatch({
@ -1728,7 +1764,7 @@ export function addTracksToPlaylist(uri, tracks_uris){
export function deleteTracksFromPlaylist(uri, snapshot_id, tracks_indexes){
return (dispatch, getState) => {
sendRequest(dispatch, getState, 'playlists/'+ helpers.getFromUri('playlistid',uri) + '/tracks', 'DELETE', { snapshot_id: snapshot_id, positions: tracks_indexes } )
request(dispatch, getState, 'playlists/'+ helpers.getFromUri('playlistid',uri) + '/tracks', 'DELETE', { snapshot_id: snapshot_id, positions: tracks_indexes } )
.then(
response => {
dispatch({
@ -1750,7 +1786,7 @@ export function deleteTracksFromPlaylist(uri, snapshot_id, tracks_indexes){
export function reorderPlaylistTracks(uri, range_start, range_length, insert_before, snapshot_id){
return (dispatch, getState) => {
sendRequest(dispatch, getState, 'playlists/'+ helpers.getFromUri('playlistid',uri) + '/tracks', 'PUT', { uri: uri, range_start: range_start, range_length: range_length, insert_before: insert_before, snapshot_id: snapshot_id } )
request(dispatch, getState, 'playlists/'+ helpers.getFromUri('playlistid',uri) + '/tracks', 'PUT', { uri: uri, range_start: range_start, range_length: range_length, insert_before: insert_before, snapshot_id: snapshot_id } )
.then(
response => {
dispatch({
@ -1808,7 +1844,7 @@ export function getLibraryPlaylists(){
export function getLibraryPlaylistsProcessor(data){
return (dispatch, getState) => {
sendRequest(dispatch, getState, data.next)
request(dispatch, getState, data.next)
.then(
response => {
dispatch({
@ -1879,7 +1915,7 @@ export function getLibraryArtists(){
export function getLibraryArtistsProcessor(data){
return (dispatch, getState) => {
sendRequest(dispatch, getState, data.next)
request(dispatch, getState, data.next)
.then(
response => {
dispatch({
@ -1949,7 +1985,7 @@ export function getLibraryAlbums(){
export function getLibraryAlbumsProcessor(data){
return (dispatch, getState) => {
sendRequest(dispatch, getState, data.next)
request(dispatch, getState, data.next)
.then(
response => {
dispatch({

View File

@ -197,7 +197,7 @@ const SpotifyMiddleware = (function(){
delete action.playlists;
// Upgrade our URIs
action.uris = helpers.upgradePlaylistsUris(action.uris);
action.uris = helpers.upgradeSpotifyPlaylistUris(action.uris);
next(action);
break;

View File

@ -50,12 +50,25 @@ export function setSlimMode(slim_mode){
}
}
export function setWindowFocus(window_focus){
return {
type: 'SET_WINDOW_FOCUS',
window_focus: window_focus
}
}
export function hideContextMenu(){
return {
type: 'HIDE_CONTEXT_MENU'
}
}
export function removeContextMenu(){
return {
type: 'REMOVE_CONTEXT_MENU'
}
}
export function showTouchContextMenu(data){
return {
type: 'SHOW_TOUCH_CONTEXT_MENU',
@ -69,6 +82,12 @@ export function hideTouchContextMenu(){
}
}
export function removeTouchContextMenu(){
return {
type: 'REMOVE_TOUCH_CONTEXT_MENU'
}
}
export function lazyLoading(start){
return {
type: 'LAZY_LOADING',
@ -136,6 +155,12 @@ export function createBrowserNotification(data){
}
export function createNotification(data){
// Allow 'message' as an alias of our content
if (data.message){
data.content = data.message;
}
return {
type: 'CREATE_NOTIFICATION',
notification: Object.assign(

View File

@ -64,12 +64,36 @@ const UIMiddleware = (function(){
case 'CLOSE_MODAL':
if (store.getState().ui.allow_reporting){
ReactGA.event({ category: 'Modal', action: 'Closed', label: null });
}
ReactGA.event({ category: 'Modal', action: 'Closed', label: null });
}
$('body').removeClass('modal-open')
next(action)
break
case 'HIDE_CONTEXT_MENU':
if (store.getState().ui.context_menu){
setTimeout(
() => {
store.dispatch(uiActions.removeContextMenu())
},
200
);
next(action);
}
break;
case 'HIDE_TOUCH_CONTEXT_MENU':
if (store.getState().ui.context_menu){
setTimeout(
() => {
store.dispatch(uiActions.removeTouchContextMenu())
},
200
);
next(action);
}
break;
case 'BROWSER_NOTIFICATION':
var notification = window.Notification || window.mozNotification || window.webkitNotification;

View File

@ -10,6 +10,9 @@ export default function reducer(ui = {}, action){
case 'SET_SLIM_MODE':
return Object.assign({}, ui, { slim_mode: action.slim_mode });
case 'SET_WINDOW_FOCUS':
return Object.assign({}, ui, { window_focus: action.window_focus });
case 'DEBUG':
return Object.assign({}, ui, { debug_response: action.response })
@ -50,6 +53,15 @@ export default function reducer(ui = {}, action){
});
case 'HIDE_CONTEXT_MENU':
return Object.assign(
{},
ui,
{
context_menu: Object.assign({}, ui.context_menu, {closing: true})
}
);
case 'REMOVE_CONTEXT_MENU':
return Object.assign({}, ui, {context_menu: null});
case 'SHOW_TOUCH_CONTEXT_MENU':
@ -58,6 +70,15 @@ export default function reducer(ui = {}, action){
});
case 'HIDE_TOUCH_CONTEXT_MENU':
return Object.assign(
{},
ui,
{
touch_context_menu: Object.assign({}, ui.touch_context_menu, {closing: true})
}
);
case 'REMOVE_TOUCH_CONTEXT_MENU':
return Object.assign({}, ui, {touch_context_menu: null});