2016-12-31 10:46:59 -05:00
/*
* Copyright ( C ) 2016 Espen Jürgensen < espenjurgensen @ gmail . com >
* Copyright ( C ) 2016 Christian Meffert < christian . meffert @ googlemail . com >
*
* This program is free software ; you can redistribute it and / or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation ; either version 2 of the License , or
* ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
*
* You should have received a copy of the GNU General Public License
* along with this program ; if not , write to the Free Software
* Foundation , Inc . , 59 Temple Place , Suite 330 , Boston , MA 02111 - 1307 USA
*/
# include "spotify_webapi.h"
# include <event2/event.h>
2017-01-13 12:28:36 -05:00
# include <json.h>
2016-12-31 10:46:59 -05:00
# include <stddef.h>
# include <stdio.h>
# include <string.h>
# include <time.h>
2018-11-28 14:31:50 -05:00
# include "artwork.h"
2018-03-02 13:24:35 -05:00
# include "cache.h"
# include "conffile.h"
2016-12-31 10:46:59 -05:00
# include "db.h"
# include "http.h"
# include "library.h"
2018-03-02 13:24:35 -05:00
# include "listener.h"
2016-12-31 10:46:59 -05:00
# include "logger.h"
2017-08-27 02:44:00 -04:00
# include "misc_json.h"
2018-03-02 13:24:35 -05:00
# include "spotify.h"
2016-12-31 10:46:59 -05:00
2018-03-02 13:24:35 -05:00
struct spotify_album
{
const char * added_at ;
time_t mtime ;
const char * album_type ;
bool is_compilation ;
const char * artist ;
const char * genre ;
const char * id ;
const char * label ;
const char * name ;
const char * release_date ;
const char * release_date_precision ;
int release_year ;
const char * uri ;
2018-11-28 14:31:50 -05:00
const char * artwork_url ;
2018-03-02 13:24:35 -05:00
} ;
struct spotify_track
{
const char * added_at ;
time_t mtime ;
const char * album ;
const char * album_artist ;
const char * artist ;
int disc_number ;
const char * album_type ;
bool is_compilation ;
int duration_ms ;
const char * id ;
const char * name ;
int track_number ;
const char * uri ;
2018-11-28 14:31:50 -05:00
const char * artwork_url ;
2018-03-02 13:24:35 -05:00
bool is_playable ;
const char * restrictions ;
const char * linked_from_uri ;
} ;
struct spotify_playlist
{
const char * id ;
const char * name ;
const char * owner ;
const char * uri ;
const char * href ;
const char * tracks_href ;
int tracks_count ;
} ;
2016-12-31 10:46:59 -05:00
// Credentials for the web api
static char * spotify_access_token ;
static char * spotify_refresh_token ;
2019-08-03 06:55:53 -04:00
static char * spotify_granted_scope ;
2017-02-05 07:56:12 -05:00
static char * spotify_user_country ;
2017-08-19 02:36:43 -04:00
static char * spotify_user ;
2016-12-31 10:46:59 -05:00
static int32_t expires_in = 3600 ;
static time_t token_requested = 0 ;
2018-03-09 11:16:36 -05:00
// Mutex to avoid conflicting requests for access tokens and protects accessing the credentials from different threads
2018-03-02 13:24:35 -05:00
static pthread_mutex_t token_lck ;
// The base playlist id for all Spotify playlists in the db
static int spotify_base_plid ;
// The base playlist id for Spotify saved tracks in the db
static int spotify_saved_plid ;
// Flag to avoid triggering playlist change events while the (re)scan is running
static bool scanning ;
2016-12-31 10:46:59 -05:00
// Endpoints and credentials for the web api
static const char * spotify_client_id = " 0e684a5422384114a8ae7ac020f01789 " ;
static const char * spotify_client_secret = " 232af95f39014c9ba218285a5c11a239 " ;
2019-08-03 06:55:53 -04:00
static const char * spotify_scope = " playlist-read-private playlist-read-collaborative user-library-read user-read-private " ;
2016-12-31 10:46:59 -05:00
static const char * spotify_auth_uri = " https://accounts.spotify.com/authorize " ;
static const char * spotify_token_uri = " https://accounts.spotify.com/api/token " ;
2019-08-03 06:55:53 -04:00
2019-02-20 03:30:02 -05:00
static const char * spotify_playlist_uri = " https://api.spotify.com/v1/playlists/%s " ;
2018-03-09 13:21:58 -05:00
static const char * spotify_track_uri = " https://api.spotify.com/v1/tracks/%s " ;
2017-02-05 07:56:12 -05:00
static const char * spotify_me_uri = " https://api.spotify.com/v1/me " ;
2018-03-02 13:24:35 -05:00
static const char * spotify_albums_uri = " https://api.spotify.com/v1/me/albums?limit=50 " ;
2018-03-09 13:21:58 -05:00
static const char * spotify_album_uri = " https://api.spotify.com/v1/albums/%s " ;
static const char * spotify_album_tracks_uri = " https://api.spotify.com/v1/albums/%s/tracks " ;
2018-03-02 13:24:35 -05:00
static const char * spotify_playlists_uri = " https://api.spotify.com/v1/me/playlists?limit=50 " ;
2019-02-20 03:30:02 -05:00
static const char * spotify_playlist_tracks_uri = " https://api.spotify.com/v1/playlists/%s/tracks " ;
2019-02-20 04:10:53 -05:00
static const char * spotify_artist_albums_uri = " https://api.spotify.com/v1/artists/%s/albums?include_groups=album,single " ;
2018-03-02 13:24:35 -05:00
2016-12-31 10:46:59 -05:00
static void
2017-01-20 10:31:33 -05:00
free_http_client_ctx ( struct http_client_ctx * ctx )
2016-12-31 10:46:59 -05:00
{
if ( ! ctx )
return ;
if ( ctx - > input_body )
evbuffer_free ( ctx - > input_body ) ;
if ( ctx - > output_headers )
{
keyval_clear ( ctx - > output_headers ) ;
free ( ctx - > output_headers ) ;
}
free ( ctx ) ;
}
2018-03-09 11:16:36 -05:00
static bool
token_valid ( void )
{
return spotify_access_token ! = NULL ;
}
2017-01-06 11:08:30 -05:00
static int
2018-03-09 11:16:36 -05:00
request_access_tokens ( struct keyval * kv , const char * * err )
2016-12-31 10:46:59 -05:00
{
2018-03-09 11:16:36 -05:00
struct http_client_ctx ctx ;
char * param ;
char * body ;
json_object * haystack ;
const char * tmp ;
2016-12-31 10:46:59 -05:00
int ret ;
2018-03-09 11:16:36 -05:00
param = http_form_urlencode ( kv ) ;
if ( ! param )
2016-12-31 10:46:59 -05:00
{
2018-03-09 11:16:36 -05:00
* err = " http_form_uriencode() failed " ;
ret = - 1 ;
goto out_clear_kv ;
2016-12-31 10:46:59 -05:00
}
2018-03-09 11:16:36 -05:00
memset ( & ctx , 0 , sizeof ( struct http_client_ctx ) ) ;
ctx . url = ( char * ) spotify_token_uri ;
ctx . output_body = param ;
ctx . input_body = evbuffer_new ( ) ;
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
ret = http_client_request ( & ctx ) ;
2016-12-31 10:46:59 -05:00
if ( ret < 0 )
{
2018-03-09 11:16:36 -05:00
* err = " Did not get a reply from Spotify " ;
goto out_free_input_body ;
2016-12-31 10:46:59 -05:00
}
// 0-terminate for safety
2018-03-09 11:16:36 -05:00
evbuffer_add ( ctx . input_body , " " , 1 ) ;
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
body = ( char * ) evbuffer_pullup ( ctx . input_body , - 1 ) ;
if ( ! body | | ( strlen ( body ) = = 0 ) )
2016-12-31 10:46:59 -05:00
{
2018-03-09 11:16:36 -05:00
* err = " The reply from Spotify is empty or invalid " ;
ret = - 1 ;
goto out_free_input_body ;
2016-12-31 10:46:59 -05:00
}
2018-03-09 11:16:36 -05:00
DPRINTF ( E_DBG , L_SPOTIFY , " Token reply: %s \n " , body ) ;
2017-01-01 05:06:00 -05:00
2018-03-09 11:16:36 -05:00
haystack = json_tokener_parse ( body ) ;
if ( ! haystack )
2016-12-31 10:46:59 -05:00
{
2018-03-09 11:16:36 -05:00
* err = " JSON parser returned an error " ;
ret = - 1 ;
goto out_free_input_body ;
2016-12-31 10:46:59 -05:00
}
2018-03-09 11:16:36 -05:00
free ( spotify_access_token ) ;
spotify_access_token = NULL ;
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
tmp = jparse_str_from_obj ( haystack , " access_token " ) ;
if ( tmp )
spotify_access_token = strdup ( tmp ) ;
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
tmp = jparse_str_from_obj ( haystack , " refresh_token " ) ;
if ( tmp )
2016-12-31 10:46:59 -05:00
{
2018-03-09 11:16:36 -05:00
free ( spotify_refresh_token ) ;
spotify_refresh_token = strdup ( tmp ) ;
2016-12-31 10:46:59 -05:00
}
2019-08-03 06:55:53 -04:00
tmp = jparse_str_from_obj ( haystack , " scope " ) ;
if ( tmp )
{
free ( spotify_granted_scope ) ;
spotify_granted_scope = strdup ( tmp ) ;
}
2018-03-09 11:16:36 -05:00
expires_in = jparse_int_from_obj ( haystack , " expires_in " ) ;
if ( expires_in = = 0 )
expires_in = 3600 ;
jparse_free ( haystack ) ;
if ( ! spotify_access_token )
2016-12-31 10:46:59 -05:00
{
2018-03-09 11:16:36 -05:00
DPRINTF ( E_LOG , L_SPOTIFY , " Could not find access token in reply: %s \n " , body ) ;
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
* err = " Could not find access token in Spotify reply (see log) " ;
ret = - 1 ;
goto out_free_input_body ;
}
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
token_requested = time ( NULL ) ;
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
if ( spotify_refresh_token )
db_admin_set ( DB_ADMIN_SPOTIFY_REFRESH_TOKEN , spotify_refresh_token ) ;
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
ret = 0 ;
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
out_free_input_body :
evbuffer_free ( ctx . input_body ) ;
free ( param ) ;
out_clear_kv :
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
return ret ;
2016-12-31 10:46:59 -05:00
}
2018-03-09 11:16:36 -05:00
/*
* Request the api endpoint at ' href ' and retuns the response body as
* an allocated JSON object ( must be freed by the caller ) or NULL .
*
* @ param href The spotify endpoint uri
* @ return Response as JSON object or NULL
*/
static json_object *
request_endpoint ( const char * uri )
2016-12-31 10:46:59 -05:00
{
2018-03-09 11:16:36 -05:00
struct http_client_ctx * ctx ;
char bearer_token [ 1024 ] ;
char * response_body ;
json_object * json_response = NULL ;
int ret ;
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
ctx = calloc ( 1 , sizeof ( struct http_client_ctx ) ) ;
ctx - > output_headers = calloc ( 1 , sizeof ( struct keyval ) ) ;
ctx - > input_body = evbuffer_new ( ) ;
ctx - > url = uri ;
2017-02-05 07:56:12 -05:00
2018-03-09 11:16:36 -05:00
snprintf ( bearer_token , sizeof ( bearer_token ) , " Bearer %s " , spotify_access_token ) ;
if ( keyval_add ( ctx - > output_headers , " Authorization " , bearer_token ) < 0 )
2017-02-05 07:56:12 -05:00
{
2018-03-09 11:16:36 -05:00
DPRINTF ( E_LOG , L_SPOTIFY , " Add bearer_token to keyval failed for request '%s' \n " , uri ) ;
goto out ;
2017-02-05 07:56:12 -05:00
}
2016-12-31 10:46:59 -05:00
2018-03-18 02:47:49 -04:00
DPRINTF ( E_DBG , L_SPOTIFY , " Request Spotify API endpoint: '%s') \n " , uri ) ;
2018-03-09 11:16:36 -05:00
ret = http_client_request ( ctx ) ;
if ( ret < 0 )
2017-01-07 01:22:40 -05:00
{
2018-03-09 11:16:36 -05:00
DPRINTF ( E_LOG , L_SPOTIFY , " Request for '%s' failed \n " , uri ) ;
goto out ;
2017-01-07 01:22:40 -05:00
}
2018-03-09 11:16:36 -05:00
// 0-terminate for safety
evbuffer_add ( ctx - > input_body , " " , 1 ) ;
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
response_body = ( char * ) evbuffer_pullup ( ctx - > input_body , - 1 ) ;
if ( ! response_body | | ( strlen ( response_body ) = = 0 ) )
2016-12-31 10:46:59 -05:00
{
2018-03-09 11:16:36 -05:00
DPRINTF ( E_LOG , L_SPOTIFY , " Request for '%s' failed, response was empty \n " , uri ) ;
goto out ;
2016-12-31 10:46:59 -05:00
}
2018-03-09 11:16:36 -05:00
// DPRINTF(E_DBG, L_SPOTIFY, "Wep api response for '%s'\n%s\n", uri, response_body);
2017-01-01 05:06:00 -05:00
2018-03-09 11:16:36 -05:00
json_response = json_tokener_parse ( response_body ) ;
if ( ! json_response )
DPRINTF ( E_LOG , L_SPOTIFY , " JSON parser returned an error for '%s' \n " , uri ) ;
else
DPRINTF ( E_DBG , L_SPOTIFY , " Spotify API endpoint request: '%s' \n " , uri ) ;
2017-01-07 01:22:40 -05:00
2018-03-09 11:16:36 -05:00
out :
free_http_client_ctx ( ctx ) ;
2017-01-01 05:06:00 -05:00
2018-03-09 11:16:36 -05:00
return json_response ;
2016-12-31 10:46:59 -05:00
}
2018-03-09 11:16:36 -05:00
/*
* Request user information
*
* API endpoint : https : //api.spotify.com/v1/me
*/
2018-03-02 13:24:35 -05:00
static int
2018-03-09 11:16:36 -05:00
request_user_info ( void )
2016-12-31 10:46:59 -05:00
{
2018-03-09 11:16:36 -05:00
json_object * response ;
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
free ( spotify_user_country ) ;
spotify_user_country = NULL ;
free ( spotify_user ) ;
spotify_user = NULL ;
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
response = request_endpoint ( spotify_me_uri ) ;
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
if ( response )
2016-12-31 10:46:59 -05:00
{
2018-03-09 11:16:36 -05:00
spotify_user = safe_strdup ( jparse_str_from_obj ( response , " id " ) ) ;
spotify_user_country = safe_strdup ( jparse_str_from_obj ( response , " country " ) ) ;
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
jparse_free ( response ) ;
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
DPRINTF ( E_DBG , L_SPOTIFY , " User '%s', country '%s' \n " , spotify_user , spotify_user_country ) ;
2016-12-31 10:46:59 -05:00
}
return 0 ;
}
2018-03-09 11:16:36 -05:00
/*
* Called from the oauth callback to get a new access and refresh token
*
* @ return 0 on success , - 1 on failure
*/
2018-03-02 13:24:35 -05:00
static int
2018-03-09 11:16:36 -05:00
token_get ( const char * code , const char * redirect_uri , const char * * err )
2016-12-31 10:46:59 -05:00
{
2018-03-09 11:16:36 -05:00
struct keyval kv ;
int ret ;
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
CHECK_ERR ( L_SPOTIFY , pthread_mutex_lock ( & token_lck ) ) ;
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
* err = " " ;
memset ( & kv , 0 , sizeof ( struct keyval ) ) ;
ret = ( ( keyval_add ( & kv , " grant_type " , " authorization_code " ) = = 0 ) & &
( keyval_add ( & kv , " code " , code ) = = 0 ) & &
( keyval_add ( & kv , " client_id " , spotify_client_id ) = = 0 ) & &
( keyval_add ( & kv , " client_secret " , spotify_client_secret ) = = 0 ) & &
( keyval_add ( & kv , " redirect_uri " , redirect_uri ) = = 0 ) ) ;
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
if ( ! ret )
2016-12-31 10:46:59 -05:00
{
2018-03-09 11:16:36 -05:00
* err = " Add parameters to keyval failed " ;
ret = - 1 ;
2016-12-31 10:46:59 -05:00
}
2018-03-09 11:16:36 -05:00
else
ret = request_access_tokens ( & kv , err ) ;
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
keyval_clear ( & kv ) ;
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
if ( ret = = 0 )
request_user_info ( ) ;
CHECK_ERR ( L_SPOTIFY , pthread_mutex_unlock ( & token_lck ) ) ;
return ret ;
2016-12-31 10:46:59 -05:00
}
2018-03-09 11:16:36 -05:00
/*
* Get a new access token for the stored refresh token ( user already granted
* access to the web api )
*
* First checks if the current access token is still valid and only requests
* a new token if not .
*
* @ return 0 on success , - 1 on failure
*/
static int
token_refresh ( void )
2016-12-31 10:46:59 -05:00
{
2018-03-09 11:16:36 -05:00
struct keyval kv ;
char * refresh_token = NULL ;
const char * err ;
int ret ;
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
memset ( & kv , 0 , sizeof ( struct keyval ) ) ;
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
CHECK_ERR ( L_SPOTIFY , pthread_mutex_lock ( & token_lck ) ) ;
if ( token_requested & & difftime ( time ( NULL ) , token_requested ) < expires_in )
2016-12-31 10:46:59 -05:00
{
2018-03-09 11:16:36 -05:00
DPRINTF ( E_DBG , L_SPOTIFY , " Spotify token still valid \n " ) ;
CHECK_ERR ( L_SPOTIFY , pthread_mutex_unlock ( & token_lck ) ) ;
return 0 ;
2016-12-31 10:46:59 -05:00
}
2020-02-22 04:03:13 -05:00
ret = db_admin_get ( & refresh_token , DB_ADMIN_SPOTIFY_REFRESH_TOKEN ) ;
if ( ret < 0 )
2016-12-31 10:46:59 -05:00
{
2018-03-09 11:16:36 -05:00
DPRINTF ( E_LOG , L_SPOTIFY , " No spotify refresh token found \n " ) ;
goto error ;
2016-12-31 10:46:59 -05:00
}
2018-03-09 11:16:36 -05:00
DPRINTF ( E_DBG , L_SPOTIFY , " Spotify refresh-token: '%s' \n " , refresh_token ) ;
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
ret = ( ( keyval_add ( & kv , " grant_type " , " refresh_token " ) = = 0 ) & &
( keyval_add ( & kv , " client_id " , spotify_client_id ) = = 0 ) & &
( keyval_add ( & kv , " client_secret " , spotify_client_secret ) = = 0 ) & &
( keyval_add ( & kv , " refresh_token " , refresh_token ) = = 0 ) ) ;
if ( ! ret )
2016-12-31 10:46:59 -05:00
{
2018-03-09 11:16:36 -05:00
DPRINTF ( E_LOG , L_SPOTIFY , " Add parameters to keyval failed " ) ;
goto error ;
2016-12-31 10:46:59 -05:00
}
2018-03-09 11:16:36 -05:00
ret = request_access_tokens ( & kv , & err ) ;
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
if ( ret = = 0 )
request_user_info ( ) ;
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
free ( refresh_token ) ;
keyval_clear ( & kv ) ;
CHECK_ERR ( L_SPOTIFY , pthread_mutex_unlock ( & token_lck ) ) ;
return ret ;
error :
free ( refresh_token ) ;
keyval_clear ( & kv ) ;
CHECK_ERR ( L_SPOTIFY , pthread_mutex_unlock ( & token_lck ) ) ;
return - 1 ;
2016-12-31 10:46:59 -05:00
}
2017-01-05 16:09:19 -05:00
/*
2018-03-09 11:16:36 -05:00
* Request the api endpoint at ' href ' and retuns the response body as
* an allocated JSON object ( must be freed by the caller ) or NULL .
2017-01-05 16:09:19 -05:00
*
2018-03-09 11:16:36 -05:00
* Before making the request , the validity of the current access token
* is checked and if necessary a token refresh request is issued before
* requesting the given endpoint .
*
* @ param href The spotify endpoint uri
* @ return Response as JSON object or NULL
2017-01-05 16:09:19 -05:00
*/
2018-03-09 11:16:36 -05:00
static json_object *
request_endpoint_with_token_refresh ( const char * href )
2017-01-05 16:09:19 -05:00
{
2018-03-09 11:16:36 -05:00
if ( 0 > token_refresh ( ) )
2017-01-05 16:09:19 -05:00
{
2018-03-09 11:16:36 -05:00
return NULL ;
2017-01-05 16:09:19 -05:00
}
2018-03-09 11:16:36 -05:00
return request_endpoint ( href ) ;
2017-01-05 16:09:19 -05:00
}
2018-03-09 11:16:36 -05:00
typedef int ( * paging_request_cb ) ( void * arg ) ;
2018-03-17 07:17:15 -04:00
typedef int ( * paging_item_cb ) ( json_object * item , int index , int total , void * arg ) ;
2018-03-09 11:16:36 -05:00
/*
* Request the spotify endpoint at ' href '
*
* The endpoint must return a " paging object " e . g . :
*
* {
* " items " : [ item1 , item2 , . . . ] ,
* " limit " : 50 ,
* " next " : " {uri for the next set of items} " ,
* " offset " : 0 ,
* " total " : { total number of items } ,
* }
*
* The given callback is invoked for every item in the " items " array .
* If " next " is set in the response , after processing all items , the next uri
* is requested and the callback is invoked for every item of this request .
* The function returns after all items are processed and there is no " next "
* request .
*
* @ param endpoint_uri The endpont uri
* @ param item_cb The callback function invoked for every item
* @ param pre_request_cb Callback function invoked before each request ( optional )
* @ param post_request_cb Callback function invoked after each request ( optional )
2018-03-18 02:47:49 -04:00
* @ param with_market If TRUE appends the user country as market to the request ( applies track relinking )
2018-03-09 11:16:36 -05:00
* @ param arg User data passed to each callback
* @ return 0 on success , - 1 on failure
*/
2018-03-02 13:24:35 -05:00
static int
2018-03-18 02:47:49 -04:00
request_pagingobject_endpoint ( const char * href , paging_item_cb item_cb , paging_request_cb pre_request_cb , paging_request_cb post_request_cb , bool with_market , void * arg )
2016-12-31 10:46:59 -05:00
{
2018-03-09 11:16:36 -05:00
char * next_href ;
json_object * response ;
json_object * items ;
2016-12-31 10:46:59 -05:00
json_object * item ;
2018-03-09 11:16:36 -05:00
int count ;
int i ;
2018-03-17 07:17:15 -04:00
int offset ;
int total ;
2018-03-09 11:16:36 -05:00
int ret ;
2016-12-31 10:46:59 -05:00
2018-03-18 02:47:49 -04:00
if ( ! with_market | | ! spotify_user_country )
{
next_href = safe_strdup ( href ) ;
}
else
{
if ( strchr ( href , ' ? ' ) )
next_href = safe_asprintf ( " %s&market=%s " , href , spotify_user_country ) ;
else
next_href = safe_asprintf ( " %s?market=%s " , href , spotify_user_country ) ;
}
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
while ( next_href )
2016-12-31 10:46:59 -05:00
{
2018-03-09 11:16:36 -05:00
if ( pre_request_cb )
pre_request_cb ( arg ) ;
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
response = request_endpoint_with_token_refresh ( next_href ) ;
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
if ( ! response )
{
DPRINTF ( E_LOG , L_SPOTIFY , " Unexpected JSON: no response for paging endpoint (API endpoint: '%s') \n " , next_href ) ;
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
if ( post_request_cb )
post_request_cb ( arg ) ;
2016-12-31 10:46:59 -05:00
2018-03-09 11:16:36 -05:00
free ( next_href ) ;
return - 1 ;
}
2017-01-05 16:09:19 -05:00
2018-03-09 11:16:36 -05:00
free ( next_href ) ;
next_href = safe_strdup ( jparse_str_from_obj ( response , " next " ) ) ;
2017-01-05 16:09:19 -05:00
2018-03-17 07:17:15 -04:00
offset = jparse_int_from_obj ( response , " offset " ) ;
total = jparse_int_from_obj ( response , " total " ) ;
2018-03-09 11:16:36 -05:00
if ( jparse_array_from_obj ( response , " items " , & items ) = = 0 )
{
count = json_object_array_length ( items ) ;
for ( i = 0 ; i < count ; i + + )
{
item = json_object_array_get_idx ( items , i ) ;
if ( ! item )
{
DPRINTF ( E_LOG , L_SPOTIFY , " Unexpected JSON: no item at index %d in '%s' (API endpoint: '%s') \n " ,
i , json_object_to_json_string ( items ) , href ) ;
continue ;
}
2018-03-17 07:17:15 -04:00
ret = item_cb ( item , ( i + offset ) , total , arg ) ;
2018-03-09 11:16:36 -05:00
if ( ret < 0 )
{
DPRINTF ( E_LOG , L_SPOTIFY , " Unexpected JSON: error processing item at index %d '%s' (API endpoint: '%s') \n " ,
i , json_object_to_json_string ( item ) , href ) ;
}
}
}
2017-01-05 16:09:19 -05:00
2018-03-09 11:16:36 -05:00
if ( post_request_cb )
post_request_cb ( arg ) ;
2017-01-05 16:09:19 -05:00
2018-03-09 11:16:36 -05:00
jparse_free ( response ) ;
2017-01-05 16:09:19 -05:00
}
return 0 ;
}
2018-11-28 14:31:50 -05:00
static const char *
2019-02-05 05:35:45 -05:00
get_album_image ( json_object * jsonalbum , int max_w )
2018-11-28 14:31:50 -05:00
{
json_object * jsonimages ;
json_object * jsonimage ;
int image_count ;
int index ;
const char * artwork_url ;
artwork_url = NULL ;
2019-02-05 05:35:45 -05:00
if ( ! json_object_object_get_ex ( jsonalbum , " images " , & jsonimages ) )
{
DPRINTF ( E_DBG , L_SPOTIFY , " No images in for spotify album object found \n " ) ;
return NULL ;
}
// Find first image that has a smaller width than the given max_w
// (this should avoid the need for resizing and improve performance at the cost of some quality loss)
// Note that Spotify returns the images ordered descending by width (widest image first)
2019-08-25 05:22:40 -04:00
// Special case is if no max width (max_w = 0) is given, the widest images will be used
2019-02-05 05:35:45 -05:00
image_count = json_object_array_length ( jsonimages ) ;
for ( index = 0 ; index < image_count ; index + + )
{
jsonimage = json_object_array_get_idx ( jsonimages , index ) ;
if ( jsonimage )
{
artwork_url = jparse_str_from_obj ( jsonimage , " url " ) ;
2019-08-25 05:22:40 -04:00
if ( max_w < = 0 | | jparse_int_from_obj ( jsonimage , " width " ) < = max_w )
2019-02-05 05:35:45 -05:00
{
// We have the first image that has a smaller width than the given max_w
break ;
2018-11-28 14:31:50 -05:00
}
}
}
return artwork_url ;
}
2018-03-09 11:16:36 -05:00
static void
2019-02-05 05:35:45 -05:00
parse_metadata_track ( json_object * jsontrack , struct spotify_track * track , int max_w )
2017-02-05 07:56:12 -05:00
{
2018-11-28 14:31:50 -05:00
json_object * jsonalbum ;
json_object * jsonartists ;
json_object * needle ;
2017-02-05 07:56:12 -05:00
2018-03-09 11:16:36 -05:00
memset ( track , 0 , sizeof ( struct spotify_track ) ) ;
2017-02-05 07:56:12 -05:00
2018-03-09 11:16:36 -05:00
if ( json_object_object_get_ex ( jsontrack , " album " , & jsonalbum ) )
2017-02-05 07:56:12 -05:00
{
2018-03-09 11:16:36 -05:00
track - > album = jparse_str_from_obj ( jsonalbum , " name " ) ;
if ( json_object_object_get_ex ( jsonalbum , " artists " , & jsonartists ) )
track - > album_artist = jparse_str_from_array ( jsonartists , 0 , " name " ) ;
2018-11-28 14:31:50 -05:00
2019-08-25 05:22:40 -04:00
track - > artwork_url = get_album_image ( jsonalbum , max_w ) ;
2017-02-05 07:56:12 -05:00
}
2017-08-19 02:36:43 -04:00
2018-03-09 11:16:36 -05:00
if ( json_object_object_get_ex ( jsontrack , " artists " , & jsonartists ) )
track - > artist = jparse_str_from_array ( jsonartists , 0 , " name " ) ;
track - > disc_number = jparse_int_from_obj ( jsontrack , " disc_number " ) ;
track - > album_type = jparse_str_from_obj ( jsonalbum , " album_type " ) ;
track - > is_compilation = ( track - > album_type & & 0 = = strcmp ( track - > album_type , " compilation " ) ) ;
track - > duration_ms = jparse_int_from_obj ( jsontrack , " duration_ms " ) ;
track - > name = jparse_str_from_obj ( jsontrack , " name " ) ;
track - > track_number = jparse_int_from_obj ( jsontrack , " track_number " ) ;
track - > uri = jparse_str_from_obj ( jsontrack , " uri " ) ;
track - > id = jparse_str_from_obj ( jsontrack , " id " ) ;
2017-02-05 07:56:12 -05:00
2018-03-09 11:16:36 -05:00
// "is_playable" is only returned for a request with a market parameter, default to true if it is not in the response
track - > is_playable = true ;
if ( json_object_object_get_ex ( jsontrack , " is_playable " , NULL ) )
{
track - > is_playable = jparse_bool_from_obj ( jsontrack , " is_playable " ) ;
2017-02-05 07:56:12 -05:00
2018-03-09 11:16:36 -05:00
if ( json_object_object_get_ex ( jsontrack , " restrictions " , & needle ) )
track - > restrictions = json_object_to_json_string ( needle ) ;
if ( json_object_object_get_ex ( jsontrack , " linked_from " , & needle ) )
track - > linked_from_uri = jparse_str_from_obj ( needle , " uri " ) ;
}
2017-02-05 07:56:12 -05:00
}
2018-03-09 11:16:36 -05:00
static int
get_year_from_date ( const char * date )
2017-02-05 07:56:12 -05:00
{
2018-03-09 11:16:36 -05:00
char tmp [ 5 ] ;
uint32_t year = 0 ;
2017-02-05 07:56:12 -05:00
2018-03-09 11:16:36 -05:00
if ( date & & strlen ( date ) > = 4 )
2017-02-05 07:56:12 -05:00
{
2018-03-09 11:16:36 -05:00
strncpy ( tmp , date , sizeof ( tmp ) ) ;
tmp [ 4 ] = ' \0 ' ;
safe_atou32 ( tmp , & year ) ;
2017-02-05 07:56:12 -05:00
}
2018-03-09 11:16:36 -05:00
return year ;
2017-02-05 07:56:12 -05:00
}
2018-03-09 11:16:36 -05:00
static void
2019-02-05 05:35:45 -05:00
parse_metadata_album ( json_object * jsonalbum , struct spotify_album * album , int max_w )
2018-03-02 13:24:35 -05:00
{
2018-03-09 11:16:36 -05:00
json_object * jsonartists ;
2018-03-02 13:24:35 -05:00
2018-03-09 11:16:36 -05:00
memset ( album , 0 , sizeof ( struct spotify_album ) ) ;
2018-03-02 13:24:35 -05:00
2018-03-09 11:16:36 -05:00
if ( json_object_object_get_ex ( jsonalbum , " artists " , & jsonartists ) )
album - > artist = jparse_str_from_array ( jsonartists , 0 , " name " ) ;
2018-03-02 13:24:35 -05:00
2018-03-09 11:16:36 -05:00
album - > name = jparse_str_from_obj ( jsonalbum , " name " ) ;
album - > uri = jparse_str_from_obj ( jsonalbum , " uri " ) ;
album - > id = jparse_str_from_obj ( jsonalbum , " id " ) ;
2018-03-02 13:24:35 -05:00
2018-03-09 11:16:36 -05:00
album - > album_type = jparse_str_from_obj ( jsonalbum , " album_type " ) ;
album - > is_compilation = ( album - > album_type & & 0 = = strcmp ( album - > album_type , " compilation " ) ) ;
2018-03-02 13:24:35 -05:00
2018-03-09 11:16:36 -05:00
album - > label = jparse_str_from_obj ( jsonalbum , " label " ) ;
2018-03-02 13:24:35 -05:00
2018-03-09 11:16:36 -05:00
album - > release_date = jparse_str_from_obj ( jsonalbum , " release_date " ) ;
album - > release_date_precision = jparse_str_from_obj ( jsonalbum , " release_date_precision " ) ;
album - > release_year = get_year_from_date ( album - > release_date ) ;
2018-03-02 13:24:35 -05:00
2019-02-05 05:35:45 -05:00
if ( max_w > 0 )
album - > artwork_url = get_album_image ( jsonalbum , max_w ) ;
2018-11-28 14:31:50 -05:00
2018-03-09 11:16:36 -05:00
// TODO Genre is an array of strings ('genres'), but it is always empty (https://github.com/spotify/web-api/issues/157)
//album->genre = jparse_str_from_obj(jsonalbum, "genre");
2018-03-02 13:24:35 -05:00
}
2018-03-09 11:16:36 -05:00
static void
parse_metadata_playlist ( json_object * jsonplaylist , struct spotify_playlist * playlist )
2017-02-05 07:56:12 -05:00
{
2018-03-09 11:16:36 -05:00
json_object * needle ;
2017-02-05 07:56:12 -05:00
2018-03-09 11:16:36 -05:00
memset ( playlist , 0 , sizeof ( struct spotify_playlist ) ) ;
2017-02-05 07:56:12 -05:00
2018-03-09 11:16:36 -05:00
playlist - > name = jparse_str_from_obj ( jsonplaylist , " name " ) ;
playlist - > uri = jparse_str_from_obj ( jsonplaylist , " uri " ) ;
playlist - > id = jparse_str_from_obj ( jsonplaylist , " id " ) ;
playlist - > href = jparse_str_from_obj ( jsonplaylist , " href " ) ;
2017-02-05 07:56:12 -05:00
2018-03-09 11:16:36 -05:00
if ( json_object_object_get_ex ( jsonplaylist , " owner " , & needle ) )
playlist - > owner = jparse_str_from_obj ( needle , " id " ) ;
2017-02-05 07:56:12 -05:00
2018-03-09 11:16:36 -05:00
if ( json_object_object_get_ex ( jsonplaylist , " tracks " , & needle ) )
2017-02-05 07:56:12 -05:00
{
2018-03-09 11:16:36 -05:00
playlist - > tracks_href = jparse_str_from_obj ( needle , " href " ) ;
playlist - > tracks_count = jparse_int_from_obj ( needle , " total " ) ;
2017-02-05 07:56:12 -05:00
}
2018-03-09 11:16:36 -05:00
}
2017-02-05 07:56:12 -05:00
2018-03-09 11:16:36 -05:00
/*
* Creates a new string for the playlist API endpoint for the given playist - uri .
* The returned string needs to be freed by the caller .
*
* @ param uri Playlist uri ( e . g . " spotify:user:username:playlist:59ZbFPES4DQwEjBpWHzrtC " )
* @ return Playlist endpoint uri ( e . g . " https://api.spotify.com/v1/users/username/playlists/59ZbFPES4DQwEjBpWHzrtC " )
*/
2018-03-09 13:21:58 -05:00
static int
get_id_from_uri ( const char * uri , char * * id )
{
char * tmp ;
tmp = strrchr ( uri , ' : ' ) ;
if ( ! tmp )
{
return - 1 ;
}
tmp + + ;
* id = strdup ( tmp ) ;
return 0 ;
}
2018-03-09 11:16:36 -05:00
static char *
get_playlist_endpoint_uri ( const char * uri )
{
char * endpoint_uri = NULL ;
char * id = NULL ;
int ret ;
2017-02-05 07:56:12 -05:00
2019-02-20 03:30:02 -05:00
ret = get_id_from_uri ( uri , & id ) ;
2018-03-09 11:16:36 -05:00
if ( ret < 0 )
2017-02-05 07:56:12 -05:00
{
2018-03-09 11:16:36 -05:00
DPRINTF ( E_LOG , L_SPOTIFY , " Error extracting owner and id from playlist uri '%s' \n " , uri ) ;
goto out ;
2017-02-05 07:56:12 -05:00
}
2019-02-20 03:30:02 -05:00
endpoint_uri = safe_asprintf ( spotify_playlist_uri , id ) ;
2017-02-05 07:56:12 -05:00
2018-03-09 11:16:36 -05:00
out :
free ( id ) ;
return endpoint_uri ;
2017-02-05 07:56:12 -05:00
}
2018-03-09 13:21:58 -05:00
static char *
get_playlist_tracks_endpoint_uri ( const char * uri )
{
char * endpoint_uri = NULL ;
char * id = NULL ;
int ret ;
2019-02-20 03:30:02 -05:00
ret = get_id_from_uri ( uri , & id ) ;
2018-03-09 13:21:58 -05:00
if ( ret < 0 )
{
DPRINTF ( E_LOG , L_SPOTIFY , " Error extracting owner and id from playlist uri '%s' \n " , uri ) ;
goto out ;
}
2019-02-20 03:30:02 -05:00
endpoint_uri = safe_asprintf ( spotify_playlist_tracks_uri , id ) ;
2018-03-09 13:21:58 -05:00
out :
free ( id ) ;
return endpoint_uri ;
}
static char *
get_album_endpoint_uri ( const char * uri )
{
char * endpoint_uri = NULL ;
char * id = NULL ;
int ret ;
ret = get_id_from_uri ( uri , & id ) ;
if ( ret < 0 )
{
DPRINTF ( E_LOG , L_SPOTIFY , " Error extracting id from uri '%s' \n " , uri ) ;
goto out ;
}
endpoint_uri = safe_asprintf ( spotify_album_uri , id ) ;
out :
free ( id ) ;
return endpoint_uri ;
}
static char *
get_album_tracks_endpoint_uri ( const char * uri )
{
char * endpoint_uri = NULL ;
char * id = NULL ;
int ret ;
ret = get_id_from_uri ( uri , & id ) ;
if ( ret < 0 )
{
DPRINTF ( E_LOG , L_SPOTIFY , " Error extracting id from uri '%s' \n " , uri ) ;
goto out ;
}
endpoint_uri = safe_asprintf ( spotify_album_tracks_uri , id ) ;
out :
free ( id ) ;
return endpoint_uri ;
}
static char *
get_track_endpoint_uri ( const char * uri )
{
char * endpoint_uri = NULL ;
char * id = NULL ;
int ret ;
ret = get_id_from_uri ( uri , & id ) ;
if ( ret < 0 )
{
DPRINTF ( E_LOG , L_SPOTIFY , " Error extracting id from track uri '%s' \n " , uri ) ;
goto out ;
}
endpoint_uri = safe_asprintf ( spotify_track_uri , id ) ;
out :
free ( id ) ;
return endpoint_uri ;
}
2019-02-20 04:10:53 -05:00
static char *
get_artist_albums_endpoint_uri ( const char * uri )
{
char * endpoint_uri = NULL ;
char * id = NULL ;
int ret ;
ret = get_id_from_uri ( uri , & id ) ;
if ( ret < 0 )
{
DPRINTF ( E_LOG , L_SPOTIFY , " Error extracting id from uri '%s' \n " , uri ) ;
goto out ;
}
endpoint_uri = safe_asprintf ( spotify_artist_albums_uri , id ) ;
out :
free ( id ) ;
return endpoint_uri ;
}
2018-03-09 13:21:58 -05:00
static json_object *
request_track ( const char * path )
{
char * endpoint_uri ;
json_object * response ;
endpoint_uri = get_track_endpoint_uri ( path ) ;
response = request_endpoint_with_token_refresh ( endpoint_uri ) ;
free ( endpoint_uri ) ;
return response ;
}
2018-03-09 11:16:36 -05:00
/* Thread: httpd */
char *
spotifywebapi_oauth_uri_get ( const char * redirect_uri )
2017-02-05 07:56:12 -05:00
{
struct keyval kv ;
2018-03-09 11:16:36 -05:00
char * param ;
char * uri ;
int uri_len ;
2017-02-05 07:56:12 -05:00
int ret ;
2018-03-09 11:16:36 -05:00
uri = NULL ;
2017-02-05 07:56:12 -05:00
memset ( & kv , 0 , sizeof ( struct keyval ) ) ;
2018-03-09 11:16:36 -05:00
ret = ( ( keyval_add ( & kv , " client_id " , spotify_client_id ) = = 0 ) & &
( keyval_add ( & kv , " response_type " , " code " ) = = 0 ) & &
( keyval_add ( & kv , " redirect_uri " , redirect_uri ) = = 0 ) & &
2019-08-03 06:55:53 -04:00
( keyval_add ( & kv , " scope " , spotify_scope ) = = 0 ) & &
2018-03-09 11:16:36 -05:00
( keyval_add ( & kv , " show_dialog " , " false " ) = = 0 ) ) ;
2017-02-05 07:56:12 -05:00
if ( ! ret )
{
2018-03-09 11:16:36 -05:00
DPRINTF ( E_LOG , L_SPOTIFY , " Cannot display Spotify oath interface (error adding parameters to keyval) \n " ) ;
goto out_clear_kv ;
2017-02-05 07:56:12 -05:00
}
2018-03-09 11:16:36 -05:00
param = http_form_urlencode ( & kv ) ;
if ( param )
2017-12-27 12:51:11 -05:00
{
2018-03-09 11:16:36 -05:00
uri_len = strlen ( spotify_auth_uri ) + strlen ( param ) + 3 ;
uri = calloc ( uri_len , sizeof ( char ) ) ;
snprintf ( uri , uri_len , " %s/?%s " , spotify_auth_uri , param ) ;
free ( param ) ;
2017-12-27 12:51:11 -05:00
}
2018-03-09 11:16:36 -05:00
out_clear_kv :
2017-02-05 07:56:12 -05:00
keyval_clear ( & kv ) ;
2018-03-09 11:16:36 -05:00
return uri ;
2017-02-05 07:56:12 -05:00
}
2018-03-09 11:16:36 -05:00
/* Thread: httpd */
int
spotifywebapi_oauth_callback ( struct evkeyvalq * param , const char * redirect_uri , char * * errmsg )
2017-02-05 07:56:12 -05:00
{
2018-03-09 11:16:36 -05:00
const char * code ;
2017-02-05 07:56:12 -05:00
const char * err ;
int ret ;
2018-03-09 11:16:36 -05:00
* errmsg = NULL ;
2017-02-05 07:56:12 -05:00
2018-03-09 11:16:36 -05:00
code = evhttp_find_header ( param , " code " ) ;
if ( ! code )
2017-02-05 07:56:12 -05:00
{
2018-03-09 11:16:36 -05:00
* errmsg = safe_asprintf ( " Error: Didn't receive a code from Spotify " ) ;
return - 1 ;
2017-02-05 07:56:12 -05:00
}
2018-03-09 11:16:36 -05:00
DPRINTF ( E_DBG , L_SPOTIFY , " Received OAuth code: %s \n " , code ) ;
2017-02-05 07:56:12 -05:00
2018-03-09 11:16:36 -05:00
ret = token_get ( code , redirect_uri , & err ) ;
if ( ret < 0 )
2017-02-05 07:56:12 -05:00
{
2018-03-09 11:16:36 -05:00
* errmsg = safe_asprintf ( " Error: %s " , err ) ;
return - 1 ;
2017-02-05 07:56:12 -05:00
}
2018-03-09 11:16:36 -05:00
// Trigger scan after successful access to spotifywebapi
spotifywebapi_fullrescan ( ) ;
2018-03-02 13:24:35 -05:00
2018-03-09 11:16:36 -05:00
listener_notify ( LISTENER_SPOTIFY ) ;
2017-02-05 07:56:12 -05:00
2018-03-09 11:16:36 -05:00
return 0 ;
}
2018-03-02 13:24:35 -05:00
2018-03-09 11:16:36 -05:00
static int
transaction_start ( void * arg )
{
db_transaction_begin ( ) ;
return 0 ;
2018-03-02 13:24:35 -05:00
}
2018-03-09 11:16:36 -05:00
static int
transaction_end ( void * arg )
{
db_transaction_end ( ) ;
return 0 ;
}
2018-03-02 13:24:35 -05:00
2018-03-09 13:21:58 -05:00
static void
map_track_to_queueitem ( struct db_queue_item * item , const struct spotify_track * track , const struct spotify_album * album )
{
char virtual_path [ PATH_MAX ] ;
memset ( item , 0 , sizeof ( struct db_queue_item ) ) ;
item - > file_id = DB_MEDIA_FILE_NON_PERSISTENT_ID ;
item - > title = safe_strdup ( track - > name ) ;
item - > artist = safe_strdup ( track - > artist ) ;
if ( album )
{
item - > album_artist = safe_strdup ( album - > artist ) ;
item - > album = safe_strdup ( album - > name ) ;
2018-11-28 14:31:50 -05:00
item - > artwork_url = safe_strdup ( album - > artwork_url ) ;
2018-03-09 13:21:58 -05:00
}
else
{
item - > album_artist = safe_strdup ( track - > album_artist ) ;
item - > album = safe_strdup ( track - > album ) ;
2018-11-28 14:31:50 -05:00
item - > artwork_url = safe_strdup ( track - > artwork_url ) ;
2018-03-09 13:21:58 -05:00
}
item - > disc = track - > disc_number ;
item - > song_length = track - > duration_ms ;
item - > track = track - > track_number ;
item - > data_kind = DATA_KIND_SPOTIFY ;
item - > media_kind = MEDIA_KIND_MUSIC ;
item - > path = safe_strdup ( track - > uri ) ;
snprintf ( virtual_path , PATH_MAX , " /%s " , track - > uri ) ;
item - > virtual_path = strdup ( virtual_path ) ;
}
static int
2018-11-10 01:41:36 -05:00
queue_add_track ( const char * uri , int position , char reshuffle , uint32_t item_id , int * count , int * new_item_id )
2018-03-09 13:21:58 -05:00
{
json_object * response ;
struct spotify_track track ;
struct db_queue_item item ;
struct db_queue_add_info queue_add_info ;
int ret ;
response = request_track ( uri ) ;
if ( ! response )
return - 1 ;
2019-02-05 05:35:45 -05:00
parse_metadata_track ( response , & track , ART_DEFAULT_WIDTH ) ;
2018-03-09 13:21:58 -05:00
DPRINTF ( E_DBG , L_SPOTIFY , " Got track: '%s' (%s) \n " , track . name , track . uri ) ;
map_track_to_queueitem ( & item , & track , NULL ) ;
2018-10-13 02:30:41 -04:00
ret = db_queue_add_start ( & queue_add_info , position ) ;
2018-03-09 13:21:58 -05:00
if ( ret = = 0 )
{
ret = db_queue_add_item ( & queue_add_info , & item ) ;
2018-11-10 01:41:36 -05:00
ret = db_queue_add_end ( & queue_add_info , reshuffle , item_id , ret ) ;
2018-10-13 02:30:41 -04:00
if ( ret = = 0 )
{
if ( count )
* count = queue_add_info . count ;
if ( new_item_id )
* new_item_id = queue_add_info . new_item_id ;
}
2018-03-09 13:21:58 -05:00
}
free_queue_item ( & item , 1 ) ;
jparse_free ( response ) ;
return 0 ;
}
struct queue_add_album_param {
struct spotify_album album ;
struct db_queue_add_info queue_add_info ;
} ;
static int
queue_add_album_tracks ( json_object * item , int index , int total , void * arg )
{
struct queue_add_album_param * param ;
struct spotify_track track ;
struct db_queue_item queue_item ;
int ret ;
param = arg ;
2019-02-05 05:35:45 -05:00
parse_metadata_track ( item , & track , ART_DEFAULT_WIDTH ) ;
2018-03-09 13:21:58 -05:00
if ( ! track . uri | | ! track . is_playable )
{
DPRINTF ( E_LOG , L_SPOTIFY , " Track not available for playback: '%s' - '%s' (%s) (restrictions: %s) \n " , track . artist , track . name , track . uri , track . restrictions ) ;
return - 1 ;
}
map_track_to_queueitem ( & queue_item , & track , & param - > album ) ;
ret = db_queue_add_item ( & param - > queue_add_info , & queue_item ) ;
free_queue_item ( & queue_item , 1 ) ;
return ret ;
}
static int
2018-11-10 01:41:36 -05:00
queue_add_album ( const char * uri , int position , char reshuffle , uint32_t item_id , int * count , int * new_item_id )
2018-03-09 13:21:58 -05:00
{
char * album_endpoint_uri = NULL ;
char * endpoint_uri = NULL ;
json_object * json_album ;
struct queue_add_album_param param ;
int ret ;
album_endpoint_uri = get_album_endpoint_uri ( uri ) ;
json_album = request_endpoint_with_token_refresh ( album_endpoint_uri ) ;
2019-02-05 05:35:45 -05:00
parse_metadata_album ( json_album , & param . album , ART_DEFAULT_WIDTH ) ;
2018-03-09 13:21:58 -05:00
2018-10-13 02:30:41 -04:00
ret = db_queue_add_start ( & param . queue_add_info , position ) ;
2018-03-09 13:21:58 -05:00
if ( ret < 0 )
goto out ;
endpoint_uri = get_album_tracks_endpoint_uri ( uri ) ;
ret = request_pagingobject_endpoint ( endpoint_uri , queue_add_album_tracks , NULL , NULL , true , & param ) ;
2018-11-10 01:41:36 -05:00
ret = db_queue_add_end ( & param . queue_add_info , reshuffle , item_id , ret ) ;
2018-10-13 02:30:41 -04:00
if ( ret = = 0 & & count )
* count = param . queue_add_info . count ;
2018-03-09 13:21:58 -05:00
out :
free ( album_endpoint_uri ) ;
free ( endpoint_uri ) ;
jparse_free ( json_album ) ;
return ret ;
}
2019-02-20 04:10:53 -05:00
static int
queue_add_albums ( json_object * item , int index , int total , void * arg )
{
struct db_queue_add_info * param ;
struct queue_add_album_param param_add_album ;
char * endpoint_uri = NULL ;
int ret ;
param = arg ;
param_add_album . queue_add_info = * param ;
parse_metadata_album ( item , & param_add_album . album , ART_DEFAULT_WIDTH ) ;
endpoint_uri = get_album_tracks_endpoint_uri ( param_add_album . album . uri ) ;
ret = request_pagingobject_endpoint ( endpoint_uri , queue_add_album_tracks , NULL , NULL , true , & param_add_album ) ;
* param = param_add_album . queue_add_info ;
free ( endpoint_uri ) ;
return ret ;
}
static int
queue_add_artist ( const char * uri , int position , char reshuffle , uint32_t item_id , int * count , int * new_item_id )
{
struct db_queue_add_info queue_add_info ;
char * endpoint_uri = NULL ;
int ret ;
ret = db_queue_add_start ( & queue_add_info , position ) ;
if ( ret < 0 )
return - 1 ;
endpoint_uri = get_artist_albums_endpoint_uri ( uri ) ;
ret = request_pagingobject_endpoint ( endpoint_uri , queue_add_albums , NULL , NULL , true , & queue_add_info ) ;
ret = db_queue_add_end ( & queue_add_info , reshuffle , item_id , ret ) ;
if ( ret = = 0 & & count )
* count = queue_add_info . count ;
free ( endpoint_uri ) ;
return ret ;
}
2018-03-09 13:21:58 -05:00
static int
queue_add_playlist_tracks ( json_object * item , int index , int total , void * arg )
{
struct db_queue_add_info * queue_add_info ;
struct spotify_track track ;
json_object * jsontrack ;
struct db_queue_item queue_item ;
int ret ;
queue_add_info = arg ;
if ( ! ( item & & json_object_object_get_ex ( item , " track " , & jsontrack ) ) )
{
DPRINTF ( E_LOG , L_SPOTIFY , " Unexpected JSON: missing 'track' in JSON object at index %d \n " , index ) ;
return - 1 ;
}
2019-02-05 05:35:45 -05:00
parse_metadata_track ( jsontrack , & track , ART_DEFAULT_WIDTH ) ;
2018-03-09 13:21:58 -05:00
track . added_at = jparse_str_from_obj ( item , " added_at " ) ;
track . mtime = jparse_time_from_obj ( item , " added_at " ) ;
if ( ! track . uri | | ! track . is_playable )
{
DPRINTF ( E_LOG , L_SPOTIFY , " Track not available for playback: '%s' - '%s' (%s) (restrictions: %s) \n " , track . artist , track . name , track . uri , track . restrictions ) ;
return - 1 ;
}
map_track_to_queueitem ( & queue_item , & track , NULL ) ;
ret = db_queue_add_item ( queue_add_info , & queue_item ) ;
free_queue_item ( & queue_item , 1 ) ;
return ret ;
}
static int
2018-11-10 01:41:36 -05:00
queue_add_playlist ( const char * uri , int position , char reshuffle , uint32_t item_id , int * count , int * new_item_id )
2018-03-09 13:21:58 -05:00
{
char * endpoint_uri ;
struct db_queue_add_info queue_add_info ;
int ret ;
2018-10-13 02:30:41 -04:00
ret = db_queue_add_start ( & queue_add_info , position ) ;
2018-03-09 13:21:58 -05:00
if ( ret < 0 )
return - 1 ;
endpoint_uri = get_playlist_tracks_endpoint_uri ( uri ) ;
ret = request_pagingobject_endpoint ( endpoint_uri , queue_add_playlist_tracks , NULL , NULL , true , & queue_add_info ) ;
2018-11-10 01:41:36 -05:00
ret = db_queue_add_end ( & queue_add_info , reshuffle , item_id , ret ) ;
2018-10-13 02:30:41 -04:00
if ( ret = = 0 & & count )
* count = queue_add_info . count ;
2018-03-09 13:21:58 -05:00
free ( endpoint_uri ) ;
return ret ;
}
static int
2020-01-20 17:13:47 -05:00
queue_item_add ( const char * uri , int position , char reshuffle , uint32_t item_id , int * count , int * new_item_id )
2018-03-09 13:21:58 -05:00
{
if ( strncasecmp ( uri , " spotify:track: " , strlen ( " spotify:track: " ) ) = = 0 )
{
2018-11-10 01:41:36 -05:00
queue_add_track ( uri , position , reshuffle , item_id , count , new_item_id ) ;
2018-03-09 13:21:58 -05:00
return LIBRARY_OK ;
}
2019-02-20 04:10:53 -05:00
else if ( strncasecmp ( uri , " spotify:artist: " , strlen ( " spotify:artist: " ) ) = = 0 )
{
queue_add_artist ( uri , position , reshuffle , item_id , count , new_item_id ) ;
return LIBRARY_OK ;
}
2018-03-09 13:21:58 -05:00
else if ( strncasecmp ( uri , " spotify:album: " , strlen ( " spotify:album: " ) ) = = 0 )
{
2018-11-10 01:41:36 -05:00
queue_add_album ( uri , position , reshuffle , item_id , count , new_item_id ) ;
2018-03-09 13:21:58 -05:00
return LIBRARY_OK ;
}
else if ( strncasecmp ( uri , " spotify: " , strlen ( " spotify: " ) ) = = 0 )
{
2018-11-10 01:41:36 -05:00
queue_add_playlist ( uri , position , reshuffle , item_id , count , new_item_id ) ;
2018-03-09 13:21:58 -05:00
return LIBRARY_OK ;
}
return LIBRARY_PATH_INVALID ;
}
2018-03-02 13:24:35 -05:00
/*
* Returns the directory id for / spotify : / < artist > / < album > , if the directory ( or the parent
* directories ) does not yet exist , they will be created .
* If an error occured the return value is - 1.
*
* @ return directory id for the given artist / album directory
*/
static int
prepare_directories ( const char * artist , const char * album )
{
int dir_id ;
char virtual_path [ PATH_MAX ] ;
int ret ;
ret = snprintf ( virtual_path , sizeof ( virtual_path ) , " /spotify:/%s " , artist ) ;
if ( ( ret < 0 ) | | ( ret > = sizeof ( virtual_path ) ) )
{
DPRINTF ( E_LOG , L_SPOTIFY , " Virtual path exceeds PATH_MAX (/spotify:/%s) \n " , artist ) ;
return - 1 ;
}
2018-11-24 03:44:18 -05:00
dir_id = db_directory_addorupdate ( virtual_path , NULL , 0 , DIR_SPOTIFY ) ;
2018-03-02 13:24:35 -05:00
if ( dir_id < = 0 )
{
DPRINTF ( E_LOG , L_SPOTIFY , " Could not add or update directory '%s' \n " , virtual_path ) ;
return - 1 ;
}
ret = snprintf ( virtual_path , sizeof ( virtual_path ) , " /spotify:/%s/%s " , artist , album ) ;
if ( ( ret < 0 ) | | ( ret > = sizeof ( virtual_path ) ) )
{
DPRINTF ( E_LOG , L_SPOTIFY , " Virtual path exceeds PATH_MAX (/spotify:/%s/%s) \n " , artist , album ) ;
return - 1 ;
}
2018-11-24 03:44:18 -05:00
dir_id = db_directory_addorupdate ( virtual_path , NULL , 0 , dir_id ) ;
2018-03-02 13:24:35 -05:00
if ( dir_id < = 0 )
{
DPRINTF ( E_LOG , L_SPOTIFY , " Could not add or update directory '%s' \n " , virtual_path ) ;
return - 1 ;
}
return dir_id ;
}
2018-03-09 11:16:36 -05:00
/*
* Purges all Spotify files from the library that are not in a playlist
* ( Note : all files from saved albums are in the spotify : savedtracks playlist )
*/
2018-03-02 13:24:35 -05:00
static int
2018-03-09 11:16:36 -05:00
cleanup_spotify_files ( void )
2018-03-02 13:24:35 -05:00
{
struct query_params qp ;
char * path ;
int ret ;
memset ( & qp , 0 , sizeof ( struct query_params ) ) ;
qp . type = Q_BROWSE_PATH ;
qp . sort = S_NONE ;
qp . filter = " f.path LIKE 'spotify:%%' AND NOT f.path IN (SELECT filepath FROM playlistitems) " ;
ret = db_query_start ( & qp ) ;
if ( ret < 0 )
{
db_query_end ( & qp ) ;
return - 1 ;
}
while ( ( ( ret = db_query_fetch_string ( & qp , & path ) ) = = 0 ) & & ( path ) )
{
cache_artwork_delete_by_path ( path ) ;
}
db_query_end ( & qp ) ;
db_spotify_files_delete ( ) ;
return 0 ;
}
static void
map_track_to_mfi ( struct media_file_info * mfi , const struct spotify_track * track , const struct spotify_album * album , const char * pl_name )
{
char virtual_path [ PATH_MAX ] ;
mfi - > title = safe_strdup ( track - > name ) ;
mfi - > artist = safe_strdup ( track - > artist ) ;
mfi - > disc = track - > disc_number ;
mfi - > song_length = track - > duration_ms ;
mfi - > track = track - > track_number ;
mfi - > data_kind = DATA_KIND_SPOTIFY ;
mfi - > media_kind = MEDIA_KIND_MUSIC ;
mfi - > type = strdup ( " spotify " ) ;
mfi - > codectype = strdup ( " wav " ) ;
mfi - > description = strdup ( " Spotify audio " ) ;
mfi - > path = strdup ( track - > uri ) ;
mfi - > fname = strdup ( track - > uri ) ;
2018-03-20 13:20:36 -04:00
mfi - > time_modified = track - > mtime ;
mfi - > time_added = track - > mtime ;
2018-03-02 13:24:35 -05:00
if ( album )
{
mfi - > album_artist = safe_strdup ( album - > artist ) ;
mfi - > album = safe_strdup ( album - > name ) ;
mfi - > genre = safe_strdup ( album - > genre ) ;
mfi - > compilation = album - > is_compilation ;
mfi - > year = album - > release_year ;
}
else
{
mfi - > album_artist = safe_strdup ( track - > album_artist ) ;
if ( cfg_getbool ( cfg_getsec ( cfg , " spotify " ) , " album_override " ) & & pl_name )
mfi - > album = safe_strdup ( pl_name ) ;
else
mfi - > album = safe_strdup ( track - > album ) ;
if ( cfg_getbool ( cfg_getsec ( cfg , " spotify " ) , " artist_override " ) & & pl_name )
mfi - > compilation = true ;
else
mfi - > compilation = track - > is_compilation ;
}
snprintf ( virtual_path , PATH_MAX , " /spotify:/%s/%s/%s " , mfi - > album_artist , mfi - > album , mfi - > title ) ;
mfi - > virtual_path = strdup ( virtual_path ) ;
}
2018-03-09 11:16:36 -05:00
static int
track_add ( struct spotify_track * track , struct spotify_album * album , const char * pl_name , int dir_id )
2018-03-02 13:24:35 -05:00
{
struct media_file_info mfi ;
int ret ;
2018-03-09 11:16:36 -05:00
if ( ! track - > uri | | ! track - > is_playable )
{
DPRINTF ( E_LOG , L_SPOTIFY , " Track not available for playback: '%s' - '%s' (%s) (restrictions: %s) \n " ,
track - > artist , track - > name , track - > uri , track - > restrictions ) ;
return - 1 ;
}
2018-03-02 13:24:35 -05:00
if ( track - > linked_from_uri )
DPRINTF ( E_DBG , L_SPOTIFY , " Track '%s' (%s) linked from %s \n " , track - > name , track - > uri , track - > linked_from_uri ) ;
ret = db_file_ping_bypath ( track - > uri , track - > mtime ) ;
if ( ret = = 0 )
{
2018-03-09 11:16:36 -05:00
DPRINTF ( E_DBG , L_SPOTIFY , " Track '%s' (%s) is new or modified (mtime is % " PRIi64 " ) \n " ,
track - > name , track - > uri , ( int64_t ) track - > mtime ) ;
2018-03-02 13:24:35 -05:00
memset ( & mfi , 0 , sizeof ( struct media_file_info ) ) ;
mfi . id = db_file_id_bypath ( track - > uri ) ;
mfi . directory_id = dir_id ;
map_track_to_mfi ( & mfi , track , album , pl_name ) ;
2020-01-20 17:13:47 -05:00
library_media_save ( & mfi ) ;
2018-03-02 13:24:35 -05:00
free_mfi ( & mfi , 1 ) ;
}
spotify_uri_register ( track - > uri ) ;
if ( album )
cache_artwork_ping ( track - > uri , album - > mtime , 0 ) ;
else
cache_artwork_ping ( track - > uri , 1 , 0 ) ;
2018-03-09 11:16:36 -05:00
return 0 ;
}
2018-03-02 13:24:35 -05:00
2020-02-04 17:15:56 -05:00
static int
playlist_add_or_update ( struct playlist_info * pli )
{
int pl_id ;
pl_id = db_pl_id_bypath ( pli - > path ) ;
if ( pl_id < 0 )
return library_playlist_save ( pli ) ;
pli - > id = pl_id ;
db_pl_clear_items ( pli - > id ) ;
return library_playlist_save ( pli ) ;
}
2018-03-09 11:16:36 -05:00
/*
* Add a saved album to the library
*/
2018-03-02 13:24:35 -05:00
static int
2018-03-17 07:17:15 -04:00
saved_album_add ( json_object * item , int index , int total , void * arg )
2018-03-02 13:24:35 -05:00
{
2018-03-09 11:16:36 -05:00
json_object * jsonalbum ;
2018-03-02 13:24:35 -05:00
struct spotify_album album ;
struct spotify_track track ;
2018-03-09 11:16:36 -05:00
json_object * needle ;
2018-03-02 13:24:35 -05:00
json_object * jsontracks ;
2018-03-09 11:16:36 -05:00
json_object * jsontrack ;
2018-03-02 13:24:35 -05:00
int track_count ;
int dir_id ;
int i ;
int ret ;
2018-03-09 11:16:36 -05:00
if ( ! json_object_object_get_ex ( item , " album " , & jsonalbum ) )
2018-03-02 13:24:35 -05:00
{
2018-03-09 11:16:36 -05:00
DPRINTF ( E_LOG , L_SPOTIFY , " Unexpected JSON: Item %d is missing the 'album' field \n " , index ) ;
return - 1 ;
}
if ( ! json_object_object_get_ex ( jsonalbum , " tracks " , & needle ) )
{
DPRINTF ( E_LOG , L_SPOTIFY , " Unexpected JSON: Item %d is missing the 'tracks' field' \n " , index ) ;
return - 1 ;
}
if ( jparse_array_from_obj ( needle , " items " , & jsontracks ) < 0 )
{
DPRINTF ( E_LOG , L_SPOTIFY , " Unexpected JSON: Item %d has an empty 'tracks' array \n " , index ) ;
return - 1 ;
}
2018-03-02 13:24:35 -05:00
2018-03-09 11:16:36 -05:00
// Map album information
2019-02-05 05:35:45 -05:00
parse_metadata_album ( jsonalbum , & album , 0 ) ;
2018-03-09 11:16:36 -05:00
album . added_at = jparse_str_from_obj ( item , " added_at " ) ;
album . mtime = jparse_time_from_obj ( item , " added_at " ) ;
2018-03-02 13:24:35 -05:00
2018-03-09 11:16:36 -05:00
// Now map the album tracks and insert/update them in the files database
db_transaction_begin ( ) ;
2018-03-02 13:24:35 -05:00
2018-03-09 11:16:36 -05:00
// Get or create the directory structure for this album
dir_id = prepare_directories ( album . artist , album . name ) ;
track_count = json_object_array_length ( jsontracks ) ;
for ( i = 0 ; i < track_count ; i + + )
{
jsontrack = json_object_array_get_idx ( jsontracks , i ) ;
if ( ! jsontrack )
break ;
2018-03-02 13:24:35 -05:00
2019-02-05 05:35:45 -05:00
parse_metadata_track ( jsontrack , & track , 0 ) ;
2018-03-20 13:20:36 -04:00
track . mtime = album . mtime ;
2018-03-02 13:24:35 -05:00
2018-03-09 11:16:36 -05:00
ret = track_add ( & track , & album , NULL , dir_id ) ;
if ( ret = = 0 & & spotify_saved_plid )
db_pl_add_item_bypath ( spotify_saved_plid , track . uri ) ;
2018-03-02 13:24:35 -05:00
}
2018-03-09 11:16:36 -05:00
db_transaction_end ( ) ;
2018-03-02 13:24:35 -05:00
2018-03-17 07:17:15 -04:00
if ( ( index + 1 ) > = total | | ( ( index + 1 ) % 10 = = 0 ) )
DPRINTF ( E_LOG , L_SPOTIFY , " Scanned %d of %d saved albums \n " , ( index + 1 ) , total ) ;
2018-03-02 13:24:35 -05:00
return 0 ;
}
2018-03-09 11:16:36 -05:00
/*
* Thread : library
*
* Scan users saved albums into the library
*/
static int
scan_saved_albums ( )
{
int ret ;
2018-03-18 02:47:49 -04:00
ret = request_pagingobject_endpoint ( spotify_albums_uri , saved_album_add , NULL , NULL , true , NULL ) ;
2018-03-09 11:16:36 -05:00
return ret ;
}
/*
* Add a saved playlist tracks to the library
*/
2018-03-02 13:24:35 -05:00
static int
2018-03-17 07:17:15 -04:00
saved_playlist_tracks_add ( json_object * item , int index , int total , void * arg )
2018-03-02 13:24:35 -05:00
{
struct spotify_track track ;
2018-03-09 11:16:36 -05:00
json_object * jsontrack ;
int * plid ;
2018-03-02 13:24:35 -05:00
int dir_id ;
2018-03-09 11:16:36 -05:00
int ret ;
2018-03-02 13:24:35 -05:00
2018-03-09 11:16:36 -05:00
plid = arg ;
2018-03-02 13:24:35 -05:00
2018-03-09 11:16:36 -05:00
if ( ! ( item & & json_object_object_get_ex ( item , " track " , & jsontrack ) ) )
2018-03-02 13:24:35 -05:00
{
2018-03-09 11:16:36 -05:00
DPRINTF ( E_LOG , L_SPOTIFY , " Unexpected JSON: missing 'track' in JSON object at index %d \n " , index ) ;
return - 1 ;
}
2018-03-02 13:24:35 -05:00
2019-02-05 05:35:45 -05:00
parse_metadata_track ( jsontrack , & track , 0 ) ;
2018-03-09 11:16:36 -05:00
track . added_at = jparse_str_from_obj ( item , " added_at " ) ;
track . mtime = jparse_time_from_obj ( item , " added_at " ) ;
2018-03-02 13:24:35 -05:00
2018-03-09 11:16:36 -05:00
if ( ! track . uri | | ! track . is_playable )
{
DPRINTF ( E_LOG , L_SPOTIFY , " Track not available for playback: '%s' - '%s' (%s) (restrictions: %s) \n " , track . artist , track . name , track . uri , track . restrictions ) ;
2018-03-18 02:47:49 -04:00
return 0 ;
2018-03-02 13:24:35 -05:00
}
2018-03-09 11:16:36 -05:00
dir_id = prepare_directories ( track . album_artist , track . album ) ;
ret = track_add ( & track , NULL , NULL , dir_id ) ;
if ( ret = = 0 )
db_pl_add_item_bypath ( * plid , track . uri ) ;
2018-03-02 13:24:35 -05:00
return 0 ;
}
/* Thread: library */
static int
2018-03-09 11:16:36 -05:00
scan_playlist_tracks ( const char * playlist_tracks_endpoint_uri , int plid )
2018-03-02 13:24:35 -05:00
{
2018-03-09 11:16:36 -05:00
int ret ;
2018-03-02 13:24:35 -05:00
2018-03-18 02:47:49 -04:00
ret = request_pagingobject_endpoint ( playlist_tracks_endpoint_uri , saved_playlist_tracks_add , transaction_start , transaction_end , true , & plid ) ;
2018-03-02 13:24:35 -05:00
2018-03-09 11:16:36 -05:00
return ret ;
2018-03-02 13:24:35 -05:00
}
2020-01-20 17:13:47 -05:00
static void
map_playlist_to_pli ( struct playlist_info * pli , struct spotify_playlist * playlist )
{
2020-02-04 17:15:56 -05:00
memset ( pli , 0 , sizeof ( struct playlist_info ) ) ;
2020-01-20 17:13:47 -05:00
pli - > type = PL_PLAIN ;
pli - > path = strdup ( playlist - > uri ) ;
pli - > title = safe_strdup ( playlist - > name ) ;
pli - > parent_id = spotify_base_plid ;
pli - > directory_id = DIR_SPOTIFY ;
if ( playlist - > owner )
pli - > virtual_path = safe_asprintf ( " /spotify:/%s (%s) " , playlist - > name , playlist - > owner ) ;
else
pli - > virtual_path = safe_asprintf ( " /spotify:/%s " , playlist - > name ) ;
}
2018-03-09 11:16:36 -05:00
/*
* Add a saved playlist to the library
*/
2018-03-02 13:24:35 -05:00
static int
2018-03-17 07:17:15 -04:00
saved_playlist_add ( json_object * item , int index , int total , void * arg )
2018-03-02 13:24:35 -05:00
{
struct spotify_playlist playlist ;
2020-01-20 17:13:47 -05:00
struct playlist_info pli ;
int pl_id ;
2018-03-02 13:24:35 -05:00
2018-03-09 11:16:36 -05:00
// Map playlist information
parse_metadata_playlist ( item , & playlist ) ;
2018-03-02 13:24:35 -05:00
2018-03-09 11:16:36 -05:00
DPRINTF ( E_DBG , L_SPOTIFY , " Got playlist: '%s' with %d tracks (%s) \n " , playlist . name , playlist . tracks_count , playlist . uri ) ;
2018-03-02 13:24:35 -05:00
2018-03-09 11:16:36 -05:00
if ( ! playlist . uri | | ! playlist . name | | playlist . tracks_count = = 0 )
{
DPRINTF ( E_LOG , L_SPOTIFY , " Ignoring playlist '%s' with %d tracks (%s) \n " , playlist . name , playlist . tracks_count , playlist . uri ) ;
return - 1 ;
}
2018-03-02 13:24:35 -05:00
2020-01-20 17:13:47 -05:00
map_playlist_to_pli ( & pli , & playlist ) ;
2020-02-04 17:15:56 -05:00
pl_id = playlist_add_or_update ( & pli ) ;
2018-03-09 11:16:36 -05:00
2020-01-20 17:13:47 -05:00
free_pli ( & pli , 1 ) ;
if ( pl_id > 0 )
scan_playlist_tracks ( playlist . tracks_href , pl_id ) ;
2018-03-09 11:16:36 -05:00
else
DPRINTF ( E_LOG , L_SPOTIFY , " Error adding playlist: '%s' (%s) \n " , playlist . name , playlist . uri ) ;
2018-03-02 13:24:35 -05:00
2018-03-17 07:17:15 -04:00
DPRINTF ( E_LOG , L_SPOTIFY , " Scanned %d of %d saved playlists \n " , ( index + 1 ) , total ) ;
2018-03-02 13:24:35 -05:00
return 0 ;
}
2018-03-09 11:16:36 -05:00
/*
* Thread : library
*
* Scan users saved playlists into the library
*/
static int
scan_playlists ( )
{
int ret ;
2018-03-18 02:47:49 -04:00
ret = request_pagingobject_endpoint ( spotify_playlists_uri , saved_playlist_add , NULL , NULL , false , NULL ) ;
2018-03-09 11:16:36 -05:00
return ret ;
}
2018-03-02 13:24:35 -05:00
static void
create_saved_tracks_playlist ( )
{
2020-01-20 17:13:47 -05:00
struct playlist_info pli =
{
2020-02-04 17:15:56 -05:00
. path = strdup ( " spotify:savedtracks " ) ,
. title = strdup ( " Spotify Saved " ) ,
. virtual_path = strdup ( " /spotify:/Spotify Saved " ) ,
2020-01-20 17:13:47 -05:00
. type = PL_PLAIN ,
. parent_id = spotify_base_plid ,
. directory_id = DIR_SPOTIFY ,
} ;
2020-02-04 17:15:56 -05:00
spotify_saved_plid = playlist_add_or_update ( & pli ) ;
if ( spotify_saved_plid < 0 )
2018-03-02 13:24:35 -05:00
{
DPRINTF ( E_LOG , L_SPOTIFY , " Error adding playlist for saved tracks \n " ) ;
spotify_saved_plid = 0 ;
}
2020-02-04 17:15:56 -05:00
free_pli ( & pli , 1 ) ;
2018-03-02 13:24:35 -05:00
}
/*
* Add or update playlist folder for all spotify playlists ( if enabled in config )
*/
static void
create_base_playlist ( )
{
cfg_t * spotify_cfg ;
2020-01-20 17:13:47 -05:00
struct playlist_info pli =
{
2020-02-04 17:15:56 -05:00
. path = strdup ( " spotify:playlistfolder " ) ,
. title = strdup ( " Spotify " ) ,
2020-01-20 17:13:47 -05:00
. type = PL_FOLDER ,
} ;
2018-03-02 13:24:35 -05:00
spotify_base_plid = 0 ;
spotify_cfg = cfg_getsec ( cfg , " spotify " ) ;
2020-01-20 17:13:47 -05:00
if ( cfg_getbool ( spotify_cfg , " base_playlist_disable " ) )
2020-02-04 17:15:56 -05:00
{
free_pli ( & pli , 1 ) ;
return ;
}
2020-01-20 17:13:47 -05:00
2020-02-04 17:15:56 -05:00
spotify_base_plid = playlist_add_or_update ( & pli ) ;
2020-01-20 17:13:47 -05:00
if ( spotify_base_plid < 0 )
2018-03-02 13:24:35 -05:00
{
2020-01-20 17:13:47 -05:00
DPRINTF ( E_LOG , L_SPOTIFY , " Error adding base playlist \n " ) ;
spotify_base_plid = 0 ;
2018-03-02 13:24:35 -05:00
}
2020-02-04 17:15:56 -05:00
free_pli ( & pli , 1 ) ;
2018-03-02 13:24:35 -05:00
}
static void
scan ( )
{
2018-03-17 07:17:15 -04:00
time_t start ;
time_t end ;
2018-03-09 11:16:36 -05:00
if ( ! token_valid ( ) | | scanning )
2018-03-02 13:24:35 -05:00
{
DPRINTF ( E_DBG , L_SPOTIFY , " No valid web api token or scan already in progress, rescan ignored \n " ) ;
2018-03-09 11:16:36 -05:00
return ;
2018-03-02 13:24:35 -05:00
}
2018-03-09 11:16:36 -05:00
2018-03-17 07:17:15 -04:00
start = time ( NULL ) ;
2018-03-09 11:16:36 -05:00
scanning = true ;
db_directory_enable_bypath ( " /spotify: " ) ;
create_base_playlist ( ) ;
create_saved_tracks_playlist ( ) ;
scan_saved_albums ( ) ;
scan_playlists ( ) ;
scanning = false ;
2018-03-17 07:17:15 -04:00
end = time ( NULL ) ;
DPRINTF ( E_LOG , L_SPOTIFY , " Spotify scan completed in %.f sec \n " , difftime ( end , start ) ) ;
2018-03-02 13:24:35 -05:00
}
/* Thread: library */
static int
initscan ( )
{
int ret ;
/* Refresh access token for the spotify webapi */
2018-03-09 11:16:36 -05:00
ret = token_refresh ( ) ;
2018-03-02 13:24:35 -05:00
if ( ret < 0 )
{
DPRINTF ( E_LOG , L_SPOTIFY , " Spotify webapi token refresh failed. "
" In order to use the web api, authorize forked-daapd to access "
" your saved tracks by visiting http://forked-daapd.local:3689 \n " ) ;
db_spotify_purge ( ) ;
return 0 ;
}
spotify_saved_plid = 0 ;
/*
* Login to spotify needs to be done before scanning tracks from the web api .
* ( Scanned tracks need to be registered with libspotify for playback )
*/
ret = spotify_relogin ( ) ;
if ( ret < 0 )
{
DPRINTF ( E_LOG , L_SPOTIFY , " libspotify-login failed. In order to use Spotify, "
" provide valid credentials for libspotify by visiting http://forked-daapd.local:3689 \n " ) ;
db_spotify_purge ( ) ;
return 0 ;
}
/*
* Scan saved tracks from the web api
*/
scan ( ) ;
return 0 ;
}
/* Thread: library */
static int
rescan ( )
{
scan ( ) ;
return 0 ;
}
/* Thread: library */
static int
fullrescan ( )
{
db_spotify_purge ( ) ;
scan ( ) ;
return 0 ;
}
/* Thread: library */
static enum command_state
webapi_fullrescan ( void * arg , int * ret )
{
* ret = fullrescan ( ) ;
return COMMAND_END ;
}
/* Thread: library */
static enum command_state
webapi_rescan ( void * arg , int * ret )
{
* ret = rescan ( ) ;
return COMMAND_END ;
}
/* Thread: library */
static enum command_state
webapi_pl_save ( void * arg , int * ret )
{
2018-03-09 11:16:36 -05:00
const char * uri ;
char * endpoint_uri ;
json_object * response ;
uri = arg ;
endpoint_uri = get_playlist_endpoint_uri ( uri ) ;
response = request_endpoint_with_token_refresh ( endpoint_uri ) ;
if ( ! response )
{
* ret = - 1 ;
goto out ;
}
2018-03-17 07:17:15 -04:00
* ret = saved_playlist_add ( response , 0 , 1 , NULL ) ;
2018-03-09 11:16:36 -05:00
jparse_free ( response ) ;
out :
free ( endpoint_uri ) ;
2018-03-02 13:24:35 -05:00
return COMMAND_END ;
}
/* Thread: library */
static enum command_state
webapi_pl_remove ( void * arg , int * ret )
{
2018-03-09 11:16:36 -05:00
const char * uri ;
struct playlist_info * pli ;
int plid ;
uri = arg ;
pli = db_pl_fetch_bypath ( uri ) ;
if ( ! pli )
{
DPRINTF ( E_LOG , L_SPOTIFY , " Playlist '%s' not found, can't delete \n " , uri ) ;
* ret = - 1 ;
return COMMAND_END ;
}
DPRINTF ( E_LOG , L_SPOTIFY , " Removing playlist '%s' (%s) \n " , pli - > title , uri ) ;
plid = pli - > id ;
free_pli ( pli , 0 ) ;
db_spotify_pl_delete ( plid ) ;
cleanup_spotify_files ( ) ;
* ret = 0 ;
2018-03-02 13:24:35 -05:00
return COMMAND_END ;
}
void
spotifywebapi_fullrescan ( void )
{
library_exec_async ( webapi_fullrescan , NULL ) ;
}
void
spotifywebapi_rescan ( void )
{
library_exec_async ( webapi_rescan , NULL ) ;
}
void
spotifywebapi_pl_save ( const char * uri )
{
if ( scanning | | ! token_valid ( ) )
{
DPRINTF ( E_DBG , L_SPOTIFY , " Scanning spotify saved tracks still in progress, ignoring update trigger for single playlist '%s' \n " , uri ) ;
return ;
}
library_exec_async ( webapi_pl_save , strdup ( uri ) ) ;
}
void
spotifywebapi_pl_remove ( const char * uri )
{
if ( scanning | | ! token_valid ( ) )
{
DPRINTF ( E_DBG , L_SPOTIFY , " Scanning spotify saved tracks still in progress, ignoring remove trigger for single playlist '%s' \n " , uri ) ;
return ;
}
library_exec_async ( webapi_pl_remove , strdup ( uri ) ) ;
}
2019-02-05 05:35:45 -05:00
char *
spotifywebapi_artwork_url_get ( const char * uri , int max_w , int max_h )
{
json_object * response ;
struct spotify_track track ;
char * artwork_url ;
response = request_track ( uri ) ;
if ( ! response )
{
return NULL ;
}
parse_metadata_track ( response , & track , max_w ) ;
DPRINTF ( E_DBG , L_SPOTIFY , " Got track artwork url: '%s' (%s) \n " , track . artwork_url , track . uri ) ;
artwork_url = safe_strdup ( track . artwork_url ) ;
jparse_free ( response ) ;
return artwork_url ;
}
2018-03-02 13:24:35 -05:00
void
spotifywebapi_status_info_get ( struct spotifywebapi_status_info * info )
{
memset ( info , 0 , sizeof ( struct spotifywebapi_status_info ) ) ;
CHECK_ERR ( L_SPOTIFY , pthread_mutex_lock ( & token_lck ) ) ;
info - > token_valid = token_valid ( ) ;
if ( spotify_user )
{
2019-08-03 06:55:53 -04:00
strncpy ( info - > user , spotify_user , ( sizeof ( info - > user ) - 1 ) ) ;
2018-03-02 13:24:35 -05:00
}
2018-03-17 07:21:24 -04:00
if ( spotify_user_country )
{
2019-08-03 06:55:53 -04:00
strncpy ( info - > country , spotify_user_country , ( sizeof ( info - > country ) - 1 ) ) ;
}
if ( spotify_granted_scope )
{
strncpy ( info - > granted_scope , spotify_granted_scope , ( sizeof ( info - > granted_scope ) - 1 ) ) ;
}
if ( spotify_scope )
{
strncpy ( info - > required_scope , spotify_scope , ( sizeof ( info - > required_scope ) - 1 ) ) ;
2018-03-17 07:21:24 -04:00
}
CHECK_ERR ( L_SPOTIFY , pthread_mutex_unlock ( & token_lck ) ) ;
}
void
spotifywebapi_access_token_get ( struct spotifywebapi_access_token * info )
{
token_refresh ( ) ;
memset ( info , 0 , sizeof ( struct spotifywebapi_access_token ) ) ;
CHECK_ERR ( L_SPOTIFY , pthread_mutex_lock ( & token_lck ) ) ;
if ( token_requested > 0 )
info - > expires_in = expires_in - difftime ( time ( NULL ) , token_requested ) ;
else
info - > expires_in = 0 ;
info - > token = safe_strdup ( spotify_access_token ) ;
2018-03-02 13:24:35 -05:00
CHECK_ERR ( L_SPOTIFY , pthread_mutex_unlock ( & token_lck ) ) ;
}
static int
spotifywebapi_init ( )
{
int ret ;
CHECK_ERR ( L_SPOTIFY , mutex_init ( & token_lck ) ) ;
ret = spotify_init ( ) ;
2017-02-05 07:56:12 -05:00
return ret ;
}
2018-03-02 13:24:35 -05:00
static void
spotifywebapi_deinit ( )
{
CHECK_ERR ( L_SPOTIFY , pthread_mutex_destroy ( & token_lck ) ) ;
spotify_deinit ( ) ;
2019-08-03 06:55:53 -04:00
free ( spotify_access_token ) ;
free ( spotify_refresh_token ) ;
free ( spotify_granted_scope ) ;
free ( spotify_user_country ) ;
free ( spotify_user ) ;
2018-03-02 13:24:35 -05:00
}
struct library_source spotifyscanner =
{
. name = " spotifyscanner " ,
. disabled = 0 ,
. init = spotifywebapi_init ,
. deinit = spotifywebapi_deinit ,
. rescan = rescan ,
2020-02-06 13:08:46 -05:00
. metarescan = rescan ,
2018-03-02 13:24:35 -05:00
. initscan = initscan ,
. fullrescan = fullrescan ,
2020-01-20 17:13:47 -05:00
. queue_item_add = queue_item_add ,
2018-03-02 13:24:35 -05:00
} ;