owntone-server/src/artwork.c

2131 lines
59 KiB
C

/*
* Copyright (C) 2015-2020 Espen Jürgensen <espenjurgensen@gmail.com>
* Copyright (C) 2010-2011 Julien BLACHE <jb@jblache.org>
*
* 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
*/
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <limits.h>
#include <pthread.h>
#include "db.h"
#include "misc.h"
#include "misc_json.h"
#include "logger.h"
#include "conffile.h"
#include "settings.h"
#include "cache.h"
#include "http.h"
#include "transcode.h"
#include "artwork.h"
#ifdef SPOTIFY
# include "library/spotify_webapi.h"
#endif
/* This artwork module will look for artwork by consulting a set of sources one
* at a time. A source is for instance the local library, the cache or a cover
* art database. For each source there is a handler function, which will do the
* actual work of getting the artwork.
*
* There are two types of handlers: item and group. Item handlers are capable of
* finding artwork for a single item (a dbmfi), while group handlers can get for
* an album or artist (a persistentid).
*
* An artwork source handler must return one of the following:
*
* ART_FMT_XXXX (positive) An image, see possible formats in artwork.h
* ART_E_NONE (zero) No artwork found
* ART_E_ERROR (negative) An error occurred while searching for artwork
* ART_E_ABORT (negative) Caller should abort artwork search (may be returned by cache)
*/
#define ART_E_NONE 0
#define ART_E_ERROR -1
#define ART_E_ABORT -2
// See online_source_is_failing()
#define ONLINE_SEARCH_COOLDOWN_TIME 3600
#define ONLINE_SEARCH_FAILURES_MAX 3
enum artwork_cache
{
NEVER = 0, // No caching of any results
ON_SUCCESS = 1, // Cache if artwork found
ON_FAILURE = 2, // Cache if artwork not found (so we don't keep asking)
};
// Input data to handlers, requested width, height and format. Can be set to
// zero then source is returned.
struct artwork_req_params {
int max_w;
int max_h;
int format;
};
/* This struct contains the data available to the handler, as well as a char
* buffer where the handler should output the path to the artwork (if it is
* local - otherwise the buffer can be left empty). The purpose of supplying the
* path is that the filescanner can then clear the cache in case the file
* changes.
*/
struct artwork_ctx {
// Handler should output path or URL to artwork here
char path[PATH_MAX];
// Handler should output artwork data to this evbuffer
struct evbuffer *evbuf;
// Requested size and format
struct artwork_req_params req_params;
// Input data to handler, did user configure to look for individual artwork
int individual;
// Input data for item handlers
struct db_media_file_info *dbmfi;
int id;
uint32_t data_kind;
uint32_t media_kind;
// Input data for group handlers
int64_t persistentid;
// Not to be used by handler - query for item or group
struct query_params qp;
// Not to be used by handler - should the result be cached
enum artwork_cache cache;
};
/* Definition of an artwork source. Covers both item and group sources.
*/
struct artwork_source {
// Name of the source, e.g. "cache"
const char *name;
// The handler
int (*handler)(struct artwork_ctx *ctx);
// data_kinds the handler can work with, combined with (1 << A) | (1 << B)
uint32_t data_kinds;
// media_kinds the handler supports, combined with A | B
uint32_t media_kinds;
// When should results from the source be cached?
enum artwork_cache cache;
};
/* Since online sources of artwork have similar characteristics there generic
* callers for them. They use the below info to request artwork.
*/
enum parse_result {
ONLINE_SOURCE_PARSE_OK,
ONLINE_SOURCE_PARSE_INVALID,
ONLINE_SOURCE_PARSE_NOT_FOUND,
ONLINE_SOURCE_PARSE_NO_PARSER,
};
// Remember previous artwork searches, used to avoid futile requests
struct online_search_history {
pthread_mutex_t mutex;
int last_id;
uint32_t last_hash;
int last_max_w;
int last_max_h;
int last_response_code;
char *last_artwork_url;
time_t last_timestamp;
int count_failures;
};
struct online_source {
// Name of online source
const char *name;
const char *setting_name;
// How to authorize (using the Authorize http header)
const char *auth_header;
int (*credentials_get)(char **auth_key, char **auth_secret);
// How to search for artwork
const char *search_endpoint;
const char *search_param;
struct {
const char *key;
const char *template;
} query_parts[8];
struct online_search_history *search_history;
// Function that can extract the artwork url from the parsed json response
enum parse_result (*response_jparse)(char **artwork_url, json_object *response, int max_w, int max_h);
};
/* File extensions that we look for or accept
*/
static const char *cover_extension[] =
{
"jpg", "png",
};
/* ----------------- DECLARE AND CONFIGURE SOURCE HANDLERS ----------------- */
/* Forward - group handlers */
static int source_group_cache_get(struct artwork_ctx *ctx);
static int source_group_dir_get(struct artwork_ctx *ctx);
/* Forward - item handlers */
static int source_item_cache_get(struct artwork_ctx *ctx);
static int source_item_embedded_get(struct artwork_ctx *ctx);
static int source_item_own_get(struct artwork_ctx *ctx);
static int source_item_artwork_url_get(struct artwork_ctx *ctx);
static int source_item_pipe_get(struct artwork_ctx *ctx);
static int source_item_spotifywebapi_track_get(struct artwork_ctx *ctx);
static int source_item_ownpl_get(struct artwork_ctx *ctx);
static int source_item_spotifywebapi_search_get(struct artwork_ctx *ctx);
static int source_item_discogs_get(struct artwork_ctx *ctx);
static int source_item_coverartarchive_get(struct artwork_ctx *ctx);
/* List of sources that can provide artwork for a group (i.e. usually an album
* identified by a persistentid). The source handlers will be called in the
* order of this list. Must be terminated by a NULL struct.
*/
static struct artwork_source artwork_group_source[] =
{
{
.name = "cache",
.handler = source_group_cache_get,
.cache = ON_FAILURE,
},
{
.name = "directory",
.handler = source_group_dir_get,
.cache = ON_SUCCESS | ON_FAILURE,
},
{
.name = NULL,
.handler = NULL,
.cache = 0,
}
};
/* List of sources that can provide artwork for an item (a track characterized
* by a dbmfi). The source handlers will be called in the order of this list.
* The handler will only be called if the data_kind matches. Must be terminated
* by a NULL struct.
*/
static struct artwork_source artwork_item_source[] =
{
{
.name = "cache",
.handler = source_item_cache_get,
.data_kinds = (1 << DATA_KIND_FILE) | (1 << DATA_KIND_SPOTIFY),
.media_kinds = MEDIA_KIND_ALL,
.cache = ON_FAILURE,
},
{
.name = "embedded",
.handler = source_item_embedded_get,
.data_kinds = (1 << DATA_KIND_FILE) | (1 << DATA_KIND_HTTP),
.media_kinds = MEDIA_KIND_ALL,
.cache = ON_SUCCESS | ON_FAILURE,
},
{
.name = "own",
.handler = source_item_own_get,
.data_kinds = (1 << DATA_KIND_FILE),
.media_kinds = MEDIA_KIND_ALL,
.cache = ON_SUCCESS | ON_FAILURE,
},
{
.name = "stream",
.handler = source_item_artwork_url_get,
.data_kinds = (1 << DATA_KIND_HTTP) | (1 << DATA_KIND_PIPE),
.media_kinds = MEDIA_KIND_MUSIC,
.cache = NEVER,
},
{
.name = "pipe",
.handler = source_item_pipe_get,
.data_kinds = (1 << DATA_KIND_PIPE),
.media_kinds = MEDIA_KIND_ALL,
.cache = NEVER,
},
{
.name = "Spotify track web api",
.handler = source_item_spotifywebapi_track_get,
.data_kinds = (1 << DATA_KIND_SPOTIFY),
.media_kinds = MEDIA_KIND_ALL,
.cache = ON_SUCCESS | ON_FAILURE,
},
{
// Note that even though caching is set for this handler, it will in most
// cases not happen because source_item_artwork_url_get() comes before
// and has NEVER. This is intentional because this handler is also a
// backup for when we don't get anything from the stream.
.name = "playlist own",
.handler = source_item_ownpl_get,
.data_kinds = (1 << DATA_KIND_HTTP),
.media_kinds = MEDIA_KIND_ALL,
.cache = ON_SUCCESS | ON_FAILURE,
},
{
.name = "Spotify search web api (files)",
.handler = source_item_spotifywebapi_search_get,
.data_kinds = (1 << DATA_KIND_FILE),
.media_kinds = MEDIA_KIND_MUSIC,
.cache = ON_SUCCESS | ON_FAILURE,
},
{
.name = "Spotify search web api (streams)",
.handler = source_item_spotifywebapi_search_get,
.data_kinds = (1 << DATA_KIND_HTTP) | (1 << DATA_KIND_PIPE),
.media_kinds = MEDIA_KIND_MUSIC,
.cache = NEVER,
},
{
.name = "Discogs (files)",
.handler = source_item_discogs_get,
.data_kinds = (1 << DATA_KIND_FILE),
.media_kinds = MEDIA_KIND_MUSIC,
.cache = ON_SUCCESS | ON_FAILURE,
},
{
.name = "Discogs (streams)",
.handler = source_item_discogs_get,
.data_kinds = (1 << DATA_KIND_HTTP) | (1 << DATA_KIND_PIPE),
.media_kinds = MEDIA_KIND_MUSIC,
.cache = NEVER,
},
{
// The Cover Art Archive seems rather slow, so low priority
.name = "Cover Art Archive (files)",
.handler = source_item_coverartarchive_get,
.data_kinds = (1 << DATA_KIND_FILE),
.media_kinds = MEDIA_KIND_MUSIC,
.cache = ON_SUCCESS | ON_FAILURE,
},
{
// The Cover Art Archive seems rather slow, so low priority
.name = "Cover Art Archive (streams)",
.handler = source_item_coverartarchive_get,
.data_kinds = (1 << DATA_KIND_HTTP) | (1 << DATA_KIND_PIPE),
.media_kinds = MEDIA_KIND_MUSIC,
.cache = NEVER,
},
{
.name = "own (pipe)",
.handler = source_item_own_get,
.data_kinds = (1 << DATA_KIND_PIPE),
.media_kinds = MEDIA_KIND_ALL,
.cache = ON_SUCCESS | ON_FAILURE,
},
{
.name = NULL,
.handler = NULL,
.data_kinds = 0,
.cache = 0,
}
};
/* Forward - parsers of online source responses */
static enum parse_result response_jparse_spotify(char **artwork_url, json_object *response, int max_w, int max_h);
static enum parse_result response_jparse_discogs(char **artwork_url, json_object *response, int max_w, int max_h);
static enum parse_result response_jparse_musicbrainz(char **artwork_url, json_object *response, int max_w, int max_h);
static int credentials_get_spotify(char **auth_key, char **auth_secret);
static int credentials_get_discogs(char **auth_key, char **auth_secret);
static struct online_search_history search_history_spotify = { .mutex = PTHREAD_MUTEX_INITIALIZER };
static struct online_search_history search_history_discogs = { .mutex = PTHREAD_MUTEX_INITIALIZER };
static struct online_search_history search_history_musicbrainz = { .mutex = PTHREAD_MUTEX_INITIALIZER };
/* Definitions of online sources */
static const struct online_source spotify_source =
{
.name = "Spotify",
.setting_name = "use_artwork_source_spotify",
.auth_header = "Bearer $SECRET$",
.credentials_get = credentials_get_spotify,
.search_endpoint = "https://api.spotify.com/v1/search",
.search_param = "type=track&limit=1&$QUERY$",
.query_parts =
{
{ "q", "artist:$ARTIST$ album:$ALBUM$" },
{ "q", "artist:$ARTIST$ track:$TITLE$" },
{ NULL, NULL },
},
.response_jparse = response_jparse_spotify,
.search_history = &search_history_spotify,
};
static const struct online_source discogs_source =
{
.name = "Discogs",
.setting_name = "use_artwork_source_discogs",
.auth_header = "Discogs key=$KEY$, secret=$SECRET$",
.credentials_get = credentials_get_discogs,
.search_endpoint = "https://api.discogs.com/database/search",
.search_param = "type=release&per_page=1&$QUERY$",
.query_parts =
{
{ "artist", "$ARTIST$" },
{ "release_title", "$ALBUM$" },
{ "track", "$TITLE$" },
{ NULL, NULL },
},
.response_jparse = response_jparse_discogs,
.search_history = &search_history_discogs,
};
static const struct online_source musicbrainz_source =
{
.name = "Musicbrainz",
.setting_name = "use_artwork_source_coverartarchive",
.search_endpoint = "http://musicbrainz.org/ws/2/release-group/",
.search_param = "limit=1&fmt=json&$QUERY$",
.query_parts =
{
{ "query", "artist:$ARTIST$ AND release:$ALBUM$ AND status:Official" },
{ "query", "artist:$ARTIST$ AND title:$TITLE$ AND status:Official" },
{ NULL, NULL },
},
.response_jparse = response_jparse_musicbrainz,
.search_history = &search_history_musicbrainz,
};
/* -------------------------------- HELPERS -------------------------------- */
/* Reads an artwork file from the given http url straight into an evbuf
*
* @out evbuf Image data
* @in url URL for the image
* @return ART_FMT_* on success, ART_E_NONE on 404, ART_E_ERROR otherwise
*/
static int
artwork_read_byurl(struct evbuffer *evbuf, const char *url)
{
struct http_client_ctx client;
struct keyval *kv;
const char *content_type;
size_t len;
int format;
int ret;
DPRINTF(E_SPAM, L_ART, "Trying internet artwork in %s\n", url);
format = ART_E_ERROR;
CHECK_NULL(L_ART, kv = keyval_alloc());
len = strlen(url);
if ((len < 14) || (len > PATH_MAX)) // Can't be shorter than http://a/1.jpg
{
DPRINTF(E_LOG, L_ART, "Artwork request URL is invalid (len=%zu): '%s'\n", len, url);
goto out;
}
memset(&client, 0, sizeof(struct http_client_ctx));
client.url = url;
client.input_headers = kv;
client.input_body = evbuf;
ret = http_client_request(&client, NULL);
if (ret < 0)
{
DPRINTF(E_LOG, L_ART, "Request to '%s' failed with return value %d\n", url, ret);
goto out;
}
if (client.response_code == HTTP_NOTFOUND)
{
DPRINTF(E_INFO, L_ART, "No artwork found at '%s' (code %d)\n", url, client.response_code);
format = ART_E_NONE;
goto out;
}
else if (client.response_code != HTTP_OK)
{
DPRINTF(E_LOG, L_ART, "Request to '%s' failed with code %d\n", url, client.response_code);
goto out;
}
content_type = keyval_get(kv, "Content-Type");
if (content_type && (strcasecmp(content_type, "image/jpeg") == 0 || strcasecmp(content_type, "image/jpg") == 0))
format = ART_FMT_JPEG;
else if (content_type && strcasecmp(content_type, "image/png") == 0)
format = ART_FMT_PNG;
else
DPRINTF(E_LOG, L_ART, "Artwork from '%s' has no known content type\n", url);
out:
keyval_clear(kv);
free(kv);
return format;
}
/* Reads an artwork file from the filesystem straight into an evbuf
* TODO Use evbuffer_add_file or evbuffer_read?
*
* @out evbuf Image data
* @in path Path to the artwork
* @return 0 on success, -1 on error
*/
static int
artwork_read_bypath(struct evbuffer *evbuf, char *path)
{
uint8_t buf[4096];
struct stat sb;
int fd;
int ret;
fd = open(path, O_RDONLY);
if (fd < 0)
{
DPRINTF(E_WARN, L_ART, "Could not open artwork file '%s': %s\n", path, strerror(errno));
return -1;
}
ret = fstat(fd, &sb);
if (ret < 0)
{
DPRINTF(E_WARN, L_ART, "Could not stat() artwork file '%s': %s\n", path, strerror(errno));
goto out_fail;
}
ret = evbuffer_expand(evbuf, sb.st_size);
if (ret < 0)
{
DPRINTF(E_LOG, L_ART, "Out of memory for artwork\n");
goto out_fail;
}
while ((ret = read(fd, buf, sizeof(buf))) > 0)
evbuffer_add(evbuf, buf, ret);
close(fd);
return 0;
out_fail:
close(fd);
return -1;
}
/* Calculates new size if the source image will not fit inside the requested
* size. If the original fits then dst_w/h will equal src_w/h.
*
* @out dst_w Rescaled width
* @out dst_h Rescaled height
* @in src_w Actual width
* @in src_h Actual height
* @in max_w Requested width
* @in max_h Requested height
*/
static void
size_calculate(int *dst_w, int *dst_h, int src_w, int src_h, int max_w, int max_h)
{
DPRINTF(E_DBG, L_ART, "Original image dimensions: w %d h %d\n", src_w, src_h);
*dst_w = src_w;
*dst_h = src_h;
// No valid target dimensions, use original
if ((max_w <= 0) || (max_h <= 0))
return;
// Smaller than target, use original
if ((src_w <= max_w) && (src_h <= max_h))
return;
// Wider aspect ratio than target
if (src_w * max_h > src_h * max_w)
{
*dst_w = max_w;
*dst_h = (double)max_w * ((double)src_h / (double)src_w);
}
// Taller or equal aspect ratio
else
{
*dst_w = (double)max_h * ((double)src_w / (double)src_h);
*dst_h = max_h;
}
if (*dst_h > max_h)
*dst_h = max_h;
// PNG prefers even row count
*dst_w += *dst_w % 2;
if (*dst_w > max_w)
*dst_w = max_w - (max_w % 2);
DPRINTF(E_DBG, L_ART, "Rescale required, destination width %d height %d\n", *dst_w, *dst_h);
}
/*
* Either gets the artwork file given in "path" (rescaled if needed) or rescales
* the artwork given in "inbuf".
*
* @out evbuf Image data (rescaled if needed)
* @in path Path to the artwork file (alternative to inbuf)
* @in in_buf Buffer with the artwork (alternative to path)
* @in is_embedded Whether the artwork in file is embedded or raw jpeg/png
* @in data_kind Used by the transcode module to determine e.g. probe size
* @in req_params Requested max size/format
* @return ART_FMT_* on success, ART_E_ERROR on error
*/
static int
artwork_get(struct evbuffer *evbuf, char *path, struct evbuffer *in_buf, bool is_embedded, enum data_kind data_kind, struct artwork_req_params req_params)
{
struct decode_ctx *xcode_decode = NULL;
struct encode_ctx *xcode_encode = NULL;
struct transcode_evbuf_io xcode_evbuf_io = { 0 };
struct evbuffer *xcode_buf = NULL;
void *frame;
int src_width;
int src_height;
int src_format;
int dst_width;
int dst_height;
int dst_format;
int ret;
DPRINTF(E_SPAM, L_ART, "Getting artwork (max destination width %d height %d)\n", req_params.max_w, req_params.max_h);
// At this point we don't know if we will need to rescale/reformat, and we
// won't know until probing the source (which the transcode module does). The
// act of probing uses evbuffer_remove(), thus consuming some of the buffer.
// So that means that if rescaling/reformating turns out not to be required,
// we could no longer just add in_buf to the evbuf buffer and return to the
// caller. The below makes that possible (with no copying).
if (in_buf)
{
CHECK_NULL(L_ART, xcode_buf = evbuffer_new());
ret = evbuffer_add_buffer_reference(xcode_buf, in_buf);
if (ret < 0)
{
DPRINTF(E_LOG, L_ART, "Could not copy/ref raw image for rescaling (ret=%d)\n", ret);
ret = ART_E_ERROR;
goto out;
}
xcode_evbuf_io.evbuf = xcode_buf;
xcode_decode = transcode_decode_setup(XCODE_JPEG, NULL, data_kind, NULL, &xcode_evbuf_io, 0); // Covers XCODE_PNG too
}
else
{
xcode_decode = transcode_decode_setup(XCODE_JPEG, NULL, data_kind, path, NULL, 0); // Covers XCODE_PNG too
}
if (!xcode_decode)
{
if (path)
DPRINTF(E_DBG, L_ART, "No artwork found in '%s'\n", path);
else
DPRINTF(E_DBG, L_ART, "No artwork provided to artwork_get()\n");
ret = ART_E_NONE;
goto out;
}
// Determine source and destination format
if (transcode_decode_query(xcode_decode, "is_jpeg"))
src_format = ART_FMT_JPEG;
else if (transcode_decode_query(xcode_decode, "is_png"))
src_format = ART_FMT_PNG;
else
{
if (path)
DPRINTF(E_DBG, L_ART, "File '%s' has no PNG or JPEG artwork\n", path);
else
DPRINTF(E_LOG, L_ART, "Artwork data provided to artwork_get() is not PNG or JPEG\n");
ret = ART_E_ERROR;
goto out;
}
dst_format = req_params.format ? req_params.format : src_format;
// Determine source and destination size
src_width = transcode_decode_query(xcode_decode, "width");
src_height = transcode_decode_query(xcode_decode, "height");
if (src_width <= 0 || src_height <= 0)
{
if (path)
DPRINTF(E_DBG, L_ART, "File '%s' has unknown artwork dimensions\n", path);
else
DPRINTF(E_LOG, L_ART, "Artwork data provided to artwork_get() has unknown dimensions\n");
ret = ART_E_ERROR;
goto out;
}
size_calculate(&dst_width, &dst_height, src_width, src_height, req_params.max_w, req_params.max_h);
// Fast path. Won't work for embedded, since we need to extract the image from
// the file.
if (!is_embedded && dst_format == src_format && dst_width == src_width && dst_height == src_height)
{
if (path)
ret = artwork_read_bypath(evbuf, path);
else
ret = evbuffer_add_buffer(evbuf, in_buf);
ret = (ret < 0) ? ART_E_ERROR : src_format;
goto out;
}
if (dst_format == ART_FMT_JPEG)
xcode_encode = transcode_encode_setup(XCODE_JPEG, NULL, xcode_decode, NULL, dst_width, dst_height);
else if (dst_format == ART_FMT_PNG)
xcode_encode = transcode_encode_setup(XCODE_PNG, NULL, xcode_decode, NULL, dst_width, dst_height);
else if (dst_format == ART_FMT_VP8)
xcode_encode = transcode_encode_setup(XCODE_VP8, NULL, xcode_decode, NULL, dst_width, dst_height);
else
xcode_encode = transcode_encode_setup(XCODE_JPEG, NULL, xcode_decode, NULL, dst_width, dst_height);
if (!xcode_encode)
{
if (path)
DPRINTF(E_WARN, L_ART, "Error preparing rescaling of '%s'\n", path);
else
DPRINTF(E_WARN, L_ART, "Error preparing rescaling of artwork data\n");
ret = ART_E_ERROR;
goto out;
}
// We don't use transcode() because we just want to process one frame
ret = transcode_decode(&frame, xcode_decode);
if (ret < 0)
{
ret = ART_E_ERROR;
goto out;
}
ret = transcode_encode(evbuf, xcode_encode, frame, 1);
if (ret < 0)
{
evbuffer_drain(evbuf, evbuffer_get_length(evbuf));
ret = ART_E_ERROR;
goto out;
}
ret = dst_format;
out:
transcode_encode_cleanup(&xcode_encode);
transcode_decode_cleanup(&xcode_decode);
if (xcode_buf)
evbuffer_free(xcode_buf);
return ret;
}
/*
* Checks if an image file with one of the configured artwork_basenames exists in
* the given directory "dir". Returns 0 if an image exists, -1 if no image was
* found or an error occurred.
*
* If an image exists, "out_path" will contain the absolute path to this image.
*
* @param out_path If return value is 0, contains the absolute path to the image
* @param len If return value is 0, contains the length of the absolute path
* @param dir The directory to search
* @return 0 if image exists, -1 otherwise
*/
static int
dir_image_find(char *out_path, size_t len, const char *dir)
{
char path[PATH_MAX];
int i;
int j;
int path_len;
int ret;
cfg_t *lib;
int nbasenames;
int nextensions;
ret = snprintf(path, sizeof(path), "%s", dir);
if ((ret < 0) || (ret >= sizeof(path)))
{
DPRINTF(E_LOG, L_ART, "Artwork path exceeds PATH_MAX (%s)\n", dir);
return -1;
}
path_len = strlen(path);
lib = cfg_getsec(cfg, "library");
nbasenames = cfg_size(lib, "artwork_basenames");
if (nbasenames == 0)
return -1;
nextensions = ARRAY_SIZE(cover_extension);
for (i = 0; i < nbasenames; i++)
{
for (j = 0; j < nextensions; j++)
{
ret = snprintf(path + path_len, sizeof(path) - path_len, "/%s.%s", cfg_getnstr(lib, "artwork_basenames", i), cover_extension[j]);
if ((ret < 0) || (ret >= sizeof(path) - path_len))
{
DPRINTF(E_LOG, L_ART, "Artwork path will exceed PATH_MAX (%s/%s)\n", dir, cfg_getnstr(lib, "artwork_basenames", i));
continue;
}
DPRINTF(E_SPAM, L_ART, "Trying directory artwork file %s\n", path);
ret = access(path, F_OK);
if (ret == 0)
{
snprintf(out_path, len, "%s", path);
return 0;
}
}
}
return -1;
}
/*
* Checks if an image file exists in the given directory "dir" with the basename
* equal to the directory name. Returns 0 if an image exists, -1 if no image was
* found or an error occurred.
*
* If an image exists, "out_path" will contain the absolute path to this image.
*
* @param out_path If return value is 0, contains the absolute path to the image
* @param len If return value is 0, contains the length of the absolute path
* @param dir The directory to search
* @return 0 if image exists, -1 otherwise
*/
static int
parent_dir_image_find(char *out_path, size_t len, const char *dir)
{
char path[PATH_MAX];
char parentdir[PATH_MAX];
char *ptr;
int i;
int nextensions;
int path_len;
int ret;
ret = snprintf(path, sizeof(path), "%s", dir);
if ((ret < 0) || (ret >= sizeof(path)))
{
DPRINTF(E_LOG, L_ART, "Artwork path exceeds PATH_MAX (%s)\n", dir);
return -1;
}
ptr = strrchr(path, '/');
if ((!ptr) || (strlen(ptr) <= 1))
{
DPRINTF(E_LOG, L_ART, "Could not find parent dir name (%s)\n", path);
return -1;
}
ret = snprintf(parentdir, sizeof(parentdir), "%s", ptr + 1);
if ((ret < 0) || (ret >= sizeof(parentdir)))
{
DPRINTF(E_LOG, L_ART, "Impossible error occured in parent_dir_image_find(), cause was: %s\n", ptr + 1);
return -1;
}
path_len = strlen(path);
nextensions = ARRAY_SIZE(cover_extension);
for (i = 0; i < nextensions; i++)
{
ret = snprintf(path + path_len, sizeof(path) - path_len, "/%s.%s", parentdir, cover_extension[i]);
if ((ret < 0) || (ret >= sizeof(path) - path_len))
{
DPRINTF(E_LOG, L_ART, "Artwork path will exceed PATH_MAX (%s)\n", parentdir);
continue;
}
DPRINTF(E_SPAM, L_ART, "Trying parent directory artwork file %s\n", path);
ret = access(path, F_OK);
if (ret == 0)
{
snprintf(out_path, len, "%s", path);
return 0;
}
}
return -1;
}
/* Looks for an artwork file in a directory. Will rescale if needed.
*
* @out evbuf Image data
* @out out_path Path to the artwork file if found, must be a char[PATH_MAX] buffer
* @in len Max size of "out_path"
* @in dir Directory to search
* @in req_params Requested max size/format
* @return ART_FMT_* on success, ART_E_NONE on nothing found, ART_E_ERROR on error
*/
static int
artwork_get_bydir(struct evbuffer *evbuf, char *out_path, size_t len, char *dir, struct artwork_req_params req_params)
{
int ret;
ret = dir_image_find(out_path, len, dir);
if (ret >= 0)
{
return artwork_get(evbuf, out_path, NULL, false, DATA_KIND_FILE, req_params);
}
ret = parent_dir_image_find(out_path, len, dir);
if (ret >= 0)
{
return artwork_get(evbuf, out_path, NULL, false, DATA_KIND_FILE, req_params);
}
return ART_E_NONE;
}
/* Retrieves artwork from an URL, will rescale if needed. Checks the cache stash
* before making a request. Stashes result in cache, also if negative.
*
* @out artwork Image data
* @in url URL of the artwork
* @in req_params Requested max size/format
* @return ART_FMT_* on success, ART_E_NONE or ART_E_ERROR
*/
static int
artwork_get_byurl(struct evbuffer *artwork, const char *url, struct artwork_req_params req_params)
{
struct evbuffer *raw;
int format;
int ret;
CHECK_NULL(L_ART, raw = evbuffer_new());
format = ART_E_ERROR;
ret = cache_artwork_read(raw, url, &format);
if (ret < 0)
{
format = artwork_read_byurl(raw, url);
cache_artwork_stash(raw, url, format);
}
// If we couldn't read, or we have cached a negative result from the last attempt, we stop now
if (format <= 0)
goto out;
// Takes care of resizing
ret = artwork_get(artwork, NULL, raw, false, 0, req_params);
if (ret < 0)
format = ART_E_ERROR;
out:
evbuffer_free(raw);
return format;
}
/* ------------------------- ONLINE SOURCE HANDLING ----------------------- */
#ifdef SPOTIFY
static int
credentials_get_spotify(char **auth_key, char **auth_secret)
{
struct spotifywebapi_status_info webapi_info;
struct spotifywebapi_access_token webapi_token;
spotifywebapi_status_info_get(&webapi_info);
if (!webapi_info.token_valid)
return -1; // Not logged in
spotifywebapi_access_token_get(&webapi_token);
if (!webapi_token.token)
return -1;
*auth_key = NULL;
*auth_secret = webapi_token.token;
return 0;
}
#else
static int
credentials_get_spotify(char **auth_key, char **auth_secret)
{
*auth_key = NULL;
*auth_secret = NULL;
return 0;
}
#endif
static int
credentials_get_discogs(char **auth_key, char **auth_secret)
{
*auth_key = strdup("ivzUxlkUiwpptDKpSCHF");
*auth_secret = strdup("CYLZyExtlznKCupoIIhTpHVDReLunhUo");
return 0;
}
static enum parse_result
response_jparse_discogs(char **artwork_url, json_object *response, int max_w, int max_h)
{
json_object *image;
const char *s;
const char *key;
if ((max_w > 0 && max_w <= 150) || (max_h > 0 && max_h <= 150))
key = "thumb";
else
key = "cover_image";
image = JPARSE_SELECT(response, "results", key);
if (!image || json_object_get_type(image) != json_type_string)
return ONLINE_SOURCE_PARSE_NOT_FOUND;
s = json_object_get_string(image);
if (!s)
return ONLINE_SOURCE_PARSE_INVALID;
*artwork_url = strdup(s);
return ONLINE_SOURCE_PARSE_OK;
}
static enum parse_result
response_jparse_musicbrainz(char **artwork_url, json_object *response, int max_w, int max_h)
{
json_object *id;
const char *s;
id = JPARSE_SELECT(response, "release-groups", "id");
if (!id || json_object_get_type(id) != json_type_string)
return ONLINE_SOURCE_PARSE_NOT_FOUND;
s = json_object_get_string(id);
if (!s)
return ONLINE_SOURCE_PARSE_INVALID;
// We will request 500 as a default. The use of https is not just for privacy
// it is also because the http client only supports redirects for https.
if ((max_w > 0 && max_w <= 250) || (max_h > 0 && max_h <= 250))
*artwork_url = safe_asprintf("https://coverartarchive.org/release-group/%s/front-250", s);
else if ((max_w == 0 && max_h == 0) || (max_w <= 500 && max_h <= 500))
*artwork_url = safe_asprintf("https://coverartarchive.org/release-group/%s/front-500", s);
else
*artwork_url = safe_asprintf("https://coverartarchive.org/release-group/%s/front-1200", s);
return ONLINE_SOURCE_PARSE_OK;
}
static enum parse_result
response_jparse_spotify(char **artwork_url, json_object *response, int max_w, int max_h)
{
json_object *images;
json_object *image;
const char *s;
int image_count;
int i;
images = JPARSE_SELECT(response, "tracks", "items", "album", "images");
if (!images || json_object_get_type(images) != json_type_array)
return ONLINE_SOURCE_PARSE_NOT_FOUND;
// Find first image that has a smaller width than the given max_w (this should
// avoid the need for resizing and improve performance at the cost of some
// quality loss). Note that Spotify returns the images ordered descending by
// width (widest image first). Special case is if no max width (max_w = 0) is
// given, then the widest images will be used.
s = NULL;
image_count = json_object_array_length(images);
for (i = 0; i < image_count; i++)
{
image = json_object_array_get_idx(images, i);
if (image)
{
s = jparse_str_from_obj(image, "url");
if (max_w <= 0 || jparse_int_from_obj(image, "width") <= max_w)
{
// We have the first image that has a smaller width than the given max_w
break;
}
}
}
if (!s)
return ONLINE_SOURCE_PARSE_NOT_FOUND;
*artwork_url = strdup(s);
return ONLINE_SOURCE_PARSE_OK;
}
static enum parse_result
online_source_response_parse(char **artwork_url, const struct online_source *src, struct evbuffer *response, int max_w, int max_h)
{
json_object *jresponse;
char *body;
int ret;
// 0-terminate for safety
evbuffer_add(response, "", 1);
body = (char *)evbuffer_pullup(response, -1);
DPRINTF(E_SPAM, L_ART, "Response from '%s': %s\n", src->name, body);
if (src->response_jparse)
{
jresponse = json_tokener_parse(body);
if (!jresponse)
return ONLINE_SOURCE_PARSE_INVALID;
ret = src->response_jparse(artwork_url, jresponse, max_w, max_h);
jparse_free(jresponse);
}
else
ret = ONLINE_SOURCE_PARSE_NO_PARSER;
return ret;
}
static int
online_source_request_url_make(char *url, size_t url_size, const struct online_source *src, struct artwork_ctx *ctx)
{
struct db_queue_item *queue_item;
struct keyval query = { 0 };
const char *artist = NULL;
const char *album = NULL;
const char *title = NULL;
char param[512];
char *encoded_query = NULL;
int ret;
int i;
// First check if the item is in the queue. When searching for artwork, it is
// better to use queue_item metadata. For stream items the queue metadata will
// for instance be updated with icy metadata. It is also possible we are asked
// for artwork for a non-library item.
queue_item = db_queue_fetch_byfileid(ctx->id);
if (queue_item && ctx->data_kind == DATA_KIND_HTTP)
{
// Normally we prefer searching by artist and album, but for streams we
// take the below approach, since they have no album information, and the
// title is in the album field
artist = queue_item->artist;
title = queue_item->album;
}
else if (queue_item)
{
artist = queue_item->artist;
album = queue_item->album;
}
else
{
// We will just search for artist and album
artist = ctx->dbmfi->artist;
album = ctx->dbmfi->album;
}
if (!artist || (!album && !title))
{
DPRINTF(E_DBG, L_ART, "Cannot construct query to %s, missing input data (artist=%s, album=%s, title=%s)\n", src->name, artist, album, title);
goto error;
}
for (i = 0; src->query_parts[i].key; i++)
{
if (!album && strstr(src->query_parts[i].template, "$ALBUM$"))
continue;
if (!title && strstr(src->query_parts[i].template, "$TITLE$"))
continue;
snprintf(param, sizeof(param), "%s", src->query_parts[i].template);
if ((safe_snreplace(param, sizeof(param), "$ARTIST$", artist) < 0) ||
(safe_snreplace(param, sizeof(param), "$ALBUM$", album) < 0) ||
(safe_snreplace(param, sizeof(param), "$TITLE$", title) < 0))
{
DPRINTF(E_WARN, L_ART, "Cannot make request for online artwork, query string is too long\n");
goto error;
}
ret = keyval_add(&query, src->query_parts[i].key, param);
if (ret < 0)
{
DPRINTF(E_LOG, L_ART, "keyval_add() failed in request_url_make()\n");
goto error;
}
}
encoded_query = http_form_urlencode(&query);
if (!encoded_query)
goto error;
snprintf(url, url_size, "%s?%s", src->search_endpoint, src->search_param);
if (safe_snreplace(url, url_size, "$QUERY$", encoded_query) < 0)
{
DPRINTF(E_WARN, L_ART, "Cannot make request for online artwork, url is too long (%zu)\n", strlen(encoded_query));
goto error;
}
free(encoded_query);
keyval_clear(&query);
free_queue_item(queue_item, 0);
return 0;
error:
free(encoded_query);
keyval_clear(&query);
free_queue_item(queue_item, 0);
return -1;
}
static int
online_source_search_check_last(char **last_artwork_url, const struct online_source *src, uint32_t hash, int max_w, int max_h)
{
struct online_search_history *history = src->search_history;
bool is_same;
pthread_mutex_lock(&history->mutex);
is_same = (hash == history->last_hash) &&
(max_w == history->last_max_w) &&
(max_h == history->last_max_h);
// Copy this to the caller while we have the lock anyway
if (is_same)
*last_artwork_url = safe_strdup(history->last_artwork_url);
pthread_mutex_unlock(&history->mutex);
return is_same ? 0 : -1;
}
static bool
online_source_is_failing(const struct online_source *src, int id)
{
struct online_search_history *history = src->search_history;
bool is_failing;
pthread_mutex_lock(&history->mutex);
// If the last request was more than ONLINE_SEARCH_COOLDOWN_TIME ago we will always try again
if (time(NULL) > history->last_timestamp + ONLINE_SEARCH_COOLDOWN_TIME)
is_failing = false;
// We won't try again if the source was not replying as expected
else if (history->last_response_code != HTTP_OK)
is_failing = true;
// The playback source has changed since the last search, let's give it a chance
// (internet streams can feed us with garbage search metadata, but will not change id)
else if (id != history->last_id)
is_failing = false;
// We allow up to ONLINE_SEARCH_FAILURES_MAX for the same track id before declaring failure
else if (history->count_failures < ONLINE_SEARCH_FAILURES_MAX)
is_failing = false;
else
is_failing = true;
pthread_mutex_unlock(&history->mutex);
return is_failing;
}
static void
online_source_history_update(const struct online_source *src, int id, uint32_t request_hash, int response_code, const char *artwork_url)
{
struct online_search_history *history = src->search_history;
pthread_mutex_lock(&history->mutex);
history->last_id = id;
history->last_hash = request_hash;
history->last_response_code = response_code;
history->last_timestamp = time(NULL);
free(history->last_artwork_url);
history->last_artwork_url = safe_strdup(artwork_url); // FIXME should free this on exit
if (artwork_url)
history->count_failures = 0;
else
history->count_failures++;
pthread_mutex_unlock(&history->mutex);
}
static int
auth_header_add(struct keyval *headers, const struct online_source *src)
{
char auth_header[256];
char *auth_key;
char *auth_secret;
int ret;
if (!src->auth_header)
return 0; // Nothing to do
ret = src->credentials_get(&auth_key, &auth_secret);
if (ret < 0)
return -1;
snprintf(auth_header, sizeof(auth_header), "%s", src->auth_header);
ret = ((safe_snreplace(auth_header, sizeof(auth_header), "$KEY$", auth_key) < 0) ||
(safe_snreplace(auth_header, sizeof(auth_header), "$SECRET$", auth_secret) < 0));
free(auth_key);
free(auth_secret);
if (ret)
{
DPRINTF(E_WARN, L_ART, "Cannot make request for online artwork, auth header is too long\n");
return -1;
}
keyval_add(headers, "Authorization", auth_header);
return 0;
}
static char *
online_source_search(const struct online_source *src, struct artwork_ctx *ctx)
{
char *artwork_url;
struct http_client_ctx client = { 0 };
struct keyval output_headers = { 0 };
uint32_t hash;
char url[2048];
int ret;
DPRINTF(E_SPAM, L_ART, "Trying %s for %s\n", src->name, ctx->dbmfi->path);
ret = online_source_request_url_make(url, sizeof(url), src, ctx);
if (ret < 0)
{
DPRINTF(E_WARN, L_ART, "Skipping artwork source %s, could not construct a request URL\n", src->name);
return NULL;
}
// Be nice to our peer + improve response times by not repeating search requests
hash = djb_hash(url, strlen(url));
ret = online_source_search_check_last(&artwork_url, src, hash, ctx->req_params.max_w, ctx->req_params.max_h);
if (ret == 0)
{
return artwork_url; // Will be NULL if we are repeating a search that failed
}
// If our recent searches have been futile we may give the source a break
if (online_source_is_failing(src, ctx->id))
{
DPRINTF(E_DBG, L_ART, "Skipping artwork source %s, too many failed requests\n", src->name);
return NULL;
}
ret = auth_header_add(&output_headers, src);
if (ret < 0)
{
return NULL;
}
CHECK_NULL(L_ART, client.input_body = evbuffer_new());
client.url = url;
client.output_headers = &output_headers;
ret = http_client_request(&client, NULL);
keyval_clear(&output_headers);
if (ret < 0 || client.response_code != HTTP_OK)
{
DPRINTF(E_WARN, L_ART, "Artwork request to '%s' failed, response code %d\n", url, client.response_code);
goto error;
}
ret = online_source_response_parse(&artwork_url, src, client.input_body, ctx->req_params.max_w, ctx->req_params.max_h);
if (ret == ONLINE_SOURCE_PARSE_NOT_FOUND)
DPRINTF(E_DBG, L_ART, "No image tag found in response from source '%s'\n", src->name);
else if (ret == ONLINE_SOURCE_PARSE_INVALID)
DPRINTF(E_WARN, L_ART, "Response from source '%s' was in an unexpected format\n", src->name);
else if (ret == ONLINE_SOURCE_PARSE_NO_PARSER)
DPRINTF(E_LOG, L_ART, "Bug! Cannot parse response from source '%s', parser missing\n", src->name);
else if (ret != ONLINE_SOURCE_PARSE_OK)
DPRINTF(E_LOG, L_ART, "Bug! Cannot parse response from source '%s', unknown error\n", src->name);
if (ret != ONLINE_SOURCE_PARSE_OK)
goto error;
online_source_history_update(src, ctx->id, hash, client.response_code, artwork_url);
evbuffer_free(client.input_body);
return artwork_url;
error:
online_source_history_update(src, ctx->id, hash, client.response_code, NULL);
evbuffer_free(client.input_body);
return NULL;
}
static bool
online_source_is_enabled(const struct online_source *src)
{
struct settings_category *category;
bool enabled;
CHECK_NULL(L_ART, category = settings_category_get("artwork"));
enabled = settings_option_getbool(settings_option_get(category, src->setting_name));
if (!enabled)
DPRINTF(E_SPAM, L_ART, "Source %s is disabled\n", src->name);
return enabled;
}
/* ---------------------- SOURCE HANDLER IMPLEMENTATION -------------------- */
/* Looks in the cache for group artwork
*/
static int
source_group_cache_get(struct artwork_ctx *ctx)
{
int format;
int cached;
int ret;
ret = cache_artwork_get(CACHE_ARTWORK_GROUP, ctx->persistentid, ctx->req_params.max_w, ctx->req_params.max_h, &cached, &format, ctx->evbuf);
if (ret < 0)
return ART_E_ERROR;
if (!cached)
return ART_E_NONE;
if (!format)
return ART_E_ABORT;
return format;
}
/* Looks for cover files in a directory, so if dir is /foo/bar and the user has
* configured the cover file names "cover" and "artwork" it will look for
* /foo/bar/cover.{png,jpg}, /foo/bar/artwork.{png,jpg} and also
* /foo/bar/bar.{png,jpg} (so-called parentdir artwork)
*/
static int
source_group_dir_get(struct artwork_ctx *ctx)
{
struct query_params qp;
char *dir;
int ret;
memset(&qp, 0, sizeof(struct query_params));
qp.type = Q_GROUP_DIRS;
qp.persistentid = ctx->persistentid;
ret = db_query_start(&qp);
if (ret < 0)
{
DPRINTF(E_LOG, L_ART, "Could not start Q_GROUP_DIRS query\n");
return ART_E_ERROR;
}
while (((ret = db_query_fetch_string(&dir, &qp)) == 0) && (dir))
{
/* The db query may return non-directories (eg if item is an internet stream or Spotify) */
if (access(dir, F_OK) < 0)
continue;
ret = artwork_get_bydir(ctx->evbuf, ctx->path, sizeof(ctx->path), dir, ctx->req_params);
if (ret > 0)
{
db_query_end(&qp);
return ret;
}
}
db_query_end(&qp);
if (ret < 0)
{
DPRINTF(E_LOG, L_ART, "Error fetching Q_GROUP_DIRS results\n");
return ART_E_ERROR;
}
return ART_E_NONE;
}
/* Looks in the cache for item artwork. Only relevant if configured to look for
* individual artwork.
*/
static int
source_item_cache_get(struct artwork_ctx *ctx)
{
int format;
int cached;
int ret;
if (!ctx->individual)
return ART_E_NONE;
ret = cache_artwork_get(CACHE_ARTWORK_INDIVIDUAL, ctx->id, ctx->req_params.max_w, ctx->req_params.max_h, &cached, &format, ctx->evbuf);
if (ret < 0)
return ART_E_ERROR;
if (!cached)
return ART_E_NONE;
if (!format)
return ART_E_ABORT;
return format;
}
/* Get an embedded artwork file from a media file. Will rescale if needed.
*/
static int
source_item_embedded_get(struct artwork_ctx *ctx)
{
int artwork;
DPRINTF(E_SPAM, L_ART, "Trying embedded artwork in %s\n", ctx->dbmfi->path);
if (safe_atoi32(ctx->dbmfi->artwork, &artwork) < 0)
{
DPRINTF(E_LOG, L_ART, "Error converting dbmfi artwork to number for '%s'\n", ctx->dbmfi->path);
return ART_E_ERROR;
}
if (artwork != ARTWORK_EMBEDDED)
return ART_E_NONE;
snprintf(ctx->path, sizeof(ctx->path), "%s", ctx->dbmfi->path);
return artwork_get(ctx->evbuf, ctx->path, NULL, true, ctx->data_kind, ctx->req_params);
}
/* Looks for basename(in_path).{png,jpg}, so if in_path is /foo/bar.mp3 it
* will look for /foo/bar.png and /foo/bar.jpg
*/
static int
source_item_own_get(struct artwork_ctx *ctx)
{
char path[PATH_MAX];
char *ptr;
int len;
int nextensions;
int i;
int ret;
ret = snprintf(path, sizeof(path), "%s", ctx->dbmfi->path);
if ((ret < 0) || (ret >= sizeof(path)))
{
DPRINTF(E_LOG, L_ART, "Artwork path exceeds PATH_MAX (%s)\n", ctx->dbmfi->path);
return ART_E_ERROR;
}
ptr = strrchr(path, '.');
if (ptr)
*ptr = '\0';
len = strlen(path);
nextensions = sizeof(cover_extension) / sizeof(cover_extension[0]);
for (i = 0; i < nextensions; i++)
{
ret = snprintf(path + len, sizeof(path) - len, ".%s", cover_extension[i]);
if ((ret < 0) || (ret >= sizeof(path) - len))
{
DPRINTF(E_LOG, L_ART, "Artwork path will exceed PATH_MAX (%s)\n", ctx->dbmfi->path);
continue;
}
DPRINTF(E_SPAM, L_ART, "Trying own artwork file %s\n", path);
ret = access(path, F_OK);
if (ret < 0)
continue;
break;
}
if (i == nextensions)
return ART_E_NONE;
snprintf(ctx->path, sizeof(ctx->path), "%s", path);
return artwork_get(ctx->evbuf, path, NULL, false, ctx->data_kind, ctx->req_params);
}
/*
* Downloads the artwork from the location pointed to by queue_item->artwork_url
*/
static int
source_item_artwork_url_get(struct artwork_ctx *ctx)
{
struct db_queue_item *queue_item;
int ret;
DPRINTF(E_SPAM, L_ART, "Trying artwork url for %s\n", ctx->dbmfi->path);
queue_item = db_queue_fetch_byfileid(ctx->id);
if (!queue_item || !queue_item->artwork_url)
{
free_queue_item(queue_item, 0);
return ART_E_NONE;
}
ret = artwork_get_byurl(ctx->evbuf, queue_item->artwork_url, ctx->req_params);
snprintf(ctx->path, sizeof(ctx->path), "%s", queue_item->artwork_url);
free_queue_item(queue_item, 0);
return ret;
}
/*
* If we are playing a pipe and there is also a metadata pipe, then input/pipe.c
* may have saved the incoming artwork in a tmp file
*
*/
static int
source_item_pipe_get(struct artwork_ctx *ctx)
{
struct db_queue_item *queue_item;
const char *proto = "file:";
char *path;
int ret;
DPRINTF(E_SPAM, L_ART, "Trying pipe metadata from %s.metadata\n", ctx->dbmfi->path);
queue_item = db_queue_fetch_byfileid(ctx->id);
if (!queue_item || !queue_item->artwork_url || strncmp(queue_item->artwork_url, proto, strlen(proto)) != 0)
goto notfound;
path = queue_item->artwork_url + strlen(proto);
// Sometimes the file has been replaced, but queue_item->artwork_url hasn't
// been updated yet. In that case just stop now.
ret = access(path, F_OK);
if (ret != 0)
goto notfound;
snprintf(ctx->path, sizeof(ctx->path), "%s", path);
free_queue_item(queue_item, 0);
return artwork_get(ctx->evbuf, ctx->path, NULL, false, ctx->data_kind, ctx->req_params);
notfound:
free_queue_item(queue_item, 0);
return ART_E_NONE;
}
static int
source_item_discogs_get(struct artwork_ctx *ctx)
{
char *url;
int ret;
if (!online_source_is_enabled(&discogs_source))
return ART_E_NONE;
url = online_source_search(&discogs_source, ctx);
if (!url)
return ART_E_NONE;
snprintf(ctx->path, sizeof(ctx->path), "%s", url);
ret = artwork_get_byurl(ctx->evbuf, url, ctx->req_params);
free(url);
return ret;
}
static int
source_item_coverartarchive_get(struct artwork_ctx *ctx)
{
char *url;
int ret;
if (!online_source_is_enabled(&musicbrainz_source))
return ART_E_NONE;
// We search Musicbrainz to get the Musicbrainz ID, which we need to get the
// artwork from the Cover Art Archive
url = online_source_search(&musicbrainz_source, ctx);
if (!url)
return ART_E_NONE;
snprintf(ctx->path, sizeof(ctx->path), "%s", url);
ret = artwork_get_byurl(ctx->evbuf, url, ctx->req_params);
free(url);
return ret;
}
#ifdef SPOTIFY
static int
source_item_spotifywebapi_track_get(struct artwork_ctx *ctx)
{
char *artwork_url;
int ret;
artwork_url = spotifywebapi_artwork_url_get(ctx->dbmfi->path, ctx->req_params.max_w, ctx->req_params.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_get_byurl(ctx->evbuf, artwork_url, ctx->req_params);
free(artwork_url);
return ret;
}
static int
source_item_spotifywebapi_search_get(struct artwork_ctx *ctx)
{
char *url;
int ret;
if (!online_source_is_enabled(&spotify_source))
return ART_E_NONE;
url = online_source_search(&spotify_source, ctx);
if (!url)
return ART_E_NONE;
snprintf(ctx->path, sizeof(ctx->path), "%s", url);
ret = artwork_get_byurl(ctx->evbuf, url, ctx->req_params);
free(url);
return ret;
}
#else
static int
source_item_spotifywebapi_track_get(struct artwork_ctx *ctx)
{
return ART_E_ERROR;
}
static int
source_item_spotifywebapi_search_get(struct artwork_ctx *ctx)
{
// Silence compiler warning about spotify_source being unused
(void)spotify_source;
return ART_E_NONE;
}
#endif
/* First looks of the mfi->path is in any playlist, and if so looks in the dir
* of the playlist file (m3u et al) to see if there is any artwork. So if the
* playlist is /foo/bar.m3u it will look for /foo/bar.png and /foo/bar.jpg.
*/
static int
source_item_ownpl_get(struct artwork_ctx *ctx)
{
struct query_params qp;
struct db_playlist_info dbpli;
char filter[PATH_MAX + 64];
char *mfi_path;
int format;
int ret;
ret = db_snprintf(filter, sizeof(filter), "filepath = '%q'", ctx->dbmfi->path);
if (ret < 0)
{
DPRINTF(E_LOG, L_ART, "Artwork path is too long: '%s'\n", ctx->dbmfi->path);
return ART_E_ERROR;
}
memset(&qp, 0, sizeof(struct query_params));
qp.type = Q_FIND_PL;
qp.filter = filter;
ret = db_query_start(&qp);
if (ret < 0)
{
DPRINTF(E_LOG, L_ART, "Could not start ownpl query\n");
return ART_E_ERROR;
}
mfi_path = ctx->dbmfi->path;
format = ART_E_NONE;
while (((ret = db_query_fetch_pl(&dbpli, &qp)) == 0) && (dbpli.id) && (format == ART_E_NONE))
{
if (!dbpli.path)
continue;
if (dbpli.artwork_url)
{
format = artwork_get_byurl(ctx->evbuf, dbpli.artwork_url, ctx->req_params);
if (format > 0)
break;
}
// Only handle non-remote paths with source_item_own_get()
if (dbpli.path && dbpli.path[0] == '/')
{
ctx->dbmfi->path = dbpli.path;
format = source_item_own_get(ctx);
}
}
ctx->dbmfi->path = mfi_path;
if ((ret < 0) || (format < 0))
format = ART_E_ERROR;
db_query_end(&qp);
return format;
}
/* --------------------------- SOURCE PROCESSING --------------------------- */
static int
process_items(struct artwork_ctx *ctx, int item_mode)
{
struct db_media_file_info dbmfi;
int i;
int ret;
ret = db_query_start(&ctx->qp);
if (ret < 0)
{
DPRINTF(E_LOG, L_ART, "Could not start query (type=%d)\n", ctx->qp.type);
ctx->cache = NEVER;
return -1;
}
while ((ret = db_query_fetch_file(&dbmfi, &ctx->qp)) == 0)
{
// Save the first songalbumid, might need it for process_group() if this search doesn't give anything
if (!ctx->persistentid)
safe_atoi64(dbmfi.songalbumid, &ctx->persistentid);
if (item_mode && !ctx->individual)
goto no_artwork;
ret = (safe_atoi32(dbmfi.id, &ctx->id) < 0) ||
(safe_atou32(dbmfi.data_kind, &ctx->data_kind) < 0) ||
(safe_atou32(dbmfi.media_kind, &ctx->media_kind) < 0) ||
(ctx->data_kind > 30);
if (ret)
{
DPRINTF(E_LOG, L_ART, "Error converting dbmfi id, data_kind or media_kind to number for '%s'\n", dbmfi.path);
continue;
}
for (i = 0; artwork_item_source[i].handler; i++)
{
if ((artwork_item_source[i].data_kinds & (1 << ctx->data_kind)) == 0)
continue;
if ((artwork_item_source[i].media_kinds & ctx->media_kind) == 0)
continue;
// If just one handler says we should not cache a negative result then we obey that
if ((artwork_item_source[i].cache & ON_FAILURE) == 0)
ctx->cache = NEVER;
DPRINTF(E_SPAM, L_ART, "Checking item source '%s'\n", artwork_item_source[i].name);
ctx->dbmfi = &dbmfi;
ret = artwork_item_source[i].handler(ctx);
ctx->dbmfi = NULL;
if (ret > 0)
{
DPRINTF(E_DBG, L_ART, "Artwork for '%s' found in source '%s'\n", dbmfi.title, artwork_item_source[i].name);
ctx->cache = artwork_item_source[i].cache;
db_query_end(&ctx->qp);
return ret;
}
else if (ret == ART_E_ABORT)
{
DPRINTF(E_DBG, L_ART, "Source '%s' stopped search for artwork for '%s'\n", artwork_item_source[i].name, dbmfi.title);
ctx->cache = NEVER;
break;
}
else if (ret == ART_E_ERROR)
{
DPRINTF(E_LOG, L_ART, "Source '%s' returned an error for '%s'\n", artwork_item_source[i].name, dbmfi.title);
ctx->cache = NEVER;
}
}
}
if (ret < 0)
{
DPRINTF(E_LOG, L_ART, "Error fetching results\n");
ctx->cache = NEVER;
}
no_artwork:
db_query_end(&ctx->qp);
return -1;
}
static int
process_group(struct artwork_ctx *ctx)
{
struct db_media_file_info dbmfi;
bool is_valid;
int i;
int ret;
if (!ctx->persistentid)
{
DPRINTF(E_LOG, L_ART, "Bug! No persistentid in call to process_group()\n");
ctx->cache = NEVER;
return -1;
}
// Check if the group is valid (exists and is not e.g. "Unknown album")
ret = db_query_start(&ctx->qp);
if (ret < 0)
{
DPRINTF(E_LOG, L_ART, "Could not start query to check if group is valid (persistentid = %" PRIi64 ")\n", ctx->qp.persistentid);
goto invalid_group;
}
is_valid = (db_query_fetch_file(&dbmfi, &ctx->qp) == 0 && strcmp(dbmfi.album, CFG_NAME_UNKNOWN_ALBUM) != 0 && strcmp(dbmfi.album_artist, CFG_NAME_UNKNOWN_ARTIST) != 0);
db_query_end(&ctx->qp);
if (!is_valid)
{
DPRINTF(E_SPAM, L_ART, "Skipping group sources due to unknown album or artist\n");
goto invalid_group;
}
for (i = 0; artwork_group_source[i].handler; i++)
{
// If just one handler says we should not cache a negative result then we obey that
if ((artwork_group_source[i].cache & ON_FAILURE) == 0)
ctx->cache = NEVER;
DPRINTF(E_SPAM, L_ART, "Checking group source '%s'\n", artwork_group_source[i].name);
ret = artwork_group_source[i].handler(ctx);
if (ret > 0)
{
DPRINTF(E_DBG, L_ART, "Artwork for group %" PRIi64 " found in source '%s'\n", ctx->persistentid, artwork_group_source[i].name);
ctx->cache = artwork_group_source[i].cache;
return ret;
}
else if (ret == ART_E_ABORT)
{
DPRINTF(E_DBG, L_ART, "Source '%s' stopped search for artwork for group %" PRIi64 "\n", artwork_group_source[i].name, ctx->persistentid);
ctx->cache = NEVER;
return -1;
}
else if (ret == ART_E_ERROR)
{
DPRINTF(E_LOG, L_ART, "Source '%s' returned an error for group %" PRIi64 "\n", artwork_group_source[i].name, ctx->persistentid);
ctx->cache = NEVER;
}
}
invalid_group:
return process_items(ctx, 0);
}
/* ------------------------------ ARTWORK API ------------------------------ */
int
artwork_get_item(struct evbuffer *evbuf, int id, int max_w, int max_h, int format)
{
struct artwork_ctx ctx;
char filter[32];
int ret;
DPRINTF(E_DBG, L_ART, "Artwork request for item %d (max_w=%d, max_h=%d)\n", id, max_w, max_h);
if (id == DB_MEDIA_FILE_NON_PERSISTENT_ID)
return -1;
memset(&ctx, 0, sizeof(struct artwork_ctx));
ctx.qp.type = Q_ITEMS;
ctx.qp.filter = filter;
ctx.evbuf = evbuf;
ctx.req_params.max_w = max_w;
ctx.req_params.max_h = max_h;
ctx.req_params.format = format;
ctx.cache = ON_FAILURE;
ctx.individual = cfg_getbool(cfg_getsec(cfg, "library"), "artwork_individual");
ret = db_snprintf(filter, sizeof(filter), "id = %d", id);
if (ret < 0)
{
DPRINTF(E_LOG, L_ART, "Could not build filter for file id %d; no artwork will be sent\n", id);
return -1;
}
// Note: process_items will set ctx.persistentid for the following process_group()
// - and do nothing else if artwork_individual is not configured by user
ret = process_items(&ctx, 1);
if (ret > 0)
{
if (ctx.cache & ON_SUCCESS)
cache_artwork_add(CACHE_ARTWORK_INDIVIDUAL, id, max_w, max_h, ret, ctx.path, evbuf);
return ret;
}
ctx.qp.type = Q_GROUP_ITEMS;
ctx.qp.persistentid = ctx.persistentid;
ret = process_group(&ctx);
if (ret > 0)
{
if (ctx.cache & ON_SUCCESS)
cache_artwork_add(CACHE_ARTWORK_GROUP, ctx.persistentid, max_w, max_h, ret, ctx.path, evbuf);
return ret;
}
DPRINTF(E_DBG, L_ART, "No artwork found for item %d\n", id);
if (ctx.cache & ON_FAILURE)
cache_artwork_add(CACHE_ARTWORK_GROUP, ctx.persistentid, max_w, max_h, 0, "", evbuf);
return -1;
}
int
artwork_get_group(struct evbuffer *evbuf, int id, int max_w, int max_h, int format)
{
struct artwork_ctx ctx;
int ret;
DPRINTF(E_DBG, L_ART, "Artwork request for group %d (max_w=%d, max_h=%d)\n", id, max_w, max_h);
memset(&ctx, 0, sizeof(struct artwork_ctx));
/* Get the persistent id for the given group id */
ret = db_group_persistentid_byid(id, &ctx.persistentid);
if (ret < 0)
{
DPRINTF(E_LOG, L_ART, "Error fetching persistent id for group id %d\n", id);
return -1;
}
ctx.qp.type = Q_GROUP_ITEMS;
ctx.qp.persistentid = ctx.persistentid;
ctx.evbuf = evbuf;
ctx.req_params.max_w = max_w;
ctx.req_params.max_h = max_h;
ctx.req_params.format = format;
ctx.cache = ON_FAILURE;
ctx.individual = cfg_getbool(cfg_getsec(cfg, "library"), "artwork_individual");
ret = process_group(&ctx);
if (ret > 0)
{
if (ctx.cache & ON_SUCCESS)
cache_artwork_add(CACHE_ARTWORK_GROUP, ctx.persistentid, max_w, max_h, ret, ctx.path, evbuf);
return ret;
}
DPRINTF(E_DBG, L_ART, "No artwork found for group %d\n", id);
if (ctx.cache & ON_FAILURE)
cache_artwork_add(CACHE_ARTWORK_GROUP, ctx.persistentid, max_w, max_h, 0, "", evbuf);
return -1;
}
/* Checks if the file is an artwork file */
bool
artwork_file_is_artwork(const char *filename)
{
cfg_t *lib;
int n;
int i;
int j;
int ret;
char artwork[PATH_MAX];
lib = cfg_getsec(cfg, "library");
n = cfg_size(lib, "artwork_basenames");
for (i = 0; i < n; i++)
{
for (j = 0; j < ARRAY_SIZE(cover_extension); j++)
{
ret = snprintf(artwork, sizeof(artwork), "%s.%s", cfg_getnstr(lib, "artwork_basenames", i), cover_extension[j]);
if ((ret < 0) || (ret >= sizeof(artwork)))
{
DPRINTF(E_INFO, L_ART, "Artwork path exceeds PATH_MAX (%s.%s)\n", cfg_getnstr(lib, "artwork_basenames", i), cover_extension[j]);
continue;
}
if (strcmp(artwork, filename) == 0)
return true;
}
}
return false;
}
bool
artwork_extension_is_artwork(const char *path)
{
char *ext;
int len;
int i;
ext = strrchr(path, '.');
if (!ext)
return false;
ext++;
for (i = 0; i < ARRAY_SIZE(cover_extension); i++)
{
len = strlen(cover_extension[i]);
if (strncasecmp(cover_extension[i], ext, len) != 0)
continue;
// Check that after the extension we either have the end or "?"
if (ext[len] != '\0' && ext[len] != '?')
continue;
return true;
}
return false;
}