diff --git a/src/spotify.c b/src/spotify.c index ccb94461..fedc0945 100644 --- a/src/spotify.c +++ b/src/spotify.c @@ -47,6 +47,7 @@ #include #include "spotify.h" +#include "spotify_webapi.h" #include "logger.h" #include "misc.h" #include "http.h" @@ -132,19 +133,6 @@ struct artwork_get_param int is_loaded; }; -struct pending_metadata -{ - sp_link *link; - sp_track *track; - struct pending_metadata *next; -}; - -struct reload_list -{ - char *uri; - struct reload_list *next; -}; - /* --- Globals --- */ // Spotify thread static pthread_t tid_spotify; @@ -170,10 +158,6 @@ static enum spotify_state g_state; static int spotify_base_plid; // The base playlist id for Spotify saved tracks in the db static int spotify_saved_plid; -// Linked list of tracks where we are waiting for metadata -static struct pending_metadata *spotify_pending_metadata; -// Linked list of saved tracks which we want to reload at startup -static struct reload_list *spotify_reload_list; // Audio fifo static audio_fifo_t *g_audio_fifo; @@ -206,15 +190,6 @@ const uint8_t g_appkey[] = { 0x09, }; -// Endpoints and credentials for the web api -static char *spotify_access_token; -static char *spotify_refresh_token; -static const char *spotify_client_id = "0e684a5422384114a8ae7ac020f01789"; -static const char *spotify_client_secret = "232af95f39014c9ba218285a5c11a239"; -static const char *spotify_auth_uri = "https://accounts.spotify.com/authorize"; -static const char *spotify_token_uri = "https://accounts.spotify.com/api/token"; -static const char *spotify_tracks_uri = "https://api.spotify.com/v1/me/tracks?limit=50"; - // This section defines and assigns function pointers to the libspotify functions // The arguments and return values must be in sync with the spotify api // Please scroll through the ugliness which follows @@ -937,11 +912,8 @@ spotify_playlist_save(sp_playlist *pl) static enum command_state spotify_uri_register(void *arg, int *retval) { - struct playlist_info pli; - struct pending_metadata *pm; sp_link *link; sp_track *track; - int ret; char *uri = arg; @@ -952,32 +924,6 @@ spotify_uri_register(void *arg, int *retval) return COMMAND_END; } - // Must have playlist for these items, otherwise spotify_cleanup_files will delete them again - if (!spotify_saved_plid) - { - memset(&pli, 0, sizeof(struct playlist_info)); - pli.title = "Spotify Saved"; - pli.type = PL_PLAIN; - pli.path = "spotify:savedtracks"; - pli.virtual_path = "/spotify:/Spotify Saved"; - - ret = db_pl_add(&pli, &spotify_saved_plid); - if (ret < 0) - { - DPRINTF(E_LOG, L_SPOTIFY, "Error adding playlist for saved tracks\n"); - *retval = -1; - return COMMAND_END; - } - } - - ret = db_pl_add_item_bypath(spotify_saved_plid, uri); - if (ret < 0) - { - DPRINTF(E_LOG, L_SPOTIFY, "Could not add '%s' to spotify:savedtracks\n", uri); - *retval = -1; - return COMMAND_END; - } - link = fptr_sp_link_create_from_string(uri); if (!link) { @@ -994,358 +940,10 @@ spotify_uri_register(void *arg, int *retval) return COMMAND_END; } - // Maybe we already had the track - if (fptr_sp_track_is_loaded(track)) - { - db_file_ping_bymatch(uri, 0); - fptr_sp_link_release(link); - *retval = 0; - return COMMAND_END; - } - - pm = malloc(sizeof(struct pending_metadata)); - if (!pm) - { - DPRINTF(E_LOG, L_SPOTIFY, "Out of memory\n"); - *retval = -1; - return COMMAND_END; - } - - pm->link = link; - pm->track = track; - pm->next = spotify_pending_metadata; - spotify_pending_metadata = pm; - *retval = 0; return COMMAND_END; } -// TODO Maybe use the commands bh instead? -static enum command_state -spotify_pending_process(void *arg, int *retval) -{ - struct pending_metadata *pm; - int i; - - *retval = 0; - if (!spotify_pending_metadata) - return COMMAND_END; - - // Too early - i = 0; - for (pm = spotify_pending_metadata; pm; pm = pm->next) - { - i++; - - if (!fptr_sp_track_is_loaded(pm->track)) - return COMMAND_END; - } - - DPRINTF(E_DBG, L_SPOTIFY, "All %d tracks loaded, now saving\n", i); - - while ((pm = spotify_pending_metadata)) - { - spotify_track_save(0, pm->track, NULL, time(NULL)); - - // Not sure if we should release link here? We are done with it, but maybe - // libspotify will unload the track if we release, and we don't want that - //fptr_sp_link_release(pm->link); - - spotify_pending_metadata = pm->next; - free(pm); - } - - return COMMAND_END; -} - -static enum command_state -spotify_saved_pl_clear_items(void *arg, int *retval) -{ - if (spotify_saved_plid) - db_pl_clear_items(spotify_saved_plid); - - - *retval = 0; - - return COMMAND_END; -} - -static enum command_state -spotify_cleanup_wrapper(void *arg, int *retval) -{ - *retval = spotify_cleanup_files(); - - return COMMAND_END; -} - -/*--------------------- HELPERS FOR SPOTIFY WEB API -------------------------*/ -/* All the below is in the httpd thread */ - -static char * -jparse_str_from_obj(json_object *haystack, const char *key) -{ - json_object *needle; - - if (json_object_object_get_ex(haystack, key, &needle) && json_object_get_type(needle) == json_type_string) - return strdup(json_object_get_string(needle)); - else - return NULL; -} - -static char * -jparse_str_from_str(const char *s, const char *key) -{ - json_object *haystack; - char *val; - - haystack = json_tokener_parse(s); - if (!haystack) - { - DPRINTF(E_LOG, L_SPOTIFY, "JSON parser returned an error\n"); - return NULL; - } - - val = jparse_str_from_obj(haystack, key); - -#ifdef HAVE_JSON_C_OLD - json_object_put(haystack); -#else - if (json_object_put(haystack) != 1) - DPRINTF(E_LOG, L_SPOTIFY, "Memleak: JSON parser did not free object\n"); -#endif - - return val; -} - -// Will find all track Spotify uri's and register them with libspotify. -// Returns the number of tracks found in the json input. "total" will be the -// total reported by Spotify in the response, and "next" will be an allocated -// string with the url of the next page, as reported by Spotify -static int -jparse_and_register_tracks(int *total, char **next, const char *s) -{ - json_object *haystack; - json_object *needle; - json_object *items; - json_object *item; - json_object *track; - char *uri; - int ret; - int len; - int i; - - haystack = json_tokener_parse(s); - if (!haystack) - { - DPRINTF(E_LOG, L_SPOTIFY, "JSON parser returned an error\n"); - return -1; - } - - if (json_object_object_get_ex(haystack, "total", &needle) && json_object_get_type(needle) == json_type_int) - *total = json_object_get_int(needle); - else - *total = -1; - - *next = jparse_str_from_obj(haystack, "next"); - - if (! (json_object_object_get_ex(haystack, "items", &items) && json_object_get_type(items) == json_type_array) ) - { - DPRINTF(E_LOG, L_SPOTIFY, "No items in reply from Spotify. See:\n%s\n", s); - ret = -1; - goto out_free_json; - } - - len = json_object_array_length(items); - - DPRINTF(E_DBG, L_SPOTIFY, "Got %d saved tracks\n", len); - - for (i = 0; i < len; i++) - { - item = json_object_array_get_idx(items, i); - if (! (item && json_object_object_get_ex(item, "track", &track) - && (uri = jparse_str_from_obj(track, "uri")) )) - { - DPRINTF(E_LOG, L_SPOTIFY, "Unexpected JSON: Item %d did not have 'track'->'uri'\n", i); - len--; - continue; - } - - commands_exec_sync(cmdbase, spotify_uri_register, NULL, uri); - - free(uri); - } - - ret = len; - - out_free_json: -#ifdef HAVE_JSON_C_OLD - json_object_put(haystack); -#else - if (json_object_put(haystack) != 1) - DPRINTF(E_LOG, L_SPOTIFY, "Memleak: JSON parser did not free object\n"); -#endif - - return ret; -} - -static int -tokens_get(const char *code, const char *redirect_uri, const char **err) -{ - struct http_client_ctx ctx; - struct keyval kv; - char *param; - char *body; - int ret; - - 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) ); - if (!ret) - { - *err = "Add parameters to keyval failed"; - ret = -1; - goto out_clear_kv; - } - - param = http_form_urlencode(&kv); - if (!param) - { - *err = "http_form_uriencode() failed"; - ret = -1; - goto out_clear_kv; - } - - memset(&ctx, 0, sizeof(struct http_client_ctx)); - ctx.url = (char *)spotify_token_uri; - ctx.output_body = param; - ctx.input_body = evbuffer_new(); - - ret = http_client_request(&ctx); - if (ret < 0) - { - *err = "Did not get a reply from Spotify"; - goto out_free_input_body; - } - - // 0-terminate for safety - evbuffer_add(ctx.input_body, "", 1); - - body = (char *)evbuffer_pullup(ctx.input_body, -1); - if (!body || (strlen(body) == 0)) - { - *err = "The reply from Spotify is empty or invalid"; - ret = -1; - goto out_free_input_body; - } - - spotify_access_token = jparse_str_from_str(body, "access_token"); - spotify_refresh_token = jparse_str_from_str(body, "refresh_token"); - - if (!spotify_access_token || !spotify_refresh_token) - { - DPRINTF(E_LOG, L_SPOTIFY, "Could not find token in reply: %s\n", body); - - *err = "Could not find token in Spotify reply (see log)"; - ret = -1; - goto out_free_input_body; - } - - ret = 0; - - out_free_input_body: - evbuffer_free(ctx.input_body); - free(param); - out_clear_kv: - keyval_clear(&kv); - - return ret; -} - -static int -saved_tracks_get(int *total, const char **err, const char *uri) -{ - struct http_client_ctx ctx; - struct keyval kv; - char bearer_token[1024]; - char *body; - char *next; - int ret; - int i; - - *total = -1; - - snprintf(bearer_token, sizeof(bearer_token), "Bearer %s", spotify_access_token); - - memset(&kv, 0, sizeof(struct keyval)); - if (keyval_add(&kv, "Authorization", bearer_token) < 0) - { - *err = "Add bearer_token to keyval failed"; - return -1; - } - - memset(&ctx, 0, sizeof(struct http_client_ctx)); - ctx.output_headers = &kv; - ctx.input_body = evbuffer_new(); - ctx.url = uri; - - next = NULL; - for (i = 0; i < SPOTIFY_WEB_REQUESTS_MAX; i++) - { - ret = http_client_request(&ctx); - if (ret < 0) - { - *err = "Request for saved tracks/albums failed"; - break; - } - - // 0-terminate for safety - evbuffer_add(ctx.input_body, "", 1); - - body = (char *)evbuffer_pullup(ctx.input_body, -1); - if (!body || (strlen(body) == 0)) - { - *err = "Request for saved tracks/albums failed, response was empty"; - ret = -1; - break; - } - - if (next) - free(next); - next = NULL; - - if (uri == spotify_tracks_uri) - ret = jparse_and_register_tracks(total, &next, body); - else - ret = -1; - - if (ret < 0) - { - *err = "Could not parse track/album response from Spotify"; - break; - } - - ret += 50 * i; // Equals total number of tracks/albums registered - - if (!next || (strncmp(next, "null", 4) == 0)) - break; - - ctx.url = next; - - evbuffer_drain(ctx.input_body, evbuffer_get_length(ctx.input_body)); - } - - if (next) - free(next); - - evbuffer_free(ctx.input_body); - keyval_clear(&kv); - - return ret; -} - /* -------------------------- PLAYLIST CALLBACKS ------------------------- */ /** @@ -2105,8 +1703,6 @@ notify_main_thread(sp_session *sess) static void metadata_updated(sp_session *session) { DPRINTF(E_DBG, L_SPOTIFY, "Session metadata updated\n"); - - commands_exec_async(cmdbase, spotify_pending_process, NULL); } /* Misc connection error callbacks */ @@ -2119,20 +1715,9 @@ static void play_token_lost(sp_session *sess) static void connectionstate_updated(sp_session *session) { - struct reload_list *reload; - int ret; - if (SP_CONNECTION_STATE_LOGGED_IN == fptr_sp_session_connectionstate(session)) { DPRINTF(E_LOG, L_SPOTIFY, "Connection to Spotify (re)established, reloading saved tracks\n"); - - while ((reload = spotify_reload_list)) - { - spotify_uri_register(reload->uri, &ret); - spotify_reload_list = reload->next; - free(reload->uri); - free(reload); - } } else if (g_state == SPOTIFY_STATE_PLAYING) { @@ -2186,42 +1771,6 @@ static sp_session_config spconfig = { /* ------------------------------- MAIN LOOP ------------------------------- */ /* Thread: spotify */ -static struct reload_list * -reload_list_create(int plid) -{ - struct query_params qp; - struct db_media_file_info dbmfi; - struct reload_list *head; - struct reload_list *reload; - int ret; - - memset(&qp, 0, sizeof(struct query_params)); - - qp.type = Q_PLITEMS; - qp.sort = S_NONE; - qp.id = plid; - - ret = db_query_start(&qp); - if (ret < 0) - { - db_query_end(&qp); - return NULL; - } - - head = NULL; - while (((ret = db_query_fetch_file(&qp, &dbmfi)) == 0) && (dbmfi.path)) - { - reload = malloc(sizeof(struct reload_list)); - reload->uri = strdup(dbmfi.path); - reload->next = head; - head = reload; - } - - db_query_end(&qp); - - return head; -} - static void * spotify(void *arg) { @@ -2416,35 +1965,18 @@ spotify_artwork_get(struct evbuffer *evbuf, char *path, int max_w, int max_h) void spotify_oauth_interface(struct evbuffer *evbuf, const char *redirect_uri) { - struct keyval kv; - char *param; - int ret; + char *uri; - memset(&kv, 0, sizeof(struct keyval)); - 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) && - (keyval_add(&kv, "scope", "playlist-read-private user-library-read") == 0) && - (keyval_add(&kv, "show_dialog", "false") == 0) ); - if (!ret) - { - DPRINTF(E_LOG, L_SPOTIFY, "Cannot display Spotify oath interface (error adding parameters to keyval)\n"); - goto out_clear_kv; - } - - param = http_form_urlencode(&kv); - if (!param) + uri = spotifywebapi_oauth_uri_get(redirect_uri); + if (!uri) { DPRINTF(E_LOG, L_SPOTIFY, "Cannot display Spotify oath interface (http_form_uriencode() failed)\n"); - goto out_clear_kv; + return; } - evbuffer_add_printf(evbuf, "Click here to authorize forked-daapd with Spotify\n", spotify_auth_uri, param); + evbuffer_add_printf(evbuf, "Click here to authorize forked-daapd with Spotify\n", uri); - free(param); - - out_clear_kv: - keyval_clear(&kv); + free(uri); } /* Thread: httpd */ @@ -2453,7 +1985,6 @@ spotify_oauth_callback(struct evbuffer *evbuf, struct evkeyvalq *param, const ch { const char *code; const char *err; - int total; int ret; code = evhttp_find_header(param, "code"); @@ -2467,42 +1998,24 @@ spotify_oauth_callback(struct evbuffer *evbuf, struct evkeyvalq *param, const ch evbuffer_add_printf(evbuf, "

