From 238ee3c1224a85f38a57694b7e7801c490066598 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Tue, 29 Dec 2015 00:26:52 +0100 Subject: [PATCH] [artwork] Refactor artwork to make it easier to add new backends (WIP) --- src/artwork.c | 905 ++++++++++++++++++++++++++++++++------------------ src/artwork.h | 27 +- 2 files changed, 604 insertions(+), 328 deletions(-) diff --git a/src/artwork.c b/src/artwork.c index 73563e0d..713c1d6c 100644 --- a/src/artwork.c +++ b/src/artwork.c @@ -1,4 +1,5 @@ /* + * Copyright (C) 2015-2016 Espen Jürgensen * Copyright (C) 2010-2011 Julien BLACHE * * This program is free software; you can redistribute it and/or modify @@ -48,17 +49,106 @@ # include "spotify.h" #endif +/* Old ffmpeg compability + */ #if LIBAVCODEC_VERSION_MAJOR <= 53 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR <= 34) # define AV_CODEC_ID_MJPEG CODEC_ID_MJPEG # define AV_CODEC_ID_PNG CODEC_ID_PNG # define AV_CODEC_ID_NONE CODEC_ID_NONE #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_JPEG (positive) Found a jpeg + * ART_FMT_PNG (positive) Found a png + * 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 + +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) +}; + +/* 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 here if artwork is local + char path[PATH_MAX]; + // Handler should output artwork data to this evbuffer + struct evbuffer *evbuf; + + // Input data for item handlers + struct db_media_file_info *dbmfi; + int id; + + // Input data for group handlers + int64_t persistentid; + + // Input data for both (requested width and height) + int max_w; + int max_h; + + // 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); + + // What data_kinds the handler can work with, combined with (1 << A) | (1 << B) + int data_kinds; + + // When should results from the source be cached? + enum artwork_cache cache; +}; + +/* File extensions that we look for or accept + */ static const char *cover_extension[] = { "jpg", "png", }; + + +/* -------------------------------- HELPERS -------------------------------- */ + +/* 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(struct evbuffer *evbuf, char *path) { @@ -103,6 +193,16 @@ artwork_read(struct evbuffer *evbuf, char *path) return -1; } +/* Will the source image fit inside requested size. If not, what size should it + * be rescaled to to maintain aspect ratio. + * + * @in src Image source + * @in max_w Requested width + * @in max_h Requested height + * @out target_w Rescaled width + * @out target_h Rescaled height + * @return 0 no rescaling needed, 1 rescaling needed + */ static int rescale_needed(AVCodecContext *src, int max_w, int max_h, int *target_w, int *target_h) { @@ -147,6 +247,15 @@ rescale_needed(AVCodecContext *src, int max_w, int max_h, int *target_w, int *ta return 1; } +/* Rescale an image + * + * @out evbuf Rescaled image data + * @in src_ctx Image source + * @in s Index of stream containing image + * @in out_w Rescaled width + * @in out_h Rescaled height + * @return ART_FMT_* on success, -1 on error + */ static int artwork_rescale(struct evbuffer *evbuf, AVFormatContext *src_ctx, int s, int out_w, int out_h) { @@ -572,6 +681,14 @@ artwork_rescale(struct evbuffer *evbuf, AVFormatContext *src_ctx, int s, int out return ret; } +/* Get an artwork file from the filesystem. Will rescale if needed. + * + * @out evbuf Image data + * @in path Path to the artwork + * @in max_w Requested width + * @in max_h Requested height + * @return ART_FMT_* on success, -1 on error + */ static int artwork_get(struct evbuffer *evbuf, char *path, int max_w, int max_h) { @@ -669,92 +786,15 @@ artwork_get(struct evbuffer *evbuf, char *path, int max_w, int max_h) return ret; } -/* - * Downloads the artwork pointed to by the ICY metadata tag in an internet radio - * stream (the StreamUrl tag). The path will be converted back to the id, which - * is given to the player. If the id is currently being played, and there is a - * valid ICY metadata artwork URL available, it will be returned to this - * function, which will then use the http client to get the artwork. Notice: No - * rescaling is done. - * - * @param evbuf the event buffer that will contain the (scaled) image - * @param path path to the item we are getting artwork for - * @return ART_FMT_* on success, 0 on error and nothing found - */ -static int -artwork_get_player_image(struct evbuffer *evbuf, char *path) -{ - struct http_client_ctx ctx; - struct keyval *kv; - const char *content_type; - char *url; - char *ext; - int format; - int id; - int len; - - DPRINTF(E_DBG, L_ART, "Trying internet stream artwork in %s\n", path); - - format = 0; - - id = db_file_id_bypath(path); - if (!id) - return 0; - - url = player_get_icy_artwork_url(id); - if (!url) - return 0; - - len = strlen(url); - if ((len < 14) || (len > PATH_MAX)) // Can't be shorter than http://a/1.jpg - goto out_url; - - ext = strrchr(url, '.'); - if (!ext) - goto out_url; - if ((strcmp(ext, ".jpg") != 0) && (strcmp(ext, ".png") != 0)) - goto out_url; - - cache_artwork_read(evbuf, url, &format); - if (format > 0) - goto out_url; - - kv = keyval_alloc(); - if (!kv) - goto out_url; - - memset(&ctx, 0, sizeof(ctx)); - ctx.url = url; - ctx.headers = kv; - ctx.body = evbuf; - - if (http_client_request(&ctx) < 0) - goto out_kv; - - content_type = keyval_get(kv, "Content-Type"); - if (content_type && (strcmp(content_type, "image/jpeg") == 0)) - format = ART_FMT_JPEG; - else if (content_type && (strcmp(content_type, "image/png") == 0)) - format = ART_FMT_PNG; - - if (format) - { - DPRINTF(E_DBG, L_ART, "Found internet stream artwork in %s (%s)\n", url, content_type); - - cache_artwork_stash(evbuf, url, format); - } - - out_kv: - keyval_clear(kv); - free(kv); - - out_url: - free(url); - - return format; -} - #if LIBAVFORMAT_VERSION_MAJOR >= 55 || (LIBAVFORMAT_VERSION_MAJOR == 54 && LIBAVFORMAT_VERSION_MINOR >= 6) +/* Get an embedded artwork file from a media file. Will rescale if needed. + * + * @out evbuf Image data + * @in path Path to the artwork + * @in max_w Requested width + * @in max_h Requested height + * @return ART_FMT_* on success, -1 on error + */ static int artwork_get_embedded_image(struct evbuffer *evbuf, char *path, int max_w, int max_h) { @@ -810,7 +850,7 @@ artwork_get_embedded_image(struct evbuffer *evbuf, char *path, int max_w, int ma DPRINTF(E_DBG, L_ART, "Did not find embedded artwork in '%s'\n", path); avformat_close_input(&src_ctx); - return -1; + return 0; } else DPRINTF(E_DBG, L_ART, "Found embedded artwork in '%s'\n", path); @@ -861,7 +901,7 @@ artwork_get_embedded_image(struct evbuffer *evbuf, char *path, int max_w, int ma #endif /* - * Looks for basename(in_path).{png,jpg}, so if is in_path is /foo/bar.mp3 it + * 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 * * @param evbuf the event buffer that will contain the (scaled) image @@ -920,7 +960,7 @@ artwork_get_own_image(struct evbuffer *evbuf, char *in_path, int max_w, int max_ DPRINTF(E_DBG, L_ART, "Found own artwork file %s\n", path); if (out_path) - strcpy(out_path, path); + strncpy(out_path, path, PATH_MAX); return artwork_get(evbuf, path, max_w, max_h); } @@ -1037,318 +1077,533 @@ artwork_get_dir_image(struct evbuffer *evbuf, char *dir, int max_w, int max_h, c DPRINTF(E_DBG, L_ART, "Found directory artwork file %s\n", path); if (out_path) - strcpy(out_path, path); + strncpy(out_path, path, PATH_MAX); return artwork_get(evbuf, path, max_w, max_h); } -/* - * Given an artwork type (eg embedded, Spotify, own) this function will direct - * to the appropriate handler - * - * @param evbuf the event buffer that will contain the (scaled) image - * @param in_path path to the item we are getting artwork for - * @param artwork type of the artwork - * @param max_w maximum image width - * @param max_h maximum image height - * @param out_path path to artwork, input must be either NULL or char[PATH_MAX] - * @return ART_FMT_* on success, 0 on nothing found, -1 on error - */ + +/* -------------------- SOURCE HANDLERS AND DEFINITIONS -------------------- */ + static int -artwork_get_item_path(struct evbuffer *evbuf, char *in_path, int artwork, int max_w, int max_h, char *out_path) +source_group_cache_get(struct artwork_ctx *ctx) { - int ret; - - ret = 0; - if (out_path) - strcpy(out_path, in_path); - - switch (artwork) - { - case ARTWORK_NONE: - break; - case ARTWORK_UNKNOWN: - case ARTWORK_OWN: - if (cfg_getbool(cfg_getsec(cfg, "library"), "artwork_individual")) - ret = artwork_get_own_image(evbuf, in_path, max_w, max_h, out_path); - break; -#ifdef HAVE_SPOTIFY_H - case ARTWORK_SPOTIFY: - ret = spotify_artwork_get(evbuf, in_path, max_w, max_h); - (ret < 0) ? (ret = 0) : (ret = ART_FMT_JPEG); - break; -#endif -#if LIBAVFORMAT_VERSION_MAJOR >= 55 || (LIBAVFORMAT_VERSION_MAJOR == 54 && LIBAVFORMAT_VERSION_MINOR >= 6) - case ARTWORK_EMBEDDED: - ret = artwork_get_embedded_image(evbuf, in_path, max_w, max_h); - break; -#endif - case ARTWORK_HTTP: - ret = artwork_get_player_image(evbuf, in_path); - break; - } - - return ret; -} - -/* - * Get the artwork for the given media file and the given maxiumum width/height - - * @param evbuf the event buffer that will contain the (scaled) image - * @param mfi the media file structure for the file whose image should be returned - * @param max_w maximum image width - * @param max_h maximum image height - * @return ART_FMT_* on success, 0 on nothing found, -1 on error - */ -static int -artwork_get_item_mfi(struct evbuffer *evbuf, struct media_file_info *mfi, int max_w, int max_h) -{ - char path[PATH_MAX]; - int cached; int format; + int cached; int ret; - DPRINTF(E_DBG, L_ART, "Looking for artwork for item with id %d\n", mfi->id); + ret = cache_artwork_get(CACHE_ARTWORK_GROUP, ctx->persistentid, ctx->max_w, ctx->max_h, &cached, &format, ctx->evbuf); + if (ret < 0) + return ART_E_ERROR; - ret = cache_artwork_get(CACHE_ARTWORK_INDIVIDUAL, mfi->id, max_w, max_h, &cached, &format, evbuf); - if ((ret == 0) && cached) - { - DPRINTF(E_DBG, L_ART, "Item %d found in cache with format %d\n", mfi->id, format); - return format; - } + if (!cached) + return ART_E_NONE; - if (mfi->data_kind == DATA_KIND_FILE) - { - format = artwork_get_item_path(evbuf, mfi->path, mfi->artwork, max_w, max_h, path); - - if (format > 0) - cache_artwork_add(CACHE_ARTWORK_INDIVIDUAL, mfi->id, max_w, max_h, format, path, evbuf); + DPRINTF(E_DBG, L_ART, "Group %" PRIi64 " found in cache with format %d\n", ctx->persistentid, format); - return format; - } + if (!format) + return ART_E_ABORT; - return -1; + return format; } -/* - * Get the artwork image for the given persistentid and the given maximum width/height - * - * The function first checks if there is a cache entry, if not it will first look for directory artwork files. - * If no directory artwork files are found, it looks for individual artwork (embedded images or images from spotify). - * - * @param evbuf the event buffer that will contain the (scaled) image - * @param persistentid persistent songalbumid or songartistid - * @param max_w maximum image width - * @param max_h maximum image height - * @return ART_FMT_* on success, 0 on nothing found, -1 on error - */ static int -artwork_get_group_persistentid(struct evbuffer *evbuf, int64_t persistentid, int max_w, int max_h) +source_group_dir_get(struct artwork_ctx *ctx) { struct query_params qp; - struct db_media_file_info dbmfi; - char path[PATH_MAX]; char *dir; - int cached; - int format; int ret; - int artwork; - int got_spotifyitem; - uint32_t data_kind; - - DPRINTF(E_DBG, L_ART, "Looking for artwork for group with persistentid %" PRIi64 "\n", persistentid); - - got_spotifyitem = 0; - - /* - * First check if the artwork cache has a cached entry for the given persistent id and requested width/height - */ - ret = cache_artwork_get(CACHE_ARTWORK_GROUP, persistentid, max_w, max_h, &cached, &format, evbuf); - if ((ret == 0) && cached) - { - DPRINTF(E_DBG, L_ART, "Group %" PRIi64 " found in cache with format %d\n", persistentid, format); - return format; - } /* Image is not in the artwork cache. Try directory artwork first */ memset(&qp, 0, sizeof(struct query_params)); qp.type = Q_GROUP_DIRS; - qp.persistentid = persistentid; + 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"); - - /* Skip to invidual files artwork */ - goto files_art; + return ART_E_ERROR; } - format = 0; while (((ret = db_query_fetch_string(&qp, &dir)) == 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; - format = artwork_get_dir_image(evbuf, dir, max_w, max_h, path); - - if (format > 0) - break; + ret = artwork_get_dir_image(ctx->evbuf, dir, ctx->max_w, ctx->max_h, ctx->path); + 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"); - goto files_art; + return ART_E_ERROR; } - /* Found artwork, cache it and return */ - if (format > 0) - { - cache_artwork_add(CACHE_ARTWORK_GROUP, persistentid, max_w, max_h, format, path, evbuf); - return format; - } - - - /* Then try individual files */ - files_art: - memset(&qp, 0, sizeof(struct query_params)); - - qp.type = Q_GROUP_ITEMS; - qp.persistentid = persistentid; - - ret = db_query_start(&qp); - if (ret < 0) - { - DPRINTF(E_LOG, L_ART, "Could not start Q_GROUP_ITEMS query\n"); - return -1; - } - - format = 0; - while (((ret = db_query_fetch_file(&qp, &dbmfi)) == 0) && (dbmfi.id)) - { - if ((safe_atoi32(dbmfi.artwork, &artwork) != 0) && (safe_atou32(dbmfi.data_kind, &data_kind) != 0)) - continue; - - format = artwork_get_item_path(evbuf, dbmfi.path, artwork, max_w, max_h, path); - - if (artwork == ARTWORK_SPOTIFY) - got_spotifyitem = 1; - - if (format > 0) - break; - } - - db_query_end(&qp); - - if (ret < 0) - { - DPRINTF(E_LOG, L_ART, "Error fetching Q_GROUP_ITEMS results\n"); - return -1; - } - - /* Found artwork, cache it and return */ - if (format > 0) - { - if (artwork != ARTWORK_HTTP) - cache_artwork_add(CACHE_ARTWORK_GROUP, persistentid, max_w, max_h, format, path, evbuf); - return format; - } - else if (format < 0) - { - DPRINTF(E_WARN, L_ART, "Error getting artwork for group %" PRIi64 "\n", persistentid); - return -1; - } - - DPRINTF(E_DBG, L_ART, "No artwork found for group %" PRIi64 "\n", persistentid); - - /* Add cache entry for no artwork available */ - if ((artwork != ARTWORK_HTTP) && (!got_spotifyitem)) - cache_artwork_add(CACHE_ARTWORK_GROUP, persistentid, max_w, max_h, 0, "", evbuf); - - return 0; + return ART_E_NONE; } -/* - * Get the artwork image for the given item id and the given maximum width/height - * - * @param evbuf the event buffer that will contain the (scaled) image - * @param id the mfi item id - * @param max_w maximum image width - * @param max_h maximum image height - * @return ART_FMT_* on success, -1 on error or no artwork found - */ -int -artwork_get_item(struct evbuffer *evbuf, int id, int max_w, int max_h) +static int +source_item_cache_get(struct artwork_ctx *ctx) { - struct media_file_info *mfi; int format; + int cached; + int ret; - mfi = db_file_fetch_byid(id); - if (!mfi) - { - DPRINTF(E_LOG, L_ART, "Artwork request for item %d, but no such item in database\n", id); - return -1; - } + ret = cache_artwork_get(CACHE_ARTWORK_INDIVIDUAL, ctx->id, ctx->max_w, ctx->max_h, &cached, &format, ctx->evbuf); + if (ret < 0) + return ART_E_ERROR; - DPRINTF(E_DBG, L_ART, "Artwork request for item %d (%s)\n", id, mfi->fname); + if (!cached) + return ART_E_NONE; - format = 0; - if (cfg_getbool(cfg_getsec(cfg, "library"), "artwork_individual")) - format = artwork_get_item_mfi(evbuf, mfi, max_w, max_h); + DPRINTF(E_DBG, L_ART, "Item %d found in cache with format %d\n", ctx->id, format); - /* No individual artwork or individual artwork disabled, try group artwork */ - if (format <= 0) - format = artwork_get_group_persistentid(evbuf, mfi->songalbumid, max_w, max_h); - - free_mfi(mfi, 0); - - if (format <= 0) - { - DPRINTF(E_DBG, L_ART, "No artwork found for item %d\n", id); - return -1; - } + if (!format) + return ART_E_ABORT; return format; } +static int +source_item_embedded_get(struct artwork_ctx *ctx) +{ + int ret; + + ret = artwork_get_embedded_image(ctx->evbuf, ctx->dbmfi->path, ctx->max_w, ctx->max_h); + if (ret < 0) + return ART_E_ERROR; + + if (ret == 0) + return ART_E_NONE; + + // TODO Use a more efficient way, and something that will always NULL-terminate + if (ret > 0) + strncpy(ctx->path, ctx->dbmfi->path, PATH_MAX); + + return ret; +} + +static int +source_item_own_get(struct artwork_ctx *ctx) +{ + int ret; + + ret = artwork_get_own_image(ctx->evbuf, ctx->dbmfi->path, ctx->max_w, ctx->max_h, ctx->path); + if (ret < 0) + return ART_E_ERROR; + + if (ret == 0) + return ART_E_NONE; + + return ret; +} + /* - * Get the artwork image for the given group id and the given maximum width/height - * - * @param evbuf the event buffer that will contain the (scaled) image - * @param id the group id (not the persistent id) - * @param max_w maximum image width - * @param max_h maximum image height - * @return ART_FMT_* on success, -1 on error or no artwork found + * Downloads the artwork pointed to by the ICY metadata tag in an internet radio + * stream (the StreamUrl tag). The path will be converted back to the id, which + * is given to the player. If the id is currently being played, and there is a + * valid ICY metadata artwork URL available, it will be returned to this + * function, which will then use the http client to get the artwork. Notice: No + * rescaling is done. */ +static int +source_item_stream_get(struct artwork_ctx *ctx) +{ + struct http_client_ctx client; + struct keyval *kv; + const char *content_type; + char *url; + char *ext; + int len; + int ret; + + DPRINTF(E_DBG, L_ART, "Trying internet stream artwork in %s\n", ctx->dbmfi->path); + + ret = ART_E_NONE; + + url = player_get_icy_artwork_url(ctx->id); + if (!url) + return ART_E_NONE; + + len = strlen(url); + if ((len < 14) || (len > PATH_MAX)) // Can't be shorter than http://a/1.jpg + goto out_url; + + ext = strrchr(url, '.'); + if (!ext) + goto out_url; + if ((strcmp(ext, ".jpg") != 0) && (strcmp(ext, ".png") != 0)) + goto out_url; + + cache_artwork_read(ctx->evbuf, url, &ret); + 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.headers = kv; + client.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; + + if (ret > 0) + { + DPRINTF(E_DBG, L_ART, "Found internet stream artwork in %s (%s)\n", url, content_type); + strncpy(ctx->path, ctx->dbmfi->path, PATH_MAX); // TODO + cache_artwork_stash(ctx->evbuf, url, ret); + } + + out_kv: + keyval_clear(kv); + free(kv); + + out_url: + free(url); + + return ret; +} + +#ifdef HAVE_SPOTIFY_H +static int +source_item_spotify_get(struct artwork_ctx *ctx) +{ + int ret; + + ret = spotify_artwork_get(ctx->evbuf, ctx->dbmfi->path, ctx->max_w, ctx->max_h); + if (ret < 0) + return ART_E_NONE; + + return ART_FMT_JPEG; +} +#else +static int +source_item_spotify_get(struct artwork_ctx *ctx) +{ + return ART_E_ERROR; +} +#endif + +/* 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), + .cache = ON_FAILURE, + }, + { + .name = "embedded", + .handler = source_item_embedded_get, + .data_kinds = (1 << DATA_KIND_FILE), + .cache = ON_SUCCESS | ON_FAILURE, + }, + { + .name = "own", + .handler = source_item_own_get, + .data_kinds = (1 << DATA_KIND_FILE), + .cache = ON_SUCCESS | ON_FAILURE, + }, + { + .name = "stream", + .handler = source_item_stream_get, + .data_kinds = (1 << DATA_KIND_HTTP), + .cache = NEVER, + }, + { + .name = "Spotify", + .handler = source_item_spotify_get, + .data_kinds = (1 << DATA_KIND_SPOTIFY), + .cache = ON_SUCCESS, + }, + { + .name = NULL, + .handler = NULL, + .data_kinds = 0, + .cache = 0, + } + }; + + +/* --------------------------- SOURCE PROCESSING --------------------------- */ + +static int +process_items(struct artwork_ctx *ctx, int item_mode) +{ + struct db_media_file_info dbmfi; + uint32_t data_kind; + int artwork_individual; + int i; + int ret; + + artwork_individual = cfg_getbool(cfg_getsec(cfg, "library"), "artwork_individual"); + + 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(&ctx->qp, &dbmfi)) == 0) && (dbmfi.id)) + { + // 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 && !artwork_individual) + goto no_artwork; + + ret = (safe_atoi32(dbmfi.id, &ctx->id) < 0) || + (safe_atou32(dbmfi.data_kind, &data_kind) < 0) || + (data_kind > 30); + if (ret) + { + DPRINTF(E_LOG, L_ART, "Error converting dbmfi id or data_kind to number\n"); + continue; + } + + for (i = 0; artwork_item_source[i].handler; i++) + { + if ((artwork_item_source[i].data_kinds & (1 << data_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_DBG, 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_INFO, L_ART, "Artwork for '%s' found in source '%s'\n", dbmfi.title, artwork_item_source[i].name); + ctx->cache = (artwork_item_source[i].cache & ON_SUCCESS); + db_query_end(&ctx->qp); + return ret; + } + else if (ret == ART_E_ABORT) + { + DPRINTF(E_INFO, 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) +{ + 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; + } + + 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_DBG, L_ART, "Checking group source '%s'\n", artwork_group_source[i].name); + + ret = artwork_group_source[i].handler(ctx); + if (ret > 0) + { + DPRINTF(E_INFO, 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 & ON_SUCCESS); + return ret; + } + else if (ret == ART_E_ABORT) + { + DPRINTF(E_INFO, 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; + } + } + + ret = process_items(ctx, 0); + + return ret; +} + + +/* ------------------------------ ARTWORK API ------------------------------ */ + +int +artwork_get_item(struct evbuffer *evbuf, int id, int max_w, int max_h) +{ + struct artwork_ctx ctx; + char filter[32]; + int ret; + + DPRINTF(E_DBG, L_ART, "Artwork request for item %d\n", id); + + memset(&ctx, 0, sizeof(struct artwork_ctx)); + + ctx.qp.type = Q_ITEMS; + ctx.qp.filter = filter; + ctx.evbuf = evbuf; + ctx.max_w = max_w; + ctx.max_h = max_h; + ctx.cache = ON_FAILURE; + + ret = snprintf(filter, sizeof(filter), "id = %d", id); + if ((ret < 0) || (ret >= sizeof(filter))) + { + 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() + 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) { - int64_t persistentid; - int format; + struct artwork_ctx ctx; + int ret; DPRINTF(E_DBG, L_ART, "Artwork request for group %d\n", id); + memset(&ctx, 0, sizeof(struct artwork_ctx)); + /* Get the persistent id for the given group id */ - if (db_group_persistentid_byid(id, &persistentid) < 0) + 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; } - /* Load artwork image for the persistent id */ - format = artwork_get_group_persistentid(evbuf, persistentid, max_w, max_h); - if (format <= 0) + ctx.qp.type = Q_GROUP_ITEMS; + ctx.qp.persistentid = ctx.persistentid; + ctx.evbuf = evbuf; + ctx.max_w = max_w; + ctx.max_h = max_h; + ctx.cache = ON_FAILURE; + + ret = process_group(&ctx); + if (ret > 0) { - DPRINTF(E_DBG, L_ART, "No artwork found for group %d\n", id); - return -1; + if (ctx.cache == ON_SUCCESS) + cache_artwork_add(CACHE_ARTWORK_GROUP, ctx.persistentid, max_w, max_h, ret, ctx.path, evbuf); + + return ret; } - return format; + 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 */ diff --git a/src/artwork.h b/src/artwork.h index be9b422f..0e64c85e 100644 --- a/src/artwork.h +++ b/src/artwork.h @@ -7,15 +7,36 @@ #include -/* Get artwork for individual track */ +/* + * Get the artwork image for an individual item (track) + * + * @out evbuf Event buffer that will contain the (scaled) image + * @in id The mfi item id + * @in max_w Requested maximum image width (may not be obeyed) + * @in max_h Requested maximum image height (may not be obeyed) + * @return ART_FMT_* on success, -1 on error or no artwork found + */ int artwork_get_item(struct evbuffer *evbuf, int id, int max_w, int max_h); -/* Get artwork for album or artist */ +/* + * Get the artwork image for a group (an album or an artist) + * + * @out evbuf Event buffer that will contain the (scaled) image + * @in id The group id (not the persistentid) + * @in max_w Requested maximum image width (may not be obeyed) + * @in max_h Requested maximum image height (may not be obeyed) + * @return ART_FMT_* on success, -1 on error or no artwork found + */ int artwork_get_group(struct evbuffer *evbuf, int id, int max_w, int max_h); -/* Checks if the file is an artwork file */ +/* + * Checks if the file is an artwork file (based on user config) + * + * @in filename Name of the file + * @return 1 if true, 0 if false + */ int artwork_file_is_artwork(const char *filename);