/* * Copyright (C) 2016 Espen Jürgensen * Copyright (C) 2016 Christian Meffert * * 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 #include #include #include #include #include #include "artwork.h" #include "cache.h" #include "conffile.h" #include "db.h" #include "http.h" #include "library.h" #include "listener.h" #include "logger.h" #include "misc_json.h" #include "spotify.h" 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; const char *artwork_url; }; 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; const char *artwork_url; 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; }; // Credentials for the web api static char *spotify_access_token; static char *spotify_refresh_token; static char *spotify_granted_scope; static char *spotify_user_country; static char *spotify_user; static int32_t expires_in = 3600; static time_t token_requested = 0; // Mutex to avoid conflicting requests for access tokens and protects accessing the credentials from different threads 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; // Endpoints and credentials for the web api static const char *spotify_client_id = "0e684a5422384114a8ae7ac020f01789"; static const char *spotify_client_secret = "232af95f39014c9ba218285a5c11a239"; static const char *spotify_scope = "playlist-read-private playlist-read-collaborative user-library-read user-read-private"; 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_playlist_uri = "https://api.spotify.com/v1/playlists/%s"; static const char *spotify_track_uri = "https://api.spotify.com/v1/tracks/%s"; static const char *spotify_me_uri = "https://api.spotify.com/v1/me"; static const char *spotify_albums_uri = "https://api.spotify.com/v1/me/albums?limit=50"; 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"; static const char *spotify_playlists_uri = "https://api.spotify.com/v1/me/playlists?limit=50"; static const char *spotify_playlist_tracks_uri = "https://api.spotify.com/v1/playlists/%s/tracks"; static const char *spotify_artist_albums_uri = "https://api.spotify.com/v1/artists/%s/albums?include_groups=album,single"; static void free_http_client_ctx(struct http_client_ctx *ctx) { 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); } static bool token_valid(void) { return spotify_access_token != NULL; } static int request_access_tokens(struct keyval *kv, const char **err) { struct http_client_ctx ctx; char *param; char *body; json_object *haystack; const char *tmp; int ret; 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; } DPRINTF(E_DBG, L_SPOTIFY, "Token reply: %s\n", body); haystack = json_tokener_parse(body); if (!haystack) { *err = "JSON parser returned an error"; ret = -1; goto out_free_input_body; } free(spotify_access_token); spotify_access_token = NULL; tmp = jparse_str_from_obj(haystack, "access_token"); if (tmp) spotify_access_token = strdup(tmp); tmp = jparse_str_from_obj(haystack, "refresh_token"); if (tmp) { free(spotify_refresh_token); spotify_refresh_token = strdup(tmp); } tmp = jparse_str_from_obj(haystack, "scope"); if (tmp) { free(spotify_granted_scope); spotify_granted_scope = strdup(tmp); } expires_in = jparse_int_from_obj(haystack, "expires_in"); if (expires_in == 0) expires_in = 3600; jparse_free(haystack); if (!spotify_access_token) { DPRINTF(E_LOG, L_SPOTIFY, "Could not find access token in reply: %s\n", body); *err = "Could not find access token in Spotify reply (see log)"; ret = -1; goto out_free_input_body; } token_requested = time(NULL); if (spotify_refresh_token) db_admin_set(DB_ADMIN_SPOTIFY_REFRESH_TOKEN, spotify_refresh_token); ret = 0; out_free_input_body: evbuffer_free(ctx.input_body); free(param); out_clear_kv: return ret; } /* * 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) { struct http_client_ctx *ctx; char bearer_token[1024]; char *response_body; json_object *json_response = NULL; int ret; ctx = calloc(1, sizeof(struct http_client_ctx)); ctx->output_headers = calloc(1, sizeof(struct keyval)); ctx->input_body = evbuffer_new(); ctx->url = uri; snprintf(bearer_token, sizeof(bearer_token), "Bearer %s", spotify_access_token); if (keyval_add(ctx->output_headers, "Authorization", bearer_token) < 0) { DPRINTF(E_LOG, L_SPOTIFY, "Add bearer_token to keyval failed for request '%s'\n", uri); goto out; } DPRINTF(E_DBG, L_SPOTIFY, "Request Spotify API endpoint: '%s')\n", uri); ret = http_client_request(ctx); if (ret < 0) { DPRINTF(E_LOG, L_SPOTIFY, "Request for '%s' failed\n", uri); goto out; } // 0-terminate for safety evbuffer_add(ctx->input_body, "", 1); response_body = (char *) evbuffer_pullup(ctx->input_body, -1); if (!response_body || (strlen(response_body) == 0)) { DPRINTF(E_LOG, L_SPOTIFY, "Request for '%s' failed, response was empty\n", uri); goto out; } // DPRINTF(E_DBG, L_SPOTIFY, "Wep api response for '%s'\n%s\n", uri, response_body); 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); out: free_http_client_ctx(ctx); return json_response; } /* * Request user information * * API endpoint: https://api.spotify.com/v1/me */ static int request_user_info(void) { json_object *response; free(spotify_user_country); spotify_user_country = NULL; free(spotify_user); spotify_user = NULL; response = request_endpoint(spotify_me_uri); if (response) { spotify_user = safe_strdup(jparse_str_from_obj(response, "id")); spotify_user_country = safe_strdup(jparse_str_from_obj(response, "country")); jparse_free(response); DPRINTF(E_DBG, L_SPOTIFY, "User '%s', country '%s'\n", spotify_user, spotify_user_country); } return 0; } /* * Called from the oauth callback to get a new access and refresh token * * @return 0 on success, -1 on failure */ static int token_get(const char *code, const char *redirect_uri, const char **err) { struct keyval kv; int ret; CHECK_ERR(L_SPOTIFY, pthread_mutex_lock(&token_lck)); *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) ); if (!ret) { *err = "Add parameters to keyval failed"; ret = -1; } else ret = request_access_tokens(&kv, err); keyval_clear(&kv); if (ret == 0) request_user_info(); CHECK_ERR(L_SPOTIFY, pthread_mutex_unlock(&token_lck)); return ret; } /* * 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) { struct keyval kv; char *refresh_token = NULL; const char *err; int ret; memset(&kv, 0, sizeof(struct keyval)); CHECK_ERR(L_SPOTIFY, pthread_mutex_lock(&token_lck)); if (token_requested && difftime(time(NULL), token_requested) < expires_in) { DPRINTF(E_DBG, L_SPOTIFY, "Spotify token still valid\n"); CHECK_ERR(L_SPOTIFY, pthread_mutex_unlock(&token_lck)); return 0; } ret = db_admin_get(&refresh_token, DB_ADMIN_SPOTIFY_REFRESH_TOKEN); if (ret < 0) { DPRINTF(E_LOG, L_SPOTIFY, "No spotify refresh token found\n"); goto error; } DPRINTF(E_DBG, L_SPOTIFY, "Spotify refresh-token: '%s'\n", refresh_token); 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) { DPRINTF(E_LOG, L_SPOTIFY, "Add parameters to keyval failed"); goto error; } ret = request_access_tokens(&kv, &err); if (ret == 0) request_user_info(); 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; } /* * Request the api endpoint at 'href' and retuns the response body as * an allocated JSON object (must be freed by the caller) or NULL. * * 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 */ static json_object * request_endpoint_with_token_refresh(const char *href) { if (0 > token_refresh()) { return NULL; } return request_endpoint(href); } typedef int (*paging_request_cb)(void *arg); typedef int (*paging_item_cb)(json_object *item, int index, int total, void *arg); /* * 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) * @param with_market If TRUE appends the user country as market to the request (applies track relinking) * @param arg User data passed to each callback * @return 0 on success, -1 on failure */ static int 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) { char *next_href; json_object *response; json_object *items; json_object *item; int count; int i; int offset; int total; int ret; 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); } while (next_href) { if (pre_request_cb) pre_request_cb(arg); response = request_endpoint_with_token_refresh(next_href); if (!response) { DPRINTF(E_LOG, L_SPOTIFY, "Unexpected JSON: no response for paging endpoint (API endpoint: '%s')\n", next_href); if (post_request_cb) post_request_cb(arg); free(next_href); return -1; } free(next_href); next_href = safe_strdup(jparse_str_from_obj(response, "next")); offset = jparse_int_from_obj(response, "offset"); total = jparse_int_from_obj(response, "total"); 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; } ret = item_cb(item, (i + offset), total, arg); 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); } } } if (post_request_cb) post_request_cb(arg); jparse_free(response); } return 0; } static const char * get_album_image(json_object *jsonalbum, int max_w) { json_object *jsonimages; json_object *jsonimage; int image_count; int index; const char *artwork_url; artwork_url = NULL; 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) // Special case is if no max width (max_w = 0) is given, the widest images will be used 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"); if (max_w <= 0 || jparse_int_from_obj(jsonimage, "width") <= max_w) { // We have the first image that has a smaller width than the given max_w break; } } } return artwork_url; } static void parse_metadata_track(json_object *jsontrack, struct spotify_track *track, int max_w) { json_object *jsonalbum; json_object *jsonartists; json_object *needle; memset(track, 0, sizeof(struct spotify_track)); if (json_object_object_get_ex(jsontrack, "album", &jsonalbum)) { 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"); track->artwork_url = get_album_image(jsonalbum, max_w); } 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"); // "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"); 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"); } } static int get_year_from_date(const char *date) { char tmp[5]; uint32_t year = 0; if (date && strlen(date) >= 4) { strncpy(tmp, date, sizeof(tmp)); tmp[4] = '\0'; safe_atou32(tmp, &year); } return year; } static void parse_metadata_album(json_object *jsonalbum, struct spotify_album *album, int max_w) { json_object* jsonartists; memset(album, 0, sizeof(struct spotify_album)); if (json_object_object_get_ex(jsonalbum, "artists", &jsonartists)) album->artist = jparse_str_from_array(jsonartists, 0, "name"); album->name = jparse_str_from_obj(jsonalbum, "name"); album->uri = jparse_str_from_obj(jsonalbum, "uri"); album->id = jparse_str_from_obj(jsonalbum, "id"); album->album_type = jparse_str_from_obj(jsonalbum, "album_type"); album->is_compilation = (album->album_type && 0 == strcmp(album->album_type, "compilation")); album->label = jparse_str_from_obj(jsonalbum, "label"); 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); if (max_w > 0) album->artwork_url = get_album_image(jsonalbum, max_w); // 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"); } static void parse_metadata_playlist(json_object *jsonplaylist, struct spotify_playlist *playlist) { json_object *needle; memset(playlist, 0, sizeof(struct spotify_playlist)); 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"); if (json_object_object_get_ex(jsonplaylist, "owner", &needle)) playlist->owner = jparse_str_from_obj(needle, "id"); if (json_object_object_get_ex(jsonplaylist, "tracks", &needle)) { playlist->tracks_href = jparse_str_from_obj(needle, "href"); playlist->tracks_count = jparse_int_from_obj(needle, "total"); } } /* * 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") */ 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; } static char * get_playlist_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 owner and id from playlist uri '%s'\n", uri); goto out; } endpoint_uri = safe_asprintf(spotify_playlist_uri, id); out: free(id); return endpoint_uri; } static char * get_playlist_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 owner and id from playlist uri '%s'\n", uri); goto out; } endpoint_uri = safe_asprintf(spotify_playlist_tracks_uri, id); 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; } 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; } 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; } /* Thread: httpd */ char * spotifywebapi_oauth_uri_get(const char *redirect_uri) { struct keyval kv; char *param; char *uri; int uri_len; int ret; uri = NULL; 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", spotify_scope) == 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_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); } out_clear_kv: keyval_clear(&kv); return uri; } /* Thread: httpd */ int spotifywebapi_oauth_callback(struct evkeyvalq *param, const char *redirect_uri, char **errmsg) { const char *code; const char *err; int ret; *errmsg = NULL; code = evhttp_find_header(param, "code"); if (!code) { *errmsg = safe_asprintf("Error: Didn't receive a code from Spotify"); return -1; } DPRINTF(E_DBG, L_SPOTIFY, "Received OAuth code: %s\n", code); ret = token_get(code, redirect_uri, &err); if (ret < 0) { *errmsg = safe_asprintf("Error: %s", err); return -1; } // Trigger scan after successful access to spotifywebapi spotifywebapi_fullrescan(); listener_notify(LISTENER_SPOTIFY); return 0; } static int transaction_start(void *arg) { db_transaction_begin(); return 0; } static int transaction_end(void *arg) { db_transaction_end(); return 0; } 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); item->artwork_url = safe_strdup(album->artwork_url); } else { item->album_artist = safe_strdup(track->album_artist); item->album = safe_strdup(track->album); item->artwork_url = safe_strdup(track->artwork_url); } 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 queue_add_track(const char *uri, int position, char reshuffle, uint32_t item_id, int *count, int *new_item_id) { 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; parse_metadata_track(response, &track, ART_DEFAULT_WIDTH); DPRINTF(E_DBG, L_SPOTIFY, "Got track: '%s' (%s) \n", track.name, track.uri); map_track_to_queueitem(&item, &track, NULL); ret = db_queue_add_start(&queue_add_info, position); if (ret == 0) { ret = db_queue_add_item(&queue_add_info, &item); ret = db_queue_add_end(&queue_add_info, reshuffle, item_id, ret); if (ret == 0) { if (count) *count = queue_add_info.count; if (new_item_id) *new_item_id = queue_add_info.new_item_id; } } 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; parse_metadata_track(item, &track, ART_DEFAULT_WIDTH); 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, ¶m->album); ret = db_queue_add_item(¶m->queue_add_info, &queue_item); free_queue_item(&queue_item, 1); return ret; } static int queue_add_album(const char *uri, int position, char reshuffle, uint32_t item_id, int *count, int *new_item_id) { 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); parse_metadata_album(json_album, ¶m.album, ART_DEFAULT_WIDTH); ret = db_queue_add_start(¶m.queue_add_info, position); 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, ¶m); ret = db_queue_add_end(¶m.queue_add_info, reshuffle, item_id, ret); if (ret == 0 && count) *count = param.queue_add_info.count; out: free(album_endpoint_uri); free(endpoint_uri); jparse_free(json_album); return ret; } 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, ¶m_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, ¶m_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; } 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; } parse_metadata_track(jsontrack, &track, ART_DEFAULT_WIDTH); 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 queue_add_playlist(const char *uri, int position, char reshuffle, uint32_t item_id, int *count, int *new_item_id) { char *endpoint_uri; struct db_queue_add_info queue_add_info; int ret; ret = db_queue_add_start(&queue_add_info, position); 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); 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; } static int queue_item_add(const char *uri, int position, char reshuffle, uint32_t item_id, int *count, int *new_item_id) { if (strncasecmp(uri, "spotify:track:", strlen("spotify:track:")) == 0) { queue_add_track(uri, position, reshuffle, item_id, count, new_item_id); return LIBRARY_OK; } 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; } else if (strncasecmp(uri, "spotify:album:", strlen("spotify:album:")) == 0) { queue_add_album(uri, position, reshuffle, item_id, count, new_item_id); return LIBRARY_OK; } else if (strncasecmp(uri, "spotify:", strlen("spotify:")) == 0) { queue_add_playlist(uri, position, reshuffle, item_id, count, new_item_id); return LIBRARY_OK; } return LIBRARY_PATH_INVALID; } /* * Returns the directory id for /spotify://, 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; } dir_id = db_directory_addorupdate(virtual_path, NULL, 0, DIR_SPOTIFY); 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; } dir_id = db_directory_addorupdate(virtual_path, NULL, 0, dir_id); if (dir_id <= 0) { DPRINTF(E_LOG, L_SPOTIFY, "Could not add or update directory '%s'\n", virtual_path); return -1; } return dir_id; } /* * 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) */ static int cleanup_spotify_files(void) { 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); mfi->time_modified = track->mtime; mfi->time_added = track->mtime; 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); } static int track_add(struct spotify_track *track, struct spotify_album *album, const char *pl_name, int dir_id) { struct media_file_info mfi; int ret; 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; } 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) { DPRINTF(E_DBG, L_SPOTIFY, "Track '%s' (%s) is new or modified (mtime is %" PRIi64 ")\n", track->name, track->uri, (int64_t)track->mtime); 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); library_media_save(&mfi); 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); return 0; } 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); } /* * Add a saved album to the library */ static int saved_album_add(json_object *item, int index, int total, void *arg) { json_object *jsonalbum; struct spotify_album album; struct spotify_track track; json_object *needle; json_object *jsontracks; json_object *jsontrack; int track_count; int dir_id; int i; int ret; if (!json_object_object_get_ex(item, "album", &jsonalbum)) { 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; } // Map album information parse_metadata_album(jsonalbum, &album, 0); album.added_at = jparse_str_from_obj(item, "added_at"); album.mtime = jparse_time_from_obj(item, "added_at"); // Now map the album tracks and insert/update them in the files database db_transaction_begin(); // 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; parse_metadata_track(jsontrack, &track, 0); track.mtime = album.mtime; ret = track_add(&track, &album, NULL, dir_id); if (ret == 0 && spotify_saved_plid) db_pl_add_item_bypath(spotify_saved_plid, track.uri); } db_transaction_end(); if ((index + 1) >= total || ((index + 1) % 10 == 0)) DPRINTF(E_LOG, L_SPOTIFY, "Scanned %d of %d saved albums\n", (index + 1), total); return 0; } /* * Thread: library * * Scan users saved albums into the library */ static int scan_saved_albums() { int ret; ret = request_pagingobject_endpoint(spotify_albums_uri, saved_album_add, NULL, NULL, true, NULL); return ret; } /* * Add a saved playlist tracks to the library */ static int saved_playlist_tracks_add(json_object *item, int index, int total, void *arg) { struct spotify_track track; json_object *jsontrack; int *plid; int dir_id; int ret; plid = 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; } parse_metadata_track(jsontrack, &track, 0); 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 0; } 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); return 0; } /* Thread: library */ static int scan_playlist_tracks(const char *playlist_tracks_endpoint_uri, int plid) { int ret; ret = request_pagingobject_endpoint(playlist_tracks_endpoint_uri, saved_playlist_tracks_add, transaction_start, transaction_end, true, &plid); return ret; } static void map_playlist_to_pli(struct playlist_info *pli, struct spotify_playlist *playlist) { memset(pli, 0, sizeof(struct playlist_info)); 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); } /* * Add a saved playlist to the library */ static int saved_playlist_add(json_object *item, int index, int total, void *arg) { struct spotify_playlist playlist; struct playlist_info pli; int pl_id; // Map playlist information parse_metadata_playlist(item, &playlist); DPRINTF(E_DBG, L_SPOTIFY, "Got playlist: '%s' with %d tracks (%s) \n", playlist.name, playlist.tracks_count, playlist.uri); 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; } map_playlist_to_pli(&pli, &playlist); pl_id = playlist_add_or_update(&pli); free_pli(&pli, 1); if (pl_id > 0) scan_playlist_tracks(playlist.tracks_href, pl_id); else DPRINTF(E_LOG, L_SPOTIFY, "Error adding playlist: '%s' (%s) \n", playlist.name, playlist.uri); DPRINTF(E_LOG, L_SPOTIFY, "Scanned %d of %d saved playlists\n", (index + 1), total); return 0; } /* * Thread: library * * Scan users saved playlists into the library */ static int scan_playlists() { int ret; ret = request_pagingobject_endpoint(spotify_playlists_uri, saved_playlist_add, NULL, NULL, false, NULL); return ret; } static void create_saved_tracks_playlist() { struct playlist_info pli = { .path = strdup("spotify:savedtracks"), .title = strdup("Spotify Saved"), .virtual_path = strdup("/spotify:/Spotify Saved"), .type = PL_PLAIN, .parent_id = spotify_base_plid, .directory_id = DIR_SPOTIFY, }; spotify_saved_plid = playlist_add_or_update(&pli); if (spotify_saved_plid < 0) { DPRINTF(E_LOG, L_SPOTIFY, "Error adding playlist for saved tracks\n"); spotify_saved_plid = 0; } free_pli(&pli, 1); } /* * Add or update playlist folder for all spotify playlists (if enabled in config) */ static void create_base_playlist() { cfg_t *spotify_cfg; struct playlist_info pli = { .path = strdup("spotify:playlistfolder"), .title = strdup("Spotify"), .type = PL_FOLDER, }; spotify_base_plid = 0; spotify_cfg = cfg_getsec(cfg, "spotify"); if (cfg_getbool(spotify_cfg, "base_playlist_disable")) { free_pli(&pli, 1); return; } spotify_base_plid = playlist_add_or_update(&pli); if (spotify_base_plid < 0) { DPRINTF(E_LOG, L_SPOTIFY, "Error adding base playlist\n"); spotify_base_plid = 0; } free_pli(&pli, 1); } static void scan() { time_t start; time_t end; if (!token_valid() || scanning) { DPRINTF(E_DBG, L_SPOTIFY, "No valid web api token or scan already in progress, rescan ignored\n"); return; } start = time(NULL); scanning = true; db_directory_enable_bypath("/spotify:"); create_base_playlist(); create_saved_tracks_playlist(); scan_saved_albums(); scan_playlists(); scanning = false; end = time(NULL); DPRINTF(E_LOG, L_SPOTIFY, "Spotify scan completed in %.f sec\n", difftime(end, start)); } /* Thread: library */ static int initscan() { int ret; /* Refresh access token for the spotify webapi */ ret = token_refresh(); 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) { 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; } *ret = saved_playlist_add(response, 0, 1, NULL); jparse_free(response); out: free(endpoint_uri); return COMMAND_END; } /* Thread: library */ static enum command_state webapi_pl_remove(void *arg, int *ret) { 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; 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)); } 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; } 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) { strncpy(info->user, spotify_user, (sizeof(info->user) - 1)); } if (spotify_user_country) { 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)); } 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); 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(); return ret; } static void spotifywebapi_deinit() { CHECK_ERR(L_SPOTIFY, pthread_mutex_destroy(&token_lck)); spotify_deinit(); free(spotify_access_token); free(spotify_refresh_token); free(spotify_granted_scope); free(spotify_user_country); free(spotify_user); } struct library_source spotifyscanner = { .name = "spotifyscanner", .disabled = 0, .init = spotifywebapi_init, .deinit = spotifywebapi_deinit, .rescan = rescan, .metarescan = rescan, .initscan = initscan, .fullrescan = fullrescan, .queue_item_add = queue_item_add, };