owntone-server/src/spotify_webapi.c
ejurgensen 153eb40b6d [db] Remove some definitions of ARTWORK_XXX
The artwork db field should only be used to save media file artwork metadata,
which mostly means whether it has embedded artwork or not. It should not be
used to save where we found artwork for the file, since that is not static.

So this commit removes for instance ARTWORK_OWN, ARTWORK_DIR etc., which we
weren't using anyway.
2020-04-25 22:07:10 +02:00

2022 lines
50 KiB
C

/*
* 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>
#include <json.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#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, &param->album);
ret = db_queue_add_item(&param->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, &param.album, ART_DEFAULT_WIDTH);
ret = db_queue_add_start(&param.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, &param);
ret = db_queue_add_end(&param.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, &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;
}
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:/<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;
}
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,
};