Merge branch 'feature/tracks-index' into develop
This commit is contained in:
154
build_tools/auth_lastfm.php
Executable file
154
build_tools/auth_lastfm.php
Executable file
@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* LastFM Authentication proxy
|
||||
*
|
||||
* To use:
|
||||
* 1. Create a LastFM App (https://www.last.fm/api/account/create), and paste in your credentials below
|
||||
* 2. Save this script to a publicly-accessible server
|
||||
* 3. Set config in mopidy.conf to point to this script
|
||||
**/
|
||||
|
||||
// LastFM app credentials
|
||||
define('API_URL','http://ws.audioscrobbler.com/2.0/?format=json');
|
||||
define('API_KEY','YOUR_KEY_HERE');
|
||||
define('API_SECRET','YOUR_SECRET_HERE');
|
||||
define('REDIRECT_URI','YOUR_REDIRECT_URI_HERE');
|
||||
|
||||
// Allow cross-domain requests
|
||||
header("Access-Control-Allow-Origin: *");
|
||||
|
||||
// Set our cookies
|
||||
if (isset($_GET['app'])){
|
||||
setcookie('mopidy_iris', $_GET['app'], time()+3600);
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================================= INIT ================ */
|
||||
/* ======================================================================================================= */
|
||||
|
||||
if (isset($_GET['action'])){
|
||||
switch ($_GET['action']){
|
||||
|
||||
case 'authorize':
|
||||
header('Location: http://www.last.fm/api/auth/?api_key='.API_KEY.'&cb='.REDIRECT_URI);
|
||||
exit;
|
||||
|
||||
case 'start_session':
|
||||
$session = startSession($_GET['token']);
|
||||
|
||||
// Pass our error back to the popup opener
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
window.opener.postMessage( '<?php echo json_encode($session) ?>', "*");
|
||||
window.close();
|
||||
</script>
|
||||
<?php
|
||||
|
||||
break;
|
||||
|
||||
case 'sign_request':
|
||||
$data = $_GET;
|
||||
$signed = signRequest($data);
|
||||
echo json_encode($signed);
|
||||
exit;
|
||||
|
||||
default:
|
||||
echo 'Invalid action specified';
|
||||
die();
|
||||
|
||||
}
|
||||
} else {
|
||||
echo "No action specified";
|
||||
die();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* ================================================================================= GETTERS ============= */
|
||||
/* ======================================================================================================= */
|
||||
|
||||
/**
|
||||
* Start a session
|
||||
*
|
||||
* @param $data = array
|
||||
* @param $post = boolean (POST request)
|
||||
**/
|
||||
function startSession($token){
|
||||
|
||||
$ch = curl_init();
|
||||
|
||||
if (FALSE === $ch){
|
||||
throw new Exception('Failed to initialize');
|
||||
}
|
||||
|
||||
$data = signRequest(
|
||||
array(
|
||||
"method" => "auth.getSession",
|
||||
"token" => $token
|
||||
),
|
||||
false
|
||||
);
|
||||
|
||||
curl_setopt($ch, CURLOPT_URL,API_URL);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
|
||||
curl_setopt($ch, CURLINFO_HEADER_OUT, true);
|
||||
curl_setopt($ch, CURLOPT_VERBOSE, true);
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
|
||||
if (curl_errno($ch)){
|
||||
echo 'CURL Error: '. curl_error($ch);
|
||||
}
|
||||
curl_close($ch);
|
||||
|
||||
$response = json_decode($response, true);
|
||||
|
||||
// Append our other important values to successful signings
|
||||
if (!$response['error']){
|
||||
$response['api_key'] = API_KEY;
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Perform a signed request
|
||||
*
|
||||
* @param $data = array
|
||||
* @param $post = boolean (POST request)
|
||||
**/
|
||||
function signRequest($data = array(), $post = true){
|
||||
unset($data['action']);
|
||||
|
||||
$ch = curl_init();
|
||||
|
||||
if (FALSE === $ch){
|
||||
throw new Exception('Failed to initialize');
|
||||
}
|
||||
|
||||
// Make sure we've got our API key included
|
||||
$request = array_merge(array("api_key" => API_KEY), $data);
|
||||
|
||||
// Loop all the values in our request and add to our signature
|
||||
$signature = "";
|
||||
ksort($request);
|
||||
foreach ($request as $key => $value){
|
||||
$signature.= $key.$value;
|
||||
}
|
||||
|
||||
// Finalize the signature
|
||||
$signature.= API_SECRET;
|
||||
$signature = md5($signature);
|
||||
$request["api_sig"] = $signature;
|
||||
|
||||
return $request;
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Spotify Authentication proxy
|
||||
*
|
||||
@ -31,36 +30,41 @@ if (isset($_GET['code'])){
|
||||
|
||||
// go get our credentials
|
||||
$response = getToken($_GET['code']);
|
||||
$responseArray = json_decode( $response, true );
|
||||
$response = json_decode( $response, true );
|
||||
|
||||
// add our code to the array of credentials, etc
|
||||
$responseArray['authorization_code'] = $_GET['code'];
|
||||
$response = json_encode($responseArray);
|
||||
|
||||
$response['authorization_code'] = $_GET['code'];
|
||||
$response['origin'] = "auth_spotify";
|
||||
|
||||
// make sure we have a successful response
|
||||
if( !isset($responseArray['access_token']) ){
|
||||
if( !isset($response['access_token']) ){
|
||||
echo 'Error!';
|
||||
die();
|
||||
}
|
||||
|
||||
// Pass our error back to the popup opener
|
||||
// Pass our error back to the popup opener
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
window.opener.postMessage( '<?php echo $response ?>', "*");
|
||||
window.opener.postMessage( '<?php echo json_encode($response) ?>', "*");
|
||||
window.close();
|
||||
</script>
|
||||
<?php
|
||||
|
||||
// authorization error
|
||||
} else if (isset($_GET['error'])){
|
||||
|
||||
$response = array(
|
||||
"error" => $_GET["error"],
|
||||
"origin" => "auth_spotify"
|
||||
);
|
||||
|
||||
// Pass our error back to the popup opener
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
window.opener.postMessage("{\"error\": \"<?php echo $_GET['error'] ?>\"}", "*");
|
||||
window.close();
|
||||
</script>
|
||||
<?php
|
||||
// Pass our error back to the popup opener
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
window.opener.postMessage('<?php echo json_encode($response) ?>', "*");
|
||||
window.close();
|
||||
</script>
|
||||
<?php
|
||||
|
||||
// refresh existing token
|
||||
} else if (isset($_GET['action']) && $_GET['action'] == 'refresh' && $_GET['refresh_token']){
|
||||
@ -171,4 +175,5 @@ function refreshToken($refresh_token){
|
||||
curl_close ($ch);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ from core import IrisCore
|
||||
from raven import Client
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
__version__ = '3.6.1'
|
||||
__version__ = '3.6.1'
|
||||
|
||||
##
|
||||
# Core extension class
|
||||
@ -35,7 +35,8 @@ class Extension( ext.Extension ):
|
||||
schema['enabled'] = config.Boolean()
|
||||
schema['country'] = config.String()
|
||||
schema['locale'] = config.String()
|
||||
schema['authorization_url'] = config.String()
|
||||
schema['spotify_authorization_url'] = config.String()
|
||||
schema['lastfm_authorization_url'] = config.String()
|
||||
return schema
|
||||
|
||||
def setup(self, registry):
|
||||
|
||||
@ -259,7 +259,8 @@ class IrisCore(object):
|
||||
"spotify_username": spotify_username,
|
||||
"country": self.config['iris']['country'],
|
||||
"locale": self.config['iris']['locale'],
|
||||
"authorization_url": self.config['iris']['authorization_url']
|
||||
"spotify_authorization_url": self.config['iris']['spotify_authorization_url'],
|
||||
"lastfm_authorization_url": self.config['iris']['lastfm_authorization_url']
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,4 +2,5 @@
|
||||
enabled = true
|
||||
country = NZ
|
||||
locale = en_NZ
|
||||
authorization_url = https://jamesbarnsley.co.nz/auth_v2.php
|
||||
spotify_authorization_url = https://jamesbarnsley.co.nz/auth_spotify.php
|
||||
lastfm_authorization_url = https://jamesbarnsley.co.nz/auth_lastfm.php
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
154
mopidy_iris/static/auth_lastfm.php
Executable file
154
mopidy_iris/static/auth_lastfm.php
Executable file
@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* LastFM Authentication proxy
|
||||
*
|
||||
* To use:
|
||||
* 1. Create a LastFM App (https://www.last.fm/api/account/create), and paste in your credentials below
|
||||
* 2. Save this script to a publicly-accessible server
|
||||
* 3. Set config in mopidy.conf to point to this script
|
||||
**/
|
||||
|
||||
// LastFM app credentials
|
||||
define('API_URL','http://ws.audioscrobbler.com/2.0/?format=json');
|
||||
define('API_KEY','269cc2b1dfb1059a89a2db21ceb81d2a');
|
||||
define('API_SECRET','1551231f57eb3b73801e31bc0e89ab09');
|
||||
define('REDIRECT_URI','https://jamesbarnsley.nz/auth_lastfm.php?action=start_session');
|
||||
|
||||
// Allow cross-domain requests
|
||||
header("Access-Control-Allow-Origin: *");
|
||||
|
||||
// Set our cookies
|
||||
if (isset($_GET['app'])){
|
||||
setcookie('mopidy_iris', $_GET['app'], time()+3600);
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================================= INIT ================ */
|
||||
/* ======================================================================================================= */
|
||||
|
||||
if (isset($_GET['action'])){
|
||||
switch ($_GET['action']){
|
||||
|
||||
case 'authorize':
|
||||
header('Location: http://www.last.fm/api/auth/?api_key='.API_KEY.'&cb='.REDIRECT_URI);
|
||||
exit;
|
||||
|
||||
case 'start_session':
|
||||
$session = startSession($_GET['token']);
|
||||
|
||||
// Pass our error back to the popup opener
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
window.opener.postMessage( '<?php echo json_encode($session) ?>', "*");
|
||||
window.close();
|
||||
</script>
|
||||
<?php
|
||||
|
||||
break;
|
||||
|
||||
case 'sign_request':
|
||||
$data = $_GET;
|
||||
$signed = signRequest($data);
|
||||
echo json_encode($signed);
|
||||
exit;
|
||||
|
||||
default:
|
||||
echo 'Invalid action specified';
|
||||
die();
|
||||
|
||||
}
|
||||
} else {
|
||||
echo "No action specified";
|
||||
die();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* ================================================================================= GETTERS ============= */
|
||||
/* ======================================================================================================= */
|
||||
|
||||
/**
|
||||
* Start a session
|
||||
*
|
||||
* @param $data = array
|
||||
* @param $post = boolean (POST request)
|
||||
**/
|
||||
function startSession($token){
|
||||
|
||||
$ch = curl_init();
|
||||
|
||||
if (FALSE === $ch){
|
||||
throw new Exception('Failed to initialize');
|
||||
}
|
||||
|
||||
$data = signRequest(
|
||||
array(
|
||||
"method" => "auth.getSession",
|
||||
"token" => $token
|
||||
),
|
||||
false
|
||||
);
|
||||
|
||||
curl_setopt($ch, CURLOPT_URL,API_URL);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
|
||||
curl_setopt($ch, CURLINFO_HEADER_OUT, true);
|
||||
curl_setopt($ch, CURLOPT_VERBOSE, true);
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
|
||||
if (curl_errno($ch)){
|
||||
echo 'CURL Error: '. curl_error($ch);
|
||||
}
|
||||
curl_close($ch);
|
||||
|
||||
$response = json_decode($response, true);
|
||||
|
||||
// Append our other important values to successful signings
|
||||
if (!$response['error']){
|
||||
$response['api_key'] = API_KEY;
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Perform a signed request
|
||||
*
|
||||
* @param $data = array
|
||||
* @param $post = boolean (POST request)
|
||||
**/
|
||||
function signRequest($data = array(), $post = true){
|
||||
unset($data['action']);
|
||||
|
||||
$ch = curl_init();
|
||||
|
||||
if (FALSE === $ch){
|
||||
throw new Exception('Failed to initialize');
|
||||
}
|
||||
|
||||
// Make sure we've got our API key included
|
||||
$request = array_merge(array("api_key" => API_KEY), $data);
|
||||
|
||||
// Loop all the values in our request and add to our signature
|
||||
$signature = "";
|
||||
ksort($request);
|
||||
foreach ($request as $key => $value){
|
||||
$signature.= $key.$value;
|
||||
}
|
||||
|
||||
// Finalize the signature
|
||||
$signature.= API_SECRET;
|
||||
$signature = md5($signature);
|
||||
$request["api_sig"] = $signature;
|
||||
|
||||
return $request;
|
||||
}
|
||||
@ -38,7 +38,7 @@
|
||||
|
||||
// Release details
|
||||
// These are automatically injected by build.sh
|
||||
var build = "1509392542";
|
||||
var build = "1509609058";
|
||||
var version = "3.6.1";
|
||||
|
||||
// Construct the script tag
|
||||
|
||||
20
src/js/bootstrap.js
vendored
20
src/js/bootstrap.js
vendored
@ -14,6 +14,7 @@ import coreMiddleware from './services/core/middleware'
|
||||
import uiMiddleware from './services/ui/middleware'
|
||||
import pusherMiddleware from './services/pusher/middleware'
|
||||
import mopidyMiddleware from './services/mopidy/middleware'
|
||||
import lastfmMiddleware from './services/lastfm/middleware'
|
||||
import spotifyMiddleware from './services/spotify/middleware'
|
||||
import localstorageMiddleware from './services/localstorage/middleware'
|
||||
|
||||
@ -31,8 +32,9 @@ let reducers = combineReducers({
|
||||
// TODO: Look at using propTypes in the component for these falsy initial states
|
||||
var initialState = {
|
||||
core: {
|
||||
current_tracklist: [],
|
||||
current_tltrack: false,
|
||||
queue: [],
|
||||
queue_metadata: {},
|
||||
current_track_uri: null,
|
||||
albums: {},
|
||||
artists: {},
|
||||
playlists: {},
|
||||
@ -63,7 +65,9 @@ var initialState = {
|
||||
}
|
||||
},
|
||||
lastfm: {
|
||||
connected: false
|
||||
connected: false,
|
||||
me: false,
|
||||
authorization_url: 'https://jamesbarnsley.co.nz/auth_lastfm.php'
|
||||
},
|
||||
genius: {
|
||||
connected: false
|
||||
@ -72,7 +76,7 @@ var initialState = {
|
||||
connected: false,
|
||||
me: false,
|
||||
autocomplete_results: {},
|
||||
authorization_url: 'https://jamesbarnsley.co.nz/auth_v2.php'
|
||||
authorization_url: 'https://jamesbarnsley.co.nz/auth_spotify.php'
|
||||
}
|
||||
};
|
||||
|
||||
@ -106,12 +110,18 @@ if (localStorage.getItem('spotify')){
|
||||
initialState.spotify = Object.assign(initialState.spotify, storedSpotify );
|
||||
}
|
||||
|
||||
// if we've got a stored version of lastfm state, load and merge
|
||||
if (localStorage.getItem('lastfm')){
|
||||
var storedLastfm = JSON.parse(localStorage.getItem('lastfm') );
|
||||
initialState.lastfm = Object.assign(initialState.lastfm, storedLastfm );
|
||||
}
|
||||
|
||||
console.log('Bootstrapping', initialState)
|
||||
|
||||
let store = createStore(
|
||||
reducers,
|
||||
initialState,
|
||||
applyMiddleware(thunk, localstorageMiddleware, coreMiddleware, uiMiddleware, mopidyMiddleware, pusherMiddleware, spotifyMiddleware )
|
||||
applyMiddleware(thunk, localstorageMiddleware, coreMiddleware, uiMiddleware, mopidyMiddleware, pusherMiddleware, spotifyMiddleware, lastfmMiddleware )
|
||||
);
|
||||
|
||||
export default store;
|
||||
|
||||
@ -68,12 +68,12 @@ class AddSeedField extends React.Component{
|
||||
switch (helpers.uriType(item.uri)){
|
||||
|
||||
case 'artist':
|
||||
this.props.coreActions.artistLoaded(item.uri,item)
|
||||
break
|
||||
this.props.coreActions.artistsLoaded(item);
|
||||
break;
|
||||
|
||||
case 'track':
|
||||
this.props.coreActions.trackLoaded(item.uri,item)
|
||||
break
|
||||
this.props.coreActions.tracksLoaded(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ import * as coreActions from '../services/core/actions'
|
||||
import * as uiActions from '../services/ui/actions'
|
||||
import * as pusherActions from '../services/pusher/actions'
|
||||
import * as mopidyActions from '../services/mopidy/actions'
|
||||
import * as lastfmActions from '../services/lastfm/actions'
|
||||
import * as spotifyActions from '../services/spotify/actions'
|
||||
import TrackList from './TrackList'
|
||||
|
||||
@ -41,10 +42,10 @@ class ContextMenu extends React.Component{
|
||||
this.setState({ submenu_expanded: false })
|
||||
$('body').addClass('context-menu-open')
|
||||
|
||||
var context = this.getContext()
|
||||
var context = this.getContext(nextProps);
|
||||
|
||||
// if we're able to be in the library, run a check
|
||||
if (this.props.spotify_authorized && context.source == 'spotify'){
|
||||
if (nextProps.spotify_authorized && context.source == 'spotify'){
|
||||
switch (nextProps.menu.context){
|
||||
case 'artist':
|
||||
case 'album':
|
||||
@ -54,6 +55,14 @@ class ContextMenu extends React.Component{
|
||||
}
|
||||
}
|
||||
|
||||
// if we're able to be in the LastFM library, run a check
|
||||
if (nextProps.lastfm_authorized && context.is_track && context.items_count == 1){
|
||||
if (nextProps.menu.items[0].uri && this.props.tracks[nextProps.menu.items[0].uri] !== undefined){
|
||||
var track = this.props.tracks[nextProps.menu.items[0].uri];
|
||||
this.props.lastfmActions.getTrack(track);
|
||||
}
|
||||
}
|
||||
|
||||
// we DID have one prior, and now we don't
|
||||
} else if (this.props.menu && !nextProps.menu){
|
||||
$('body').removeClass('context-menu-open')
|
||||
@ -73,41 +82,44 @@ class ContextMenu extends React.Component{
|
||||
}
|
||||
}
|
||||
|
||||
getContext(){
|
||||
getContext(props = this.props){
|
||||
var context = {
|
||||
name: null,
|
||||
nice_name: 'Unknown'
|
||||
nice_name: 'Unknown',
|
||||
is_track: false
|
||||
}
|
||||
|
||||
if (this.props.menu && this.props.menu.context){
|
||||
context.name = this.props.menu.context
|
||||
context.nice_name = this.props.menu.context
|
||||
if (props.menu && props.menu.context){
|
||||
context.name = props.menu.context;
|
||||
context.nice_name = props.menu.context;
|
||||
|
||||
// handle ugly labels
|
||||
switch (this.props.menu.context){
|
||||
switch (props.menu.context){
|
||||
case 'playlist':
|
||||
case 'editable-playlist':
|
||||
context.nice_name = 'playlist'
|
||||
context.nice_name = 'playlist';
|
||||
break
|
||||
|
||||
case 'track':
|
||||
case 'queue-track':
|
||||
case 'playlist-track':
|
||||
case 'editable-playlist-track':
|
||||
context.nice_name = 'track'
|
||||
context.nice_name = 'track';
|
||||
context.is_track = true;
|
||||
break
|
||||
}
|
||||
|
||||
// Consider the object(s) themselves
|
||||
// We can only really accommodate the first item. The only instances where
|
||||
// there is multiple is tracklists, when they're all of the same source (except search?)
|
||||
if (this.props.menu.items && this.props.menu.items.length > 0){
|
||||
var item = this.props.menu.items[0]
|
||||
context.item = item
|
||||
context.items_count = this.props.menu.items.length
|
||||
context.source = helpers.uriSource(item.uri)
|
||||
context.type = helpers.uriType(item.uri)
|
||||
context.in_library = this.inLibrary(item)
|
||||
if (props.menu.items && props.menu.items.length > 0){
|
||||
var item = props.menu.items[0]
|
||||
context.item = item;
|
||||
context.items_count = props.menu.items.length;
|
||||
context.source = helpers.uriSource(item.uri);
|
||||
context.type = helpers.uriType(item.uri);
|
||||
context.in_library = this.inLibrary(item);
|
||||
context.is_loved = this.isLoved(item);
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,6 +145,23 @@ class ContextMenu extends React.Component{
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Currently the select track keys are the only details available. We need
|
||||
* the actual track object reference (including name and artists) to getTrack from LastFM
|
||||
**/
|
||||
isLoved(item = null){
|
||||
if (!item){
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.props.tracks[item.uri] === undefined){
|
||||
return false;
|
||||
}
|
||||
var track = this.props.tracks[item.uri];
|
||||
|
||||
return (track.userloved !== undefined && track.userloved == "1");
|
||||
}
|
||||
|
||||
canBeInLibrary(){
|
||||
if (!this.props.spotify_authorized){
|
||||
return false
|
||||
@ -190,6 +219,20 @@ class ContextMenu extends React.Component{
|
||||
this.props.coreActions.addTracksToPlaylist(playlist_uri, this.props.menu.uris)
|
||||
}
|
||||
|
||||
toggleLoved(e, is_loved){
|
||||
this.props.uiActions.hideContextMenu()
|
||||
if (is_loved){
|
||||
this.props.lastfmActions.unloveTrack(this.props.menu.items[0])
|
||||
} else {
|
||||
this.props.lastfmActions.loveTrack(this.props.menu.items[0])
|
||||
}
|
||||
}
|
||||
|
||||
unloveTrack(e){
|
||||
this.props.uiActions.hideContextMenu()
|
||||
this.props.lastfmActions.unloveTrack(this.props.menu.items[0])
|
||||
}
|
||||
|
||||
removeFromPlaylist(e){
|
||||
this.props.uiActions.hideContextMenu()
|
||||
this.props.coreActions.removeTracksFromPlaylist(this.props.menu.tracklist_uri, this.props.menu.indexes)
|
||||
@ -445,6 +488,29 @@ class ContextMenu extends React.Component{
|
||||
</span>
|
||||
)
|
||||
|
||||
|
||||
if (helpers.isLoading(this.props.load_queue,['lastfm_track.getInfo'])){
|
||||
var toggle_loved = (
|
||||
<span className="menu-item-wrapper">
|
||||
<a className="menu-item">
|
||||
<span className="label grey-text">
|
||||
Love track
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
)
|
||||
} else {
|
||||
var toggle_loved = (
|
||||
<span className="menu-item-wrapper">
|
||||
<a className="menu-item" onClick={e => this.toggleLoved(e, context.is_loved)}>
|
||||
<span className="label">
|
||||
{context.is_loved ? 'Unlove' : 'Love'} track
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
var go_to_artist = (
|
||||
<span className="menu-item-wrapper">
|
||||
<a className="menu-item" onClick={e => this.goToArtist(e)}>
|
||||
@ -580,6 +646,7 @@ class ContextMenu extends React.Component{
|
||||
{context.items_count == 1 ? play_queue_item : null}
|
||||
{context.items_count == 1 ? <div className="divider" /> : null}
|
||||
{add_to_playlist}
|
||||
{context.items_count == 1 ? toggle_loved : null}
|
||||
{context.source == 'spotify' && context.items_count <= 5 ? go_to_recommendations : null}
|
||||
{context.items_count == 1 ? go_to_track : null}
|
||||
<div className="divider" />
|
||||
@ -598,6 +665,7 @@ class ContextMenu extends React.Component{
|
||||
{context.source == 'spotify' && context.items_count == 1 ? start_radio : null}
|
||||
<div className="divider" />
|
||||
{add_to_playlist}
|
||||
{context.items_count == 1 ? toggle_loved : null}
|
||||
{context.source == 'spotify' && context.items_count <= 5 ? go_to_recommendations : null}
|
||||
{context.items_count == 1 ? go_to_track : null}
|
||||
<div className="divider" />
|
||||
@ -616,6 +684,7 @@ class ContextMenu extends React.Component{
|
||||
{context.source == 'spotify' && context.items_count == 1 ? start_radio : null}
|
||||
<div className="divider" />
|
||||
{add_to_playlist}
|
||||
{context.items_count == 1 ? toggle_loved : null}
|
||||
{context.source == 'spotify' && context.items_count <= 5 ? go_to_recommendations : null}
|
||||
{context.items_count == 1 ? go_to_track : null}
|
||||
<div className="divider" />
|
||||
@ -663,6 +732,7 @@ class ContextMenu extends React.Component{
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
menu: state.ui.context_menu,
|
||||
load_queue: state.ui.load_queue,
|
||||
processes: state.ui.processes,
|
||||
current_track: state.core.current_track,
|
||||
current_tracklist: state.core.current_tracklist,
|
||||
@ -675,7 +745,9 @@ const mapStateToProps = (state, ownProps) => {
|
||||
spotify_library_albums: state.spotify.library_albums,
|
||||
mopidy_library_albums: state.mopidy.library_albums,
|
||||
playlists: state.core.playlists,
|
||||
spotify_authorized: state.spotify.authorization
|
||||
tracks: state.core.tracks,
|
||||
spotify_authorized: state.spotify.authorization,
|
||||
lastfm_authorized: state.lastfm.session
|
||||
}
|
||||
}
|
||||
|
||||
@ -685,6 +757,7 @@ const mapDispatchToProps = (dispatch) => {
|
||||
uiActions: bindActionCreators(uiActions, dispatch),
|
||||
pusherActions: bindActionCreators(pusherActions, dispatch),
|
||||
spotifyActions: bindActionCreators(spotifyActions, dispatch),
|
||||
lastfmActions: bindActionCreators(lastfmActions, dispatch),
|
||||
mopidyActions: bindActionCreators(mopidyActions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@ class FollowButton extends React.Component{
|
||||
}
|
||||
|
||||
if (!this.props.spotify_authorized){
|
||||
return <button className={className+' disabled'} onClick={e => this.props.uiActions.createNotification('You must authorize Iris first','warning')}>{this.props.addText}</button>
|
||||
return <button className={className+' disabled'} onClick={e => this.props.uiActions.createNotification('You must authorize Spotify first','warning')}>{this.props.addText}</button>
|
||||
} else if (this.props.is_following === true){
|
||||
return <button className={className+' destructive'} onClick={e => this.remove()}>{this.props.removeText}</button>
|
||||
} else {
|
||||
|
||||
124
src/js/components/LastfmAuthenticationFrame.js
Executable file
124
src/js/components/LastfmAuthenticationFrame.js
Executable file
@ -0,0 +1,124 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { createStore, bindActionCreators } from 'redux'
|
||||
import ReactGA from 'react-ga'
|
||||
|
||||
import FontAwesome from 'react-fontawesome'
|
||||
import Thumbnail from './Thumbnail'
|
||||
|
||||
import * as uiActions from '../services/ui/actions'
|
||||
import * as lastfmActions from '../services/lastfm/actions'
|
||||
|
||||
class LastfmAuthenticationFrame extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
authorizing: false
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
let self = this;
|
||||
|
||||
// Listen for incoming messages from the authorization popup
|
||||
window.addEventListener('message', function(event){
|
||||
var data = JSON.parse(event.data);
|
||||
|
||||
// Only digest messages relevant to us
|
||||
if (data.origin == 'auth_lastfm'){
|
||||
self.handleMessage(event, data);
|
||||
}
|
||||
}, false);
|
||||
}
|
||||
|
||||
handleMessage(event, data){
|
||||
|
||||
// Only allow incoming data from our authorized authenticator proxy
|
||||
var authorization_domain = this.props.authorization_url.substring(0,this.props.authorization_url.indexOf('/',8))
|
||||
if (event.origin != authorization_domain){
|
||||
this.props.uiActions.createNotification('Authorization failed. '+event.origin+' is not the configured authorization_url.','bad')
|
||||
return false
|
||||
}
|
||||
|
||||
// Bounced with an error
|
||||
if (data.error !== undefined){
|
||||
this.props.uiActions.createNotification(data.message,'bad')
|
||||
|
||||
// No errors? We're in!
|
||||
} else {
|
||||
this.props.lastfmActions.authorizationGranted(data)
|
||||
this.props.lastfmActions.getMe()
|
||||
}
|
||||
|
||||
// Turn off our authorizing switch
|
||||
this.setState({authorizing: false})
|
||||
}
|
||||
|
||||
startAuthorization(){
|
||||
|
||||
var self = this
|
||||
this.setState({authorizing: true})
|
||||
|
||||
// Open an authentication request window
|
||||
var url = this.props.authorization_url+'?action=authorize'
|
||||
var popup = window.open(url,"popup","height=580,width=350");
|
||||
popup.name = "LastfmAuthenticationWindow";
|
||||
|
||||
// Start timer to check our popup's state
|
||||
var timer = setInterval(checkPopup, 1000);
|
||||
function checkPopup(){
|
||||
|
||||
// Popup has been closed
|
||||
if (typeof(popup) !== 'undefined' && popup){
|
||||
if (popup.closed){
|
||||
self.setState({authorizing: false})
|
||||
clearInterval(timer);
|
||||
}
|
||||
|
||||
// Popup does not exist, so must have been blocked
|
||||
} else {
|
||||
self.props.uiActions.createNotification('Popup blocked. Please allow popups and try again.','bad')
|
||||
self.setState({authorizing: false})
|
||||
clearInterval(timer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render(){
|
||||
if (this.state.authorizing){
|
||||
return (
|
||||
<button className="working">
|
||||
Authorizing...
|
||||
</button>
|
||||
)
|
||||
} else if (this.props.authorized){
|
||||
return (
|
||||
<button className="destructive" onClick={() => this.props.lastfmActions.revokeAuthorization()}>Log out</button>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<button className="primary" onClick={() => this.startAuthorization()}>Log in</button>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
authorization_url: state.lastfm.authorization_url,
|
||||
authorized: state.lastfm.session,
|
||||
authorizing: state.lastfm.authorizing
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
uiActions: bindActionCreators(uiActions, dispatch),
|
||||
lastfmActions: bindActionCreators(lastfmActions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LastfmAuthenticationFrame)
|
||||
62
src/js/components/LastfmLoveButton.js
Executable file
62
src/js/components/LastfmLoveButton.js
Executable file
@ -0,0 +1,62 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { Link } from 'react-router'
|
||||
import { createStore, bindActionCreators } from 'redux'
|
||||
import FontAwesome from 'react-fontawesome'
|
||||
|
||||
import * as helpers from '../helpers'
|
||||
import * as uiActions from '../services/ui/actions'
|
||||
import * as lastfmActions from '../services/lastfm/actions'
|
||||
|
||||
class FollowButton extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
}
|
||||
|
||||
remove(){
|
||||
this.props.lastfmActions.unloveTrack(this.props.uri, this.props.artist, this.props.track);
|
||||
}
|
||||
|
||||
add(){
|
||||
this.props.lastfmActions.loveTrack(this.props.uri, this.props.artist, this.props.track);
|
||||
}
|
||||
|
||||
render(){
|
||||
if (!this.props.uri){
|
||||
return false;
|
||||
}
|
||||
|
||||
var className = '';
|
||||
|
||||
// Inherit passed-down classes
|
||||
if (this.props.className){
|
||||
className += ' '+this.props.className;
|
||||
}
|
||||
|
||||
if (!this.props.lastfm_authorized){
|
||||
return <button className={className+' disabled'} onClick={e => this.props.uiActions.createNotification('You must authorize LastFM first','warning')}>{this.props.addText}</button>
|
||||
} else if (this.props.is_loved && this.props.is_loved !== "0"){
|
||||
return <button className={className+' destructive'} onClick={e => this.remove()}>{this.props.removeText}</button>
|
||||
} else {
|
||||
return <button className={className} onClick={e => this.add()}>{this.props.addText}</button>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
load_queue: state.ui.load_queue,
|
||||
lastfm_authorized: state.lastfm.session
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
uiActions: bindActionCreators(uiActions, dispatch),
|
||||
lastfmActions: bindActionCreators(lastfmActions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(FollowButton)
|
||||
@ -145,7 +145,7 @@ class Modal extends React.Component{
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
current_track: (state.core.current_track !== undefined && state.core.tracks !== undefined && state.core.tracks[state.core.current_track] !== undefined ? state.core.tracks[state.core.current_track] : null),
|
||||
current_track: (state.core.tracks[state.core.current_track_uri] !== undefined ? state.core.tracks[state.core.current_track_uri] : null),
|
||||
uri_schemes: (state.mopidy.uri_schemes ? state.mopidy.uri_schemes : []),
|
||||
search_uri_schemes: (state.ui.search_uri_schemes ? state.ui.search_uri_schemes : []),
|
||||
volume: state.mopidy.volume,
|
||||
|
||||
@ -135,7 +135,7 @@ class PlaybackControls extends React.Component{
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
current_track: (state.core.current_track !== undefined && state.core.tracks !== undefined && state.core.tracks[state.core.current_track] !== undefined ? state.core.tracks[state.core.current_track] : null),
|
||||
current_track: (state.core.tracks[state.core.current_track_uri] !== undefined ? state.core.tracks[state.core.current_track_uri] : null),
|
||||
radio_enabled: (state.ui.radio && state.ui.radio.enabled ? true : false),
|
||||
play_state: state.mopidy.play_state,
|
||||
time_position: state.mopidy.time_position,
|
||||
|
||||
@ -21,20 +21,20 @@ class SpotifyAuthenticationFrame extends React.Component{
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
|
||||
let self = this;
|
||||
|
||||
// Listen for incoming messages from the authorization iframe
|
||||
// This is triggered when the popup posts a message, which is then passed to
|
||||
// the iframe, and then passed on to the parent frame (our application)
|
||||
// Listen for incoming messages from the authorization popup
|
||||
window.addEventListener('message', function(event){
|
||||
self.handleMessage(event)
|
||||
var data = JSON.parse(event.data);
|
||||
|
||||
// Only digest messages relevant to us
|
||||
if (data.origin == 'auth_spotify'){
|
||||
self.handleMessage(event, data);
|
||||
}
|
||||
}, false);
|
||||
}
|
||||
|
||||
handleMessage(event){
|
||||
|
||||
var data = JSON.parse(event.data)
|
||||
handleMessage(event, data){
|
||||
|
||||
// Only allow incoming data from our authorized authenticator proxy
|
||||
var authorization_domain = this.props.authorization_url.substring(0,this.props.authorization_url.indexOf('/',8))
|
||||
@ -44,7 +44,7 @@ class SpotifyAuthenticationFrame extends React.Component{
|
||||
}
|
||||
|
||||
// Spotify bounced with an error
|
||||
if (typeof(data.error) !== 'undefined'){
|
||||
if (data.error !== undefined){
|
||||
this.props.uiActions.createNotification(data.error,'bad')
|
||||
|
||||
// No errors? We're in!
|
||||
@ -80,7 +80,7 @@ class SpotifyAuthenticationFrame extends React.Component{
|
||||
'playlist-read-collaborative',
|
||||
'ugc-image-upload' // playlist image uploading
|
||||
]
|
||||
var popup = window.open(url+'&scope='+scopes.join('%20'),"popup","height=500,width=350");
|
||||
var popup = window.open(url+'&scope='+scopes.join('%20'),"popup","height=580,width=350");
|
||||
|
||||
// Start timer to check our popup's state
|
||||
var timer = setInterval(checkPopup, 1000);
|
||||
|
||||
@ -202,7 +202,7 @@ class TrackList extends React.Component{
|
||||
uris: selected_tracks_uris,
|
||||
indexes: selected_tracks_indexes
|
||||
}
|
||||
this.props.uiActions.showContextMenu(data)
|
||||
this.props.uiActions.showContextMenu(data);
|
||||
}
|
||||
|
||||
handleSelection(e,track_key){
|
||||
|
||||
@ -39,7 +39,7 @@ export default class URILink extends React.Component{
|
||||
break;
|
||||
|
||||
default:
|
||||
return null;
|
||||
to = null;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -65,6 +65,7 @@ ReactDOM.render(
|
||||
<Route path="queue/history" component={QueueHistory} />
|
||||
<Route path="settings" component={Settings} />
|
||||
<Route path="settings/debug" component={Debug} />
|
||||
<Route path="settings(/:sub_view)" component={Settings} />
|
||||
|
||||
<Route path="search(/iris::search::query)" component={Search} />
|
||||
<Route path="album/:uri" component={Album} />
|
||||
|
||||
@ -81,8 +81,8 @@ export function set(data){
|
||||
**/
|
||||
|
||||
export function reorderPlaylistTracks(uri, indexes, insert_before, snapshot_id = false){
|
||||
var range = helpers.createRange(indexes );
|
||||
switch(helpers.uriSource(uri )){
|
||||
var range = helpers.createRange(indexes);
|
||||
switch(helpers.uriSource(uri)){
|
||||
|
||||
case 'spotify':
|
||||
return {
|
||||
@ -221,11 +221,20 @@ export function getLibraryArtists(){
|
||||
* Assets loaded
|
||||
**/
|
||||
|
||||
export function albumLoaded(key,album){
|
||||
export function loadedMore(parent_type, parent_key, records_type, records_data){
|
||||
return {
|
||||
type: 'ALBUM_LOADED',
|
||||
key: key,
|
||||
album: album
|
||||
type: 'LOADED_MORE',
|
||||
parent_type: parent_type,
|
||||
parent_key: parent_key,
|
||||
records_type: records_type,
|
||||
records_data: records_data
|
||||
}
|
||||
}
|
||||
|
||||
export function tracksLoaded(tracks){
|
||||
return {
|
||||
type: 'TRACKS_LOADED',
|
||||
tracks: tracks
|
||||
}
|
||||
}
|
||||
|
||||
@ -236,14 +245,6 @@ export function albumsLoaded(albums){
|
||||
}
|
||||
}
|
||||
|
||||
export function artistLoaded(key,artist){
|
||||
return {
|
||||
type: 'ARTIST_LOADED',
|
||||
key: key,
|
||||
artist: artist
|
||||
}
|
||||
}
|
||||
|
||||
export function artistsLoaded(artists){
|
||||
return {
|
||||
type: 'ALBUMS_LOADED',
|
||||
@ -251,17 +252,16 @@ export function artistsLoaded(artists){
|
||||
}
|
||||
}
|
||||
|
||||
export function trackLoaded(key,track){
|
||||
export function playlistsLoaded(playlists){
|
||||
return {
|
||||
type: 'TRACK_LOADED',
|
||||
key: key,
|
||||
track: track
|
||||
type: 'PLAYLISTS_LOADED',
|
||||
playlists: playlists
|
||||
}
|
||||
}
|
||||
|
||||
export function tracksLoaded(tracks){
|
||||
export function usersLoaded(users){
|
||||
return {
|
||||
type: 'TRACKS_LOADED',
|
||||
tracks: tracks
|
||||
type: 'USERS_LOADED',
|
||||
users: users
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ const CoreMiddleware = (function(){
|
||||
* The actual middleware inteceptor
|
||||
**/
|
||||
return store => next => action => {
|
||||
var core = store.getState().core;
|
||||
|
||||
switch(action.type){
|
||||
|
||||
@ -101,43 +102,6 @@ const CoreMiddleware = (function(){
|
||||
next(action)
|
||||
break
|
||||
|
||||
case 'TRACK_LOADED':
|
||||
if (action.data) ReactGA.event({ category: 'Track', action: 'Load', label: action.key });
|
||||
|
||||
if (action.track.album && action.track.album.images && action.track.album.images.length > 0){
|
||||
action.track.album.images = helpers.digestMopidyImages(store.getState().mopidy, action.track.album.images);
|
||||
}
|
||||
|
||||
next(action)
|
||||
break
|
||||
|
||||
case 'ALBUM_LOADED':
|
||||
if (action.data) ReactGA.event({ category: 'Album', action: 'Load', label: action.key })
|
||||
|
||||
if (action.album.images && action.album.images.length > 0){
|
||||
action.album.images = helpers.digestMopidyImages(store.getState().mopidy, action.album.images);
|
||||
}
|
||||
|
||||
next(action)
|
||||
break
|
||||
|
||||
case 'ALBUMS_LOADED':
|
||||
if (action.data) ReactGA.event({ category: 'Albums', action: 'Load', label: action.albums.length+' items' })
|
||||
|
||||
for (var i = 0; i < action.albums.length; i++){
|
||||
if (action.albums[i].images && action.albums[i].images.length > 0){
|
||||
action.albums[i].images = helpers.digestMopidyImages(store.getState().mopidy, action.albums[i].images);
|
||||
}
|
||||
}
|
||||
|
||||
next(action)
|
||||
break
|
||||
|
||||
case 'ARTIST_LOADED':
|
||||
if (action.data) ReactGA.event({ category: 'Artist', action: 'Load', label: action.artist.uri })
|
||||
next(action)
|
||||
break
|
||||
|
||||
case 'PLAY_PLAYLIST':
|
||||
ReactGA.event({ category: 'Playlist', action: 'Play', label: action.uri })
|
||||
next(action)
|
||||
@ -222,73 +186,6 @@ const CoreMiddleware = (function(){
|
||||
next(action)
|
||||
break
|
||||
|
||||
case 'PLAYLIST_LOADED':
|
||||
if (action.data) ReactGA.event({ category: 'Playlist', action: 'Load', label: action.playlist.uri })
|
||||
|
||||
var playlist = Object.assign({}, action.playlist)
|
||||
switch (helpers.uriSource(playlist.uri)){
|
||||
|
||||
case 'm3u':
|
||||
playlist.can_edit = true
|
||||
break
|
||||
|
||||
case 'spotify':
|
||||
if (store.getState().spotify.authorization && store.getState().spotify.me){
|
||||
playlist.can_edit = (helpers.getFromUri('playlistowner',playlist.uri) == store.getState().spotify.me.id)
|
||||
}
|
||||
}
|
||||
|
||||
// proceed as usual
|
||||
action.playlist = playlist
|
||||
next(action)
|
||||
break
|
||||
|
||||
case 'PLAYLISTS_LOADED':
|
||||
if (action.data) ReactGA.event({ category: 'Playlists', action: 'Load', label: action.playlists.length+' items' })
|
||||
|
||||
var playlists = []
|
||||
for (var i = 0; i < action.playlists.length; i++){
|
||||
var playlist = Object.assign({}, action.playlists[i])
|
||||
|
||||
switch (helpers.uriSource(playlist.uri)){
|
||||
|
||||
case 'm3u':
|
||||
playlist.can_edit = true
|
||||
break
|
||||
|
||||
case 'spotify':
|
||||
if (store.getState().spotify.authorization && store.getState().spotify.me){
|
||||
playlist.can_edit = (helpers.getFromUri('playlistowner',playlist.uri) == store.getState().spotify.me.id)
|
||||
}
|
||||
}
|
||||
|
||||
playlists.push(playlist)
|
||||
}
|
||||
|
||||
// proceed as usual
|
||||
action.playlists = playlists
|
||||
next(action)
|
||||
break
|
||||
|
||||
case 'MOPIDY_CURRENTTLTRACK':
|
||||
if (action.data && action.data.track){
|
||||
helpers.setWindowTitle(action.data.track, store.getState().mopidy.play_state);
|
||||
|
||||
// make sure our images use mopidy host:port
|
||||
if (action.data.track.album && action.data.track.album.images && action.data.track.album.images.length > 0){
|
||||
var images = Object.assign([], action.data.track.album.images)
|
||||
for (var i = 0; i < images.length; i++){
|
||||
if (typeof(images[i]) === 'string' && images[i].startsWith('/images/')){
|
||||
images[i] = '//'+store.getState().mopidy.host+':'+store.getState().mopidy.port+images[i]
|
||||
}
|
||||
}
|
||||
action.data.track.album.images = images
|
||||
}
|
||||
}
|
||||
|
||||
next(action)
|
||||
break
|
||||
|
||||
// Get assets from all of our providers
|
||||
case 'GET_LIBRARY_PLAYLISTS':
|
||||
if (store.getState().spotify.connected){
|
||||
@ -323,9 +220,410 @@ const CoreMiddleware = (function(){
|
||||
break
|
||||
|
||||
case 'RESTART':
|
||||
location.reload()
|
||||
location.reload();
|
||||
break;
|
||||
|
||||
|
||||
/**
|
||||
* Playlist manipulation
|
||||
**/
|
||||
|
||||
case 'PLAYLIST_KEY_UPDATED':
|
||||
var playlists = Object.assign({}, core.playlists);
|
||||
|
||||
if (playlists[action.key] === undefined){
|
||||
dispatch(coreActions.handleException("Cannot change key of playlist not in index"));
|
||||
}
|
||||
|
||||
// Delete our old playlist by key, and add by new key
|
||||
var playlist = Object.assign({}, playlists[action.key]);
|
||||
delete playlists[action.key];
|
||||
playlists[action.new_key] = playlist;
|
||||
|
||||
store.dispatch({
|
||||
type: 'UPDATE_PLAYLISTS_INDEX',
|
||||
playlists: playlists
|
||||
});
|
||||
break;
|
||||
|
||||
case 'PLAYLIST_TRACKS_REORDERED':
|
||||
var playlists = Object.assign({}, core.playlists);
|
||||
var playlist = Object.assign({}, playlists[action.key]);
|
||||
var tracks_uris = Object.assign([], playlist.tracks_uris);
|
||||
|
||||
// handle insert_before offset if we're moving BENEATH where we're slicing tracks
|
||||
var insert_before = action.insert_before
|
||||
if (insert_before > action.range_start){
|
||||
insert_before = insert_before - action.range_length;
|
||||
}
|
||||
|
||||
// cut our moved tracks into a new array
|
||||
var tracks_to_move = tracks_uris.splice(action.range_start, action.range_length)
|
||||
tracks_to_move.reverse()
|
||||
|
||||
for (i = 0; i < tracks_to_move.length; i++){
|
||||
tracks_uris.splice(insert_before, 0, tracks_to_move[i])
|
||||
}
|
||||
|
||||
var snapshot_id = null;
|
||||
if (action.snapshot_id){
|
||||
snapshot_id = action.snapshot_id;
|
||||
}
|
||||
|
||||
// Update our playlist
|
||||
playlist.tracks_uris = tracks_uris;
|
||||
playlist.snapshot_id = snapshot_id;
|
||||
|
||||
// Trigger normal playlist updating
|
||||
store.dispatch({
|
||||
type: 'PLAYLISTS_LOADED',
|
||||
playlists: [playlist]
|
||||
});
|
||||
break;
|
||||
|
||||
case 'PLAYLIST_TRACKS_REMOVED':
|
||||
var playlists = Object.assign({}, core.playlists);
|
||||
var playlist = Object.assign({}, playlists[action.key]);
|
||||
var tracks_uris = Object.assign([], playlist.tracks_uris);
|
||||
|
||||
var indexes = action.tracks_indexes.reverse()
|
||||
for(var i = 0; i < indexes.length; i++){
|
||||
tracks_uris.splice(indexes[i], 1);
|
||||
}
|
||||
|
||||
var snapshot_id = null;
|
||||
if (action.snapshot_id){
|
||||
snapshot_id = action.snapshot_id;
|
||||
}
|
||||
|
||||
// Update our playlist
|
||||
playlist.tracks_uris = tracks_uris;
|
||||
playlist.snapshot_id = snapshot_id;
|
||||
|
||||
// Trigger normal playlist updating
|
||||
store.dispatch({
|
||||
type: 'PLAYLISTS_LOADED',
|
||||
playlists: [playlist]
|
||||
});
|
||||
break;
|
||||
|
||||
|
||||
/**
|
||||
* Queue and playback info
|
||||
**/
|
||||
|
||||
case 'CURRENT_TRACK_LOADED':
|
||||
store.dispatch({
|
||||
type: 'TRACKS_LOADED',
|
||||
tracks: [action.current_track]
|
||||
});
|
||||
next(action);
|
||||
break;
|
||||
|
||||
case 'QUEUE_LOADED':
|
||||
store.dispatch({
|
||||
type: 'TRACKS_LOADED',
|
||||
tracks: action.tracks
|
||||
});
|
||||
|
||||
action.tracks_uris = helpers.arrayOf('uri',action.tracks);
|
||||
next(action);
|
||||
break;
|
||||
|
||||
|
||||
/**
|
||||
* Index actions
|
||||
* These modify our asset indexes, which are used globally
|
||||
**/
|
||||
|
||||
// Array wrapper for TRACKS_LOADED
|
||||
case 'TRACK_LOADED':
|
||||
store.dispatch({
|
||||
type: 'TRACKS_LOADED',
|
||||
tracks: [action.track]
|
||||
});
|
||||
break;
|
||||
|
||||
// Array wrapper for ALBUMS_LOADED
|
||||
case 'ALBUM_LOADED':
|
||||
store.dispatch({
|
||||
type: 'ALBUMS_LOADED',
|
||||
albums: [action.album]
|
||||
});
|
||||
break;
|
||||
|
||||
// Array wrapper for ARTISTS_LOADED
|
||||
case 'ARTIST_LOADED':
|
||||
store.dispatch({
|
||||
type: 'ARTISTS_LOADED',
|
||||
artists: [action.artist]
|
||||
});
|
||||
break;
|
||||
|
||||
// Array wrapper for PLAYLISTS_LOADED
|
||||
case 'PLAYLIST_LOADED':
|
||||
store.dispatch({
|
||||
type: 'PLAYLISTS_LOADED',
|
||||
playlists: [action.playlist]
|
||||
});
|
||||
break;
|
||||
|
||||
// Array wrapper for USERS_LOADED
|
||||
case 'USER_LOADED':
|
||||
store.dispatch({
|
||||
type: 'USERS_LOADED',
|
||||
users: [action.user]
|
||||
});
|
||||
break;
|
||||
|
||||
case 'TRACKS_LOADED':
|
||||
var tracks = Object.assign({}, core.tracks);
|
||||
|
||||
for (var i = 0; i < action.tracks.length; i++){
|
||||
var track = Object.assign({}, helpers.formatTracks(action.tracks[i]));
|
||||
|
||||
if (tracks[track.uri]){
|
||||
track = Object.assign({}, tracks[track.uri], track);
|
||||
}
|
||||
|
||||
if (track.album && track.album.images && track.album.images.length > 0){
|
||||
track.album.images = helpers.digestMopidyImages(store.getState().mopidy, track.album.images);
|
||||
track.images = track.album.images;
|
||||
}
|
||||
|
||||
tracks[track.uri] = track;
|
||||
}
|
||||
|
||||
// Update index
|
||||
store.dispatch({
|
||||
type: 'UPDATE_TRACKS_INDEX',
|
||||
tracks: tracks
|
||||
});
|
||||
|
||||
next(action);
|
||||
break;
|
||||
|
||||
case 'ALBUMS_LOADED':
|
||||
var albums = Object.assign({}, core.albums);
|
||||
var tracks_loaded = [];
|
||||
|
||||
for (var i = 0; i < action.albums.length; i++){
|
||||
var album = Object.assign({}, action.albums[i]);
|
||||
|
||||
if (albums[album.uri]){
|
||||
album = Object.assign({}, albums[album.uri], album);
|
||||
}
|
||||
|
||||
if (album.images && album.images.length > 0){
|
||||
album.images = helpers.digestMopidyImages(store.getState().mopidy, album.images);
|
||||
}
|
||||
|
||||
// Load our tracks
|
||||
if (album.tracks){
|
||||
var tracks = helpers.formatTracks(album.tracks);
|
||||
var tracks_uris = helpers.arrayOf('uri', tracks);
|
||||
album.tracks_uris = tracks_uris;
|
||||
delete album.tracks;
|
||||
tracks_loaded = [...tracks_loaded, ...tracks];
|
||||
}
|
||||
|
||||
albums[album.uri] = album;
|
||||
}
|
||||
|
||||
// Load these new tracks
|
||||
store.dispatch({
|
||||
type: 'TRACKS_LOADED',
|
||||
tracks: tracks_loaded
|
||||
});
|
||||
|
||||
// Update index
|
||||
store.dispatch({
|
||||
type: 'UPDATE_ALBUMS_INDEX',
|
||||
albums: albums
|
||||
});
|
||||
|
||||
next(action);
|
||||
break
|
||||
|
||||
case 'ARTISTS_LOADED':
|
||||
var artists = Object.assign({}, core.artists);
|
||||
var tracks_loaded = [];
|
||||
|
||||
for (var i = 0; i < action.artists.length; i++){
|
||||
var artist = action.artists[i];
|
||||
|
||||
if (artists[artist.uri]){
|
||||
|
||||
// if we've already got images, remove and add as additional_images
|
||||
// this is to prevent LastFM overwriting Spotify images
|
||||
if (artists[artist.uri].images){
|
||||
artist.images_additional = artist.images
|
||||
delete artist.images
|
||||
}
|
||||
|
||||
artist = Object.assign({}, artists[artist.uri], artist);
|
||||
}
|
||||
|
||||
if (artist.tracks){
|
||||
var tracks = helpers.formatTracks(artist.tracks);
|
||||
var tracks_uris = helpers.arrayOf('uri', tracks);
|
||||
artist.tracks_uris = tracks_uris;
|
||||
delete artist.tracks;
|
||||
tracks_loaded = [...tracks_loaded, ...tracks];
|
||||
}
|
||||
|
||||
// Update index
|
||||
artists[artist.uri] = artist;
|
||||
}
|
||||
|
||||
// Load our tracks
|
||||
store.dispatch({
|
||||
type: 'TRACKS_LOADED',
|
||||
tracks: tracks_loaded
|
||||
});
|
||||
|
||||
store.dispatch({
|
||||
type: 'UPDATE_ARTISTS_INDEX',
|
||||
artists: artists
|
||||
});
|
||||
|
||||
next(action);
|
||||
break;
|
||||
|
||||
case 'PLAYLISTS_LOADED':
|
||||
var playlists = Object.assign({}, core.playlists);
|
||||
var tracks_loaded = [];
|
||||
|
||||
for (var i = 0; i < action.playlists.length; i++){
|
||||
var playlist = Object.assign({}, action.playlists[i]);
|
||||
|
||||
// Detect editability
|
||||
switch (helpers.uriSource(playlist.uri)){
|
||||
|
||||
case 'm3u':
|
||||
playlist.can_edit = true
|
||||
break
|
||||
|
||||
case 'spotify':
|
||||
if (store.getState().spotify.authorization && store.getState().spotify.me){
|
||||
playlist.can_edit = (helpers.getFromUri('playlistowner',playlist.uri) == store.getState().spotify.me.id)
|
||||
}
|
||||
}
|
||||
|
||||
if (playlists[playlist.uri] !== undefined){
|
||||
playlist = Object.assign({}, playlists[playlist.uri], playlist);
|
||||
}
|
||||
|
||||
// Load our tracks
|
||||
if (playlist.tracks){
|
||||
var tracks = helpers.formatTracks(playlist.tracks);
|
||||
var tracks_uris = helpers.arrayOf('uri', tracks);
|
||||
playlist.tracks_uris = tracks_uris;
|
||||
delete playlist.tracks;
|
||||
tracks_loaded = [...tracks_loaded, ...tracks];
|
||||
}
|
||||
|
||||
// Update index
|
||||
playlists[playlist.uri] = playlist;
|
||||
}
|
||||
|
||||
// Load our tracks
|
||||
store.dispatch({
|
||||
type: 'TRACKS_LOADED',
|
||||
tracks: tracks_loaded
|
||||
});
|
||||
|
||||
store.dispatch({
|
||||
type: 'UPDATE_PLAYLISTS_INDEX',
|
||||
playlists: playlists
|
||||
});
|
||||
|
||||
next(action);
|
||||
break;
|
||||
|
||||
case 'USERS_LOADED':
|
||||
var users = Object.assign({}, core.users);
|
||||
|
||||
for (var i = 0; i < action.users.length; i++){
|
||||
var user = Object.assign({}, action.users[i]);
|
||||
|
||||
if (users[user.uri]){
|
||||
user = Object.assign({}, users[user.uri], user);
|
||||
}
|
||||
|
||||
users[user.uri] = user;
|
||||
}
|
||||
|
||||
// Update index
|
||||
store.dispatch({
|
||||
type: 'UPDATE_USERS_INDEX',
|
||||
users: users
|
||||
});
|
||||
|
||||
next(action);
|
||||
break;
|
||||
|
||||
/**
|
||||
* Loaded more linked assets
|
||||
* Often fired during lazy-loading or async asset grabbing.
|
||||
* We link the parent to these indexed records by {type}s_uris
|
||||
**/
|
||||
|
||||
case 'LOADED_MORE':
|
||||
console.log(action);
|
||||
var parent_type_plural = action.parent_type+'s';
|
||||
var parent_index = Object.assign({}, core[action.parent_type+'s']);
|
||||
var parent = Object.assign({}, parent_index[action.parent_key]);
|
||||
|
||||
if (action.records_data.items !== undefined){
|
||||
var records = action.records_data.items;
|
||||
} else if (action.records_data.tracks !== undefined){
|
||||
var records = action.records_data.tracks;
|
||||
} else if (action.records_data.artists !== undefined){
|
||||
var records = action.records_data.artists;
|
||||
} else if (action.records_data.albums !== undefined){
|
||||
var records = action.records_data.albums;
|
||||
} else if (action.records_data.playlists !== undefined){
|
||||
var records = action.records_data.playlists;
|
||||
} else {
|
||||
var records = action.records_data;
|
||||
}
|
||||
|
||||
if (action.records_type == 'track'){
|
||||
records = helpers.formatTracks(records);
|
||||
}
|
||||
|
||||
var records_type_plural = action.records_type+'s';
|
||||
var records_index = Object.assign({});
|
||||
var records_uris = helpers.arrayOf('uri', records);
|
||||
|
||||
// Append our records_uris array with our new records
|
||||
var uris = records_uris;
|
||||
if (parent[records_type_plural+'_uris'] !== undefined){
|
||||
uris = [...parent[records_type_plural+'_uris'], ...uris];
|
||||
}
|
||||
parent[records_type_plural+'_uris'] = uris;
|
||||
if (action.records_data.next !== undefined){
|
||||
parent[records_type_plural+'_more'] = action.records_data.next;
|
||||
}
|
||||
|
||||
// Parent loaded (well, changed)
|
||||
var parent_action = {
|
||||
type: parent_type_plural.toUpperCase()+'_LOADED'
|
||||
};
|
||||
parent_action[parent_type_plural] = [parent];
|
||||
store.dispatch(parent_action);
|
||||
|
||||
// Records loaded
|
||||
var records_action = {
|
||||
type: records_type_plural.toUpperCase()+'_LOADED'
|
||||
};
|
||||
records_action[records_type_plural] = records;
|
||||
store.dispatch(records_action);
|
||||
|
||||
next(action);
|
||||
break;
|
||||
|
||||
// This action is irrelevant to us, pass it on to the next middleware
|
||||
default:
|
||||
return next(action)
|
||||
|
||||
@ -11,94 +11,15 @@ export default function reducer(core = {}, action){
|
||||
* Current track and tracklist
|
||||
**/
|
||||
|
||||
case 'MOPIDY_TLTRACKS':
|
||||
if (!action.data ) return core
|
||||
|
||||
var tracklist = []
|
||||
for (var i = 0; i < action.data.length; i++){
|
||||
|
||||
var tltrack = helpers.formatTracks(action.data[i]);
|
||||
|
||||
// load our metadata (if we have any for that tlid)
|
||||
if (core.queue_metadata !== undefined && core.queue_metadata['tlid_'+tltrack.tlid] !== undefined){
|
||||
var metadata = core.queue_metadata['tlid_'+tltrack.tlid]
|
||||
} else {
|
||||
var metadata = {}
|
||||
}
|
||||
|
||||
var current_tlid = null;
|
||||
if (core.current_track && core.tracks && core.tracks[core.current_track] !== undefined && core.tracks[core.current_track].tlid !== undefined){
|
||||
current_tlid = core.tracks[core.current_track].tlid;
|
||||
}
|
||||
|
||||
var track = Object.assign(
|
||||
{},
|
||||
tltrack,
|
||||
metadata,
|
||||
{
|
||||
playing: (tltrack.tlid == current_tlid)
|
||||
})
|
||||
tracklist.push(track)
|
||||
}
|
||||
|
||||
tracklist = helpers.formatTracks(tracklist);
|
||||
|
||||
return Object.assign({}, core, { current_tracklist: tracklist });
|
||||
|
||||
case 'MOPIDY_CURRENTTLTRACK':
|
||||
if (!action.data) return core
|
||||
|
||||
var current_tracklist = []
|
||||
Object.assign(current_tracklist, core.current_tracklist)
|
||||
|
||||
for (var i = 0; i < current_tracklist.length; i++){
|
||||
Object.assign(
|
||||
current_tracklist[i],
|
||||
{ playing: (current_tracklist[i].tlid == action.data.tlid ) }
|
||||
)
|
||||
}
|
||||
|
||||
case 'CURRENT_TRACK_LOADED':
|
||||
return Object.assign({}, core, {
|
||||
current_tracklist: current_tracklist,
|
||||
current_track: action.data.track.uri
|
||||
current_track_uri: action.current_track_uri
|
||||
});
|
||||
|
||||
case 'TRACK_LOADED':
|
||||
if (!action.key || !action.track) return core
|
||||
|
||||
var tracks = Object.assign({}, core.tracks)
|
||||
if (tracks[action.key]){
|
||||
var track = Object.assign(
|
||||
{},
|
||||
tracks[action.key],
|
||||
helpers.formatTracks(action.track)
|
||||
);
|
||||
} else {
|
||||
var track = Object.assign(
|
||||
{},
|
||||
helpers.formatTracks(action.track)
|
||||
);
|
||||
}
|
||||
|
||||
tracks[action.key] = track
|
||||
return Object.assign({}, core, { tracks: tracks });
|
||||
|
||||
case 'TRACKS_LOADED':
|
||||
var tracks = Object.assign({}, core.tracks)
|
||||
|
||||
for (var i = 0; i < action.tracks.length; i++){
|
||||
var track = action.tracks[i]
|
||||
if (tracks[track.uri] !== undefined){
|
||||
track = Object.assign(
|
||||
{},
|
||||
tracks[track.uri],
|
||||
track
|
||||
);
|
||||
}
|
||||
tracks[track.uri] = helpers.formatTracks(track);
|
||||
}
|
||||
|
||||
return Object.assign({}, core, { tracks: tracks });
|
||||
case 'QUEUE_LOADED':
|
||||
return Object.assign({}, core, {
|
||||
queue: action.tracks_uris
|
||||
});
|
||||
|
||||
case 'PUSHER_QUEUE_METADATA':
|
||||
case 'PUSHER_QUEUE_METADATA_CHANGED':
|
||||
@ -181,37 +102,27 @@ export default function reducer(core = {}, action){
|
||||
|
||||
|
||||
/**
|
||||
* Albums
|
||||
* Index updates
|
||||
* These actions are only ever called by middleware after we've digested one more many assets
|
||||
* and appended to their relevant index.
|
||||
**/
|
||||
|
||||
case 'ALBUM_LOADED':
|
||||
var albums = Object.assign([], core.albums)
|
||||
case 'UPDATE_TRACKS_INDEX':
|
||||
return Object.assign({}, core, { tracks: action.tracks });
|
||||
|
||||
if (albums[action.key]){
|
||||
var album = Object.assign({}, albums[action.key], action.album)
|
||||
} else {
|
||||
var album = Object.assign({}, action.album)
|
||||
}
|
||||
case 'UPDATE_ALBUMS_INDEX':
|
||||
return Object.assign({}, core, { albums: action.albums });
|
||||
|
||||
album.tracks = helpers.formatTracks(album.tracks);
|
||||
albums[action.key] = album
|
||||
case 'UPDATE_ARTISTS_INDEX':
|
||||
return Object.assign({}, core, { artists: action.artists });
|
||||
|
||||
return Object.assign({}, core, { albums: albums });
|
||||
case 'UPDATE_PLAYLISTS_INDEX':
|
||||
return Object.assign({}, core, { playlists: action.playlists });
|
||||
|
||||
case 'ALBUMS_LOADED':
|
||||
var albums = Object.assign([], core.albums)
|
||||
case 'UPDATE_USERS_INDEX':
|
||||
return Object.assign({}, core, { users: action.users });
|
||||
|
||||
for (var i = 0; i < action.albums.length; i++){
|
||||
var album = action.albums[i]
|
||||
if (albums[album.uri]){
|
||||
album = Object.assign({}, albums[album.uri], album)
|
||||
}
|
||||
|
||||
album.tracks = helpers.formatTracks(album.tracks);
|
||||
albums[album.uri] = album
|
||||
}
|
||||
|
||||
return Object.assign({}, core, { albums: albums });
|
||||
|
||||
case 'NEW_RELEASES_LOADED':
|
||||
if (!action.uris){
|
||||
@ -232,54 +143,6 @@ export default function reducer(core = {}, action){
|
||||
});
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Artists
|
||||
**/
|
||||
|
||||
case 'ARTIST_LOADED':
|
||||
var artists = Object.assign([], core.artists)
|
||||
|
||||
if (artists[action.key]){
|
||||
|
||||
// if we've already got images, remove and add as additional_images
|
||||
// this is to prevent LastFM overwriting Spotify images
|
||||
if (artists[action.key].images){
|
||||
action.artist.images_additional = action.artist.images
|
||||
delete action.artist.images
|
||||
}
|
||||
|
||||
var artist = Object.assign({}, artists[action.key], action.artist)
|
||||
if (artist.tracks){
|
||||
artist.tracks = helpers.formatTracks(artist.tracks);
|
||||
}
|
||||
} else {
|
||||
var artist = Object.assign({}, action.artist)
|
||||
if (artist.tracks){
|
||||
artist.tracks = helpers.formatTracks(artist.tracks);
|
||||
}
|
||||
}
|
||||
|
||||
artists[action.key] = artist
|
||||
return Object.assign({}, core, { artists: artists });
|
||||
|
||||
case 'ARTISTS_LOADED':
|
||||
var artists = Object.assign([], core.artists)
|
||||
|
||||
for (var i = 0; i < action.artists.length; i++){
|
||||
var artist = action.artists[i]
|
||||
if (typeof(artists[artist.uri]) !== 'undefined'){
|
||||
artist = Object.assign({}, artists[artist.uri], artist)
|
||||
}
|
||||
|
||||
if (artist.tracks){
|
||||
artist.tracks = helpers.formatTracks(artist.tracks);
|
||||
}
|
||||
artists[artist.uri] = artist
|
||||
}
|
||||
|
||||
return Object.assign({}, core, { artists: artists });
|
||||
|
||||
case 'ARTIST_ALBUMS_LOADED':
|
||||
var artists = Object.assign([], core.artists)
|
||||
var albums_uris = []
|
||||
@ -298,22 +161,6 @@ export default function reducer(core = {}, action){
|
||||
return Object.assign({}, core, { artists: artists });
|
||||
|
||||
|
||||
/**
|
||||
* User profiles
|
||||
**/
|
||||
|
||||
case 'USER_LOADED':
|
||||
var users = Object.assign([], core.users)
|
||||
|
||||
if (users[action.key]){
|
||||
var user = Object.assign({}, users[action.key], action.user)
|
||||
} else {
|
||||
var user = Object.assign({}, action.user)
|
||||
}
|
||||
|
||||
users[action.key] = user
|
||||
return Object.assign({}, core, { users: users });
|
||||
|
||||
case 'USER_PLAYLISTS_LOADED':
|
||||
var users = Object.assign([], core.users)
|
||||
var playlists_uris = []
|
||||
@ -338,119 +185,6 @@ export default function reducer(core = {}, action){
|
||||
* Playlists
|
||||
**/
|
||||
|
||||
case 'PLAYLIST_LOADED':
|
||||
case 'PLAYLIST_UPDATED':
|
||||
var playlists = Object.assign([], core.playlists)
|
||||
|
||||
if (typeof(playlists[action.key]) !== 'undefined'){
|
||||
var existing_playlist = Object.assign({}, playlists[action.key])
|
||||
|
||||
if (existing_playlist.tracks && action.playlist.tracks){
|
||||
var tracks = [...existing_playlist.tracks, ...action.playlist.tracks]
|
||||
} else if (existing_playlist.tracks){
|
||||
var tracks = existing_playlist.tracks
|
||||
} else if (action.playlist.tracks){
|
||||
var tracks = action.playlist.tracks
|
||||
} else {
|
||||
var tracks = []
|
||||
}
|
||||
|
||||
var merged_playlist = Object.assign(
|
||||
{},
|
||||
existing_playlist,
|
||||
action.playlist,
|
||||
{
|
||||
tracks: helpers.formatTracks(tracks)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
var merged_playlist = Object.assign({}, action.playlist)
|
||||
}
|
||||
|
||||
playlists[action.key] = merged_playlist
|
||||
return Object.assign({}, core, { playlists: playlists })
|
||||
|
||||
case 'PLAYLIST_KEY_UPDATED':
|
||||
var playlists = Object.assign([], core.playlists)
|
||||
|
||||
// URI not in our index? No change needed then
|
||||
if (typeof(playlists[action.key]) === 'undefined'){
|
||||
return core
|
||||
}
|
||||
|
||||
// Delete our old playlist by key, and add by new key
|
||||
var playlist = Object.assign({}, playlists[action.key])
|
||||
delete playlists[action.key]
|
||||
playlists[playlist.uri] = playlist
|
||||
|
||||
return Object.assign({}, core, { playlists: playlists })
|
||||
|
||||
case 'PLAYLISTS_LOADED':
|
||||
var playlists = Object.assign([], core.playlists)
|
||||
|
||||
for (var i = 0; i < action.playlists.length; i++){
|
||||
var loaded_playlist = action.playlists[i]
|
||||
|
||||
if (typeof(playlists[loaded_playlist.uri]) !== 'undefined'){
|
||||
var existing_playlist = Object.assign({}, playlists[loaded_playlist.uri])
|
||||
|
||||
if (existing_playlist.tracks && loaded_playlist.tracks){
|
||||
var tracks = [...existing_playlist.tracks, ...loaded_playlist.tracks]
|
||||
} else if (existing_playlist.tracks){
|
||||
var tracks = existing_playlist.tracks
|
||||
} else if (loaded_playlist.tracks){
|
||||
var tracks = loaded_playlist.tracks
|
||||
} else {
|
||||
var tracks = []
|
||||
}
|
||||
|
||||
var merged_playlist = Object.assign(
|
||||
{},
|
||||
existing_playlist,
|
||||
loaded_playlist,
|
||||
{
|
||||
tracks: helpers.formatTracks(tracks)
|
||||
}
|
||||
)
|
||||
|
||||
} else {
|
||||
var merged_playlist = loaded_playlist
|
||||
}
|
||||
|
||||
playlists[merged_playlist.uri] = merged_playlist
|
||||
}
|
||||
|
||||
return Object.assign({}, core, { playlists: playlists });
|
||||
|
||||
case 'PLAYLIST_LOADED_MORE_TRACKS':
|
||||
var playlists = Object.assign([], core.playlists)
|
||||
var playlist = Object.assign(
|
||||
{},
|
||||
playlists[action.key],
|
||||
{
|
||||
tracks: [...playlists[action.key].tracks, ...helpers.formatTracks(action.data.items)],
|
||||
tracks_more: action.data.next,
|
||||
tracks_total: action.data.total
|
||||
}
|
||||
)
|
||||
|
||||
playlists[action.key] = playlist
|
||||
return Object.assign({}, core, { playlists: playlists });
|
||||
|
||||
case 'PLAYLIST_TRACKS_REMOVED':
|
||||
var playlists = Object.assign([], core.playlists)
|
||||
var playlist = Object.assign({}, playlists[action.key])
|
||||
var tracks = Object.assign([], playlist.tracks)
|
||||
var indexes = action.tracks_indexes.reverse()
|
||||
for(var i = 0; i < indexes.length; i++){
|
||||
tracks.splice(indexes[i], 1 )
|
||||
}
|
||||
var snapshot_id = null
|
||||
if (action.snapshot_id ) snapshot_id = action.snapshot_id
|
||||
Object.assign(playlist, { tracks: tracks, snapshot_id: snapshot_id })
|
||||
playlists[action.key] = playlist
|
||||
return Object.assign({}, core, { playlists: playlists });
|
||||
|
||||
case 'PLAYLIST_TRACKS':
|
||||
var playlists = Object.assign([], core.playlists)
|
||||
var playlist = Object.assign({}, playlists[action.key], { tracks: helpers.formatTracks(action.tracks) })
|
||||
@ -458,29 +192,6 @@ export default function reducer(core = {}, action){
|
||||
playlists[action.key] = playlist
|
||||
return Object.assign({}, core, { playlists: playlists });
|
||||
|
||||
case 'PLAYLIST_TRACKS_REORDERED':
|
||||
var playlists = Object.assign([], core.playlists)
|
||||
var playlist = Object.assign({}, playlists[action.key])
|
||||
var tracks = Object.assign([], playlist.tracks)
|
||||
|
||||
// handle insert_before offset if we're moving BENEATH where we're slicing tracks
|
||||
var insert_before = action.insert_before
|
||||
if (insert_before > action.range_start ) insert_before = insert_before - action.range_length
|
||||
|
||||
// cut our moved tracks into a new array
|
||||
var tracks_to_move = tracks.splice(action.range_start, action.range_length)
|
||||
tracks_to_move.reverse()
|
||||
|
||||
for(i = 0; i < tracks_to_move.length; i++){
|
||||
tracks.splice(insert_before, 0, tracks_to_move[i])
|
||||
}
|
||||
|
||||
var snapshot_id = null
|
||||
if (action.snapshot_id ) snapshot_id = action.snapshot_id
|
||||
Object.assign(playlist, { tracks: tracks, snapshot_id: snapshot_id })
|
||||
playlists[action.key] = playlist
|
||||
return Object.assign({}, core, { playlists: playlists });
|
||||
|
||||
case 'LIBRARY_PLAYLISTS_LOADED':
|
||||
if (core.library_playlists){
|
||||
var library_playlists = [...core.library_playlists, ...action.uris]
|
||||
|
||||
@ -64,8 +64,8 @@ export function getTrackLyrics(uri, url){
|
||||
|
||||
dispatch({
|
||||
type: 'TRACK_LOADED',
|
||||
key: uri,
|
||||
track: {
|
||||
uri: uri,
|
||||
lyrics: null,
|
||||
lyrics_url: null
|
||||
}
|
||||
@ -89,8 +89,8 @@ export function getTrackLyrics(uri, url){
|
||||
|
||||
dispatch({
|
||||
type: 'TRACK_LOADED',
|
||||
key: uri,
|
||||
track: {
|
||||
uri: uri,
|
||||
lyrics: lyrics_html,
|
||||
lyrics_url: url
|
||||
}
|
||||
@ -131,8 +131,8 @@ export function findTrackLyrics(track){
|
||||
}
|
||||
dispatch({
|
||||
type: 'TRACK_LOADED',
|
||||
key: track.uri,
|
||||
track: {
|
||||
uri: track.uri,
|
||||
lyrics_results: lyrics_results
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,59 +1,245 @@
|
||||
|
||||
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');
|
||||
|
||||
/**
|
||||
* Send an ajax request to the Spotify API
|
||||
* Send an ajax request to the LastFM API
|
||||
*
|
||||
* @param dispatch obj
|
||||
* @param getState obj
|
||||
* @param endpoint params = the url params to send
|
||||
* @param dispatch = obj
|
||||
* @param getState = obj
|
||||
* @param params = string, the url params to send
|
||||
* @params signed = boolean, whether we've got a signed request with baked-in api_key
|
||||
**/
|
||||
const sendRequest = (dispatch, getState, params ) => {
|
||||
const sendRequest = (dispatch, getState, params, signed = false) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
var loader_key = helpers.generateGuid()
|
||||
dispatch(uiActions.startLoading(loader_key, 'lastfm_'+params))
|
||||
var loader_key = helpers.generateGuid();
|
||||
var method = params.substring(params.indexOf("method=")+7, params.length);
|
||||
method = method.substring(0, method.indexOf("&"));
|
||||
|
||||
dispatch(uiActions.startLoading(loader_key, 'lastfm_'+method));
|
||||
|
||||
var config = {
|
||||
method: 'GET',
|
||||
cache: true,
|
||||
timeout: 30000,
|
||||
url: '//ws.audioscrobbler.com/2.0/?format=json&api_key=4320a3ef51c9b3d69de552ac083c55e3&'+params
|
||||
url: '//ws.audioscrobbler.com/2.0/?format=json&'+params
|
||||
}
|
||||
|
||||
// Signed requests don't need our api_key as the proxy has it's own
|
||||
if (!signed){
|
||||
config.url += '&api_key=4320a3ef51c9b3d69de552ac083c55e3';
|
||||
} else {
|
||||
config.method = 'POST';
|
||||
}
|
||||
|
||||
$.ajax(config).then(
|
||||
response => {
|
||||
dispatch(uiActions.stopLoading(loader_key))
|
||||
resolve(response)
|
||||
dispatch(uiActions.stopLoading(loader_key));
|
||||
if (response.error){
|
||||
reject({
|
||||
config: config,
|
||||
error: response
|
||||
});
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
},
|
||||
(xhr, status, error) => {
|
||||
dispatch(uiActions.stopLoading(loader_key))
|
||||
dispatch(coreActions.handleException(
|
||||
'LastFM: '+xhr.responseText,
|
||||
{
|
||||
config: config,
|
||||
error: error,
|
||||
status: status,
|
||||
xhr: xhr
|
||||
}
|
||||
));
|
||||
reject(error)
|
||||
dispatch(uiActions.stopLoading(loader_key));
|
||||
|
||||
// Snatch a more meaningful error
|
||||
var description = null;
|
||||
if (xhr.responseJSON.message){
|
||||
description = xhr.responseJSON.message;
|
||||
}
|
||||
|
||||
reject({
|
||||
config: config,
|
||||
error: error,
|
||||
description: description,
|
||||
status: status,
|
||||
xhr: xhr
|
||||
});
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export function connect(){
|
||||
/**
|
||||
* Send a SIGNED ajax request to the LastFM API
|
||||
*
|
||||
* @param dispatch = obj
|
||||
* @param getState = obj
|
||||
* @param params = string, the url params to send
|
||||
* @param signed = boolean
|
||||
**/
|
||||
const sendSignedRequest = (dispatch, getState, params) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
// Not authorized
|
||||
if (!getState().lastfm.session){
|
||||
reject({
|
||||
params: params,
|
||||
error: "No active LastFM session"
|
||||
});
|
||||
}
|
||||
|
||||
var loader_key = helpers.generateGuid();
|
||||
var method = params.substring(params.indexOf("method=")+7, params.length);
|
||||
method = method.substring(0, method.indexOf("&"));
|
||||
|
||||
dispatch(uiActions.startLoading(loader_key, 'lastfm_'+method));
|
||||
|
||||
params += "&sk="+getState().lastfm.session.key;
|
||||
|
||||
var config = {
|
||||
method: 'GET',
|
||||
cache: false,
|
||||
timeout: 30000,
|
||||
url: getState().lastfm.authorization_url+"?action=sign_request&"+params
|
||||
}
|
||||
|
||||
// Get our server proxy to sign our request
|
||||
$.ajax(config).then(
|
||||
response => {
|
||||
dispatch(uiActions.stopLoading(loader_key));
|
||||
|
||||
// Now we have signed params, we can make the actual request
|
||||
sendRequest(dispatch, getState, response.params, true)
|
||||
.then(
|
||||
response => {
|
||||
resolve(response);
|
||||
},
|
||||
error => {
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
},
|
||||
(xhr, status, error) => {
|
||||
dispatch(uiActions.stopLoading(loader_key));
|
||||
reject(error)
|
||||
}
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export function set(data){
|
||||
return {
|
||||
type: 'LASTFM_SET',
|
||||
data: data
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle authorization process
|
||||
**/
|
||||
|
||||
export function authorizationGranted(data){
|
||||
data.session.expiry = new Date().getTime() + 3600;
|
||||
return {
|
||||
type: 'LASTFM_AUTHORIZATION_GRANTED',
|
||||
data: data
|
||||
}
|
||||
}
|
||||
|
||||
export function revokeAuthorization(){
|
||||
return { type: 'LASTFM_AUTHORIZATION_REVOKED' }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Signed requests
|
||||
**/
|
||||
|
||||
export function loveTrack(uri, artist, track){
|
||||
return (dispatch, getState) => {
|
||||
|
||||
dispatch({ type: 'LASTFM_CONNECTING' })
|
||||
|
||||
sendRequest(dispatch, getState, 'method=artist.getInfo&artist=')
|
||||
artist = encodeURIComponent(artist);
|
||||
var params = 'method=track.love&track='+track+'&artist='+artist;
|
||||
sendSignedRequest(dispatch, getState, params)
|
||||
.then(
|
||||
response => {
|
||||
dispatch({ type: 'LASTFM_CONNECTED' })
|
||||
dispatch({
|
||||
type: 'TRACK_LOADED',
|
||||
track: {
|
||||
uri: uri,
|
||||
userloved: true
|
||||
}
|
||||
});
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function unloveTrack(uri, artist, track){
|
||||
return (dispatch, getState) => {
|
||||
artist = encodeURIComponent(artist);
|
||||
var params = 'method=track.unlove&track='+track+'&artist='+artist;
|
||||
sendSignedRequest(dispatch, getState, params)
|
||||
.then(
|
||||
response => {
|
||||
dispatch({
|
||||
type: 'TRACK_LOADED',
|
||||
track: {
|
||||
uri: uri,
|
||||
userloved: false
|
||||
}
|
||||
});
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function scrobble(track){
|
||||
return (dispatch, getState) => {
|
||||
var track_name = track.name;
|
||||
var artist_name = "Unknown";
|
||||
if (track.artists){
|
||||
artist_name = track.artists[0].name;
|
||||
}
|
||||
var artist_name = encodeURIComponent(artist_name);
|
||||
|
||||
var params = 'method=track.scrobble';
|
||||
params += '&track='+track_name+'&artist='+artist_name;
|
||||
params += '×tamp='+Math.floor(Date.now() / 1000);
|
||||
|
||||
sendSignedRequest(dispatch, getState, params)
|
||||
.then(
|
||||
response => {
|
||||
console.log("Scrobbled", response);
|
||||
},
|
||||
error => {
|
||||
dispatch(coreActions.handleException(
|
||||
'Could not scrobble track',
|
||||
error,
|
||||
(error.description ? error.description : null)
|
||||
));
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Non-signed requests
|
||||
**/
|
||||
|
||||
export function getMe(){
|
||||
return (dispatch, getState) => {
|
||||
var params = 'method=user.getInfo&user='+getState().lastfm.session.name
|
||||
sendRequest(dispatch, getState, params)
|
||||
.then(
|
||||
response => {
|
||||
if (response.user){
|
||||
dispatch({
|
||||
type: 'LASTFM_USER_LOADED',
|
||||
user: response.user
|
||||
});
|
||||
dispatch({ type: 'LASTFM_CONNECTED' })
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -73,8 +259,8 @@ export function getArtist(uri, artist, mbid = false){
|
||||
if (response.artist){
|
||||
dispatch({
|
||||
type: 'ARTIST_LOADED',
|
||||
key: uri,
|
||||
artist: {
|
||||
uri: uri,
|
||||
images: response.artist.image,
|
||||
bio: response.artist.bio,
|
||||
listeners: parseInt(response.artist.stats.listeners),
|
||||
@ -114,19 +300,34 @@ export function getAlbum(artist, album, mbid = false){
|
||||
}
|
||||
}
|
||||
|
||||
export function getTrack(artist, track){
|
||||
export function getTrack(track, artist_name = null, track_name = null){
|
||||
return (dispatch, getState) => {
|
||||
|
||||
dispatch({ type: 'LASTFM_TRACK_LOADED', data: false });
|
||||
|
||||
artist = encodeURIComponent(artist );
|
||||
sendRequest(dispatch, getState, 'method=track.getInfo&track='+track+'&artist='+artist)
|
||||
if (track){
|
||||
track_name = track.name;
|
||||
if (track.artists){
|
||||
artist_name = track.artists[0].name;
|
||||
}
|
||||
}
|
||||
artist_name = encodeURIComponent(artist_name);
|
||||
var params = 'method=track.getInfo&track='+track_name+'&artist='+artist_name;
|
||||
if (getState().lastfm.session){
|
||||
params += '&username='+getState().lastfm.session.name;
|
||||
}
|
||||
sendRequest(dispatch, getState, params)
|
||||
.then(
|
||||
response => {
|
||||
if (response.track){
|
||||
var merged_track = Object.assign(
|
||||
{},
|
||||
{
|
||||
uri: track.uri
|
||||
},
|
||||
response.track,
|
||||
track
|
||||
);
|
||||
dispatch({
|
||||
type: 'LASTFM_TRACK_LOADED',
|
||||
data: response.track
|
||||
type: 'TRACK_LOADED',
|
||||
track: merged_track
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
43
src/js/services/lastfm/middleware.js
Executable file
43
src/js/services/lastfm/middleware.js
Executable file
@ -0,0 +1,43 @@
|
||||
|
||||
import ReactGA from 'react-ga'
|
||||
|
||||
var helpers = require('./../../helpers')
|
||||
var lastfmActions = require('./actions')
|
||||
var uiActions = require('../ui/actions')
|
||||
var pusherActions = require('../pusher/actions')
|
||||
|
||||
const LastfmMiddleware = (function(){
|
||||
|
||||
/**
|
||||
* The actual middleware inteceptor
|
||||
**/
|
||||
return store => next => action => {
|
||||
var state = store.getState();
|
||||
|
||||
switch(action.type){
|
||||
|
||||
case 'LASTFM_USER_LOADED':
|
||||
var user = Object.assign(
|
||||
{},
|
||||
action.user,
|
||||
{
|
||||
uri: "lastfm:user:"+action.user.name
|
||||
}
|
||||
);
|
||||
store.dispatch({
|
||||
type: "USERS_LOADED",
|
||||
users: [user]
|
||||
});
|
||||
next(action);
|
||||
break;
|
||||
|
||||
|
||||
// This action is irrelevant to us, pass it on to the next middleware
|
||||
default:
|
||||
return next(action);
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
export default LastfmMiddleware
|
||||
@ -9,6 +9,22 @@ export default function reducer(lastfm = {}, action){
|
||||
case 'LASTFM_CONNECTED':
|
||||
return Object.assign({}, lastfm, { connected: true, connecting: false });
|
||||
|
||||
case 'LASTFM_SET':
|
||||
return Object.assign({}, lastfm, action.data)
|
||||
|
||||
case 'LASTFM_AUTHORIZATION_GRANTED':
|
||||
return Object.assign({}, lastfm, {
|
||||
authorizing: false,
|
||||
session: action.data.session
|
||||
})
|
||||
|
||||
case 'LASTFM_AUTHORIZATION_REVOKED':
|
||||
return Object.assign({}, lastfm, {
|
||||
authorizing: false,
|
||||
session: false,
|
||||
me: false
|
||||
});
|
||||
|
||||
default:
|
||||
return lastfm
|
||||
}
|
||||
|
||||
@ -167,6 +167,28 @@ const localstorageMiddleware = (function(){
|
||||
);
|
||||
localStorage.setItem('ui', JSON.stringify(ui))
|
||||
break
|
||||
|
||||
case 'LASTFM_AUTHORIZATION_GRANTED':
|
||||
var lastfm = JSON.parse(localStorage.getItem('lastfm') );
|
||||
lastfm = Object.assign(
|
||||
{},
|
||||
{
|
||||
session: action.data.session
|
||||
}
|
||||
);
|
||||
localStorage.setItem('lastfm', JSON.stringify(lastfm));
|
||||
break;
|
||||
|
||||
case 'LASTFM_AUTHORIZATION_REVOKED':
|
||||
var lastfm = JSON.parse(localStorage.getItem('lastfm') );
|
||||
lastfm = Object.assign(
|
||||
{},
|
||||
{
|
||||
session: null
|
||||
}
|
||||
);
|
||||
localStorage.setItem('lastfm', JSON.stringify(lastfm));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1259,11 +1259,11 @@ const MopidyMiddleware = (function(){
|
||||
type: 'PLAYLIST_KEY_UPDATED',
|
||||
key: action.key,
|
||||
new_key: response.uri
|
||||
})
|
||||
hashHistory.push(global.baseURL+'playlist/'+response.uri)
|
||||
});
|
||||
hashHistory.push(global.baseURL+'playlist/'+encodeURIComponent(response.uri));
|
||||
}
|
||||
|
||||
store.dispatch(uiActions.createNotification('Saved'))
|
||||
store.dispatch(uiActions.createNotification('Saved'));
|
||||
})
|
||||
});
|
||||
break
|
||||
@ -1703,17 +1703,17 @@ const MopidyMiddleware = (function(){
|
||||
* ======================================================================================
|
||||
**/
|
||||
|
||||
case 'MOPIDY_TLTRACKS':
|
||||
store.dispatch({
|
||||
type: 'QUEUE_LOADED',
|
||||
tracks: helpers.formatTracks(action.data)
|
||||
})
|
||||
break;
|
||||
|
||||
case 'MOPIDY_CURRENTTLTRACK':
|
||||
if (action.data && action.data.track){
|
||||
var track = helpers.formatTracks(action.data);
|
||||
|
||||
// Fire off our universal track index loader
|
||||
store.dispatch({
|
||||
type: 'TRACK_LOADED',
|
||||
key: track.uri,
|
||||
track: track
|
||||
});
|
||||
|
||||
// We've got Spotify running, and it's a spotify track - go straight to the source!
|
||||
if (helpers.uriSource(track.uri) == 'spotify' && store.getState().spotify.enabled){
|
||||
store.dispatch(spotifyActions.getTrack(track.uri))
|
||||
@ -1722,9 +1722,16 @@ const MopidyMiddleware = (function(){
|
||||
} else {
|
||||
store.dispatch(mopidyActions.getImages('tracks',[track.uri]))
|
||||
}
|
||||
}
|
||||
|
||||
next(action);
|
||||
// Set our window title to the track title
|
||||
helpers.setWindowTitle(track, store.getState().mopidy.play_state);
|
||||
|
||||
store.dispatch({
|
||||
type: 'CURRENT_TRACK_LOADED',
|
||||
current_track: track,
|
||||
current_track_uri: track.uri
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'MOPIDY_GET_TRACK':
|
||||
|
||||
@ -5,6 +5,7 @@ var helpers = require('../../helpers.js')
|
||||
var coreActions = require('../core/actions.js')
|
||||
var uiActions = require('../ui/actions.js')
|
||||
var pusherActions = require('./actions.js')
|
||||
var lastfmActions = require('../lastfm/actions.js')
|
||||
var spotifyActions = require('../spotify/actions.js')
|
||||
|
||||
const PusherMiddleware = (function(){
|
||||
@ -431,7 +432,10 @@ const PusherMiddleware = (function(){
|
||||
store.dispatch(spotifyActions.set({
|
||||
locale: (action.config.locale ? action.config.locale : null),
|
||||
country: (action.config.country ? action.config.country : null),
|
||||
authorization_url: (action.config.authorization_url ? action.config.authorization_url : null)
|
||||
authorization_url: (action.config.spotify_authorization_url ? action.config.spotify_authorization_url : null)
|
||||
}))
|
||||
store.dispatch(lastfmActions.set({
|
||||
authorization_url: (action.config.lastfm_authorization_url ? action.config.lastfm_authorization_url : null)
|
||||
}))
|
||||
|
||||
next(action )
|
||||
|
||||
@ -192,25 +192,8 @@ export function set(data){
|
||||
|
||||
export function connect(){
|
||||
return (dispatch, getState) => {
|
||||
|
||||
dispatch({ type: 'SPOTIFY_CONNECTING' });
|
||||
|
||||
// send a generic request to ensure spotify is up and running
|
||||
// there is no 'test' or 'ping' endpoint on the Spotify API
|
||||
sendRequest(dispatch, getState, 'browse/categories?limit=1' )
|
||||
.then(
|
||||
response => {
|
||||
dispatch({
|
||||
type: 'SPOTIFY_CONNECTED'
|
||||
});
|
||||
},
|
||||
error => {
|
||||
dispatch(coreActions.handleException(
|
||||
'Could not connect to Spotify',
|
||||
error
|
||||
));
|
||||
}
|
||||
);
|
||||
dispatch(getMe());
|
||||
}
|
||||
}
|
||||
|
||||
@ -249,10 +232,6 @@ export function importAuthorization(data){
|
||||
**/
|
||||
export function getMe(){
|
||||
return (dispatch, getState) => {
|
||||
|
||||
// flush out the previous store value
|
||||
dispatch({ type: 'SPOTIFY_ME_LOADED', data: false });
|
||||
|
||||
sendRequest(dispatch, getState, 'me' )
|
||||
.then(
|
||||
response => {
|
||||
@ -260,12 +239,14 @@ export function getMe(){
|
||||
type: 'SPOTIFY_ME_LOADED',
|
||||
data: response
|
||||
});
|
||||
dispatch({ type: 'SPOTIFY_CONNECTED' });
|
||||
},
|
||||
error => {
|
||||
dispatch(coreActions.handleException(
|
||||
'Could not load your profile',
|
||||
error
|
||||
));
|
||||
dispatch({ type: 'SPOTIFY_DISCONNECTED' });
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -279,11 +260,7 @@ export function getMe(){
|
||||
**/
|
||||
export function getTrack(uri){
|
||||
return (dispatch, getState) => {
|
||||
|
||||
// flush out the previous store value
|
||||
dispatch({ type: 'SPOTIFY_TRACK_LOADED', data: false });
|
||||
|
||||
sendRequest(dispatch, getState, 'tracks/'+ helpers.getFromUri('trackid', uri) )
|
||||
sendRequest(dispatch, getState, 'tracks/'+ helpers.getFromUri('trackid', uri))
|
||||
.then(
|
||||
response => {
|
||||
let track = Object.assign(
|
||||
@ -524,6 +501,40 @@ export function getURL(url, action_name, key = false){
|
||||
}
|
||||
}
|
||||
|
||||
export function loadMore(url, loaded_more_action = null, custom_action = null){
|
||||
return (dispatch, getState) => {
|
||||
sendRequest(dispatch, getState, url)
|
||||
.then(
|
||||
response => {
|
||||
if (loaded_more_action){
|
||||
dispatch(coreActions.loadedMore(
|
||||
loaded_more_action.parent_type,
|
||||
loaded_more_action.parent_key,
|
||||
loaded_more_action.records_type,
|
||||
response
|
||||
));
|
||||
} else if (custom_action){
|
||||
dispatch({
|
||||
type: custom_action.type,
|
||||
key: custom_action.key,
|
||||
data: response
|
||||
});
|
||||
} else {
|
||||
dispatch(coreActions.handleException(
|
||||
'No callback handler for loading more items'
|
||||
));
|
||||
}
|
||||
},
|
||||
error => {
|
||||
dispatch(coreActions.handleException(
|
||||
'Could not load more '+callback_action.parent_type+' '+callback_action.records_type+'s',
|
||||
error
|
||||
));
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function clearSearchResults(){
|
||||
return {
|
||||
type: 'SPOTIFY_CLEAR_SEARCH_RESULTS'
|
||||
@ -692,6 +703,13 @@ export function following(uri, method = 'GET'){
|
||||
var asset_name = helpers.uriType(uri);
|
||||
var endpoint, data
|
||||
switch(asset_name){
|
||||
case 'track':
|
||||
if (method == 'GET'){
|
||||
endpoint = 'me/tracks/contains/?ids='+ helpers.getFromUri('trackid', uri)
|
||||
} else {
|
||||
endpoint = 'me/tracks/?ids='+ helpers.getFromUri('trackid', uri)
|
||||
}
|
||||
break
|
||||
case 'album':
|
||||
if (method == 'GET'){
|
||||
endpoint = 'me/albums/contains/?ids='+ helpers.getFromUri('albumid', uri)
|
||||
@ -724,7 +742,7 @@ export function following(uri, method = 'GET'){
|
||||
break
|
||||
}
|
||||
|
||||
sendRequest(dispatch, getState, endpoint, method, data )
|
||||
sendRequest(dispatch, getState, endpoint, method, data)
|
||||
.then(
|
||||
response => {
|
||||
if (response ) is_following = response
|
||||
@ -1170,7 +1188,7 @@ export function playArtistTopTracks(uri){
|
||||
* ======================================================================================
|
||||
**/
|
||||
|
||||
export function getUser(uri){
|
||||
export function getUser(uri, and_playlists = false){
|
||||
return (dispatch, getState) => {
|
||||
|
||||
// get the user
|
||||
@ -1178,9 +1196,8 @@ export function getUser(uri){
|
||||
.then(
|
||||
response => {
|
||||
dispatch({
|
||||
type: 'USER_LOADED',
|
||||
key: response.uri,
|
||||
user: response
|
||||
type: 'USERS_LOADED',
|
||||
users: [response]
|
||||
});
|
||||
},
|
||||
error => {
|
||||
@ -1191,7 +1208,9 @@ export function getUser(uri){
|
||||
}
|
||||
)
|
||||
|
||||
dispatch(getUserPlaylists(uri))
|
||||
if (and_playlists){
|
||||
dispatch(getUserPlaylists(uri));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1221,14 +1240,11 @@ export function getUserPlaylists(user_uri){
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: 'PLAYLISTS_LOADED',
|
||||
playlists: playlists
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: 'SPOTIFY_USER_PLAYLISTS_LOADED',
|
||||
key: user_uri,
|
||||
data: response
|
||||
type: 'LOADED_MORE',
|
||||
parent_type: 'user',
|
||||
parent_key: user_uri,
|
||||
records_type: 'playlist',
|
||||
records_data: response
|
||||
});
|
||||
},
|
||||
error => {
|
||||
|
||||
@ -27,10 +27,7 @@ const SpotifyMiddleware = (function(){
|
||||
store.dispatch(spotifyActions.getAllLibraryPlaylists())
|
||||
}
|
||||
|
||||
// Get the current logged-in user
|
||||
store.dispatch(spotifyActions.getMe())
|
||||
|
||||
next(action)
|
||||
next(action);
|
||||
break
|
||||
|
||||
case 'SPOTIFY_AUTHORIZATION_GRANTED':
|
||||
@ -416,10 +413,9 @@ const SpotifyMiddleware = (function(){
|
||||
ReactGA.event({category: 'Spotify', action: 'Authorization verified', label: action.data.id});
|
||||
|
||||
store.dispatch({
|
||||
type: 'USER_LOADED',
|
||||
key: action.data.uri,
|
||||
user: action.data
|
||||
})
|
||||
type: 'USERS_LOADED',
|
||||
users: [action.data]
|
||||
});
|
||||
|
||||
next(action);
|
||||
break;
|
||||
|
||||
@ -85,7 +85,14 @@ class Album extends React.Component{
|
||||
}
|
||||
|
||||
loadMore(){
|
||||
this.props.spotifyActions.getURL(this.props.album.tracks_more, 'SPOTIFY_ALBUM_LOADED_MORE' );
|
||||
this.props.spotifyActions.loadMore(
|
||||
this.props.album.tracks_more,
|
||||
{
|
||||
parent_type: 'album',
|
||||
parent_key: this.props.album.uri,
|
||||
records_type: 'track'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
play(){
|
||||
@ -120,6 +127,16 @@ class Album extends React.Component{
|
||||
}
|
||||
}
|
||||
|
||||
var tracks = [];
|
||||
if (this.props.album.tracks_uris && this.props.tracks){
|
||||
for (var i = 0; i < this.props.album.tracks_uris.length; i++){
|
||||
var uri = this.props.album.tracks_uris[i]
|
||||
if (this.props.tracks.hasOwnProperty(uri)){
|
||||
tracks.push(this.props.tracks[uri])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="view album-view content-wrapper">
|
||||
<div className="thumbnail-wrapper">
|
||||
@ -135,8 +152,7 @@ class Album extends React.Component{
|
||||
{ !this.props.slim_mode && artists.length > 0 ? <li><ArtistSentence artists={artists} /></li> : null }
|
||||
{ this.props.album.release_date ? <li><Dater type="date" data={ this.props.album.release_date } /></li> : null }
|
||||
<li>
|
||||
{ this.props.album.tracks_total ? this.props.album.tracks_total : '0' } tracks,
|
||||
{ this.props.album.tracks ? <Dater type="total-time" data={this.props.album.tracks} /> : '0 mins' }
|
||||
{tracks ? <span>{tracks.length} tracks, <Dater type="total-time" data={tracks} /></span> : '0 tracks, 0 mins' }
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -148,7 +164,7 @@ class Album extends React.Component{
|
||||
</div>
|
||||
|
||||
<section className="list-wrapper">
|
||||
{ this.props.album.tracks ? <TrackList className="album-track-list" tracks={ this.props.album.tracks } uri={this.props.params.uri} /> : null }
|
||||
<TrackList className="album-track-list" tracks={tracks} uri={this.props.params.uri} />
|
||||
<LazyLoadListener loading={this.props.album.tracks_more} loadMore={ () => this.loadMore() }/>
|
||||
</section>
|
||||
|
||||
@ -169,6 +185,7 @@ const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
slim_mode: state.ui.slim_mode,
|
||||
load_queue: state.ui.load_queue,
|
||||
tracks: state.core.tracks,
|
||||
artists: state.core.artists,
|
||||
album: (state.core.albums && state.core.albums[uri] !== undefined ? state.core.albums[uri] : false ),
|
||||
albums: state.core.albums,
|
||||
|
||||
@ -78,7 +78,14 @@ class Artist extends React.Component{
|
||||
}
|
||||
|
||||
loadMore(){
|
||||
this.props.spotifyActions.getURL(this.props.artist.albums_more, 'SPOTIFY_ARTIST_ALBUMS_LOADED', this.props.params.uri );
|
||||
this.props.spotifyActions.loadMore(
|
||||
this.props.artist.albums_more,
|
||||
{
|
||||
parent_type: 'artist',
|
||||
parent_key: this.props.params.uri,
|
||||
records_type: 'album'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
inLibrary(){
|
||||
@ -161,12 +168,23 @@ class Artist extends React.Component{
|
||||
)
|
||||
|
||||
default:
|
||||
|
||||
var tracks = [];
|
||||
if (this.props.artist.tracks_uris && this.props.tracks){
|
||||
for (var i = 0; i < this.props.artist.tracks_uris.length; i++){
|
||||
var uri = this.props.artist.tracks_uris[i]
|
||||
if (this.props.tracks.hasOwnProperty(uri)){
|
||||
tracks.push(this.props.tracks[uri])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="body overview">
|
||||
<div className={related_artists.length > 0 ? "col w70" : "col w100"}>
|
||||
<h4>Top tracks</h4>
|
||||
<div className="list-wrapper">
|
||||
{ this.props.artist.tracks ? <TrackList className="artist-track-list" uri={this.props.params.uri} tracks={this.props.artist.tracks} /> : null }
|
||||
<TrackList className="artist-track-list" uri={this.props.params.uri} tracks={tracks} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -179,7 +197,7 @@ class Artist extends React.Component{
|
||||
<h4>Albums</h4>
|
||||
<section className="grid-wrapper no-top-padding">
|
||||
<AlbumGrid albums={albums} />
|
||||
<LazyLoadListener loading={this.props.artist.albums_more} loadMore={ () => this.loadMore() }/>
|
||||
<LazyLoadListener loading={this.props.artist.albums_more} loadMore={() => this.loadMore()} />
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
@ -263,8 +281,9 @@ const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
slim_mode: state.ui.slim_mode,
|
||||
load_queue: state.ui.load_queue,
|
||||
artist: (state.core.artists && state.core.artists[uri] !== undefined ? state.core.artists[uri] : false),
|
||||
artists: (state.core.artists ? state.core.artists : []),
|
||||
artist: (state.core.artists[uri] !== undefined ? state.core.artists[uri] : false),
|
||||
tracks: state.core.tracks,
|
||||
artists: state.core.artists,
|
||||
spotify_library_artists: state.spotify.library_artists,
|
||||
local_library_artists: state.mopidy.library_artists,
|
||||
albums: (state.core.albums ? state.core.albums : []),
|
||||
|
||||
@ -14,6 +14,7 @@ import LazyLoadListener from '../components/LazyLoadListener'
|
||||
import FollowButton from '../components/FollowButton'
|
||||
import Header from '../components/Header'
|
||||
import ContextMenuTrigger from '../components/ContextMenuTrigger'
|
||||
import URILink from '../components/URILink'
|
||||
|
||||
import * as helpers from '../helpers'
|
||||
import * as coreActions from '../services/core/actions'
|
||||
@ -74,7 +75,14 @@ class Playlist extends React.Component{
|
||||
}
|
||||
|
||||
loadMore(){
|
||||
this.props.spotifyActions.getURL(this.props.playlist.tracks_more, 'PLAYLIST_LOADED_MORE_TRACKS', this.props.playlist.uri );
|
||||
this.props.spotifyActions.loadMore(
|
||||
this.props.playlist.tracks_more,
|
||||
{
|
||||
parent_type: 'playlist',
|
||||
parent_key: this.props.playlist.uri,
|
||||
records_type: 'track'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
play(){
|
||||
@ -94,20 +102,20 @@ class Playlist extends React.Component{
|
||||
|
||||
// TODO: Once deletion occurs, remove playlist from global playlists list
|
||||
delete(){
|
||||
this.props.mopidyActions.deletePlaylist(this.props.playlist.uri )
|
||||
this.props.mopidyActions.deletePlaylist(this.props.playlist.uri);
|
||||
}
|
||||
|
||||
reorderTracks(indexes, index){
|
||||
this.props.coreActions.reorderPlaylistTracks(this.props.playlist.uri, indexes, index, this.props.playlist.snapshot_id )
|
||||
this.props.coreActions.reorderPlaylistTracks(this.props.playlist.uri, indexes, index, this.props.playlist.snapshot_id);
|
||||
}
|
||||
|
||||
removeTracks(tracks_indexes){
|
||||
this.props.coreActions.removeTracksFromPlaylist(this.props.playlist.uri, tracks_indexes )
|
||||
this.props.coreActions.removeTracksFromPlaylist(this.props.playlist.uri, tracks_indexes);
|
||||
}
|
||||
|
||||
inLibrary(){
|
||||
var library = helpers.uriSource(this.props.params.uri)+'_library_playlists'
|
||||
return (this.props[library] && this.props[library].indexOf(this.props.params.uri) > -1)
|
||||
var library = helpers.uriSource(this.props.params.uri)+'_library_playlists';
|
||||
return (this.props[library] && this.props[library].indexOf(this.props.params.uri) > -1);
|
||||
}
|
||||
|
||||
renderActions(){
|
||||
@ -167,6 +175,16 @@ class Playlist extends React.Component{
|
||||
)
|
||||
}
|
||||
|
||||
var tracks = [];
|
||||
if (this.props.playlist.tracks_uris && this.props.tracks){
|
||||
for (var i = 0; i < this.props.playlist.tracks_uris.length; i++){
|
||||
var uri = this.props.playlist.tracks_uris[i]
|
||||
if (this.props.tracks.hasOwnProperty(uri)){
|
||||
tracks.push(this.props.tracks[uri])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="view playlist-view content-wrapper">
|
||||
<div className="thumbnail-wrapper">
|
||||
@ -178,13 +196,13 @@ class Playlist extends React.Component{
|
||||
{ this.props.playlist.description ? <h2 className="description grey-text" dangerouslySetInnerHTML={{__html: this.props.playlist.description}}></h2> : null }
|
||||
|
||||
<ul className="details">
|
||||
{ !this.props.slim_mode ? <li className="has-tooltip"><FontAwesome name={helpers.sourceIcon(this.props.params.uri )} /><span className="tooltip">{helpers.uriSource(this.props.params.uri )} playlist</span></li> : null }
|
||||
{ this.props.playlist.owner && !this.props.slim_mode ? <li><Link to={'/user/'+this.props.playlist.owner.uri}>{this.props.playlist.owner.id}</Link></li> : null }
|
||||
{ !this.props.slim_mode ? <li className="has-tooltip"><FontAwesome name={helpers.sourceIcon(this.props.params.uri )} /><span className="tooltip">{helpers.uriSource(this.props.params.uri)} playlist</span></li> : null }
|
||||
{ this.props.playlist.owner && !this.props.slim_mode ? <li><URILink type="user" uri={this.props.playlist.owner.uri}>{this.props.playlist.owner.id}</URILink></li> : null }
|
||||
{ this.props.playlist.followers ? <li>{this.props.playlist.followers.total.toLocaleString()} followers</li> : null }
|
||||
{ this.props.playlist.last_modified ? <li>Edited <Dater type="ago" data={this.props.playlist.last_modified} /></li> : null }
|
||||
<li>
|
||||
{ this.props.playlist.tracks_total ? this.props.playlist.tracks_total : '0'} tracks,
|
||||
{ this.props.playlist.tracks ? <Dater type="total-time" data={this.props.playlist.tracks} /> : '0 mins' }
|
||||
{ this.props.playlist.tracks_total ? this.props.playlist.tracks_total : tracks.length} tracks,
|
||||
<Dater type="total-time" data={tracks} />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -192,7 +210,7 @@ class Playlist extends React.Component{
|
||||
{ this.renderActions() }
|
||||
|
||||
<section className="list-wrapper">
|
||||
{ this.props.playlist.tracks ? <TrackList uri={this.props.params.uri} className="playlist-track-list" context={context} tracks={this.props.playlist.tracks} removeTracks={ tracks_indexes => this.removeTracks(tracks_indexes) } reorderTracks={ (indexes, index) => this.reorderTracks(indexes, index) } /> : null }
|
||||
<TrackList uri={this.props.params.uri} className="playlist-track-list" context={context} tracks={tracks} removeTracks={ tracks_indexes => this.removeTracks(tracks_indexes) } reorderTracks={ (indexes, index) => this.reorderTracks(indexes, index) } />
|
||||
<LazyLoadListener loading={this.props.playlist.tracks_more} loadMore={ () => this.loadMore() }/>
|
||||
</section>
|
||||
</div>
|
||||
@ -212,7 +230,8 @@ const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
slim_mode: state.ui.slim_mode,
|
||||
load_queue: state.ui.load_queue,
|
||||
playlist: (state.core.playlists && state.core.playlists[uri] !== undefined ? state.core.playlists[uri] : false ),
|
||||
tracks: state.core.tracks,
|
||||
playlist: (state.core.playlists[uri] !== undefined ? state.core.playlists[uri] : false ),
|
||||
spotify_library_playlists: state.spotify.library_playlists,
|
||||
local_library_playlists: state.mopidy.library_playlists,
|
||||
mopidy_connected: state.mopidy.connected,
|
||||
|
||||
@ -92,6 +92,31 @@ class Queue extends React.Component{
|
||||
}
|
||||
}
|
||||
|
||||
var tracks = [];
|
||||
if (this.props.queue && this.props.tracks){
|
||||
for (var i = 0; i < this.props.queue.length; i++){
|
||||
var uri = this.props.queue[i];
|
||||
if (this.props.tracks.hasOwnProperty(uri)){
|
||||
var track = this.props.tracks[uri];
|
||||
track.playing = (track.uri == this.props.current_track_uri);
|
||||
tracks.push(track);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge our metadata with each track
|
||||
for (var i = 0; i < tracks.length; i++){
|
||||
var track = tracks[i];
|
||||
if (this.props.queue_metadata["tlid_"+track.tlid] !== undefined){
|
||||
track = Object.assign(
|
||||
{},
|
||||
track,
|
||||
this.props.queue_metadata["tlid_"+track.tlid]
|
||||
);
|
||||
tracks[i] = track;
|
||||
}
|
||||
}
|
||||
|
||||
var options = (
|
||||
<span>
|
||||
{this.props.spotify_enabled ? <button className="no-hover" onClick={e => this.props.uiActions.openModal('edit_radio')}>
|
||||
@ -133,11 +158,11 @@ class Queue extends React.Component{
|
||||
show_source_icon={true}
|
||||
context="queue"
|
||||
className="queue-track-list"
|
||||
tracks={this.props.current_tracklist}
|
||||
removeTracks={tracks => this.removeTracks(tracks )}
|
||||
playTracks={tracks => this.playTracks(tracks )}
|
||||
playTrack={track => this.playTrack(track )}
|
||||
reorderTracks={(indexes, index) => this.reorderTracks(indexes, index) } />
|
||||
tracks={tracks}
|
||||
removeTracks={tracks => this.removeTracks(tracks)}
|
||||
playTracks={tracks => this.playTracks(tracks)}
|
||||
playTrack={track => this.playTrack(track)}
|
||||
reorderTracks={(indexes, index) => this.reorderTracks(indexes, index)} />
|
||||
</section>
|
||||
|
||||
</div>
|
||||
@ -158,8 +183,11 @@ const mapStateToProps = (state, ownProps) => {
|
||||
spotify_enabled: state.spotify.enabled,
|
||||
radio: state.core.radio,
|
||||
radio_enabled: (state.core.radio && state.core.radio.enabled ? true : false),
|
||||
current_tracklist: state.core.current_tracklist,
|
||||
current_track: (state.core.current_track !== undefined && state.core.tracks !== undefined && state.core.tracks[state.core.current_track] !== undefined ? state.core.tracks[state.core.current_track] : null)
|
||||
tracks: state.core.tracks,
|
||||
queue: state.core.queue,
|
||||
queue_metadata: state.core.queue_metadata,
|
||||
current_track_uri: state.core.current_track_uri,
|
||||
current_track: (state.core.tracks[state.core.current_track_uri] !== undefined ? state.core.tracks[state.core.current_track_uri] : null)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import { bindActionCreators } from 'redux'
|
||||
import FontAwesome from 'react-fontawesome'
|
||||
|
||||
import SpotifyAuthenticationFrame from '../components/SpotifyAuthenticationFrame'
|
||||
import LastfmAuthenticationFrame from '../components/LastfmAuthenticationFrame'
|
||||
import ConfirmationButton from '../components/ConfirmationButton'
|
||||
import PusherConnectionList from '../components/PusherConnectionList'
|
||||
import URISchemesList from '../components/URISchemesList'
|
||||
@ -14,11 +15,13 @@ import Header from '../components/Header'
|
||||
import Parallax from '../components/Parallax'
|
||||
import Icon from '../components/Icon'
|
||||
import Thumbnail from '../components/Thumbnail'
|
||||
import URILink from '../components/URILink'
|
||||
|
||||
import * as coreActions from '../services/core/actions'
|
||||
import * as uiActions from '../services/ui/actions'
|
||||
import * as pusherActions from '../services/pusher/actions'
|
||||
import * as mopidyActions from '../services/mopidy/actions'
|
||||
import * as lastfmActions from '../services/lastfm/actions'
|
||||
import * as spotifyActions from '../services/spotify/actions'
|
||||
|
||||
class Settings extends React.Component {
|
||||
@ -108,21 +111,46 @@ class Settings extends React.Component {
|
||||
|
||||
if (user){
|
||||
return (
|
||||
<Link className="user" to={global.baseURL+'user/'+user.uri}>
|
||||
<URILink className="user" type="user" uri={user.uri}>
|
||||
<Thumbnail circle={true} size="small" images={user.images} />
|
||||
<span className="user-name">
|
||||
{user.display_name ? user.display_name : user.id}
|
||||
{!this.props.spotify.authorization ? <span className="grey-text"> (Limited access)</span> : null}
|
||||
</span>
|
||||
</Link>
|
||||
</URILink>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Link className="user">
|
||||
<URILink className="user">
|
||||
<Thumbnail circle={true} size="small" />
|
||||
<span className="user-name">
|
||||
Unknown
|
||||
</span>
|
||||
</Link>
|
||||
</URILink>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
renderLastfmUser(){
|
||||
var user = this.props.core.users["lastfm:user:"+this.props.lastfm.session.name];
|
||||
|
||||
if (user){
|
||||
return (
|
||||
<URILink className="user" type="user" uri={user.uri}>
|
||||
<Thumbnail circle={true} size="small" images={user.image} />
|
||||
<span className="user-name">
|
||||
{user.realname ? user.realname : user.name}
|
||||
</span>
|
||||
</URILink>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<URILink className="user" type="user" uri={false}>
|
||||
<Thumbnail circle={true} size="small" />
|
||||
<span className="user-name">
|
||||
Unknown
|
||||
</span>
|
||||
</URILink>
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -138,13 +166,11 @@ class Settings extends React.Component {
|
||||
}
|
||||
|
||||
renderServiceStatus(service){
|
||||
|
||||
let colour = 'red'
|
||||
let icon = 'close'
|
||||
let name = service.charAt(0).toUpperCase() + service.slice(1).toLowerCase()
|
||||
let text = 'Disconnected'
|
||||
let tooltip = null
|
||||
|
||||
service = this.props[service]
|
||||
|
||||
if (service.connecting){
|
||||
@ -156,11 +182,6 @@ class Settings extends React.Component {
|
||||
colour = 'red'
|
||||
text = 'Not installed'
|
||||
tooltip = 'Mopidy-Spotify is not installed or enabled'
|
||||
} else if (service.connected && name == 'Spotify' && !service.authorization){
|
||||
icon = 'lock'
|
||||
colour = 'orange'
|
||||
text = 'Limited access'
|
||||
tooltip = 'Authorize Iris for full Spotify functionality'
|
||||
} else if (service.connected){
|
||||
icon = 'check'
|
||||
colour = 'green'
|
||||
@ -211,10 +232,13 @@ class Settings extends React.Component {
|
||||
|
||||
<section className="content-wrapper">
|
||||
|
||||
<h4 className="underline">Services</h4>
|
||||
|
||||
<div className="services">
|
||||
{this.renderServiceStatus('mopidy')}
|
||||
{this.renderServiceStatus('pusher')}
|
||||
{this.renderServiceStatus('spotify')}
|
||||
{this.renderServiceStatus('lastfm')}
|
||||
</div>
|
||||
|
||||
<h4 className="underline">System</h4>
|
||||
@ -301,6 +325,7 @@ class Settings extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<div className="name">Authorization</div>
|
||||
<div className="input">
|
||||
@ -310,6 +335,24 @@ class Settings extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 className="underline">LastFM</h4>
|
||||
|
||||
{this.props.lastfm.session ? <div className="field current-user">
|
||||
<div className="name">Current user</div>
|
||||
<div className="input">
|
||||
<div className="text">
|
||||
{ this.renderLastfmUser() }
|
||||
</div>
|
||||
</div>
|
||||
</div> : null}
|
||||
|
||||
<div className="field">
|
||||
<div className="name">Authorization</div>
|
||||
<div className="input">
|
||||
<LastfmAuthenticationFrame />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 className="underline">Advanced</h4>
|
||||
|
||||
<div className="field checkbox">
|
||||
@ -398,6 +441,7 @@ const mapDispatchToProps = (dispatch) => {
|
||||
uiActions: bindActionCreators(uiActions, dispatch),
|
||||
pusherActions: bindActionCreators(pusherActions, dispatch),
|
||||
mopidyActions: bindActionCreators(mopidyActions, dispatch),
|
||||
lastfmActions: bindActionCreators(lastfmActions, dispatch),
|
||||
spotifyActions: bindActionCreators(spotifyActions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import Thumbnail from '../components/Thumbnail'
|
||||
import ArtistSentence from '../components/ArtistSentence'
|
||||
import ArtistGrid from '../components/ArtistGrid'
|
||||
import FollowButton from '../components/FollowButton'
|
||||
import LastfmLoveButton from '../components/LastfmLoveButton'
|
||||
import Dater from '../components/Dater'
|
||||
import LazyLoadListener from '../components/LazyLoadListener'
|
||||
import ContextMenuTrigger from '../components/ContextMenuTrigger'
|
||||
@ -19,6 +20,7 @@ import * as helpers from '../helpers'
|
||||
import * as uiActions from '../services/ui/actions'
|
||||
import * as mopidyActions from '../services/mopidy/actions'
|
||||
import * as spotifyActions from '../services/spotify/actions'
|
||||
import * as lastfmActions from '../services/lastfm/actions'
|
||||
import * as geniusActions from '../services/genius/actions'
|
||||
|
||||
class Track extends React.Component{
|
||||
@ -39,20 +41,29 @@ class Track extends React.Component{
|
||||
|
||||
componentWillReceiveProps(nextProps){
|
||||
|
||||
// if our URI has changed, fetch new album
|
||||
// if our URI has changed, fetch new track
|
||||
if (nextProps.params.uri != this.props.params.uri){
|
||||
this.loadTrack(nextProps )
|
||||
this.loadTrack(nextProps)
|
||||
|
||||
// if mopidy has just connected AND we're a local album, go get
|
||||
// if mopidy has just connected AND we're not a Spotify track, go get
|
||||
} else if (!this.props.mopidy_connected && nextProps.mopidy_connected){
|
||||
if (helpers.uriSource(this.props.params.uri ) != 'spotify'){
|
||||
if (helpers.uriSource(this.props.params.uri) != 'spotify'){
|
||||
this.loadTrack(nextProps);
|
||||
}
|
||||
}
|
||||
|
||||
// We don't have lyrics, and we have just received our artists
|
||||
if (!nextProps.track.lyrics_results && !this.props.track.artists && nextProps.track.artists){
|
||||
this.props.geniusActions.findTrackLyrics(nextProps.track);
|
||||
// We have just received our full track info (with artists)
|
||||
if (!this.props.track.artists && nextProps.track.artists){
|
||||
|
||||
// Ready to load LastFM
|
||||
if (nextProps.lastfm_authorized){
|
||||
this.props.lastfmActions.getTrack(nextProps.track);
|
||||
}
|
||||
|
||||
// Ready to load lyrics
|
||||
if (!nextProps.track.lyrics_results){
|
||||
this.props.geniusActions.findTrackLyrics(nextProps.track);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,9 +100,18 @@ class Track extends React.Component{
|
||||
break;
|
||||
}
|
||||
|
||||
// We don't have lyrics, but the track (and artists) is already loaded
|
||||
if (props.track && !props.track.lyrics_results && props.track.artists){
|
||||
this.props.geniusActions.findTrackLyrics(props.track);
|
||||
// We have artist info already
|
||||
if (props.track && props.track.artists){
|
||||
|
||||
// Get the LastFM version of this track (provided we have artist info)
|
||||
if (props.lastfm_authorized){
|
||||
this.props.lastfmActions.getTrack(props.track);
|
||||
}
|
||||
|
||||
// Ready for lyrics
|
||||
if (props.track && !props.track.lyrics_results){
|
||||
this.props.geniusActions.findTrackLyrics(props.track);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -211,7 +231,8 @@ class Track extends React.Component{
|
||||
|
||||
<div className="actions">
|
||||
<button className="primary" onClick={e => this.play()}>Play</button>
|
||||
{this.props.slim_mode ? null : <ContextMenuTrigger onTrigger={e => this.handleContextMenu(e)} />}
|
||||
<LastfmLoveButton uri={this.props.params.uri} artist={this.props.track.artists[0].name} track={this.props.track.name} addText="Love" removeText="Unlove" is_loved={this.props.track.userloved} />
|
||||
<ContextMenuTrigger onTrigger={e => this.handleContextMenu(e)} />
|
||||
</div>
|
||||
|
||||
{this.renderLyricsSelector()}
|
||||
@ -240,6 +261,7 @@ const mapStateToProps = (state, ownProps) => {
|
||||
albums: state.core.albums,
|
||||
spotify_library_albums: state.spotify.library_albums,
|
||||
local_library_albums: state.mopidy.library_albums,
|
||||
lastfm_authorized: state.lastfm.session,
|
||||
spotify_authorized: state.spotify.authorization,
|
||||
mopidy_connected: state.mopidy.connected
|
||||
};
|
||||
@ -250,6 +272,7 @@ const mapDispatchToProps = (dispatch) => {
|
||||
uiActions: bindActionCreators(uiActions, dispatch),
|
||||
uiActions: bindActionCreators(uiActions, dispatch),
|
||||
mopidyActions: bindActionCreators(mopidyActions, dispatch),
|
||||
lastfmActions: bindActionCreators(lastfmActions, dispatch),
|
||||
spotifyActions: bindActionCreators(spotifyActions, dispatch),
|
||||
geniusActions: bindActionCreators(geniusActions, dispatch)
|
||||
}
|
||||
|
||||
@ -33,18 +33,20 @@ class User extends React.Component{
|
||||
|
||||
loadUser(props = this.props){
|
||||
if (!props.user){
|
||||
this.props.spotifyActions.getUser(props.params.uri);
|
||||
this.props.spotifyActions.getUser(props.params.uri, true);
|
||||
this.props.spotifyActions.following(props.params.uri);
|
||||
}
|
||||
|
||||
// We got a user, but we haven't fetched their playlists yet
|
||||
if (props.user && !props.user.playlists_uris){
|
||||
this.props.spotifyActions.getUserPlaylists(props.params.uri);
|
||||
}
|
||||
}
|
||||
|
||||
loadMore(){
|
||||
this.props.spotifyActions.getURL(this.props.user.playlists_more, 'SPOTIFY_USER_PLAYLISTS_LOADED', this.props.params.uri);
|
||||
this.props.spotifyActions.loadMore(
|
||||
this.props.user.playlists_more,
|
||||
{
|
||||
parent_type: 'user',
|
||||
parent_key: this.props.params.uri,
|
||||
records_type: 'playlist'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
isMe(){
|
||||
@ -105,7 +107,7 @@ class User extends React.Component{
|
||||
<section className="grid-wrapper">
|
||||
<h4>Playlists</h4>
|
||||
<PlaylistGrid playlists={playlists} />
|
||||
<LazyLoadListener enabled={this.props.user.playlists_more} loadMore={ () => this.loadMore() }/>
|
||||
<LazyLoadListener loading={this.props.user.playlists_more} loadMore={() => this.loadMore()} />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@ -120,8 +122,7 @@ const mapStateToProps = (state, ownProps) => {
|
||||
spotify_authorized: state.spotify.authorization,
|
||||
me: state.spotify.me,
|
||||
playlists: state.core.playlists,
|
||||
user: (state.core.users && state.core.users[uri] !== undefined ? state.core.users[uri] : false),
|
||||
users: state.core.users
|
||||
user: (state.core.users[uri] !== undefined ? state.core.users[uri] : false)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -30,7 +30,14 @@ class DiscoverCategory extends React.Component{
|
||||
}
|
||||
|
||||
loadMore(){
|
||||
this.props.spotifyActions.getURL(this.props.category.playlists_more, 'SPOTIFY_CATEGORY_PLAYLISTS_LOADED', 'category:'+this.props.params.id );
|
||||
this.props.spotifyActions.loadMore(
|
||||
this.props.category.playlists_more,
|
||||
null,
|
||||
{
|
||||
type: 'SPOTIFY_CATEGORY_PLAYLISTS_LOADED',
|
||||
key: 'category:'+this.props.params.id
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render(){
|
||||
|
||||
@ -27,7 +27,14 @@ class DiscoverNewReleases extends React.Component{
|
||||
}
|
||||
|
||||
loadMore(){
|
||||
this.props.spotifyActions.getURL(this.props.new_releases_more, 'SPOTIFY_NEW_RELEASES_LOADED');
|
||||
this.props.spotifyActions.loadMore(
|
||||
this.props.new_releases_more,
|
||||
null,
|
||||
{
|
||||
type: 'SPOTIFY_NEW_RELEASES_LOADED',
|
||||
key: null
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
playAlbum(e,album){
|
||||
|
||||
Reference in New Issue
Block a user