owntone-server/src/library/spotify_webapi.c

2355 lines
61 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 "inputs/spotify.h"
enum spotify_request_type {
SPOTIFY_REQUEST_TYPE_DEFAULT,
SPOTIFY_REQUEST_TYPE_RESCAN,
SPOTIFY_REQUEST_TYPE_METARESCAN,
};
enum spotify_item_type {
SPOTIFY_ITEM_TYPE_ALBUM,
SPOTIFY_ITEM_TYPE_ARTIST,
SPOTIFY_ITEM_TYPE_TRACK,
SPOTIFY_ITEM_TYPE_PLAYLIST,
SPOTIFY_ITEM_TYPE_SHOW,
SPOTIFY_ITEM_TYPE_EPISODE,
SPOTIFY_ITEM_TYPE_UNKNOWN,
};
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;
time_t release_date_time;
int release_year;
const char *uri;
const char *artwork_url;
const char *type;
};
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 *release_date;
const char *release_date_precision;
time_t release_date_time;
int release_year;
const char *uri;
const char *artwork_url;
bool is_playable;
const char *restrictions;
const char *linked_from_uri;
const char *type;
};
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
struct spotify_credentials
{
char *access_token;
char *refresh_token;
char *granted_scope;
char *user_country;
char *user;
int32_t token_expires_in;
time_t token_time_requested;
};
static struct spotify_credentials spotify_credentials;
// 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 streaming";
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 const char *spotify_shows_uri = "https://api.spotify.com/v1/me/shows?limit=50";
static const char *spotify_shows_episodes_uri = "https://api.spotify.com/v1/shows/%s/episodes";
static const char *spotify_episode_uri = "https://api.spotify.com/v1/episodes/%s";
static struct http_client_session session = { 0 };
static enum spotify_item_type
parse_type_from_uri(const char *uri)
{
if (strncasecmp(uri, "spotify:track:", strlen("spotify:track:")) == 0)
{
return SPOTIFY_ITEM_TYPE_TRACK;
}
else if (strncasecmp(uri, "spotify:artist:", strlen("spotify:artist:")) == 0)
{
return SPOTIFY_ITEM_TYPE_ARTIST;
}
else if (strncasecmp(uri, "spotify:album:", strlen("spotify:album:")) == 0)
{
return SPOTIFY_ITEM_TYPE_ALBUM;
}
else if (strncasecmp(uri, "spotify:show:", strlen("spotify:show:")) == 0)
{
return SPOTIFY_ITEM_TYPE_SHOW;
}
else if (strncasecmp(uri, "spotify:episode:", strlen("spotify:episode:")) == 0)
{
return SPOTIFY_ITEM_TYPE_EPISODE;
}
else if (strncasecmp(uri, "spotify:", strlen("spotify:")) == 0 && strstr(uri, "playlist:"))
{
return SPOTIFY_ITEM_TYPE_PLAYLIST;
}
DPRINTF(E_WARN, L_SPOTIFY, "Could not parse item type from Spotify uri: %s\n", uri);
return SPOTIFY_ITEM_TYPE_UNKNOWN;
}
static void
free_credentials(void)
{
free(spotify_credentials.access_token);
free(spotify_credentials.refresh_token);
free(spotify_credentials.granted_scope);
free(spotify_credentials.user_country);
free(spotify_credentials.user);
memset(&spotify_credentials, 0, sizeof(struct spotify_credentials));
}
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_credentials.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, NULL);
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_credentials.access_token);
spotify_credentials.access_token = NULL;
tmp = jparse_str_from_obj(haystack, "access_token");
if (tmp)
spotify_credentials.access_token = strdup(tmp);
tmp = jparse_str_from_obj(haystack, "refresh_token");
if (tmp)
{
free(spotify_credentials.refresh_token);
spotify_credentials.refresh_token = strdup(tmp);
}
tmp = jparse_str_from_obj(haystack, "scope");
if (tmp)
{
free(spotify_credentials.granted_scope);
spotify_credentials.granted_scope = strdup(tmp);
}
spotify_credentials.token_expires_in = jparse_int_from_obj(haystack, "expires_in");
if (spotify_credentials.token_expires_in == 0)
spotify_credentials.token_expires_in = 3600;
jparse_free(haystack);
if (!spotify_credentials.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;
}
spotify_credentials.token_time_requested = time(NULL);
if (spotify_credentials.refresh_token)
db_admin_set(DB_ADMIN_SPOTIFY_REFRESH_TOKEN, spotify_credentials.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;
CHECK_NULL(L_SPOTIFY, ctx = calloc(1, sizeof(struct http_client_ctx)));
CHECK_NULL(L_SPOTIFY, ctx->output_headers = calloc(1, sizeof(struct keyval)));
CHECK_NULL(L_SPOTIFY, ctx->input_body = evbuffer_new());
ctx->url = uri;
snprintf(bearer_token, sizeof(bearer_token), "Bearer %s", spotify_credentials.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, &session);
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_credentials.user_country);
spotify_credentials.user_country = NULL;
free(spotify_credentials.user);
spotify_credentials.user = NULL;
response = request_endpoint(spotify_me_uri);
if (response)
{
spotify_credentials.user = safe_strdup(jparse_str_from_obj(response, "id"));
spotify_credentials.user_country = safe_strdup(jparse_str_from_obj(response, "country"));
jparse_free(response);
DPRINTF(E_DBG, L_SPOTIFY, "User '%s', country '%s'\n", spotify_credentials.user, spotify_credentials.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 (spotify_credentials.token_time_requested && difftime(time(NULL), spotify_credentials.token_time_requested) < spotify_credentials.token_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, enum spotify_request_type request_type, 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, enum spotify_request_type request_type, 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_credentials.user_country)
{
next_href = safe_strdup(href);
}
else
{
if (strchr(href, '?'))
next_href = safe_asprintf("%s&market=%s", href, spotify_credentials.user_country);
else
next_href = safe_asprintf("%s?market=%s", href, spotify_credentials.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, request_type, 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");
track->type = jparse_str_from_obj(jsontrack, "type");
// "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->type = jparse_str_from_obj(jsonalbum, "type");
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");
if (album->release_date_precision && strcmp(album->release_date_precision, "day") == 0)
album->release_date_time = jparse_time_from_obj(jsonalbum, "release_date");
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");
}
}
static void
parse_metadata_show(json_object *jsonshow, struct spotify_album *show)
{
memset(show, 0, sizeof(struct spotify_album));
show->name = jparse_str_from_obj(jsonshow, "name");
show->artist = jparse_str_from_obj(jsonshow, "publisher");
show->uri = jparse_str_from_obj(jsonshow, "uri");
show->id = jparse_str_from_obj(jsonshow, "id");
show->type = jparse_str_from_obj(jsonshow, "type");
}
static void
parse_metadata_episode(json_object *jsonepisode, struct spotify_track *episode, int max_w)
{
json_object *jsonshow;
memset(episode, 0, sizeof(struct spotify_track));
if (json_object_object_get_ex(jsonepisode, "show", &jsonshow))
{
episode->album = jparse_str_from_obj(jsonshow, "name");
episode->artwork_url = get_album_image(jsonshow, max_w);
}
episode->name = jparse_str_from_obj(jsonepisode, "name");
episode->uri = jparse_str_from_obj(jsonepisode, "uri");
episode->id = jparse_str_from_obj(jsonepisode, "id");
episode->type = jparse_str_from_obj(jsonepisode, "type");
episode->duration_ms = jparse_int_from_obj(jsonepisode, "duration_ms");
episode->release_date = jparse_str_from_obj(jsonepisode, "release_date");
episode->release_date_precision = jparse_str_from_obj(jsonepisode, "release_date_precision");
if (episode->release_date_precision && strcmp(episode->release_date_precision, "day") == 0)
episode->release_date_time = jparse_time_from_obj(jsonepisode, "release_date");
episode->release_year = get_year_from_date(episode->release_date);
episode->mtime = episode->release_date_time;
// "is_playable" is only returned for a request with a market parameter, default to true if it is not in the response
episode->is_playable = true;
if (json_object_object_get_ex(jsonepisode, "is_playable", NULL))
{
episode->is_playable = jparse_bool_from_obj(jsonepisode, "is_playable");
}
}
/*
* 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 char *
get_episode_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_episode_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;
}
static json_object *
request_episode(const char *path)
{
char *endpoint_uri;
json_object *response;
endpoint_uri = get_episode_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;
CHECK_NULL(L_SPOTIFY, 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, const char **errmsg)
{
const char *code;
int ret;
*errmsg = NULL;
code = evhttp_find_header(param, "code");
if (!code)
{
*errmsg = "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, errmsg);
if (ret < 0)
return -1;
ret = spotify_login_token(spotify_credentials.user, spotify_credentials.access_token, errmsg);
if (ret < 0)
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 = NULL;
struct spotify_track track;
struct db_queue_item item = { 0 };
struct db_queue_add_info queue_add_info;
int ret;
response = request_track(uri);
if (!response)
goto error;
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)
goto error;
ret = db_queue_add_next(&queue_add_info, &item);
ret = db_queue_add_end(&queue_add_info, reshuffle, item_id, ret);
if (ret < 0)
goto error;
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;
error:
free_queue_item(&item, 1);
jparse_free(response);
return -1;
}
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, enum spotify_request_type request_type, 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_next(&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, SPOTIFY_REQUEST_TYPE_DEFAULT, &param);
ret = db_queue_add_end(&param.queue_add_info, reshuffle, item_id, ret);
if (ret < 0)
goto out;
if (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, enum spotify_request_type request_type, 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, SPOTIFY_REQUEST_TYPE_DEFAULT, &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)
goto out;
endpoint_uri = get_artist_albums_endpoint_uri(uri);
ret = request_pagingobject_endpoint(endpoint_uri, queue_add_albums, NULL, NULL, true, SPOTIFY_REQUEST_TYPE_DEFAULT, &queue_add_info);
ret = db_queue_add_end(&queue_add_info, reshuffle, item_id, ret);
if (ret < 0)
goto out;
if (count)
*count = queue_add_info.count;
out:
free(endpoint_uri);
return ret;
}
static int
queue_add_playlist_tracks(json_object *item, int index, int total, enum spotify_request_type request_type, 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_next(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 = NULL;
struct db_queue_add_info queue_add_info;
int ret;
ret = db_queue_add_start(&queue_add_info, position);
if (ret < 0)
goto out;
endpoint_uri = get_playlist_tracks_endpoint_uri(uri);
ret = request_pagingobject_endpoint(endpoint_uri, queue_add_playlist_tracks, NULL, NULL, true, SPOTIFY_REQUEST_TYPE_DEFAULT, &queue_add_info);
ret = db_queue_add_end(&queue_add_info, reshuffle, item_id, ret);
if (ret < 0)
goto out;
if (count)
*count = queue_add_info.count;
out:
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)
{
enum spotify_item_type type;
type = parse_type_from_uri(uri);
if (type == SPOTIFY_ITEM_TYPE_TRACK)
{
queue_add_track(uri, position, reshuffle, item_id, count, new_item_id);
return LIBRARY_OK;
}
else if (type == SPOTIFY_ITEM_TYPE_ARTIST)
{
queue_add_artist(uri, position, reshuffle, item_id, count, new_item_id);
return LIBRARY_OK;
}
else if (type == SPOTIFY_ITEM_TYPE_ALBUM)
{
queue_add_album(uri, position, reshuffle, item_id, count, new_item_id);
return LIBRARY_OK;
}
else if (type == SPOTIFY_ITEM_TYPE_PLAYLIST)
{
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 = library_directory_save(virtual_path, NULL, 0, DIR_SPOTIFY, SCAN_KIND_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 = library_directory_save(virtual_path, NULL, 0, dir_id, SCAN_KIND_SPOTIFY);
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(&path, &qp)) == 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;
if (strcmp(track->type, "episode") == 0)
mfi->media_kind = MEDIA_KIND_PODCAST;
else
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 && album->uri)
{
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->date_released = album->release_date_time;
mfi->year = album->release_year;
}
else
{
mfi->album_artist = safe_strdup(track->album_artist);
mfi->album = safe_strdup(track->album);
mfi->compilation = track->is_compilation;
}
if (cfg_getbool(cfg_getsec(cfg, "spotify"), "album_override") && pl_name)
{
free(mfi->album);
mfi->album = safe_strdup(pl_name);
}
if (cfg_getbool(cfg_getsec(cfg, "spotify"), "artist_override") && pl_name)
{
mfi->compilation = true;
}
if (mfi->media_kind == MEDIA_KIND_PODCAST)
{
// For podcasts we want the tracks/episodes release date
mfi->date_released = track->release_date_time;
mfi->year = track->release_year;
}
snprintf(virtual_path, PATH_MAX, "/spotify:/%s/%s/%s", mfi->album_artist, mfi->album, mfi->title);
mfi->virtual_path = strdup(virtual_path);
mfi->scan_kind = SCAN_KIND_SPOTIFY;
}
static int
track_add(struct spotify_track *track, struct spotify_album *album, const char *pl_name, int dir_id, enum spotify_request_type request_type)
{
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 || request_type == SPOTIFY_REQUEST_TYPE_METARESCAN)
{
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);
}
if (album && album->uri)
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, enum spotify_request_type request_type, 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, request_type);
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(enum spotify_request_type request_type)
{
int ret;
ret = request_pagingobject_endpoint(spotify_albums_uri, saved_album_add, NULL, NULL, true, request_type, NULL);
return ret;
}
/*
* Add a saved podcast show to the library
*/
static int
saved_episodes_add(json_object *item, int index, int total, enum spotify_request_type request_type, void *arg)
{
struct spotify_album *show = arg;
struct spotify_track episode;
int dir_id;
int ret;
DPRINTF(E_DBG, L_SPOTIFY, "saved_episodes_add: %s\n", json_object_to_json_string(item));
// Map episode information
parse_metadata_episode(item, &episode, 0);
// Get or create the directory structure for this album
dir_id = prepare_directories(show->artist, show->name);
ret = track_add(&episode, show, NULL, dir_id, request_type);
if (ret == 0 && spotify_saved_plid)
db_pl_add_item_bypath(spotify_saved_plid, episode.uri);
return 0;
}
/*
* Add a saved podcast show to the library
*/
static int
saved_show_add(json_object *item, int index, int total, enum spotify_request_type request_type, void *arg)
{
json_object *jsonshow;
struct spotify_album show;
char *endpoint_uri;
DPRINTF(E_DBG, L_SPOTIFY, "saved_show_add: %s\n", json_object_to_json_string(item));
if (!json_object_object_get_ex(item, "show", &jsonshow))
{
DPRINTF(E_LOG, L_SPOTIFY, "Unexpected JSON: Item %d is missing the 'show' field\n", index);
return -1;
}
// Map show information
parse_metadata_show(jsonshow, &show);
show.added_at = jparse_str_from_obj(item, "added_at");
show.mtime = jparse_time_from_obj(item, "added_at");
// Now map the show episodes and insert/update them in the files database
endpoint_uri = safe_asprintf(spotify_shows_episodes_uri, show.id);
request_pagingobject_endpoint(endpoint_uri, saved_episodes_add, transaction_start, transaction_end, true, request_type, &show);
free(endpoint_uri);
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 podcast shows into the library
*/
static int
scan_saved_shows(enum spotify_request_type request_type)
{
int ret;
ret = request_pagingobject_endpoint(spotify_shows_uri, saved_show_add, NULL, NULL, true, request_type, NULL);
return ret;
}
/*
* Add a saved playlist tracks to the library
*/
static int
saved_playlist_tracks_add(json_object *item, int index, int total, enum spotify_request_type request_type, void *arg)
{
struct spotify_track track;
struct spotify_album album;
json_object *jsontrack;
json_object *jsonalbum;
struct playlist_info *pli;
int dir_id;
int ret;
pli = 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;
}
if (!cfg_getbool(cfg_getsec(cfg, "spotify"), "album_override")
&& json_object_object_get_ex(jsontrack, "album", &jsonalbum))
{
parse_metadata_album(jsonalbum, &album, 0);
}
else
{
memset(&album, 0, sizeof(struct spotify_album));
}
dir_id = prepare_directories(track.album_artist, track.album);
ret = track_add(&track, &album, pli->title, dir_id, request_type);
if (ret == 0)
db_pl_add_item_bypath(pli->id, track.uri);
return 0;
}
/* Thread: library */
static int
scan_playlist_tracks(const char *playlist_tracks_endpoint_uri, struct playlist_info *pli, enum spotify_request_type request_type)
{
int ret;
ret = request_pagingobject_endpoint(playlist_tracks_endpoint_uri, saved_playlist_tracks_add, transaction_start, transaction_end, true, request_type, pli);
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;
pli->scan_kind = SCAN_KIND_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, enum spotify_request_type request_type, 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);
pli.id = pl_id;
if (pl_id > 0)
scan_playlist_tracks(playlist.tracks_href, &pli, request_type);
else
DPRINTF(E_LOG, L_SPOTIFY, "Error adding playlist: '%s' (%s) \n", playlist.name, playlist.uri);
free_pli(&pli, 1);
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(enum spotify_request_type request_type)
{
int ret;
ret = request_pagingobject_endpoint(spotify_playlists_uri, saved_playlist_add, NULL, NULL, false, request_type, NULL);
return ret;
}
static void
create_saved_tracks_playlist(void)
{
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,
.scan_kind = SCAN_KIND_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(void)
{
cfg_t *spotify_cfg;
struct playlist_info pli =
{
.path = strdup("spotify:playlistfolder"),
.title = strdup("Spotify"),
.type = PL_FOLDER,
.scan_kind = SCAN_KIND_SPOTIFY,
};
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(enum spotify_request_type request_type)
{
struct spotify_status sp_status;
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(request_type);
scan_playlists(request_type);
spotify_status_get(&sp_status);
if (sp_status.has_podcast_support)
scan_saved_shows(request_type);
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(void)
{
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 Spotify, authorize the server to access your saved "
"tracks by visiting http://owntone.local:3689\n");
db_spotify_purge();
return 0;
}
spotify_saved_plid = 0;
/*
* Check that the playback Spotify backend can log in, so we don't add tracks
* to the library that can't be played.
*/
ret = spotify_relogin();
if (ret < 0)
{
DPRINTF(E_LOG, L_SPOTIFY, "Spotify playback library could not log in. In order to use Spotify, "
"provide valid credentials by visiting http://owntone.local:3689\n");
db_spotify_purge();
return 0;
}
/*
* Scan saved tracks from the web api
*/
scan(SPOTIFY_REQUEST_TYPE_RESCAN);
return 0;
}
/* Thread: library */
static int
rescan(void)
{
scan(SPOTIFY_REQUEST_TYPE_RESCAN);
return 0;
}
/* Thread: library */
static int
metarescan(void)
{
scan(SPOTIFY_REQUEST_TYPE_METARESCAN);
return 0;
}
/* Thread: library */
static int
fullrescan(void)
{
db_spotify_purge();
scan(SPOTIFY_REQUEST_TYPE_RESCAN);
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_purge(void *arg, int *ret)
{
free_credentials();
db_spotify_purge();
db_admin_delete(DB_ADMIN_SPOTIFY_REFRESH_TOKEN);
*ret = 0;
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, SPOTIFY_REQUEST_TYPE_DEFAULT, 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_purge(void)
{
library_exec_async(webapi_purge, 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)
{
enum spotify_item_type type;
json_object *response;
struct spotify_track track;
char *artwork_url;
type = parse_type_from_uri(uri);
if (type == SPOTIFY_ITEM_TYPE_TRACK)
{
response = request_track(uri);
if (response)
parse_metadata_track(response, &track, max_w);
}
else if (type == SPOTIFY_ITEM_TYPE_EPISODE)
{
response = request_episode(uri);
if (response)
parse_metadata_episode(response, &track, max_w);
}
else
{
DPRINTF(E_WARN, L_SPOTIFY, "Unsupported Spotify type for artwork request: '%s'\n", uri);
return NULL;
}
if (!response)
{
return NULL;
}
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_credentials.user)
{
strncpy(info->user, spotify_credentials.user, (sizeof(info->user) - 1));
}
if (spotify_credentials.user_country)
{
strncpy(info->country, spotify_credentials.user_country, (sizeof(info->country) - 1));
}
if (spotify_credentials.granted_scope)
{
strncpy(info->granted_scope, spotify_credentials.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 (spotify_credentials.token_time_requested > 0)
info->expires_in = spotify_credentials.token_expires_in - difftime(time(NULL), spotify_credentials.token_time_requested);
else
info->expires_in = 0;
info->token = safe_strdup(spotify_credentials.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();
if (ret < 0)
return -1;
http_client_session_init(&session);
return 0;
}
static void
spotifywebapi_deinit()
{
CHECK_ERR(L_SPOTIFY, pthread_mutex_destroy(&token_lck));
spotify_deinit();
http_client_session_deinit(&session);
free_credentials();
}
struct library_source spotifyscanner =
{
.scan_kind = SCAN_KIND_SPOTIFY,
.disabled = 0,
.init = spotifywebapi_init,
.deinit = spotifywebapi_deinit,
.rescan = rescan,
.metarescan = metarescan,
.initscan = initscan,
.fullrescan = fullrescan,
.queue_item_add = queue_item_add,
};