Merge pull request #1836 from owntone/streamurl_direct2

Streamurl options and artwork fixes, see issue #1829
This commit is contained in:
ejurgensen 2025-01-17 17:36:37 +01:00 committed by GitHub
commit 450a333fd6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 179 additions and 99 deletions

View File

@ -71,7 +71,7 @@
// See online_source_is_failing()
#define ONLINE_SEARCH_COOLDOWN_TIME 3600
#define ONLINE_SEARCH_FAILURES_MAX 3
#define ONLINE_SEARCH_FAILURES_MAX 5
enum artwork_cache
{
@ -192,6 +192,8 @@ static const char *cover_extension[] =
"jpg", "png",
};
static pthread_mutex_t artwork_cache_stash_mutex = PTHREAD_MUTEX_INITIALIZER;
/* ----------------- DECLARE AND CONFIGURE SOURCE HANDLERS ----------------- */
/* Forward - group handlers */
@ -457,7 +459,6 @@ artwork_read_byurl(struct evbuffer *evbuf, const char *url)
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;
}
@ -941,12 +942,18 @@ artwork_get_byurl(struct evbuffer *artwork, const char *url, struct artwork_req_
CHECK_NULL(L_ART, raw = evbuffer_new());
format = ART_E_ERROR;
// Accessing the cache is thread safe, the purpose of the lock is to make the
// artwork stash more effective if we have parallel requests for the same url.
// It will assure that the artwork from the first request is downloaded and
// stashed before processing the next request.
pthread_mutex_lock(&artwork_cache_stash_mutex);
ret = cache_artwork_read(raw, url, &format);
if (ret < 0)
{
format = artwork_read_byurl(raw, url);
cache_artwork_stash(raw, url, format);
}
pthread_mutex_unlock(&artwork_cache_stash_mutex);
// If we couldn't read, or we have cached a negative result from the last attempt, we stop now
if (format <= 0)
@ -1123,7 +1130,7 @@ online_source_response_parse(char **artwork_url, const struct online_source *src
}
static int
online_source_request_url_make(char *url, size_t url_size, const struct online_source *src, struct artwork_ctx *ctx)
online_source_search_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 };
@ -1166,6 +1173,13 @@ online_source_request_url_make(char *url, size_t url_size, const struct online_s
goto error;
}
if ((artist && strncmp(CFG_NAME_UNKNOWN_ARTIST, artist, strlen(CFG_NAME_UNKNOWN_ARTIST)) == 0) ||
(album && strncmp(CFG_NAME_UNKNOWN_ALBUM, album, strlen(CFG_NAME_UNKNOWN_ARTIST)) == 0) )
{
DPRINTF(E_DBG, L_ART, "Skipping online artwork search for unknown artist/album\n");
goto error;
}
for (i = 0; src->query_parts[i].key; i++)
{
if (!album && strstr(src->query_parts[i].template, "$ALBUM$"))
@ -1185,14 +1199,17 @@ online_source_request_url_make(char *url, size_t url_size, const struct online_s
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");
DPRINTF(E_LOG, L_ART, "keyval_add() failed in online_source_request_url_make()\n");
goto error;
}
}
encoded_query = http_form_urlencode(&query);
if (!encoded_query)
{
DPRINTF(E_WARN, L_ART, "URL encoding for online artwork search failed\n");
goto error;
}
snprintf(url, url_size, "%s?%s", src->search_endpoint, src->search_param);
if (safe_snreplace(url, url_size, "$QUERY$", encoded_query) < 0)
@ -1220,18 +1237,13 @@ online_source_search_check_last(char **last_artwork_url, const struct online_sou
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;
}
@ -1241,8 +1253,6 @@ 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;
@ -1259,22 +1269,20 @@ online_source_is_failing(const struct online_source *src, int id)
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)
online_source_history_update(const struct online_source *src, int id, uint32_t request_hash, int response_code, const char *artwork_url, int max_w, int max_h)
{
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);
history->last_max_w = max_w;
history->last_max_h = max_h;
free(history->last_artwork_url);
history->last_artwork_url = safe_strdup(artwork_url); // FIXME should free this on exit
@ -1283,8 +1291,6 @@ online_source_history_update(const struct online_source *src, int id, uint32_t r
history->count_failures = 0;
else
history->count_failures++;
pthread_mutex_unlock(&history->mutex);
}
static int
@ -1320,34 +1326,24 @@ auth_header_add(struct keyval *headers, const struct online_source *src)
}
static char *
online_source_search(const struct online_source *src, struct artwork_ctx *ctx)
online_source_artwork_url_get(const char *search_url, const struct online_source *src, int id, int max_w, int max_h)
{
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);
hash = djb_hash(search_url, strlen(search_url));
ret = online_source_search_check_last(&artwork_url, src, hash, max_w, 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))
if (online_source_is_failing(src, id))
{
DPRINTF(E_DBG, L_ART, "Skipping artwork source %s, too many failed requests\n", src->name);
return NULL;
@ -1360,18 +1356,18 @@ online_source_search(const struct online_source *src, struct artwork_ctx *ctx)
}
CHECK_NULL(L_ART, client.input_body = evbuffer_new());
client.url = url;
client.url = search_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);
DPRINTF(E_WARN, L_ART, "Artwork request to '%s' failed, response code %d\n", search_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);
ret = online_source_response_parse(&artwork_url, src, client.input_body, max_w, 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)
@ -1384,16 +1380,41 @@ online_source_search(const struct online_source *src, struct artwork_ctx *ctx)
if (ret != ONLINE_SOURCE_PARSE_OK)
goto error;
online_source_history_update(src, ctx->id, hash, client.response_code, artwork_url);
online_source_history_update(src, id, hash, client.response_code, artwork_url, max_w, max_h);
evbuffer_free(client.input_body);
return artwork_url;
error:
online_source_history_update(src, ctx->id, hash, client.response_code, NULL);
online_source_history_update(src, id, hash, client.response_code, NULL, max_w, max_h);
evbuffer_free(client.input_body);
return NULL;
}
static char *
online_source_search(const struct online_source *src, struct artwork_ctx *ctx)
{
struct online_search_history *history = src->search_history;
char search_url[2048];
char *artwork_url;
int ret;
DPRINTF(E_SPAM, L_ART, "Trying %s for %s\n", src->name, ctx->dbmfi->path);
ret = online_source_search_url_make(search_url, sizeof(search_url), src, ctx);
if (ret < 0)
return NULL;
// The protection against flooding the online source with requests requires
// that online_source_request() isn't called in parallel
pthread_mutex_lock(&history->mutex);
artwork_url = online_source_artwork_url_get(search_url, src, ctx->id, ctx->req_params.max_w, ctx->req_params.max_h);
pthread_mutex_unlock(&history->mutex);
return artwork_url;
}
static bool
online_source_is_enabled(const struct online_source *src)
{

View File

@ -36,6 +36,7 @@
#include <libavutil/opt.h>
#include <event2/event.h>
#include <event2/keyvalq_struct.h>
#include <curl/curl.h>
@ -171,7 +172,7 @@ http_client_request(struct http_client_ctx *ctx, struct http_client_session *ses
res = curl_easy_perform(curl);
if (res != CURLE_OK)
{
DPRINTF(E_LOG, L_HTTP, "Request to %s failed: %s\n", ctx->url, curl_easy_strerror(res));
DPRINTF(E_WARN, L_HTTP, "Request to %s failed: %s\n", ctx->url, curl_easy_strerror(res));
curl_slist_free_all(headers);
if (!session)
{
@ -193,6 +194,41 @@ http_client_request(struct http_client_ctx *ctx, struct http_client_session *ses
return 0;
}
int
http_form_urldecode(struct keyval *kv, const char *uri)
{
struct evhttp_uri *ev_uri = NULL;
struct evkeyvalq ev_query = { 0 };
struct evkeyval *param;
const char *query;
int ret;
ev_uri = evhttp_uri_parse_with_flags(uri, EVHTTP_URI_NONCONFORMANT);
if (!ev_uri)
return -1;
query = evhttp_uri_get_query(ev_uri);
if (!query)
goto error;
ret = evhttp_parse_query_str(query, &ev_query);
if (ret < 0)
goto error;
// musl libc doesn't have sys/queue.h so don't use TAILQ_FOREACH
for (param = ev_query.tqh_first; param; param = param->next.tqe_next)
keyval_add(kv, param->key, param->value);
evhttp_uri_free(ev_uri);
evhttp_clear_headers(&ev_query);
return 0;
error:
evhttp_uri_free(ev_uri);
evhttp_clear_headers(&ev_query);
return -1;
}
char *
http_form_urlencode(struct keyval *kv)
{

View File

@ -86,6 +86,14 @@ http_client_request(struct http_client_ctx *ctx, struct http_client_session *ses
char *
http_form_urlencode(struct keyval *kv);
/* The reverse of http_form_urlencode, except takes a full url as input.
*
* @param kv keyval struct allocated by caller where values will be added
* @param url with the query to decode
* @return 0 if ok, otherwise -1
*/
int
http_form_urldecode(struct keyval *kv, const char *uri);
/* Returns a newly allocated string with the first stream in the m3u given in
* url. If url is not a m3u, the string will be a copy of url.

View File

@ -173,8 +173,9 @@ streamurl_process(struct input_metadata *metadata, const char *url)
{
struct http_client_ctx client = { 0 };
struct keyval kv = { 0 };
struct evbuffer *evbuf;
struct evbuffer *evbuf = NULL;
const char *content_type;
const char *artwork_url;
char *body;
int ret;
@ -186,6 +187,21 @@ streamurl_process(struct input_metadata *metadata, const char *url)
return -1;
}
// If the StreamUrl contains a keyword followed by the actual url, e.g. http://metadata.cdnstream1.com/?yadayada&ALBUM_ART=https%3A%2F%2Fis1-ssl.mzstatic.com%2Fimage%2Fthumb%2FMusic%2F11%2Fcc%2F21%2Fmzi.nepwiuir.jpg
if (streamurl_map[0].words)
{
ret = http_form_urldecode(&kv, url);
if (ret < 0)
return -1;
artwork_url = keyval_get(&kv, streamurl_map[0].words);
metadata->artwork_url = safe_strdup(artwork_url);
keyval_clear(&kv);
if (metadata->artwork_url)
goto out;
}
DPRINTF(E_DBG, L_PLAYER, "Downloading StreamUrl resource '%s'\n", url);
CHECK_NULL(L_PLAYER, evbuf = evbuffer_new());
@ -219,6 +235,7 @@ streamurl_process(struct input_metadata *metadata, const char *url)
out:
keyval_clear(&kv);
if (evbuf)
evbuffer_free(evbuf);
streamurl_settings_unload();
return ret;
@ -276,11 +293,14 @@ metadata_prepare(struct input_source *source)
// Note we map title to album, because clients should show stream name as title
swap_pointers(&prepared_metadata.parsed.album, &m->title);
if (! SETTINGS_GETBOOL("artwork", "streamurl_ignore"))
{
// In this case we have to go async to download the url and process the content
if (m->url && !artwork_extension_is_artwork(m->url))
worker_execute(streamurl_cb, m->url, strlen(m->url) + 1, 0);
else
swap_pointers(&prepared_metadata.parsed.artwork_url, &m->url);
}
http_icy_metadata_free(m, 0);
return 0;

View File

@ -308,7 +308,6 @@ static int player_flush_pending;
// Config values and player settings category
static int speaker_autoselect;
static int clear_queue_on_stop_disabled;
static struct settings_category *player_settings_category;
// Player status
static enum play_status player_state;
@ -3161,7 +3160,7 @@ repeat_set(void *arg, int *retval)
}
// Persist
SETTINGS_SETINT(player_settings_category, PLAYER_SETTINGS_MODE_REPEAT, repeat);
SETTINGS_SETINT("player", PLAYER_SETTINGS_MODE_REPEAT, repeat);
if (repeat == REPEAT_ALL || repeat == REPEAT_SONG)
{
@ -3203,7 +3202,7 @@ shuffle_set(void *arg, int *retval)
shuffle = new_shuffle;
// Persist
SETTINGS_SETBOOL(player_settings_category, PLAYER_SETTINGS_MODE_SHUFFLE, shuffle);
SETTINGS_SETBOOL("player", PLAYER_SETTINGS_MODE_SHUFFLE, shuffle);
out:
*retval = 0;
@ -3219,7 +3218,7 @@ consume_set(void *arg, int *retval)
consume = cmdarg->intval;
// Persist
SETTINGS_SETBOOL(player_settings_category, PLAYER_SETTINGS_MODE_CONSUME, consume);
SETTINGS_SETBOOL("player", PLAYER_SETTINGS_MODE_CONSUME, consume);
if (consume)
{
@ -3879,11 +3878,10 @@ player_init(void)
clear_queue_on_stop_disabled = cfg_getbool(cfg_getsec(cfg, "mpd"), "clear_queue_on_stop_disable");
}
CHECK_NULL(L_PLAYER, player_settings_category = settings_category_get("player"));
ret = SETTINGS_GETINT(player_settings_category, PLAYER_SETTINGS_MODE_REPEAT);
ret = SETTINGS_GETINT("player", PLAYER_SETTINGS_MODE_REPEAT);
repeat = (ret > 0) ? ret : REPEAT_OFF;
shuffle = SETTINGS_GETBOOL(player_settings_category, PLAYER_SETTINGS_MODE_SHUFFLE);
consume = SETTINGS_GETBOOL(player_settings_category, PLAYER_SETTINGS_MODE_CONSUME);
shuffle = SETTINGS_GETBOOL("player", PLAYER_SETTINGS_MODE_SHUFFLE);
consume = SETTINGS_GETBOOL("player", PLAYER_SETTINGS_MODE_CONSUME);
player_state = PLAY_STOPPED;

View File

@ -30,7 +30,6 @@ static struct settings_option webinterface_options[] =
{ "show_composer_now_playing", SETTINGS_TYPE_BOOL },
{ "show_filepath_now_playing", SETTINGS_TYPE_BOOL },
{ "show_composer_for_genre", SETTINGS_TYPE_STR },
{ "show_cover_artwork_in_album_lists", SETTINGS_TYPE_BOOL, { true } },
{ "show_menu_item_playlists", SETTINGS_TYPE_BOOL, { true } },
{ "show_menu_item_music", SETTINGS_TYPE_BOOL, { true } },
{ "show_menu_item_podcasts", SETTINGS_TYPE_BOOL, { true } },
@ -49,6 +48,8 @@ static struct settings_option artwork_options[] =
{ "use_artwork_source_spotify", SETTINGS_TYPE_BOOL, { true } },
{ "use_artwork_source_discogs", SETTINGS_TYPE_BOOL, { false } },
{ "use_artwork_source_coverartarchive", SETTINGS_TYPE_BOOL, { false } },
{ "show_cover_artwork_in_album_lists", SETTINGS_TYPE_BOOL, { true } },
{ "streamurl_ignore", SETTINGS_TYPE_BOOL, { false } },
};
static struct settings_option misc_options[] =

View File

@ -59,9 +59,9 @@ settings_option_getbool(struct settings_option *option);
char *
settings_option_getstr(struct settings_option *option);
#define SETTINGS_GETINT(category, name) settings_option_getint(settings_option_get((category), (name)))
#define SETTINGS_GETBOOL(category, name) settings_option_getbool(settings_option_get((category), (name)))
#define SETTINGS_GETSTR(category, name) settings_option_getstr(settings_option_get((category), (name)))
#define SETTINGS_GETINT(category, name) settings_option_getint(settings_option_get(settings_category_get(category), (name)))
#define SETTINGS_GETBOOL(category, name) settings_option_getbool(settings_option_get(settings_category_get(category), (name)))
#define SETTINGS_GETSTR(category, name) settings_option_getstr(settings_option_get(settings_category_get(category), (name)))
int
settings_option_setint(struct settings_option *option, int value);
@ -72,9 +72,9 @@ settings_option_setbool(struct settings_option *option, bool value);
int
settings_option_setstr(struct settings_option *option, const char *value);
#define SETTINGS_SETINT(category, name, value) settings_option_setint(settings_option_get((category), (name)), (value))
#define SETTINGS_SETBOOL(category, name, value) settings_option_setbool(settings_option_get((category), (name)), (value))
#define SETTINGS_SETSTR(category, name, value) settings_option_setstr(settings_option_get((category), (name)), (value))
#define SETTINGS_SETINT(category, name, value) settings_option_setint(settings_option_get(settings_category_get(category), (name)), (value))
#define SETTINGS_SETBOOL(category, name, value) settings_option_setbool(settings_option_get(settings_category_get(category), (name)), (value))
#define SETTINGS_SETSTR(category, name, value) settings_option_setstr(settings_option_get(settings_category_get(category), (name)), (value))
int

View File

@ -440,12 +440,13 @@
},
"settings": {
"artwork": {
"artwork": "Artwork",
"title": "Artwork",
"coverartarchive": "Cover Art Archive",
"discogs": "Discogs",
"explanation-1": "OwnTone verarbeitet PNG- und JPEG-Artwork, welches in einer eigenen Datei in der Bibliothek, in die Dateien eingebettet oder online von Radiostationen bereitgestellt werden kann.",
"explanation-2": "Zusätzlich kann auf folgende Artwork-Anbieter zugegriffen werden:",
"spotify": "Spotify"
"spotify": "Spotify",
"streaming": "Bereitgestellte Artwork von Radiostationen ignorieren"
},
"devices": {
"no-active-pairing": "Keine aktive Pairing-Anfrage",

View File

@ -440,12 +440,14 @@
},
"settings": {
"artwork": {
"artwork": "Artwork",
"title": "Artwork",
"coverartarchive": "Cover Art Archive",
"discogs": "Discogs",
"explanation-1": "OwnTone supports PNG and JPEG artwork which is either placed as separate image files in the library, embedded in the media files or made available online by radio stations.",
"explanation-2": "In addition to that, you can enable fetching artwork from the following artwork providers:",
"spotify": "Spotify"
"show-coverart": "Show cover artwork in album list",
"spotify": "Spotify",
"streaming": "Ignore artwork provided by radio stations"
},
"devices": {
"no-active-pairing": "No active pairing request.",
@ -459,7 +461,6 @@
"verify": "Verify"
},
"general": {
"album-lists": "Album Lists",
"audiobooks": "Audiobooks",
"files": "Files",
"genres": "Genres",
@ -481,7 +482,6 @@
"show-composer-genres": "Show composer only for listed genres",
"show-composer-info": "If enabled the composer of the current playing track is shown on the \"Now playing page\"",
"show-composer": "Show composer",
"show-coverart": "Show cover artwork in album list",
"show-path": "Show filepath on the \"Now playing\" page"
},
"services": {

View File

@ -440,12 +440,14 @@
},
"settings": {
"artwork": {
"artwork": "Illustrations",
"title": "Illustrations",
"coverartarchive": "Cover Art Archive",
"discogs": "Discogs",
"explanation-1": "Prend en charge les illustrations au format PNG et JPEG qui sont soit placées dans la bibliothèque en tant que fichiers image séparés, soit intégrées dans les fichiers média, soit mises à disposition en ligne par les stations de radio.",
"explanation-1": "OwnTone prend en charge les illustrations au format PNG et JPEG qui sont soit placées dans la bibliothèque en tant que fichiers image séparés, soit intégrées dans les fichiers média, soit mises à disposition en ligne par les stations de radio.",
"explanation-2": "En outre, vous pouvez activer la récupération des illustrations à partir des fournisseurs dillustrations suivants :",
"spotify": "Spotify"
"show-coverart": "Afficher les illustrations dans la liste dalbums",
"spotify": "Spotify",
"streaming": "Ignorer les illustrations fournies par les stations de radio"
},
"devices": {
"no-active-pairing": "Aucune demande de jumelage active.",
@ -459,7 +461,6 @@
"verify": "Vérifier"
},
"general": {
"album-lists": "Listes dalbums",
"audiobooks": "Livres audio",
"files": "Fichiers",
"genres": "Genres",
@ -481,7 +482,6 @@
"show-composer-genres": "Afficher le compositeur uniquement pour les genres listés",
"show-composer-info": "Si actif, le compositeur de la piste en cours de lecture est affiché sur la page « En cours de lecture »",
"show-composer": "Afficher le compositeur",
"show-coverart": "Afficher les illustrations dans la liste dalbums",
"show-path": "Afficher le chemin du fichier sur la page « En cours de lecture »"
},
"services": {

View File

@ -440,12 +440,14 @@
},
"settings": {
"artwork": {
"artwork": "封面",
"title": "封面",
"coverartarchive": "Cover Art Archive",
"discogs": "Discogs",
"explanation-1": "OwnTone支持PNG和 JPEG封面这些封面可以作为单独的图像文件放置在库中或嵌入到媒体文件中也可以通过广播电台在线提供",
"explanation-2": "除此之外,您还可以从以下素材提供者获取封面:",
"spotify": "Spotify"
"show-coverart": "在专辑列表中显示封面艺术作品",
"spotify": "Spotify",
"streaming": "忽略广播电台提供的作品"
},
"devices": {
"no-active-pairing": "没有活跃的配对请求",
@ -459,7 +461,6 @@
"verify": "验证"
},
"general": {
"album-lists": "专辑列表",
"audiobooks": "有声读物",
"files": "文件",
"genres": "流派",
@ -481,7 +482,6 @@
"show-composer-genres": "仅显示列出的流派的作曲家",
"show-composer-info": "如果启用,当前播放曲目的作曲家将显示在“正在播放页面”上",
"show-composer": "显示作曲家",
"show-coverart": "在专辑列表中显示封面艺术作品",
"show-path": "在“正在播放”页面显示文件路径"
},
"services": {

View File

@ -440,12 +440,14 @@
},
"settings": {
"artwork": {
"artwork": "封面",
"title": "封面",
"coverartarchive": "Cover Art Archive",
"discogs": "Discogs",
"explanation-1": "OwnTone支持PNG和 JPEG封面這些封面可以作為單獨的圖像文件放置在庫中或嵌入到媒體文件中也可以通過電台在線提供",
"explanation-2": "除此之外,您還可以從以下素材提供者獲取封面:",
"spotify": "Spotify"
"show-coverart": "在專輯列表中顯示封面藝術作品",
"spotify": "Spotify",
"streaming": "忽略電台提供的作品"
},
"devices": {
"no-active-pairing": "沒有活躍的配對請求",
@ -459,7 +461,6 @@
"verify": "驗證"
},
"general": {
"album-lists": "專輯列表",
"audiobooks": "有聲書",
"files": "文件",
"genres": "音樂類型",
@ -481,7 +482,6 @@
"show-composer-genres": "僅顯示列出的音樂類型的作曲家",
"show-composer-info": "如果啓用,當前播放曲目的作曲家將顯示在“正在播放頁面”上",
"show-composer": "顯示作曲家",
"show-coverart": "在專輯列表中顯示封面藝術作品",
"show-path": "在“正在播放”頁面顯示文件路徑"
},
"services": {

View File

@ -3,13 +3,26 @@
<tabs-settings />
<content-with-heading>
<template #heading-left>
<div class="title is-4" v-text="$t('page.settings.artwork.artwork')" />
<div class="title is-4" v-text="$t('page.settings.artwork.title')" />
</template>
<template #content>
<div
class="content"
v-text="$t('page.settings.artwork.explanation-1')"
/>
<settings-checkbox category="artwork" name="streamurl_ignore">
<template #label>
<span v-text="$t('page.settings.artwork.streaming')" />
</template>
</settings-checkbox>
<settings-checkbox
category="artwork"
name="show_cover_artwork_in_album_lists"
>
<template #label>
<span v-text="$t('page.settings.artwork.show-coverart')" />
</template>
</settings-checkbox>
<div
class="content"
v-text="$t('page.settings.artwork.explanation-2')"

View File

@ -71,24 +71,6 @@
</settings-checkbox>
</template>
</content-with-heading>
<content-with-heading>
<template #heading-left>
<div
class="title is-4"
v-text="$t('page.settings.general.album-lists')"
/>
</template>
<template #content>
<settings-checkbox
category="webinterface"
name="show_cover_artwork_in_album_lists"
>
<template #label>
<span v-text="$t('page.settings.general.show-coverart')" />
</template>
</settings-checkbox>
</template>
</content-with-heading>
<content-with-heading>
<template #heading-left>
<div