mirror of
https://github.com/owntone/owntone-server.git
synced 2025-01-05 20:13:23 -05:00
8d8663e93d
queue
1852 lines
45 KiB
C
1852 lines
45 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 "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;
|
|
};
|
|
|
|
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;
|
|
|
|
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_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_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/users/%s/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/users/%s/playlists/%s/tracks";
|
|
|
|
|
|
|
|
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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
refresh_token = db_admin_get(DB_ADMIN_SPOTIFY_REFRESH_TOKEN);
|
|
if (!refresh_token)
|
|
{
|
|
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 void
|
|
parse_metadata_track(json_object *jsontrack, struct spotify_track *track)
|
|
{
|
|
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");
|
|
}
|
|
|
|
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)
|
|
{
|
|
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);
|
|
|
|
// 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");
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Extracts the owner and the id from a spotify playlist uri
|
|
*
|
|
* Playlist-uri has the following format: spotify:user:[owner]:playlist:[id]
|
|
* Owner and plid must be freed by the caller.
|
|
*/
|
|
static int
|
|
get_owner_plid_from_uri(const char *uri, char **owner, char **plid)
|
|
{
|
|
char *ptr1;
|
|
char *ptr2;
|
|
char *tmp;
|
|
size_t len;
|
|
|
|
ptr1 = strchr(uri, ':');
|
|
if (!ptr1)
|
|
return -1;
|
|
ptr1++;
|
|
ptr1 = strchr(ptr1, ':');
|
|
if (!ptr1)
|
|
return -1;
|
|
ptr1++;
|
|
ptr2 = strchr(ptr1, ':');
|
|
|
|
len = ptr2 - ptr1;
|
|
|
|
tmp = malloc(sizeof(char) * (len + 1));
|
|
strncpy(tmp, ptr1, len);
|
|
tmp[len] = '\0';
|
|
*owner = tmp;
|
|
|
|
ptr2++;
|
|
ptr1 = strchr(ptr2, ':');
|
|
if (!ptr1)
|
|
{
|
|
free(tmp);
|
|
*owner = NULL;
|
|
return -1;
|
|
}
|
|
ptr1++;
|
|
*plid = strdup(ptr1);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* 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 *owner = NULL;
|
|
char *id = NULL;
|
|
int ret;
|
|
|
|
ret = get_owner_plid_from_uri(uri, &owner, &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, owner, id);
|
|
|
|
out:
|
|
free(owner);
|
|
free(id);
|
|
return endpoint_uri;
|
|
}
|
|
|
|
static char *
|
|
get_playlist_tracks_endpoint_uri(const char *uri)
|
|
{
|
|
char *endpoint_uri = NULL;
|
|
char *owner = NULL;
|
|
char *id = NULL;
|
|
int ret;
|
|
|
|
ret = get_owner_plid_from_uri(uri, &owner, &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, owner, id);
|
|
|
|
out:
|
|
free(owner);
|
|
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 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", "user-read-private playlist-read-private user-library-read") == 0) &&
|
|
(keyval_add(&kv, "show_dialog", "false") == 0) );
|
|
if (!ret)
|
|
{
|
|
DPRINTF(E_LOG, L_SPOTIFY, "Cannot display Spotify oath interface (error adding parameters to keyval)\n");
|
|
goto out_clear_kv;
|
|
}
|
|
|
|
param = http_form_urlencode(&kv);
|
|
if (param)
|
|
{
|
|
uri_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);
|
|
}
|
|
else
|
|
{
|
|
item->album_artist = safe_strdup(track->album_artist);
|
|
item->album = safe_strdup(track->album);
|
|
}
|
|
|
|
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, 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);
|
|
|
|
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, 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);
|
|
|
|
if (!track.uri || !track.is_playable)
|
|
{
|
|
DPRINTF(E_LOG, L_SPOTIFY, "Track not available for playback: '%s' - '%s' (%s) (restrictions: %s)\n", track.artist, track.name, track.uri, track.restrictions);
|
|
return -1;
|
|
}
|
|
|
|
map_track_to_queueitem(&queue_item, &track, ¶m->album);
|
|
|
|
ret = db_queue_add_item(¶m->queue_add_info, &queue_item);
|
|
|
|
free_queue_item(&queue_item, 1);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int
|
|
queue_add_album(const char *uri, int position, int *count, int *new_item_id)
|
|
{
|
|
char *album_endpoint_uri = NULL;
|
|
char *endpoint_uri = NULL;
|
|
json_object *json_album;
|
|
struct queue_add_album_param param;
|
|
int ret;
|
|
|
|
album_endpoint_uri = get_album_endpoint_uri(uri);
|
|
json_album = request_endpoint_with_token_refresh(album_endpoint_uri);
|
|
parse_metadata_album(json_album, ¶m.album);
|
|
|
|
ret = db_queue_add_start(¶m.queue_add_info, position);
|
|
if (ret < 0)
|
|
goto out;
|
|
|
|
endpoint_uri = get_album_tracks_endpoint_uri(uri);
|
|
|
|
ret = request_pagingobject_endpoint(endpoint_uri, queue_add_album_tracks, NULL, NULL, true, ¶m);
|
|
|
|
ret = db_queue_add_end(¶m.queue_add_info, 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_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);
|
|
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, 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, ret);
|
|
if (ret == 0 && count)
|
|
*count = queue_add_info.count;
|
|
|
|
free(endpoint_uri);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int
|
|
queue_add(const char *uri, int position, int *count, int *new_item_id)
|
|
{
|
|
if (strncasecmp(uri, "spotify:track:", strlen("spotify:track:")) == 0)
|
|
{
|
|
queue_add_track(uri, position, count, new_item_id);
|
|
return LIBRARY_OK;
|
|
}
|
|
else if (strncasecmp(uri, "spotify:album:", strlen("spotify:album:")) == 0)
|
|
{
|
|
queue_add_album(uri, position, count, new_item_id);
|
|
return LIBRARY_OK;
|
|
}
|
|
else if (strncasecmp(uri, "spotify:", strlen("spotify:")) == 0)
|
|
{
|
|
queue_add_playlist(uri, position, 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, 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, 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->artwork = ARTWORK_SPOTIFY;
|
|
mfi->type = strdup("spotify");
|
|
mfi->codectype = strdup("wav");
|
|
mfi->description = strdup("Spotify audio");
|
|
|
|
mfi->path = strdup(track->uri);
|
|
mfi->fname = strdup(track->uri);
|
|
|
|
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;
|
|
mfi->time_modified = album->mtime;
|
|
}
|
|
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;
|
|
|
|
mfi->time_modified = time(NULL);
|
|
}
|
|
|
|
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_add_media(&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;
|
|
}
|
|
|
|
/*
|
|
* 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);
|
|
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);
|
|
|
|
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);
|
|
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;
|
|
}
|
|
|
|
/*
|
|
* 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;
|
|
char virtual_path[PATH_MAX];
|
|
int plid;
|
|
|
|
// 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;
|
|
}
|
|
|
|
if (playlist.owner)
|
|
{
|
|
snprintf(virtual_path, PATH_MAX, "/spotify:/%s (%s)", playlist.name, playlist.owner);
|
|
}
|
|
else
|
|
{
|
|
snprintf(virtual_path, PATH_MAX, "/spotify:/%s", playlist.name);
|
|
}
|
|
|
|
db_transaction_begin();
|
|
plid = library_add_playlist_info(playlist.uri, playlist.name, virtual_path, PL_PLAIN, spotify_base_plid, DIR_SPOTIFY);
|
|
db_transaction_end();
|
|
|
|
if (plid > 0)
|
|
scan_playlist_tracks(playlist.tracks_href, plid);
|
|
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()
|
|
{
|
|
spotify_saved_plid = library_add_playlist_info("spotify:savedtracks", "Spotify Saved", "/spotify:/Spotify Saved", PL_PLAIN, spotify_base_plid, DIR_SPOTIFY);
|
|
|
|
if (spotify_saved_plid <= 0)
|
|
{
|
|
DPRINTF(E_LOG, L_SPOTIFY, "Error adding playlist for saved tracks\n");
|
|
spotify_saved_plid = 0;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Add or update playlist folder for all spotify playlists (if enabled in config)
|
|
*/
|
|
static void
|
|
create_base_playlist()
|
|
{
|
|
cfg_t *spotify_cfg;
|
|
int ret;
|
|
|
|
spotify_base_plid = 0;
|
|
spotify_cfg = cfg_getsec(cfg, "spotify");
|
|
if (!cfg_getbool(spotify_cfg, "base_playlist_disable"))
|
|
{
|
|
ret = library_add_playlist_info("spotify:playlistfolder", "Spotify", NULL, PL_FOLDER, 0, 0);
|
|
if (ret < 0)
|
|
DPRINTF(E_LOG, L_SPOTIFY, "Error adding base playlist\n");
|
|
else
|
|
spotify_base_plid = ret;
|
|
}
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
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)
|
|
{
|
|
memcpy(info->user, spotify_user, (sizeof(info->user) - 1));
|
|
}
|
|
if (spotify_user_country)
|
|
{
|
|
memcpy(info->country, spotify_user_country, (sizeof(info->country) - 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();
|
|
}
|
|
|
|
struct library_source spotifyscanner =
|
|
{
|
|
.name = "spotifyscanner",
|
|
.disabled = 0,
|
|
.init = spotifywebapi_init,
|
|
.deinit = spotifywebapi_deinit,
|
|
.rescan = rescan,
|
|
.initscan = initscan,
|
|
.fullrescan = fullrescan,
|
|
.queue_add = queue_add,
|
|
};
|
|
|