Requesting access token from Spotify...\n"); - ret = tokens_get(code, redirect_uri, &err); + ret = spotifywebapi_token_get(code, redirect_uri, &err); if (ret < 0) { evbuffer_add_printf(evbuf, "failed

\n

Error: %s

\n", err); return; } - commands_exec_sync(cmdbase, spotify_saved_pl_clear_items, NULL, NULL); - - evbuffer_add_printf(evbuf, "ok

\n

Retrieving saved tracks...\n"); - - ret = saved_tracks_get(&total, &err, spotify_tracks_uri); - if (ret < 0) - { - evbuffer_add_printf(evbuf, "failed

\n

Error: %s

\n", err); - return; - } - - evbuffer_add_printf(evbuf, "ok, got %d out of %d tracks

\n", ret, total); - - evbuffer_add_printf(evbuf, "

Purging removed tracks/albums...\n"); - - // TODO release links to the items we are going to clean up - - commands_exec_sync(cmdbase, spotify_cleanup_wrapper, NULL, NULL); + // TODO Trigger init-scan after successful access to spotifywebapi evbuffer_add_printf(evbuf, "ok, all done

\n"); return; } -/* Thread: filescanner */ +/* Thread: library */ void spotify_login(char *path) { - struct playlist_info *pli; sp_error err; char *username; char *password; @@ -2555,13 +2068,6 @@ spotify_login(char *path) } else { - pli = db_pl_fetch_bypath("spotify:savedtracks"); - if (pli) - { - spotify_reload_list = reload_list_create(pli->id); - free_pli(pli, 0); - } - db_spotify_purge(); spotify_saved_plid = 0;