From 997b4da4ad8edf299f2437f49f0a8e7337cfa658 Mon Sep 17 00:00:00 2001 From: chme Date: Tue, 11 Jul 2017 18:14:43 +0200 Subject: [PATCH] [spotify/artwork] Load artwork for spotify through the wep api --- src/artwork.c | 99 +++++++++++++------ src/artwork_legacy.c | 97 +++++++++++++------ src/spotify.c | 226 ------------------------------------------- src/spotify.h | 3 - src/spotify_webapi.c | 135 ++++++++++++++++++++++++++ src/spotify_webapi.h | 9 ++ 6 files changed, 278 insertions(+), 291 deletions(-) diff --git a/src/artwork.c b/src/artwork.c index 7ae53ed4..03bf2dfe 100644 --- a/src/artwork.c +++ b/src/artwork.c @@ -42,7 +42,7 @@ #include "artwork.h" #ifdef HAVE_SPOTIFY_H -# include "spotify.h" +# include "spotify_webapi.h" #endif /* This artwork module will look for artwork by consulting a set of sources one @@ -218,6 +218,57 @@ static struct artwork_source artwork_item_source[] = /* -------------------------------- HELPERS -------------------------------- */ +/* Reads an artwork file from the given url straight into an evbuf + * + * @out evbuf Image data + * @in url URL for the image + * @return 0 on success, -1 on error + */ +static int +artwork_url_read(struct evbuffer *evbuf, const char *url) +{ + struct http_client_ctx client; + struct keyval *kv; + const char *content_type; + int len; + int ret; + + DPRINTF(E_SPAM, L_ART, "Trying internet artwork in %s\n", url); + + ret = ART_E_NONE; + + len = strlen(url); + if ((len < 14) || (len > PATH_MAX)) // Can't be shorter than http://a/1.jpg + goto out_url; + + kv = keyval_alloc(); + if (!kv) + goto out_url; + + memset(&client, 0, sizeof(struct http_client_ctx)); + client.url = url; + client.input_headers = kv; + client.input_body = evbuf; + + if (http_client_request(&client) < 0) + goto out_kv; + + content_type = keyval_get(kv, "Content-Type"); + if (content_type && (strcmp(content_type, "image/jpeg") == 0)) + ret = ART_FMT_JPEG; + else if (content_type && (strcmp(content_type, "image/png") == 0)) + ret = ART_FMT_PNG; + else + ret = ART_FMT_JPEG; + + out_kv: + keyval_clear(kv); + free(kv); + + out_url: + return ret; +} + /* Reads an artwork file from the filesystem straight into an evbuf * TODO Use evbuffer_add_file or evbuffer_read? * @@ -727,10 +778,7 @@ source_item_own_get(struct artwork_ctx *ctx) static int source_item_stream_get(struct artwork_ctx *ctx) { - struct http_client_ctx client; struct db_queue_item *queue_item; - struct keyval *kv; - const char *content_type; char *url; char *ext; int len; @@ -764,34 +812,14 @@ source_item_stream_get(struct artwork_ctx *ctx) if (ret > 0) goto out_url; - kv = keyval_alloc(); - if (!kv) - goto out_url; - - memset(&client, 0, sizeof(struct http_client_ctx)); - client.url = url; - client.input_headers = kv; - client.input_body = ctx->evbuf; - - if (http_client_request(&client) < 0) - goto out_kv; - - content_type = keyval_get(kv, "Content-Type"); - if (content_type && (strcmp(content_type, "image/jpeg") == 0)) - ret = ART_FMT_JPEG; - else if (content_type && (strcmp(content_type, "image/png") == 0)) - ret = ART_FMT_PNG; + ret = artwork_url_read(ctx->evbuf, url); if (ret > 0) { - DPRINTF(E_SPAM, L_ART, "Found internet stream artwork in %s (%s)\n", url, content_type); + DPRINTF(E_SPAM, L_ART, "Found internet stream artwork in %s (%d)\n", url, ret); cache_artwork_stash(ctx->evbuf, url, ret); } - out_kv: - keyval_clear(kv); - free(kv); - out_url: free(url); @@ -804,8 +832,11 @@ source_item_spotify_get(struct artwork_ctx *ctx) { struct evbuffer *raw; struct evbuffer *evbuf; + char *artwork_url; + int content_type; int ret; + artwork_url = NULL; raw = evbuffer_new(); evbuf = evbuffer_new(); if (!raw || !evbuf) @@ -814,15 +845,19 @@ source_item_spotify_get(struct artwork_ctx *ctx) return ART_E_ERROR; } - ret = spotify_artwork_get(raw, ctx->dbmfi->path, ctx->max_w, ctx->max_h); - if (ret < 0) + artwork_url = spotifywebapi_artwork_get(ctx->dbmfi->path, ctx->max_w, ctx->max_h); + if (!artwork_url) { DPRINTF(E_WARN, L_ART, "No artwork from Spotify for %s\n", ctx->dbmfi->path); - evbuffer_free(raw); - evbuffer_free(evbuf); return ART_E_NONE; } + ret = artwork_url_read(raw, artwork_url); + if (ret <= 0) + goto out_free_evbuf; + + content_type = ret; + // Make a refbuf of raw for ffmpeg image size probing and possibly rescaling. // We keep raw around in case rescaling is not necessary. #ifdef HAVE_LIBEVENT2_OLD @@ -858,12 +893,14 @@ source_item_spotify_get(struct artwork_ctx *ctx) evbuffer_free(evbuf); evbuffer_free(raw); + free(artwork_url); - return ART_FMT_JPEG; + return content_type; out_free_evbuf: evbuffer_free(evbuf); evbuffer_free(raw); + free(artwork_url); return ART_E_ERROR; } diff --git a/src/artwork_legacy.c b/src/artwork_legacy.c index e25d502c..8790dddc 100644 --- a/src/artwork_legacy.c +++ b/src/artwork_legacy.c @@ -46,7 +46,7 @@ #include "artwork.h" #ifdef HAVE_SPOTIFY_H -# include "spotify.h" +# include "spotify_webapi.h" #endif #include "ffmpeg-compat.h" @@ -224,6 +224,57 @@ static struct artwork_source artwork_item_source[] = /* -------------------------------- HELPERS -------------------------------- */ +/* Reads an artwork file from the given url straight into an evbuf + * + * @out evbuf Image data + * @in url URL for the image + * @return 0 on success, -1 on error + */ +static int +artwork_url_read(struct evbuffer *evbuf, const char *url) +{ + struct http_client_ctx client; + struct keyval *kv; + const char *content_type; + int len; + int ret; + + DPRINTF(E_SPAM, L_ART, "Trying internet artwork in %s\n", url); + + ret = ART_E_NONE; + + len = strlen(url); + if ((len < 14) || (len > PATH_MAX)) // Can't be shorter than http://a/1.jpg + goto out_url; + + kv = keyval_alloc(); + if (!kv) + goto out_url; + + memset(&client, 0, sizeof(struct http_client_ctx)); + client.url = url; + client.input_headers = kv; + client.input_body = evbuf; + + if (http_client_request(&client) < 0) + goto out_kv; + + content_type = keyval_get(kv, "Content-Type"); + if (content_type && (strcmp(content_type, "image/jpeg") == 0)) + ret = ART_FMT_JPEG; + else if (content_type && (strcmp(content_type, "image/png") == 0)) + ret = ART_FMT_PNG; + else + ret = ART_FMT_JPEG; + + out_kv: + keyval_clear(kv); + free(kv); + + out_url: + return ret; +} + /* Reads an artwork file from the filesystem straight into an evbuf * TODO Use evbuffer_add_file or evbuffer_read? * @@ -1103,10 +1154,7 @@ source_item_own_get(struct artwork_ctx *ctx) static int source_item_stream_get(struct artwork_ctx *ctx) { - struct http_client_ctx client; struct db_queue_item *queue_item; - struct keyval *kv; - const char *content_type; char *url; char *ext; int len; @@ -1140,34 +1188,14 @@ source_item_stream_get(struct artwork_ctx *ctx) if (ret > 0) goto out_url; - kv = keyval_alloc(); - if (!kv) - goto out_url; - - memset(&client, 0, sizeof(struct http_client_ctx)); - client.url = url; - client.input_headers = kv; - client.input_body = ctx->evbuf; - - if (http_client_request(&client) < 0) - goto out_kv; - - content_type = keyval_get(kv, "Content-Type"); - if (content_type && (strcmp(content_type, "image/jpeg") == 0)) - ret = ART_FMT_JPEG; - else if (content_type && (strcmp(content_type, "image/png") == 0)) - ret = ART_FMT_PNG; + ret = artwork_url_read(ctx->evbuf, url); if (ret > 0) { - DPRINTF(E_SPAM, L_ART, "Found internet stream artwork in %s (%s)\n", url, content_type); + DPRINTF(E_SPAM, L_ART, "Found internet stream artwork in %s (%d)\n", url, ret); cache_artwork_stash(ctx->evbuf, url, ret); } - out_kv: - keyval_clear(kv); - free(kv); - out_url: free(url); @@ -1183,10 +1211,13 @@ source_item_spotify_get(struct artwork_ctx *ctx) AVInputFormat *ifmt; struct evbuffer *raw; struct evbuffer *evbuf; + char *artwork_url; + int content_type; int target_w; int target_h; int ret; + artwork_url = NULL; raw = evbuffer_new(); evbuf = evbuffer_new(); if (!raw || !evbuf) @@ -1195,15 +1226,19 @@ source_item_spotify_get(struct artwork_ctx *ctx) return ART_E_ERROR; } - ret = spotify_artwork_get(raw, ctx->dbmfi->path, ctx->max_w, ctx->max_h); - if (ret < 0) + artwork_url = spotifywebapi_artwork_get(ctx->dbmfi->path, ctx->max_w, ctx->max_h); + if (!artwork_url) { DPRINTF(E_WARN, L_ART, "No artwork from Spotify for %s\n", ctx->dbmfi->path); - evbuffer_free(raw); - evbuffer_free(evbuf); return ART_E_NONE; } + ret = artwork_url_read(raw, artwork_url); + if (ret <= 0) + goto out_free_evbuf; + + content_type = ret; + // Make a refbuf of raw for ffmpeg image size probing and possibly rescaling. // We keep raw around in case rescaling is not necessary. #ifdef HAVE_LIBEVENT2_OLD @@ -1278,7 +1313,7 @@ source_item_spotify_get(struct artwork_ctx *ctx) evbuffer_free(evbuf); evbuffer_free(raw); - return ART_FMT_JPEG; + return content_type; out_close_input: avformat_close_input(&src_ctx); diff --git a/src/spotify.c b/src/spotify.c index acda80e0..cc0c4d0d 100644 --- a/src/spotify.c +++ b/src/spotify.c @@ -141,9 +141,6 @@ static bool scanning; static pthread_mutex_t status_lck; static struct spotify_status_info spotify_status_info; -// Timeout timespec -static struct timespec spotify_artwork_timeout = { SPOTIFY_ARTWORK_TIMEOUT, 0 }; - // Audio buffer static struct evbuffer *spotify_audio_buffer; @@ -891,194 +888,6 @@ playback_eot(void *arg, int *retval) return COMMAND_END; } -static void -artwork_loaded_cb(sp_image *image, void *userdata) -{ - struct artwork_get_param *artwork; - - artwork = userdata; - - CHECK_ERR(L_SPOTIFY, pthread_mutex_lock(&artwork->mutex)); - - artwork->is_loaded = 1; - - CHECK_ERR(L_SPOTIFY, pthread_cond_signal(&artwork->cond)); - CHECK_ERR(L_SPOTIFY, pthread_mutex_unlock(&artwork->mutex)); -} - -static enum command_state -artwork_get_bh(void *arg, int *retval) -{ - struct artwork_get_param *artwork; - sp_imageformat imageformat; - sp_error err; - const void *data; - char *path; - size_t data_size; - int ret; - - artwork = arg; - sp_image *image = artwork->image; - path = artwork->path; - - fptr_sp_image_remove_load_callback(image, artwork_loaded_cb, artwork); - - err = fptr_sp_image_error(image); - if (err != SP_ERROR_OK) - { - DPRINTF(E_WARN, L_SPOTIFY, "Getting artwork (%s) failed, Spotify error: %s\n", path, fptr_sp_error_message(err)); - goto fail; - } - - if (!fptr_sp_image_is_loaded(image)) - { - DPRINTF(E_LOG, L_SPOTIFY, "Load callback returned, but no image? Possible bug: %s\n", path); - goto fail; - } - - imageformat = fptr_sp_image_format(image); - if (imageformat != SP_IMAGE_FORMAT_JPEG) - { - DPRINTF(E_WARN, L_SPOTIFY, "Getting artwork failed, invalid image format from Spotify: %s\n", path); - goto fail; - } - - data_size = 0; - data = fptr_sp_image_data(image, &data_size); - if (!data) - { - DPRINTF(E_LOG, L_SPOTIFY, "Getting artwork failed, no image data from Spotify: %s\n", path); - goto fail; - } - - if ((data_size < 200) || (data_size > 20000000)) - { - // Sometimes we get strange data size even though fptr_sp_image_data returns success - DPRINTF(E_LOG, L_SPOTIFY, "Skipping artwork, data size is weird (%zu)\n", data_size); - goto fail; - } - - ret = evbuffer_expand(artwork->evbuf, data_size); - if (ret < 0) - { - DPRINTF(E_LOG, L_SPOTIFY, "Out of memory for artwork (data size requested was %zu)\n", data_size); - goto fail; - } - - ret = evbuffer_add(artwork->evbuf, data, data_size); - if (ret < 0) - { - DPRINTF(E_LOG, L_SPOTIFY, "Could not add Spotify image to event buffer\n"); - goto fail; - } - - DPRINTF(E_DBG, L_SPOTIFY, "Spotify artwork loaded ok\n"); - - fptr_sp_image_release(image); - - *retval = 0; - return COMMAND_END; - - fail: - fptr_sp_image_release(image); - - *retval = -1; - return COMMAND_END; -} - -static enum command_state -artwork_get(void *arg, int *retval) -{ - struct artwork_get_param *artwork; - char *path; - sp_link *link; - sp_track *track; - sp_album *album; - const byte *image_id; - sp_image *image; - sp_image_size image_size; - sp_error err; - - artwork = arg; - path = artwork->path; - - // Now begins: path -> link -> track -> album -> image_id -> image -> format -> data - link = fptr_sp_link_create_from_string(path); - if (!link) - { - DPRINTF(E_WARN, L_SPOTIFY, "Getting artwork failed, invalid Spotify link: %s\n", path); - goto level1_exit; - } - - track = fptr_sp_link_as_track(link); - if (!track) - { - DPRINTF(E_WARN, L_SPOTIFY, "Getting artwork failed, invalid Spotify track: %s\n", path); - goto level2_exit; - } - - album = fptr_sp_track_album(track); - if (!album) - { - DPRINTF(E_WARN, L_SPOTIFY, "Getting artwork failed, invalid Spotify album: %s\n", path); - goto level2_exit; - } - - // Get an image at least the same size as requested - image_size = SP_IMAGE_SIZE_SMALL; // 64x64 - if ((artwork->max_w > 64) || (artwork->max_h > 64)) - image_size = SP_IMAGE_SIZE_NORMAL; // 300x300 - if ((artwork->max_w > 300) || (artwork->max_h > 300)) - image_size = SP_IMAGE_SIZE_LARGE; // 640x640 - - image_id = fptr_sp_album_cover(album, image_size); - if (!image_id) - { - DPRINTF(E_DBG, L_SPOTIFY, "Getting artwork failed, no Spotify image id: %s\n", path); - goto level2_exit; - } - - image = fptr_sp_image_create(g_sess, image_id); - if (!image) - { - DPRINTF(E_DBG, L_SPOTIFY, "Getting artwork failed, no Spotify image: %s\n", path); - goto level2_exit; - } - - fptr_sp_link_release(link); - - artwork->image = image; - artwork->is_loaded = fptr_sp_image_is_loaded(image); - - /* If the image is ready we can return it straight away, otherwise we will - * let the calling thread wait, since the Spotify thread should not wait - */ - if (artwork->is_loaded) - return artwork_get_bh(artwork, retval); - - DPRINTF(E_SPAM, L_SPOTIFY, "Will wait for Spotify to call artwork_loaded_cb\n"); - - /* Async - we will return to spotify_artwork_get which will wait for callback */ - err = fptr_sp_image_add_load_callback(image, artwork_loaded_cb, artwork); - if (err != SP_ERROR_OK) - { - DPRINTF(E_WARN, L_SPOTIFY, "Adding artwork cb failed, Spotify error: %s\n", fptr_sp_error_message(err)); - *retval = -1; - return COMMAND_END; - } - - *retval = 0; - return COMMAND_END; - - level2_exit: - fptr_sp_link_release(link); - - level1_exit: - *retval = -1; - return COMMAND_END; -} - - /* --------------------------- SESSION CALLBACKS ------------------------- */ /** * This callback is called when an attempt to login has succeeded or failed. @@ -1449,41 +1258,6 @@ spotify_playback_seek(int ms) return -1; } -/* Thread: httpd (artwork) and worker */ -int -spotify_artwork_get(struct evbuffer *evbuf, char *path, int max_w, int max_h) -{ - struct artwork_get_param artwork; - struct timespec ts; - int ret; - - artwork.evbuf = evbuf; - artwork.path = path; - artwork.max_w = max_w; - artwork.max_h = max_h; - - CHECK_ERR(L_SPOTIFY, mutex_init(&artwork.mutex)); - CHECK_ERR(L_SPOTIFY, pthread_cond_init(&artwork.cond, NULL)); - - ret = commands_exec_sync(cmdbase, artwork_get, NULL, &artwork); - - // Artwork was not ready, wait for callback from libspotify - if (ret == 0) - { - CHECK_ERR(L_SPOTIFY, pthread_mutex_lock(&artwork.mutex)); - ts = timespec_reltoabs(spotify_artwork_timeout); - while ((!artwork.is_loaded) && (ret != ETIMEDOUT)) - CHECK_ERR_EXCEPT(L_SPOTIFY, pthread_cond_timedwait(&artwork.cond, &artwork.mutex, &ts), ret, ETIMEDOUT); - if (ret == ETIMEDOUT) - DPRINTF(E_LOG, L_SPOTIFY, "Timeout waiting for artwork from Spotify\n"); - CHECK_ERR(L_SPOTIFY, pthread_mutex_unlock(&artwork.mutex)); - - ret = commands_exec_sync(cmdbase, artwork_get_bh, NULL, &artwork); - } - - return ret; -} - /* Thread: httpd */ void spotify_oauth_interface(struct evbuffer *evbuf, const char *redirect_uri) diff --git a/src/spotify.h b/src/spotify.h index 8bbbf06b..af21611f 100644 --- a/src/spotify.h +++ b/src/spotify.h @@ -39,9 +39,6 @@ spotify_playback_stop_nonblock(void); int spotify_playback_seek(int ms); -int -spotify_artwork_get(struct evbuffer *evbuf, char *path, int max_w, int max_h); - void spotify_oauth_interface(struct evbuffer *evbuf, const char *redirect_uri); diff --git a/src/spotify_webapi.c b/src/spotify_webapi.c index 3452d3bf..6f824121 100644 --- a/src/spotify_webapi.c +++ b/src/spotify_webapi.c @@ -49,6 +49,7 @@ 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"; @@ -421,6 +422,28 @@ get_owner_plid_from_uri(const char *uri, char **owner, char **plid) return 0; } +/* + * Extracts the id from a spotify album/artist/track uri + * + * E. g. album-uri has the following format: spotify:album:[id] + * The id must be freed by the caller. + */ +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; +} + int spotifywebapi_playlisttracks_fetch(struct spotify_request *request, struct spotify_track *track) { @@ -491,6 +514,118 @@ spotifywebapi_playlist_start(struct spotify_request *request, const char *path, return 0; } + +static int +spotifywebapi_track_start(struct spotify_request *request, const char *path, struct spotify_track *track, json_object **jsonimages, int *image_count) +{ + char uri[1024]; + char *id; + json_object *jsonalbum; + int ret; + + memset(track, 0, sizeof(struct spotify_track)); + + ret = get_id_from_uri(path, &id); + if (ret < 0) + { + DPRINTF(E_LOG, L_SPOTIFY, "Error extracting id from track uri '%s'\n", path); + return -1; + } + + ret = snprintf(uri, sizeof(uri), spotify_track_uri, id); + free(id); + if (ret < 0 || ret >= sizeof(uri)) + { + DPRINTF(E_LOG, L_SPOTIFY, "Error creating playlist endpoint uri for playlist '%s'\n", path); + return -1; + } + + ret = request_uri(request, uri); + if (ret < 0) + { + return -1; + } + + request->haystack = json_tokener_parse(request->response_body); + parse_metadata_track(request->haystack, track); + + if (jsonimages && image_count) + { + if (json_object_object_get_ex(request->haystack, "album", &jsonalbum)) + { + if (json_object_object_get_ex(jsonalbum, "images", jsonimages)) + { + *image_count = json_object_array_length(*jsonimages); + } + } + } + + return 0; +} + +static int +spotifywebapi_image_fetch(json_object *jsonimages, int index, struct spotify_image *image) +{ + json_object *jsonimage; + + memset(image, 0, sizeof(struct spotify_image)); + + jsonimage = json_object_array_get_idx(jsonimages, index); + + if (!jsonimage) + { + return -1; + } + + image->url = jparse_str_from_obj(jsonimage, "url"); + image->width = jparse_int_from_obj(jsonimage, "width"); + image->height = jparse_int_from_obj(jsonimage, "height"); + + return 0; +} + +char * +spotifywebapi_artwork_get(const char *path, int max_w, int max_h) +{ + struct spotify_request request; + struct spotify_track track; + json_object *jsonimages; + struct spotify_image image; + int image_count; + int i; + char *artwork_url; + int ret; + + artwork_url = NULL; + image_count = 0; + + memset(&request, 0, sizeof(struct spotify_request)); + ret = spotifywebapi_track_start(&request, path, &track, &jsonimages, &image_count); + if (ret == 0) + { + DPRINTF(E_DBG, L_SPOTIFY, "Got track: '%s' (%s) \n", track.name, track.uri); + + // Get an image at least the same size as requested, images are returned with decreasing size + for (i = (image_count - 1); i >= 0 && ret == 0; i--) + { + ret = spotifywebapi_image_fetch(jsonimages, i, &image); + if (ret < 0 || !image.url) + continue; + + free(artwork_url); + + DPRINTF(E_DBG, L_SPOTIFY, "Got image for album '%s' - '%s': '%s' (%dx%d)\n", track.album_artist, track.album, image.url, image.height, image.width); + artwork_url = strdup(image.url); + + if ((max_w < image.width) || (max_h < image.height)) + break; + } + } + spotifywebapi_request_end(&request); + + return artwork_url; +} + static int request_user_info() { diff --git a/src/spotify_webapi.h b/src/spotify_webapi.h index 11af153c..c50be7ac 100644 --- a/src/spotify_webapi.h +++ b/src/spotify_webapi.h @@ -82,6 +82,13 @@ struct spotify_playlist int tracks_count; }; +struct spotify_image +{ + const char *url; + int width; + int height; +}; + struct spotify_request { struct http_client_ctx *ctx; @@ -116,5 +123,7 @@ int spotifywebapi_playlisttracks_fetch(struct spotify_request *request, struct spotify_track *track); int spotifywebapi_playlist_start(struct spotify_request *request, const char *path, struct spotify_playlist *playlist); +char * +spotifywebapi_artwork_get(const char *path, int max_w, int max_h); #endif /* SRC_SPOTIFY_WEBAPI_H_ */