From 5efadb6fc29527319354cb904a9ad51da940a798 Mon Sep 17 00:00:00 2001 From: chme Date: Sat, 31 Dec 2016 16:46:59 +0100 Subject: [PATCH] [spotify_webapi] Add separate file for accessing the web api --- src/Makefile.am | 2 +- src/spotify_webapi.c | 725 +++++++++++++++++++++++++++++++++++++++++++ src/spotify_webapi.h | 124 ++++++++ 3 files changed, 850 insertions(+), 1 deletion(-) create mode 100644 src/spotify_webapi.c create mode 100644 src/spotify_webapi.h diff --git a/src/Makefile.am b/src/Makefile.am index 71d21290..08a26531 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -6,7 +6,7 @@ ITUNES_SRC=filescanner_itunes.c endif if COND_SPOTIFY -SPOTIFY_SRC=spotify.c spotify.h +SPOTIFY_SRC=spotify.c spotify.h spotify_webapi.c spotify_webapi.h endif if COND_LASTFM diff --git a/src/spotify_webapi.c b/src/spotify_webapi.c new file mode 100644 index 00000000..7184ffcc --- /dev/null +++ b/src/spotify_webapi.c @@ -0,0 +1,725 @@ +/* + * Copyright (C) 2016 Espen Jürgensen + * Copyright (C) 2016 Christian Meffert + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "spotify_webapi.h" + +#include +#include +#include +#include +#include + +#ifdef HAVE_JSON_C_OLD +# include +#else +# include +#endif + +#include "db.h" +#include "http.h" +#include "library.h" +#include "logger.h" + + + +// Credentials for the web api +static char *spotify_access_token; +static char *spotify_refresh_token; + +static int32_t expires_in = 3600; +static time_t token_requested = 0; + +// 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"; + + +/*--------------------- HELPERS FOR SPOTIFY WEB API -------------------------*/ +/* All the below is in the httpd thread */ + + +static void +jparse_free(json_object* haystack) +{ + if (haystack) + { +#ifdef HAVE_JSON_C_OLD + json_object_put(haystack); +#else + if (json_object_put(haystack) != 1) + DPRINTF(E_LOG, L_SPOTIFY, "Memleak: JSON parser did not free object\n"); +#endif + } +} + +static int +jparse_array_from_obj(json_object *haystack, const char *key, json_object **needle) +{ + if (! (json_object_object_get_ex(haystack, key, needle) && json_object_get_type(*needle) == json_type_array) ) + return -1; + else + return 0; +} + +static const char * +jparse_str_from_obj(json_object *haystack, const char *key) +{ + json_object *needle; + + if (json_object_object_get_ex(haystack, key, &needle) && json_object_get_type(needle) == json_type_string) + return json_object_get_string(needle); + else + return NULL; +} + +static int +jparse_int_from_obj(json_object *haystack, const char *key) +{ + json_object *needle; + + if (json_object_object_get_ex(haystack, key, &needle) && json_object_get_type(needle) == json_type_int) + return json_object_get_int(needle); + else + return 0; +} + +static time_t +jparse_time_from_obj(json_object *haystack, const char *key) +{ + const char *tmp; + struct tm tp; + time_t parsed_time; + + memset(&tp, 0, sizeof(struct tm)); + + tmp = jparse_str_from_obj(haystack, key); + if (!tmp) + return 0; + + strptime(tmp, "%Y-%m-%dT%H:%M:%SZ", &tp); + parsed_time = mktime(&tp); + if (parsed_time < 0) + return 0; + + return parsed_time; +} + +static char * +jparse_artist_from_obj(json_object *artists) +{ + char artistnames[1024]; + json_object *artist; + int count; + int i; + const char *tmp; + + count = json_object_array_length(artists); + if (count == 0) + { + return NULL; + } + + for (i = 0; i < count; i++) + { + artist = json_object_array_get_idx(artists, i); + tmp = jparse_str_from_obj(artist, "name"); + + if (i == 0) + { + strcpy(artistnames, tmp); + } + else + { + strncat(artistnames, ", ", sizeof(artistnames) - 2); + strncat(artistnames, tmp, sizeof(artistnames) - strlen(artistnames)); + } + } + + return strdup(artistnames); +} + +static void +http_client_ctx_free(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); +} + +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", "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; +} + +static int +tokens_get(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; + } + + 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) + 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); + + DPRINTF(E_LOG, L_SPOTIFY, "token: '%s'\n", spotify_access_token); + DPRINTF(E_LOG, L_SPOTIFY, "refresh-token: '%s'\n", spotify_refresh_token); + DPRINTF(E_LOG, L_SPOTIFY, "expires in: %d\n", expires_in); + + if (spotify_refresh_token) + db_admin_set("spotify_refresh_token", spotify_refresh_token); + + ret = 0; + + out_free_input_body: + evbuffer_free(ctx.input_body); + free(param); + out_clear_kv: + + return ret; +} + +int +spotifywebapi_token_get(const char *code, const char *redirect_uri, const char **err) +{ + struct keyval kv; + int ret; + + memset(&kv, 0, sizeof(struct keyval)); + ret = ( (keyval_add(&kv, "grant_type", "authorization_code") == 0) && + (keyval_add(&kv, "code", code) == 0) && + (keyval_add(&kv, "client_id", spotify_client_id) == 0) && + (keyval_add(&kv, "client_secret", spotify_client_secret) == 0) && + (keyval_add(&kv, "redirect_uri", redirect_uri) == 0) ); + + if (!ret) + { + *err = "Add parameters to keyval failed"; + ret = -1; + } + else + ret = tokens_get(&kv, err); + + keyval_clear(&kv); + + return ret; +} + +int +spotifywebapi_token_refresh() +{ + struct keyval kv; + char *refresh_token; + const char *err; + int ret; + + if (token_requested && difftime(token_requested, time(NULL)) < expires_in) + { + DPRINTF(E_DBG, L_SPOTIFY, "Spotify token still valid\n"); + return 0; + } + + refresh_token = db_admin_get("spotify_refresh_token"); + if (!refresh_token) + { + DPRINTF(E_LOG, L_SPOTIFY, "No spotify refresh token found\n"); + return -1; + } + + DPRINTF(E_DBG, L_SPOTIFY, "Spotify refresh-token: '%s'\n", refresh_token); + + memset(&kv, 0, sizeof(struct keyval)); + 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"); + ret = -1; + } + else + ret = tokens_get(&kv, &err); + + free(refresh_token); + keyval_clear(&kv); + + return ret; +} + +int +spotifywebapi_request_uri(struct spotify_request *request, const char *uri) +{ + char bearer_token[1024]; + int ret; + + memset(request, 0, sizeof(struct spotify_request)); + + if (0 > spotifywebapi_token_refresh()) + { + return -1; + } + + request->ctx = calloc(1, sizeof(struct http_client_ctx)); + request->ctx->output_headers = calloc(1, sizeof(struct keyval)); + request->ctx->input_body = evbuffer_new(); + request->ctx->url = uri; + + snprintf(bearer_token, sizeof(bearer_token), "Bearer %s", spotify_access_token); + if (keyval_add(request->ctx->output_headers, "Authorization", bearer_token) < 0) + { + DPRINTF(E_LOG, L_SPOTIFY, "Add bearer_token to keyval failed"); + return -1; + } + + ret = http_client_request(request->ctx); + if (ret < 0) + { + DPRINTF(E_LOG, L_SPOTIFY, "Request for saved tracks/albums failed"); + return -1; + } + + // 0-terminate for safety + evbuffer_add(request->ctx->input_body, "", 1); + + request->response_body = (char *) evbuffer_pullup(request->ctx->input_body, -1); + if (!request->response_body || (strlen(request->response_body) == 0)) + { + DPRINTF(E_LOG, L_SPOTIFY, "Request for saved tracks/albums failed, response was empty"); + return -1; + } + + request->haystack = json_tokener_parse(request->response_body); + if (!request->haystack) + { + DPRINTF(E_LOG, L_SPOTIFY, "JSON parser returned an error\n"); + return -1; + } + + DPRINTF(E_DBG, L_SPOTIFY, "Got response for '%s'\n", uri); + return 0; +} + +void +spotifywebapi_request_end(struct spotify_request *request) +{ + http_client_ctx_free(request->ctx); + jparse_free(request->haystack); +} + +int +spotifywebapi_request_next(struct spotify_request *request, const char *uri) +{ + char *next_uri; + const char *tmp; + int ret; + + if (request->ctx && !request->next_uri) + { + // Reached end of paging requests, terminate loop + return -1; + } + + if (!request->ctx) + { + // First paging request + next_uri = strdup (uri); + } + else + { + // Next paging request + next_uri = strdup(request->next_uri); + spotifywebapi_request_end(request); + } + + ret = spotifywebapi_request_uri(request, next_uri); + free(next_uri); + + if (ret < 0) + return ret; + + request->total = jparse_int_from_obj(request->haystack, "total"); + tmp = jparse_str_from_obj(request->haystack, "next"); + if (tmp) + request->next_uri = strdup(tmp); + + if (jparse_array_from_obj(request->haystack, "items", &request->items) < 0) + { + DPRINTF(E_LOG, L_SPOTIFY, "No items in reply from Spotify. See:\n%s\n", request->response_body); + return -1; + } + + request->count = json_object_array_length(request->items); + + DPRINTF(E_DBG, L_SPOTIFY, "Got %d items\n", request->count); + return 0; +} + +void +track_metadata(json_object* jsontrack, struct spotify_track* track) +{ + json_object* jsonalbum; + json_object* jsonartists; + + 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) && json_object_get_type(jsonartists) == json_type_array) + { + track->album_artist = jparse_artist_from_obj(jsonartists); + } + } + if (json_object_object_get_ex(jsontrack, "artists", &jsonartists) && json_object_get_type(jsonartists) == json_type_array) + { + track->artist = jparse_artist_from_obj(jsonartists); + } + track->disc_number = jparse_int_from_obj(jsontrack, "disc_number"); + 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"); +} + +int +spotifywebapi_saved_tracks_fetch(struct spotify_request *request, struct spotify_track *track) +{ + json_object *item; + json_object *jsontrack; + + memset(track, 0, sizeof(struct spotify_track)); + + if (request->index >= request->count) + { + return -1; + } + + item = json_object_array_get_idx(request->items, request->index); + if (!(item && json_object_object_get_ex(item, "track", &jsontrack))) + { + DPRINTF(E_LOG, L_SPOTIFY, "Unexpected JSON: Item %d did not have 'track'->'uri'\n", request->index); + request->index++; + return -1; + } + + track_metadata(jsontrack, track); + track->added_at = jparse_str_from_obj(item, "added_at"); +// if (track->added_at) +// strptime("%Y-%m-%dT%H:%M:%SZ"); + + request->index++; + + return 0; +} + +int +spotifywebapi_track(const char *path, struct spotify_track *track) +{ +// char uri[1024]; +// struct http_client_ctx *ctx; +// char *response_body; +// json_object *haystack; +// +// memset(track, 0, sizeof(struct spotify_track)); +// +// if (strlen(path) < 15) // spotify:track:3RZwWEcQHD0Sy1hN1xNYeh +// return -1; +// +// snprintf(uri, sizeof(uri), "%s%s", spotify_track_uri, (path + 14)); +// ctx = request_uri(uri); +// +// if (!ctx) +// return -1; +// +// response_body = (char *) evbuffer_pullup(ctx->input_body, -1); +// if (!response_body || (strlen(response_body) == 0)) +// { +// DPRINTF(E_LOG, L_SPOTIFY, "Request for track failed, response was empty"); +// http_client_ctx_free(ctx); +// return -1; +// } +// +// haystack = json_tokener_parse(response_body); +// track_metadata(haystack, track); +// +// http_client_ctx_free(ctx); +// jparse_free(haystack); + + return 0; +} + +void +album_metadata(json_object *jsonalbum, struct spotify_album *album) +{ + json_object* jsonartists; + + if (json_object_object_get_ex(jsonalbum, "artists", &jsonartists) && json_object_get_type(jsonartists) == json_type_array) + { + album->artist = jparse_artist_from_obj(jsonartists); + } + 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->genre = jparse_str_from_obj(jsonalbum, "genre"); // FIXME Genre is an array of strings ('genres'), but it seems to be always empty + album->label = jparse_str_from_obj(jsonalbum, "label"); + album->release_date = jparse_str_from_obj(jsonalbum, "release_date"); +} + +int +spotifywebapi_saved_albums_fetch(struct spotify_request *request, json_object **jsontracks, int *track_count, struct spotify_album *album) +{ + json_object *jsonalbum; + json_object *item; + json_object *needle; + + memset(album, 0, sizeof(struct spotify_album)); + *track_count = 0; + + if (request->index >= request->count) + { + return -1; + } + + item = json_object_array_get_idx(request->items, request->index); + if (!(item && json_object_object_get_ex(item, "album", &jsonalbum))) + { + DPRINTF(E_LOG, L_SPOTIFY, "Unexpected JSON: Item %d did not have 'album'->'uri'\n", request->index); + request->index++; + return -1; + } + + album_metadata(jsonalbum, album); + + album->added_at = jparse_str_from_obj(item, "added_at"); + album->mtime = jparse_time_from_obj(item, "added_at"); + + if (json_object_object_get_ex(jsonalbum, "tracks", &needle)) + { + if (jparse_array_from_obj(needle, "items", jsontracks) == 0) + { + *track_count = json_object_array_length(*jsontracks); + } + } + + request->index++; + + return 0; +} + +int +spotifywebapi_album_track_fetch(json_object *jsontracks, int index, struct spotify_track *track) +{ + json_object *jsontrack; + + memset(track, 0, sizeof(struct spotify_track)); + + jsontrack = json_object_array_get_idx(jsontracks, index); + + if (!jsontrack) + { + return -1; + } + + track_metadata(jsontrack, track); + + return 0; +} + +void +playlist_metadata(json_object *jsonplaylist, struct spotify_playlist *playlist) +{ + json_object *needle; + + 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"); + } +} + +int +spotifywebapi_playlists_fetch(struct spotify_request *request, struct spotify_playlist *playlist) +{ + json_object *jsonplaylist; + + memset(playlist, 0, sizeof(struct spotify_playlist)); + + if (request->index >= request->count) + { + return -1; + } + + jsonplaylist = json_object_array_get_idx(request->items, request->index); + + playlist_metadata(jsonplaylist, playlist); + + request->index++; + + return 0; +} + +int +spotifywebapi_playlisttracks_fetch(struct spotify_request *request, struct spotify_track *track) +{ + json_object *item; + json_object *jsontrack; + + memset(track, 0, sizeof(struct spotify_track)); + + if (request->index >= request->count) + { + return -1; + } + + item = json_object_array_get_idx(request->items, request->index); + if (!(item && json_object_object_get_ex(item, "track", &jsontrack))) + { + DPRINTF(E_LOG, L_SPOTIFY, "Unexpected JSON: Item %d did not have 'track'->'uri'\n", request->index); + request->index++; + return -1; + } + + track_metadata(jsontrack, track); + track->added_at = jparse_str_from_obj(item, "added_at"); + track->mtime = jparse_time_from_obj(item, "added_at"); + + request->index++; + + return 0; +} diff --git a/src/spotify_webapi.h b/src/spotify_webapi.h new file mode 100644 index 00000000..455d0758 --- /dev/null +++ b/src/spotify_webapi.h @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2016 Espen Jürgensen + * Copyright (C) 2016 Christian Meffert + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef SRC_SPOTIFY_WEBAPI_H_ +#define SRC_SPOTIFY_WEBAPI_H_ + + +#include + +#include "http.h" + +#ifdef HAVE_JSON_C_OLD +# include +#else +# include +#endif + + +#define SPOTIFY_WEBAPI_SAVED_TRACKS "https://api.spotify.com/v1/me/tracks?limit=50" +#define SPOTIFY_WEBAPI_TRACKS "https://api.spotify.com/v1/tracks/" +#define SPOTIFY_WEBAPI_SAVED_ALBUMS "https://api.spotify.com/v1/me/albums?limit=50" +#define SPOTIFY_WEBAPI_SAVED_PLAYLISTS "https://api.spotify.com/v1/me/playlists?limit=50" + +struct spotify_album +{ + const char *added_at; + time_t mtime; + + const char *album_type; + const char *artist; + const char *genre; + const char *id; + const char *label; + const char *name; + const char *release_date; + 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; + int duration_ms; + const char *id; + const char *name; + int track_number; + const char *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; +}; + +struct spotify_request +{ + struct http_client_ctx *ctx; + char *response_body; + json_object *haystack; + json_object *items; + int count; + int total; + char *next_uri; + + int index; +}; + +char * +spotifywebapi_oauth_uri_get(const char *redirect_uri); +int +spotifywebapi_token_get(const char *code, const char *redirect_uri, const char **err); +int +spotifywebapi_token_refresh(); + +int +spotifywebapi_request_uri(struct spotify_request *request, const char *uri); +void +spotifywebapi_request_end(struct spotify_request *request); +int +spotifywebapi_request_next(struct spotify_request *request, const char *uri); +int +spotifywebapi_saved_tracks_fetch(struct spotify_request *request, struct spotify_track *track); +int +spotifywebapi_track(const char *path, struct spotify_track *track); +int +spotifywebapi_saved_albums_fetch(struct spotify_request *request, json_object **jsontracks, int *track_count, struct spotify_album *album); +int +spotifywebapi_album_track_fetch(json_object *jsontracks, int index, struct spotify_track *track); +int +spotifywebapi_playlists_fetch(struct spotify_request *request, struct spotify_playlist* playlist); +int +spotifywebapi_playlisttracks_fetch(struct spotify_request *request, struct spotify_track *track); + + +#endif /* SRC_SPOTIFY_WEBAPI_H_ */