From d2ac216f4717b999c09bac107694a256f8558c4f Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Wed, 13 May 2020 23:20:14 +0200 Subject: [PATCH 1/9] [http] Change name of struct field for StreamUrl Should not be called artwork_url, since it also can link to other ressources. Also some fixup, e.g. use new macros. --- src/http.c | 46 ++++++++++++++---------------------------- src/http.h | 2 +- src/inputs/file_http.c | 2 +- 3 files changed, 17 insertions(+), 33 deletions(-) diff --git a/src/http.c b/src/http.c index 98c289c8..f7500120 100644 --- a/src/http.c +++ b/src/http.c @@ -667,9 +667,9 @@ metadata_packet_get(struct http_icy_metadata *metadata, AVFormatContext *fmtctx) else metadata->title = strdup(metadata->title); } - else if ((strncmp(icy_token, "StreamUrl", strlen("StreamUrl")) == 0) && !metadata->artwork_url && strlen(ptr) > 0) + else if ((strncmp(icy_token, "StreamUrl", strlen("StreamUrl")) == 0) && !metadata->url && strlen(ptr) > 0) { - metadata->artwork_url = strdup(ptr); + metadata->url = strdup(ptr); } if (end) @@ -741,10 +741,7 @@ http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only) int got_packet; int got_header; - metadata = malloc(sizeof(struct http_icy_metadata)); - if (!metadata) - return NULL; - memset(metadata, 0, sizeof(struct http_icy_metadata)); + CHECK_NULL(L_HTTP, metadata = calloc(1, sizeof(struct http_icy_metadata))); got_packet = (metadata_packet_get(metadata, fmtctx) == 0); got_header = (!packet_only) && (metadata_header_get(metadata, fmtctx) == 0); @@ -761,7 +758,7 @@ http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only) metadata->genre, metadata->title, metadata->artist, - metadata->artwork_url, + metadata->url, metadata->hash ); */ @@ -816,10 +813,7 @@ http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only) return NULL; } - metadata = malloc(sizeof(struct http_icy_metadata)); - if (!metadata) - return NULL; - memset(metadata, 0, sizeof(struct http_icy_metadata)); + CHECK_NULL(L_HTTP, metadata = calloc(1, sizeof(struct http_icy_metadata))); got_header = 0; if ( (value = keyval_get(ctx.input_headers, "icy-name")) ) @@ -853,7 +847,7 @@ http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only) metadata->genre, metadata->title, metadata->artist, - metadata->artwork_url, + metadata->url, metadata->hash );*/ @@ -864,24 +858,14 @@ http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only) void http_icy_metadata_free(struct http_icy_metadata *metadata, int content_only) { - if (metadata->name) - free(metadata->name); + if (!metadata) + return; - if (metadata->description) - free(metadata->description); - - if (metadata->genre) - free(metadata->genre); - - if (metadata->title) - free(metadata->title); - - if (metadata->artist) - free(metadata->artist); - - if (metadata->artwork_url) - free(metadata->artwork_url); - - if (!content_only) - free(metadata); + free(metadata->name); + free(metadata->description); + free(metadata->genre); + free(metadata->title); + free(metadata->artist); + free(metadata->url); + free(metadata); } diff --git a/src/http.h b/src/http.h index faa7ed9a..6cf52a08 100644 --- a/src/http.h +++ b/src/http.h @@ -49,7 +49,7 @@ struct http_icy_metadata /* Track specific, comes from icy_metadata_packet */ char *title; char *artist; - char *artwork_url; + char *url; uint32_t hash; }; diff --git a/src/inputs/file_http.c b/src/inputs/file_http.c index 5206fee3..6d0f9b51 100644 --- a/src/inputs/file_http.c +++ b/src/inputs/file_http.c @@ -147,7 +147,7 @@ metadata_get_http(struct input_metadata *metadata, struct input_source *source) swap_pointers(&metadata->artist, &m->artist); // Note we map title to album, because clients should show stream name as titel swap_pointers(&metadata->album, &m->title); - swap_pointers(&metadata->artwork_url, &m->artwork_url); + swap_pointers(&metadata->artwork_url, &m->url); http_icy_metadata_free(m, 0); return 0; From ca82857bfe726291fd7b377a392062853cbebdf5 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 15 May 2020 23:14:14 +0200 Subject: [PATCH 2/9] [artwork] Add artwork_extension_is_artwork() + some fixup/renaming --- src/artwork.c | 81 ++++++++++++++++++++++++++++----------------------- src/artwork.h | 16 ++++++++-- 2 files changed, 59 insertions(+), 38 deletions(-) diff --git a/src/artwork.c b/src/artwork.c index 4b536ba6..b3793f2c 100644 --- a/src/artwork.c +++ b/src/artwork.c @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -193,7 +194,7 @@ static int source_group_dir_get(struct artwork_ctx *ctx); 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_stream_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); @@ -254,7 +255,7 @@ static struct artwork_source artwork_item_source[] = }, { .name = "stream", - .handler = source_item_stream_get, + .handler = source_item_artwork_url_get, .data_kinds = (1 << DATA_KIND_HTTP), .media_kinds = MEDIA_KIND_MUSIC, .cache = STASH, @@ -1503,25 +1504,15 @@ source_item_own_get(struct artwork_ctx *ctx) } /* - * 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. + * Downloads the artwork from the location pointed to by queue_item->artwork_url */ static int -source_item_stream_get(struct artwork_ctx *ctx) +source_item_artwork_url_get(struct artwork_ctx *ctx) { struct db_queue_item *queue_item; - char *url; - char *ext; - int len; int ret; - - DPRINTF(E_SPAM, L_ART, "Trying internet stream artwork in %s\n", ctx->dbmfi->path); - - ret = ART_E_NONE; + 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) @@ -1530,24 +1521,12 @@ source_item_stream_get(struct artwork_ctx *ctx) return ART_E_NONE; } - url = strdup(queue_item->artwork_url); + ret = artwork_get_byurl(ctx->evbuf, queue_item->artwork_url, ctx->max_w, ctx->max_h); + + snprintf(ctx->path, sizeof(ctx->path), "%s", queue_item->artwork_url); + free_queue_item(queue_item, 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; - - ret = artwork_get_byurl(ctx->evbuf, url, ctx->max_w, ctx->max_h); - - out_url: - free(url); - return ret; } @@ -2024,7 +2003,7 @@ artwork_get_group(struct evbuffer *evbuf, int id, int max_w, int max_h) } /* Checks if the file is an artwork file */ -int +bool artwork_file_is_artwork(const char *filename) { cfg_t *lib; @@ -2039,7 +2018,7 @@ artwork_file_is_artwork(const char *filename) for (i = 0; i < n; i++) { - for (j = 0; j < (sizeof(cover_extension) / sizeof(cover_extension[0])); j++) + 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))) @@ -2049,12 +2028,42 @@ artwork_file_is_artwork(const char *filename) } if (strcmp(artwork, filename) == 0) - return 1; + return true; } - if (j < (sizeof(cover_extension) / sizeof(cover_extension[0]))) + if (j < ARRAY_SIZE(cover_extension)) break; } - return 0; + 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; } diff --git a/src/artwork.h b/src/artwork.h index 57b23a7a..40025797 100644 --- a/src/artwork.h +++ b/src/artwork.h @@ -9,6 +9,7 @@ #define ART_DEFAULT_WIDTH 600 #include +#include /* * Get the artwork image for an individual item (track) @@ -38,9 +39,20 @@ artwork_get_group(struct evbuffer *evbuf, int id, int max_w, int max_h); * 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 + * @return true/false */ -int +bool artwork_file_is_artwork(const char *filename); +/* + * Checks if the path (or URL) has file extension that is recognized as a + * supported file type (e.g. ".jpg"). Also supports URL-encoded paths, e.g. + * http://foo.com/bar.jpg?something + * + * @in path Path to the file (can also be a URL) + * @return true/false + */ +bool +artwork_extension_is_artwork(const char *path); + #endif /* !__ARTWORK_H__ */ From b44e5b3edeedab9dbe737e0afc035e043b723bb6 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 15 May 2020 23:15:15 +0200 Subject: [PATCH 3/9] [input] Add handler/parser for StreamUrl tags --- src/inputs/file_http.c | 190 ++++++++++++++++++++++++++++++++++++++++- src/settings.c | 8 +- 2 files changed, 194 insertions(+), 4 deletions(-) diff --git a/src/inputs/file_http.c b/src/inputs/file_http.c index 6d0f9b51..a2e2876c 100644 --- a/src/inputs/file_http.c +++ b/src/inputs/file_http.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 Espen Jurgensen + * Copyright (C) 2017-2020 Espen Jurgensen * * 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 @@ -20,15 +20,192 @@ #include #include #include +#include // strcasestr +#include // strcasecmp #include #include "transcode.h" #include "http.h" #include "misc.h" +#include "misc_json.h" +#include "settings.h" #include "logger.h" +#include "artwork.h" #include "input.h" + +/* ------- Handling/parsing of StreamUrl tags from some http streams ---------*/ + +struct streamurl_map +{ + const char *setting; + enum json_type jtype; + int (*parser)(struct input_metadata *, const char *, json_object *); + char *words; +}; + +static int +streamurl_parse_artwork_url(struct input_metadata *metadata, const char *key, json_object *val) +{ + const char *url = json_object_get_string(val); + + if (metadata->artwork_url) + return -1; // Already found artwork + + if (!artwork_extension_is_artwork(url)) + return -1; + + metadata->artwork_url = strdup(url); + return 0; +} + +static int +streamurl_parse_length(struct input_metadata *metadata, const char *key, json_object *val) +{ + int len = json_object_get_int(val); + + if (len <= 0 || len > 7200) + return -1; // We expect seconds, so if it is longer than 2 hours we are probably wrong + + metadata->len_ms = len * 1000; + metadata->pos_is_updated = true; + metadata->pos_ms = 0; + return 0; +} + +// Lookup is case-insensitive and partial, first occurrence takes precedence +static struct streamurl_map streamurl_map[] = + { + { "streamurl_keywords_artwork_url", json_type_string, streamurl_parse_artwork_url }, + { "streamurl_keywords_length", json_type_int, streamurl_parse_length }, + }; + +static void +streamurl_field_parse(struct input_metadata *metadata, struct streamurl_map *map, const char *jkey, json_object *jval) +{ + char *word; + char *ptr; + + if (!map->words) + return; + + for (word = atrim(strtok_r(map->words, ",", &ptr)); word; free(word), word = atrim(strtok_r(NULL, ",", &ptr))) + { + if (json_object_get_type(jval) != map->jtype) + continue; + + if (!strcasestr(jkey, word)) // True if e.g. word="duration" and jkey="eventDuration" + continue; + + map->parser(metadata, jkey, jval); + } +} + +static void +streamurl_json_parse(struct input_metadata *metadata, const char *body) +{ + json_object *jresponse; + int i; + + jresponse = json_tokener_parse(body); + if (!jresponse) + return; + + json_object_object_foreach(jresponse, jkey, jval) + { + for (i = 0; i < ARRAY_SIZE(streamurl_map); i++) + streamurl_field_parse(metadata, &streamurl_map[i], jkey, jval); + } + + jparse_free(jresponse); +} + +static void +streamurl_settings_unload(void) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(streamurl_map); i++) + { + free(streamurl_map[i].words); + streamurl_map[i].words = NULL; + } +} + +static int +streamurl_settings_load(void) +{ + struct settings_category *category; + bool enabled; + int i; + + category = settings_category_get("misc"); + if (!category) + return -1; + + for (i = 0, enabled = false; i < ARRAY_SIZE(streamurl_map); i++) + { + streamurl_map[i].words = settings_option_getstr(settings_option_get(category, streamurl_map[i].setting)); + if (streamurl_map[i].words) + enabled = true; + } + + return enabled ? 0 : -1; +} + +static void +streamurl_process(struct input_metadata *metadata, const char *url) +{ + struct http_client_ctx client = { 0 }; + struct keyval kv = { 0 }; + struct evbuffer *evbuf; + const char *content_type; + char *body; + int ret; + + // If the user didn't configure any keywords to look for then we can stop now + ret = streamurl_settings_load(); + if (ret < 0) + { + DPRINTF(E_DBG, L_PLAYER, "Ignoring StreamUrl resource '%s', no settings\n", url); + return; + } + + DPRINTF(E_DBG, L_PLAYER, "Downloading StreamUrl resource '%s'\n", url); + + CHECK_NULL(L_PLAYER, evbuf = evbuffer_new()); + + client.url = url; + client.input_headers = &kv; + client.input_body = evbuf; + + ret = http_client_request(&client); + if (ret < 0 || client.response_code != HTTP_OK) + { + DPRINTF(E_WARN, L_PLAYER, "Request for StreamUrl resource '%s' failed, response code %d\n", url, client.response_code); + goto out; + } + + // 0-terminate for safety + evbuffer_add(evbuf, "", 1); + body = (char *)evbuffer_pullup(evbuf, -1); + + content_type = keyval_get(&kv, "Content-Type"); + if (content_type && strcasecmp(content_type, "application/json") == 0) + streamurl_json_parse(metadata, body); + else + DPRINTF(E_WARN, L_PLAYER, "No handler for StreamUrl resource '%s' with content type '%s'\n", url, content_type); + + out: + keyval_clear(&kv); + evbuffer_free(evbuf); + streamurl_settings_unload(); +} + + +/*---------------------------- Input implementation --------------------------*/ + static int setup(struct input_source *source) { @@ -141,13 +318,20 @@ metadata_get_http(struct input_metadata *metadata, struct input_source *source) if (!changed) { http_icy_metadata_free(m, 0); - return -1; // TODO Perhaps a problem since this prohibits the player updating metadata + return -1; } swap_pointers(&metadata->artist, &m->artist); // Note we map title to album, because clients should show stream name as titel swap_pointers(&metadata->album, &m->title); - swap_pointers(&metadata->artwork_url, &m->url); + + if (m->url) + { + if (artwork_extension_is_artwork(m->url)) + swap_pointers(&metadata->artwork_url, &m->url); + else + streamurl_process(metadata, m->url); + } http_icy_metadata_free(m, 0); return 0; diff --git a/src/settings.c b/src/settings.c index 1338db4f..50e10882 100644 --- a/src/settings.c +++ b/src/settings.c @@ -42,10 +42,17 @@ static struct settings_option artwork_options[] = { "use_artwork_source_coverartarchive", SETTINGS_TYPE_BOOL, NULL, artwork_coverartarchive_default_getbool, NULL }, }; +static struct settings_option misc_options[] = + { + { "streamurl_keywords_artwork_url", SETTINGS_TYPE_STR }, + { "streamurl_keywords_length", SETTINGS_TYPE_STR }, + }; + static struct settings_category categories[] = { { "webinterface", webinterface_options, ARRAY_SIZE(webinterface_options) }, { "artwork", artwork_options, ARRAY_SIZE(artwork_options) }, + { "misc", misc_options, ARRAY_SIZE(misc_options) }, }; @@ -94,7 +101,6 @@ artwork_coverartarchive_default_getbool(struct settings_option *option) return artwork_default_getbool(false, "coverartarchive"); } - /* ------------------------------ IMPLEMENTATION -----------------------------*/ int From 552c201cf342a3b67bb19f5a73c919bc9447dfd1 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sat, 16 May 2020 21:31:18 +0200 Subject: [PATCH 4/9] [artwork] Change cache strategy for artwork_url_get Credit @sfeakes --- src/artwork.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/artwork.c b/src/artwork.c index b3793f2c..cd0fe7f1 100644 --- a/src/artwork.c +++ b/src/artwork.c @@ -275,11 +275,14 @@ static struct artwork_source artwork_item_source[] = .cache = ON_SUCCESS | ON_FAILURE, }, { + // Here we must use STASH because this handler must always just be a + // backup when artwork_url_get fails. If we used ON_SUCCESS this image + // would go in permanent cache, and artwork_url_get not get called again. .name = "playlist own", .handler = source_item_ownpl_get, .data_kinds = (1 << DATA_KIND_HTTP), .media_kinds = MEDIA_KIND_ALL, - .cache = ON_SUCCESS | ON_FAILURE, + .cache = STASH, }, { .name = "Spotify search web api (files)", From 28df2fb0f927abb770a8bd5b3acb429560bd2ebf Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 17 May 2020 18:46:10 +0200 Subject: [PATCH 5/9] [input] Extra comment --- src/input.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/input.h b/src/input.h index 2d098465..c50425f7 100644 --- a/src/input.h +++ b/src/input.h @@ -133,7 +133,7 @@ struct input_definition /* * Transfer stream data to the player's input buffer. Data must be PCM-LE * samples. The input evbuf will be drained on succesful write. This is to avoid - * copying memory. + * copying memory. Thread-safe. * * @in evbuf Raw PCM_LE audio data to write * @in evbuf Quality of the PCM (sample rate etc.) From a69cc65ff67ec1ddee02b6126cd7017748cc999e Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 17 May 2020 22:36:02 +0200 Subject: [PATCH 6/9] [input] Download StreamUrl resource async so input thread is not blocked --- src/inputs/file_http.c | 143 ++++++++++++++++++++++++++++++++--------- 1 file changed, 112 insertions(+), 31 deletions(-) diff --git a/src/inputs/file_http.c b/src/inputs/file_http.c index a2e2876c..5801e5cf 100644 --- a/src/inputs/file_http.c +++ b/src/inputs/file_http.c @@ -22,6 +22,7 @@ #include #include // strcasestr #include // strcasecmp +#include // mutex #include @@ -32,8 +33,17 @@ #include "settings.h" #include "logger.h" #include "artwork.h" +#include "worker.h" #include "input.h" +struct prepared_metadata +{ + // Parsed metadata goes here + struct input_metadata parsed; + // Mutex to share the parsed metadata + pthread_mutex_t lock; +} prepared_metadata; + /* ------- Handling/parsing of StreamUrl tags from some http streams ---------*/ @@ -102,7 +112,7 @@ streamurl_field_parse(struct input_metadata *metadata, struct streamurl_map *map } } -static void +static int streamurl_json_parse(struct input_metadata *metadata, const char *body) { json_object *jresponse; @@ -110,15 +120,19 @@ streamurl_json_parse(struct input_metadata *metadata, const char *body) jresponse = json_tokener_parse(body); if (!jresponse) - return; + return -1; json_object_object_foreach(jresponse, jkey, jval) { for (i = 0; i < ARRAY_SIZE(streamurl_map); i++) - streamurl_field_parse(metadata, &streamurl_map[i], jkey, jval); + { + streamurl_field_parse(metadata, &streamurl_map[i], jkey, jval); + } } jparse_free(jresponse); + + return 0; } static void @@ -154,7 +168,7 @@ streamurl_settings_load(void) return enabled ? 0 : -1; } -static void +static int streamurl_process(struct input_metadata *metadata, const char *url) { struct http_client_ctx client = { 0 }; @@ -169,7 +183,7 @@ streamurl_process(struct input_metadata *metadata, const char *url) if (ret < 0) { DPRINTF(E_DBG, L_PLAYER, "Ignoring StreamUrl resource '%s', no settings\n", url); - return; + return -1; } DPRINTF(E_DBG, L_PLAYER, "Downloading StreamUrl resource '%s'\n", url); @@ -184,6 +198,7 @@ streamurl_process(struct input_metadata *metadata, const char *url) if (ret < 0 || client.response_code != HTTP_OK) { DPRINTF(E_WARN, L_PLAYER, "Request for StreamUrl resource '%s' failed, response code %d\n", url, client.response_code); + ret = -1; goto out; } @@ -193,14 +208,82 @@ streamurl_process(struct input_metadata *metadata, const char *url) content_type = keyval_get(&kv, "Content-Type"); if (content_type && strcasecmp(content_type, "application/json") == 0) - streamurl_json_parse(metadata, body); + { + ret = streamurl_json_parse(metadata, body); + } else - DPRINTF(E_WARN, L_PLAYER, "No handler for StreamUrl resource '%s' with content type '%s'\n", url, content_type); + { + DPRINTF(E_WARN, L_PLAYER, "No handler for StreamUrl resource '%s' with content type '%s'\n", url, content_type); + ret = -1; + } out: keyval_clear(&kv); evbuffer_free(evbuf); streamurl_settings_unload(); + return ret; +} + +// Thread: worker +static void +streamurl_cb(void *arg) +{ + struct input_metadata metadata = { 0 }; + char *url = arg; + int ret; + + ret = streamurl_process(&metadata, url); + if (ret < 0) // Only negative on error/unconfigured (not if no metadata) + return; + + pthread_mutex_lock(&prepared_metadata.lock); + + swap_pointers(&prepared_metadata.parsed.artwork_url, &metadata.artwork_url); + prepared_metadata.parsed.pos_is_updated = metadata.pos_is_updated; + prepared_metadata.parsed.pos_ms = metadata.pos_ms; + prepared_metadata.parsed.len_ms = metadata.len_ms; + + pthread_mutex_unlock(&prepared_metadata.lock); + + input_metadata_free(&metadata, 1); + + input_write(NULL, NULL, INPUT_FLAG_METADATA); +} + + +/*-------------------------------- http metadata -----------------------------*/ + +// Checks if there is new metadata, which means getting the ICY data plus the +// StreamTitle and StreamUrl fields from libav. If StreamUrl is not an artwork +// link then we also kick off async downloading of it. +static int +metadata_prepare(struct input_source *source) +{ + struct http_icy_metadata *m; + int changed; + + m = transcode_metadata(source->input_ctx, &changed); + if (!m) + return -1; + + if (!changed) + { + http_icy_metadata_free(m, 0); + return -1; + } + + swap_pointers(&prepared_metadata.parsed.artist, &m->artist); + // Note we map title to album, because clients should show stream name as title + swap_pointers(&prepared_metadata.parsed.album, &m->title); + + // 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; } @@ -280,7 +363,7 @@ play(struct input_source *source) return -1; } - flags = (icy_timer ? INPUT_FLAG_METADATA : 0); + flags = (icy_timer && metadata_prepare(source) == 0) ? INPUT_FLAG_METADATA : 0; input_write(source->evbuf, &source->quality, flags); @@ -308,35 +391,31 @@ seek_http(struct input_source *source, int seek_ms) static int metadata_get_http(struct input_metadata *metadata, struct input_source *source) { - struct http_icy_metadata *m; - int changed; + pthread_mutex_lock(&prepared_metadata.lock); - m = transcode_metadata(source->input_ctx, &changed); - if (!m) - return -1; + *metadata = prepared_metadata.parsed; - if (!changed) - { - http_icy_metadata_free(m, 0); - return -1; - } + // Ownership transferred to caller, null all pointers in the struct + memset(&prepared_metadata.parsed, 0, sizeof(struct input_metadata)); - swap_pointers(&metadata->artist, &m->artist); - // Note we map title to album, because clients should show stream name as titel - swap_pointers(&metadata->album, &m->title); + pthread_mutex_unlock(&prepared_metadata.lock); - if (m->url) - { - if (artwork_extension_is_artwork(m->url)) - swap_pointers(&metadata->artwork_url, &m->url); - else - streamurl_process(metadata, m->url); - } - - http_icy_metadata_free(m, 0); return 0; } +static int +init_http(void) +{ + CHECK_ERR(L_PLAYER, mutex_init(&prepared_metadata.lock)); + return 0; +} + +static void +deinit_http(void) +{ + CHECK_ERR(L_PLAYER, pthread_mutex_destroy(&prepared_metadata.lock)); +} + struct input_definition input_file = { .name = "file", @@ -357,5 +436,7 @@ struct input_definition input_http = .play = play, .stop = stop, .metadata_get = metadata_get_http, - .seek = seek_http + .seek = seek_http, + .init = init_http, + .deinit = deinit_http, }; From 8acb2a647defc7a448a4daf3f330301ead9a073a Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 17 May 2020 23:28:05 +0200 Subject: [PATCH 7/9] [http] Don't set ICY metadata if they are just empty strings An empty string will mean that m3u tags won't get used (unless m3u_override is configured), but they should be used, since they are probably better than an empty string. --- src/http.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/http.c b/src/http.c index f7500120..329c8f24 100644 --- a/src/http.c +++ b/src/http.c @@ -720,11 +720,11 @@ metadata_header_get(struct http_icy_metadata *metadata, AVFormatContext *fmtctx) if (ptr[0] == ' ') ptr++; - if ((strncmp(icy_token, "icy-name", strlen("icy-name")) == 0) && !metadata->name) + if ((strncmp(icy_token, "icy-name", strlen("icy-name")) == 0) && ptr[0] != '\0' && !metadata->name) metadata->name = strdup(ptr); - else if ((strncmp(icy_token, "icy-description", strlen("icy-description")) == 0) && !metadata->description) + else if ((strncmp(icy_token, "icy-description", strlen("icy-description")) == 0) && ptr[0] != '\0' && !metadata->description) metadata->description = strdup(ptr); - else if ((strncmp(icy_token, "icy-genre", strlen("icy-genre")) == 0) && !metadata->genre) + else if ((strncmp(icy_token, "icy-genre", strlen("icy-genre")) == 0) && ptr[0] != '\0' && !metadata->genre) metadata->genre = strdup(ptr); icy_token = strtok_r(NULL, "\r\n", &save_pr); @@ -816,17 +816,17 @@ http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only) CHECK_NULL(L_HTTP, metadata = calloc(1, sizeof(struct http_icy_metadata))); got_header = 0; - if ( (value = keyval_get(ctx.input_headers, "icy-name")) ) + if ( (value = keyval_get(ctx.input_headers, "icy-name")) && value[0] != '\0' ) { metadata->name = strdup(value); got_header = 1; } - if ( (value = keyval_get(ctx.input_headers, "icy-description")) ) + if ( (value = keyval_get(ctx.input_headers, "icy-description")) && value[0] != '\0' ) { metadata->description = strdup(value); got_header = 1; } - if ( (value = keyval_get(ctx.input_headers, "icy-genre")) ) + if ( (value = keyval_get(ctx.input_headers, "icy-genre")) && value[0] != '\0' ) { metadata->genre = strdup(value); got_header = 1; From 3c65f8a71e415275b34f9b629f6538aa2109fe4f Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 17 May 2020 23:51:02 +0200 Subject: [PATCH 8/9] [docs] Add README_RADIO_STREAMS.md Credit @sfeakes --- Makefile.am | 1 + README.md | 4 ++ README_RADIO_STREAMS.md | 97 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 README_RADIO_STREAMS.md diff --git a/Makefile.am b/Makefile.am index 817b7dfa..83a4d938 100644 --- a/Makefile.am +++ b/Makefile.am @@ -20,6 +20,7 @@ nobase_dist_doc_DATA = \ README_ALSA.md \ README_SMARTPL.md \ README_PLAYER_WEBINTERFACE.md \ + README_RADIO_STREAMS.md \ scripts/pairinghelper.sh EXTRA_DIST = \ diff --git a/README.md b/README.md index 11222830..f79a98ee 100644 --- a/README.md +++ b/README.md @@ -323,6 +323,10 @@ forked-daapd has support for smart playlists. How to create a smart playlist is documented in [README_SMARTPL.md](https://github.com/ejurgensen/forked-daapd/blob/master/README_SMARTPL.md). +If you're not satisfied with internet radio metadata that forked-daapd shows, +then you can read about tweaking it in +[README_RADIO_STREAMS.md](https://github.com/ejurgensen/forked-daapd/blob/master/README_RADIO_STREAMS.md). + ## Artwork diff --git a/README_RADIO_STREAMS.md b/README_RADIO_STREAMS.md new file mode 100644 index 00000000..43e52eef --- /dev/null +++ b/README_RADIO_STREAMS.md @@ -0,0 +1,97 @@ +# forked-daapd and Radio Stream tweaking + +Radio streams have many different ways in how metadata is sent. Many should +just work as expected, but a few may require some tweaking. If you are not +seeing expected title, track, artist, artwork in forked-daapd clients or web UI, +the following may help. + +First, understand what and how the particular stream is sending information. +ffprobe is a command that can be used to interegrate most of the stream +information. `ffprobe ` should give you some useful output, +look at the Metadata section, below is an example. + +``` + Metadata: + icy-br : 320 + icy-description : DJ-mixed blend of modern and classic rock, electronica, world music, and more. Always 100% commercial-free + icy-genre : Eclectic + icy-name : Radio Paradise (320k aac) + icy-pub : 1 + icy-url : https://radioparadise.com + StreamTitle : Depeche Mode - Strangelove + StreamUrl : http://img.radioparadise.com/covers/l/B000002LCI.jpg +``` + +In the example above, all tags are populated with correct information, no +modifications to forked-daapd configuration should be needed. Note that +StreamUrl points to the artwork image file. + + +Below is another example that will require some tweaks to forked-daapd, Notice +`icy-name` is blank and `StreamUrl` doesn't point to an image. + +``` +Metadata: + icy-br : 127 + icy-pub : 0 + icy-description : Unspecified description + icy-url : + icy-genre : various + icy-name : + StreamTitle : Pour Some Sugar On Me - Def Leppard + StreamUrl : https://radio.stream.domain/api9/eventdata/49790578 +``` + +In the above, first fix is the blank name, second is the image artwork. +### 1) Stream Name/Title +Set the name with an EXTINF tag in the m3u playlist file: + +``` +#EXTM3U +#EXTINF:-1, - My Radio Stream Name +http://radio.stream.domain/stream.url +``` + +The format is basically `#EXTINF:, - `. +Length is -1 since it's a stream, `` was left blank since +`StreamTitle` is accurate in the Metadata but `` was set to +`My Radio Stream Name` since `icy-name` was blank. + +### 2) Artwork (and track duration) +If `StreamUrl` does not point directly to an artwork file then the link may be +to a json file that contains an artwork link. If so, you can make forked-daapd +download the file automatically and search for an artwork link, and also track +duration. + +Try to download the file, e.g. with `curl "https://radio.stream.domain/api9/eventdata/49790578"`. +Let's assume you get something like this: + +``` +{ + "eventId": 49793707, + "eventStart": "2020-05-08 16:23:03", + "eventFinish": "2020-05-08 16:27:21", + "eventDuration": 254, + "eventType": "Song", + "eventSongTitle": "Pour Some Sugar On Me", + "eventSongArtist": "Def Leppard", + "eventImageUrl": "https://radio.stream.domain/artist/1-1/320x320/562.jpg?ver=1465083491", + "eventImageUrlSmall": "https://radio.stream.domain/artist/1-1/160x160/562.jpg?ver=1465083491", + "eventAppleMusicUrl": "https://geo.itunes.apple.com/dk/album/530707298?i=530707313" +} +``` + +In this case, you would need to tell forked-daapd to look for "eventDuration" +and "eventImageUrl" (or just "duration" and "url"). You can do that like this: + +``` +curl -X PUT "http://localhost:3689/api/settings/misc/streamurl_keywords_length" --data "{\"name\":\"streamurl_keywords_length\",\"value\":\"duration\"}" +curl -X PUT "http://localhost:3689/api/settings/misc/streamurl_keywords_artwork_url" --data "{\"name\":\"streamurl_keywords_artwork_url\",\"value\":\"url\"} +``` + +If you want multiple search phrases then comma separate, e.g. "duration,length". + + +If your radio station is not returning any artwork links, you can also just make +a static artwork by placing a png/jpg in the same directory as the m3u, and with +the same name, e.g. `My Radio Stream.jpg` for `My Radio Stream.m3u`. From 37521406f37620a86d457c3eefc43b806f1175d1 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 17 May 2020 22:48:06 +0200 Subject: [PATCH 9/9] [input] Split file_http.c input into file.c and http.c The common code is by now limited, and there is a lot of http-specific code. --- src/Makefile.am | 2 +- src/inputs/file.c | 114 +++++++++++++++++++++++++++++ src/inputs/{file_http.c => http.c} | 57 +++++---------- 3 files changed, 133 insertions(+), 40 deletions(-) create mode 100644 src/inputs/file.c rename src/inputs/{file_http.c => http.c} (94%) diff --git a/src/Makefile.am b/src/Makefile.am index a65d2372..157fe5bc 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -130,7 +130,7 @@ forked_daapd_SOURCES = main.c \ worker.c worker.h \ settings.c settings.h \ input.h input.c \ - inputs/file_http.c inputs/pipe.c inputs/timer.c \ + inputs/file.c inputs/http.c inputs/pipe.c inputs/timer.c \ outputs.h outputs.c \ outputs/rtp_common.h outputs/rtp_common.c \ outputs/raop.c $(RAOP_VERIFICATION_SRC) \ diff --git a/src/inputs/file.c b/src/inputs/file.c new file mode 100644 index 00000000..5a5ee3d0 --- /dev/null +++ b/src/inputs/file.c @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2017-2020 Espen Jurgensen + * + * 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 + */ + +#include +#include +#include +#include + +#include + +#include "transcode.h" +#include "misc.h" +#include "logger.h" +#include "input.h" + +/*---------------------------- Input implementation --------------------------*/ + +// Important! If you change any of the below then consider if the change also +// should be made in http.c + +static int +setup(struct input_source *source) +{ + struct transcode_ctx *ctx; + + ctx = transcode_setup(XCODE_PCM_NATIVE, NULL, source->data_kind, source->path, source->len_ms, NULL); + if (!ctx) + return -1; + + CHECK_NULL(L_PLAYER, source->evbuf = evbuffer_new()); + + source->quality.sample_rate = transcode_encode_query(ctx->encode_ctx, "sample_rate"); + source->quality.bits_per_sample = transcode_encode_query(ctx->encode_ctx, "bits_per_sample"); + source->quality.channels = transcode_encode_query(ctx->encode_ctx, "channels"); + + source->input_ctx = ctx; + + return 0; +} + +static int +stop(struct input_source *source) +{ + struct transcode_ctx *ctx = source->input_ctx; + + transcode_cleanup(&ctx); + + if (source->evbuf) + evbuffer_free(source->evbuf); + + source->input_ctx = NULL; + source->evbuf = NULL; + + return 0; +} + +static int +play(struct input_source *source) +{ + struct transcode_ctx *ctx = source->input_ctx; + int ret; + + // We set "wanted" to 1 because the read size doesn't matter to us + // TODO optimize? + ret = transcode(source->evbuf, NULL, ctx, 1); + if (ret == 0) + { + input_write(source->evbuf, &source->quality, INPUT_FLAG_EOF); + stop(source); + return -1; + } + else if (ret < 0) + { + input_write(NULL, NULL, INPUT_FLAG_ERROR); + stop(source); + return -1; + } + + input_write(source->evbuf, &source->quality, 0); + + return 0; +} + +static int +seek(struct input_source *source, int seek_ms) +{ + return transcode_seek(source->input_ctx, seek_ms); +} + +struct input_definition input_file = +{ + .name = "file", + .type = INPUT_TYPE_FILE, + .disabled = 0, + .setup = setup, + .play = play, + .stop = stop, + .seek = seek, +}; diff --git a/src/inputs/file_http.c b/src/inputs/http.c similarity index 94% rename from src/inputs/file_http.c rename to src/inputs/http.c index 5801e5cf..7b75a928 100644 --- a/src/inputs/file_http.c +++ b/src/inputs/http.c @@ -289,10 +289,20 @@ metadata_prepare(struct input_source *source) /*---------------------------- Input implementation --------------------------*/ +// Important! If you change any of the below then consider if the change also +// should be made in file.c + static int setup(struct input_source *source) { struct transcode_ctx *ctx; + char *url; + + if (http_stream_setup(&url, source->path) < 0) + return -1; + + free(source->path); + source->path = url; ctx = transcode_setup(XCODE_PCM_NATIVE, NULL, source->data_kind, source->path, source->len_ms, NULL); if (!ctx) @@ -309,20 +319,6 @@ setup(struct input_source *source) return 0; } -static int -setup_http(struct input_source *source) -{ - char *url; - - if (http_stream_setup(&url, source->path) < 0) - return -1; - - free(source->path); - source->path = url; - - return setup(source); -} - static int stop(struct input_source *source) { @@ -372,12 +368,6 @@ play(struct input_source *source) static int seek(struct input_source *source, int seek_ms) -{ - return transcode_seek(source->input_ctx, seek_ms); -} - -static int -seek_http(struct input_source *source, int seek_ms) { // Stream is live/unknown length so can't seek. We return 0 anyway, because // it is valid for the input to request a seek, since the input is not @@ -389,7 +379,7 @@ seek_http(struct input_source *source, int seek_ms) } static int -metadata_get_http(struct input_metadata *metadata, struct input_source *source) +metadata_get(struct input_metadata *metadata, struct input_source *source) { pthread_mutex_lock(&prepared_metadata.lock); @@ -404,39 +394,28 @@ metadata_get_http(struct input_metadata *metadata, struct input_source *source) } static int -init_http(void) +init(void) { CHECK_ERR(L_PLAYER, mutex_init(&prepared_metadata.lock)); return 0; } static void -deinit_http(void) +deinit(void) { CHECK_ERR(L_PLAYER, pthread_mutex_destroy(&prepared_metadata.lock)); } -struct input_definition input_file = -{ - .name = "file", - .type = INPUT_TYPE_FILE, - .disabled = 0, - .setup = setup, - .play = play, - .stop = stop, - .seek = seek, -}; - struct input_definition input_http = { .name = "http", .type = INPUT_TYPE_HTTP, .disabled = 0, - .setup = setup_http, + .setup = setup, .play = play, .stop = stop, - .metadata_get = metadata_get_http, - .seek = seek_http, - .init = init_http, - .deinit = deinit_http, + .metadata_get = metadata_get, + .seek = seek, + .init = init, + .deinit = deinit, };