diff --git a/src/artwork.c b/src/artwork.c index dece08b8..6735d140 100644 --- a/src/artwork.c +++ b/src/artwork.c @@ -42,6 +42,7 @@ #include "artwork.h" #ifdef HAVE_SPOTIFY_H +# include "spotify_webapi.h" # include "spotify.h" #endif @@ -138,6 +139,7 @@ static int source_item_embedded_get(struct artwork_ctx *ctx); static int source_item_own_get(struct artwork_ctx *ctx); static int source_item_stream_get(struct artwork_ctx *ctx); static int source_item_spotify_get(struct artwork_ctx *ctx); +static int source_item_spotifywebapi_get(struct artwork_ctx *ctx); static int source_item_ownpl_get(struct artwork_ctx *ctx); /* List of sources that can provide artwork for a group (i.e. usually an album @@ -200,6 +202,12 @@ static struct artwork_source artwork_item_source[] = .data_kinds = (1 << DATA_KIND_SPOTIFY), .cache = ON_SUCCESS, }, + { + .name = "Spotify web api", + .handler = source_item_spotifywebapi_get, + .data_kinds = (1 << DATA_KIND_SPOTIFY), + .cache = ON_SUCCESS, + }, { .name = "playlist own", .handler = source_item_ownpl_get, @@ -218,6 +226,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? * @@ -322,9 +381,10 @@ rescale_calculate(int *target_w, int *target_h, int width, int height, int max_w return 0; } -/* Get an artwork file from the filesystem. Will rescale if needed. +/* + * Either gets the artwork file given in "path" from the file system (rescaled if needed) or rescales the artwork given in "inbuf". * - * @out evbuf Image data + * @out evbuf Image data (rescaled if needed) * @in path Path to the artwork file (alternative to inbuf) * @in inbuf Buffer with the artwork (alternative to path) * @in max_w Requested width @@ -727,10 +787,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 +821,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); @@ -867,12 +904,96 @@ source_item_spotify_get(struct artwork_ctx *ctx) return ART_E_ERROR; } + +static int +source_item_spotifywebapi_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) + { + DPRINTF(E_LOG, L_ART, "Out of memory for Spotify evbuf\n"); + return ART_E_ERROR; + } + + artwork_url = spotifywebapi_artwork_url_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); + 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 + uint8_t *buf = evbuffer_pullup(raw, -1); + if (!buf) + { + DPRINTF(E_LOG, L_ART, "Could not pullup raw artwork\n"); + goto out_free_evbuf; + } + + ret = evbuffer_add_reference(evbuf, buf, evbuffer_get_length(raw), NULL, NULL); +#else + ret = evbuffer_add_buffer_reference(evbuf, raw); +#endif + if (ret < 0) + { + DPRINTF(E_LOG, L_ART, "Could not copy/ref raw image for ffmpeg\n"); + goto out_free_evbuf; + } + + // For non-file input, artwork_get() will also fail if no rescaling is required + ret = artwork_get(ctx->evbuf, NULL, evbuf, ctx->max_w, ctx->max_h, false); + if (ret == ART_E_ERROR) + { + DPRINTF(E_DBG, L_ART, "Not rescaling Spotify image\n"); + ret = evbuffer_add_buffer(ctx->evbuf, raw); + if (ret < 0) + { + DPRINTF(E_LOG, L_ART, "Could not add or rescale image to output evbuf\n"); + goto out_free_evbuf; + } + } + + evbuffer_free(evbuf); + evbuffer_free(raw); + free(artwork_url); + + return content_type; + + out_free_evbuf: + evbuffer_free(evbuf); + evbuffer_free(raw); + free(artwork_url); + + return ART_E_ERROR; +} #else static int source_item_spotify_get(struct artwork_ctx *ctx) { return ART_E_ERROR; } + +static int +source_item_spotifywebapi_get(struct artwork_ctx *ctx) +{ + return ART_E_ERROR; +} #endif /* First looks of the mfi->path is in any playlist, and if so looks in the dir diff --git a/src/spotify_webapi.c b/src/spotify_webapi.c index d9021fcb..2be2cfe1 100644 --- a/src/spotify_webapi.c +++ b/src/spotify_webapi.c @@ -93,7 +93,6 @@ struct spotify_playlist int tracks_count; }; - // Credentials for the web api static char *spotify_access_token; static char *spotify_refresh_token; @@ -581,36 +580,42 @@ request_pagingobject_endpoint(const char *href, paging_item_cb item_cb, paging_r } static const char * -get_album_image(json_object *jsonalbum) +get_album_image(json_object *jsonalbum, int max_w) { json_object *jsonimages; json_object *jsonimage; int image_count; int index; const char *artwork_url; - int width; - int temp; artwork_url = NULL; - temp = 0; - width = 0; - if (json_object_object_get_ex(jsonalbum, "images", &jsonimages)) + if (max_w <= 0) { - // Find image closest to ART_DEFAULT_WIDTH - image_count = json_object_array_length(jsonimages); - for (index = 0; index < image_count; index++) - { - jsonimage = json_object_array_get_idx(jsonimages, index); - if (jsonimage) - { - temp = jparse_int_from_obj(jsonimage, "width"); + return NULL; + } - if (temp > width && temp < ART_DEFAULT_WIDTH) - { - artwork_url = jparse_str_from_obj(jsonimage, "url"); - width = temp; - } + if (!json_object_object_get_ex(jsonalbum, "images", &jsonimages)) + { + DPRINTF(E_DBG, L_SPOTIFY, "No images in for spotify album object found\n"); + return NULL; + } + + // Find first image that has a smaller width than the given max_w + // (this should avoid the need for resizing and improve performance at the cost of some quality loss) + // Note that Spotify returns the images ordered descending by width (widest image first) + image_count = json_object_array_length(jsonimages); + for (index = 0; index < image_count; index++) + { + jsonimage = json_object_array_get_idx(jsonimages, index); + if (jsonimage) + { + artwork_url = jparse_str_from_obj(jsonimage, "url"); + + if (jparse_int_from_obj(jsonimage, "width") <= max_w) + { + // We have the first image that has a smaller width than the given max_w + break; } } } @@ -619,7 +624,7 @@ get_album_image(json_object *jsonalbum) } static void -parse_metadata_track(json_object *jsontrack, struct spotify_track *track) +parse_metadata_track(json_object *jsontrack, struct spotify_track *track, int max_w) { json_object *jsonalbum; json_object *jsonartists; @@ -633,7 +638,8 @@ parse_metadata_track(json_object *jsontrack, struct spotify_track *track) if (json_object_object_get_ex(jsonalbum, "artists", &jsonartists)) track->album_artist = jparse_str_from_array(jsonartists, 0, "name"); - track->artwork_url = get_album_image(jsonalbum); + if (max_w > 0) + track->artwork_url = get_album_image(jsonalbum, max_w); } if (json_object_object_get_ex(jsontrack, "artists", &jsonartists)) @@ -679,7 +685,7 @@ get_year_from_date(const char *date) } static void -parse_metadata_album(json_object *jsonalbum, struct spotify_album *album) +parse_metadata_album(json_object *jsonalbum, struct spotify_album *album, int max_w) { json_object* jsonartists; @@ -701,7 +707,8 @@ parse_metadata_album(json_object *jsonalbum, struct spotify_album *album) album->release_date_precision = jparse_str_from_obj(jsonalbum, "release_date_precision"); album->release_year = get_year_from_date(album->release_date); - album->artwork_url = get_album_image(jsonalbum); + if (max_w > 0) + album->artwork_url = get_album_image(jsonalbum, max_w); // TODO Genre is an array of strings ('genres'), but it is always empty (https://github.com/spotify/web-api/issues/157) //album->genre = jparse_str_from_obj(jsonalbum, "genre"); @@ -1056,7 +1063,7 @@ queue_add_track(const char *uri, int position, char reshuffle, uint32_t item_id, if (!response) return -1; - parse_metadata_track(response, &track); + parse_metadata_track(response, &track, ART_DEFAULT_WIDTH); DPRINTF(E_DBG, L_SPOTIFY, "Got track: '%s' (%s) \n", track.name, track.uri); @@ -1097,7 +1104,7 @@ queue_add_album_tracks(json_object *item, int index, int total, void *arg) param = arg; - parse_metadata_track(item, &track); + parse_metadata_track(item, &track, ART_DEFAULT_WIDTH); if (!track.uri || !track.is_playable) { @@ -1125,7 +1132,7 @@ queue_add_album(const char *uri, int position, char reshuffle, uint32_t item_id, 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); + parse_metadata_album(json_album, ¶m.album, ART_DEFAULT_WIDTH); ret = db_queue_add_start(¶m.queue_add_info, position); if (ret < 0) @@ -1164,7 +1171,7 @@ queue_add_playlist_tracks(json_object *item, int index, int total, void *arg) return -1; } - parse_metadata_track(jsontrack, &track); + parse_metadata_track(jsontrack, &track, ART_DEFAULT_WIDTH); track.added_at = jparse_str_from_obj(item, "added_at"); track.mtime = jparse_time_from_obj(item, "added_at"); @@ -1436,7 +1443,7 @@ saved_album_add(json_object *item, int index, int total, void *arg) } // Map album information - parse_metadata_album(jsonalbum, &album); + parse_metadata_album(jsonalbum, &album, 0); album.added_at = jparse_str_from_obj(item, "added_at"); album.mtime = jparse_time_from_obj(item, "added_at"); @@ -1453,7 +1460,7 @@ saved_album_add(json_object *item, int index, int total, void *arg) if (!jsontrack) break; - parse_metadata_track(jsontrack, &track); + parse_metadata_track(jsontrack, &track, 0); track.mtime = album.mtime; ret = track_add(&track, &album, NULL, dir_id); @@ -1506,7 +1513,7 @@ saved_playlist_tracks_add(json_object *item, int index, int total, void *arg) return -1; } - parse_metadata_track(jsontrack, &track); + parse_metadata_track(jsontrack, &track, 0); track.added_at = jparse_str_from_obj(item, "added_at"); track.mtime = jparse_time_from_obj(item, "added_at"); @@ -1827,6 +1834,29 @@ spotifywebapi_pl_remove(const char *uri) library_exec_async(webapi_pl_remove, strdup(uri)); } +char * +spotifywebapi_artwork_url_get(const char *uri, int max_w, int max_h) +{ + json_object *response; + struct spotify_track track; + char *artwork_url; + + response = request_track(uri); + if (!response) + { + return NULL; + } + + parse_metadata_track(response, &track, max_w); + + DPRINTF(E_DBG, L_SPOTIFY, "Got track artwork url: '%s' (%s) \n", track.artwork_url, track.uri); + + artwork_url = safe_strdup(track.artwork_url); + jparse_free(response); + + return artwork_url; +} + void spotifywebapi_status_info_get(struct spotifywebapi_status_info *info) { diff --git a/src/spotify_webapi.h b/src/spotify_webapi.h index 00090441..aa62b574 100644 --- a/src/spotify_webapi.h +++ b/src/spotify_webapi.h @@ -53,6 +53,8 @@ void spotifywebapi_pl_save(const char *uri); void spotifywebapi_pl_remove(const char *uri); +char * +spotifywebapi_artwork_url_get(const char *uri, int max_w, int max_h); void spotifywebapi_status_info_get(struct spotifywebapi_status_info *info);