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`. 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/artwork.c b/src/artwork.c index 4b536ba6..cd0fe7f1 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, @@ -274,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)", @@ -1503,25 +1507,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 +1524,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 +2006,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 +2021,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 +2031,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__ */ diff --git a/src/http.c b/src/http.c index 98c289c8..329c8f24 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) @@ -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); @@ -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,23 +813,20 @@ 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")) ) + 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; @@ -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/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.) diff --git a/src/inputs/file_http.c b/src/inputs/file.c similarity index 59% rename from src/inputs/file_http.c rename to src/inputs/file.c index 5206fee3..5a5ee3d0 100644 --- a/src/inputs/file_http.c +++ b/src/inputs/file.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 @@ -24,11 +24,15 @@ #include #include "transcode.h" -#include "http.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) { @@ -49,20 +53,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) { @@ -83,13 +73,11 @@ static int play(struct input_source *source) { struct transcode_ctx *ctx = source->input_ctx; - int icy_timer; int ret; - short flags; // We set "wanted" to 1 because the read size doesn't matter to us // TODO optimize? - ret = transcode(source->evbuf, &icy_timer, ctx, 1); + ret = transcode(source->evbuf, NULL, ctx, 1); if (ret == 0) { input_write(source->evbuf, &source->quality, INPUT_FLAG_EOF); @@ -103,9 +91,7 @@ play(struct input_source *source) return -1; } - flags = (icy_timer ? INPUT_FLAG_METADATA : 0); - - input_write(source->evbuf, &source->quality, flags); + input_write(source->evbuf, &source->quality, 0); return 0; } @@ -116,43 +102,6 @@ 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 - // supposed to concern itself about this. - if (source->len_ms == 0) - return 0; - - return transcode_seek(source->input_ctx, seek_ms); -} - -static int -metadata_get_http(struct input_metadata *metadata, 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; // TODO Perhaps a problem since this prohibits the player updating 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); - swap_pointers(&metadata->artwork_url, &m->artwork_url); - - http_icy_metadata_free(m, 0); - return 0; -} - struct input_definition input_file = { .name = "file", @@ -163,15 +112,3 @@ struct input_definition input_file = .stop = stop, .seek = seek, }; - -struct input_definition input_http = -{ - .name = "http", - .type = INPUT_TYPE_HTTP, - .disabled = 0, - .setup = setup_http, - .play = play, - .stop = stop, - .metadata_get = metadata_get_http, - .seek = seek_http -}; diff --git a/src/inputs/http.c b/src/inputs/http.c new file mode 100644 index 00000000..7b75a928 --- /dev/null +++ b/src/inputs/http.c @@ -0,0 +1,421 @@ +/* + * 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 // strcasestr +#include // strcasecmp +#include // mutex + +#include + +#include "transcode.h" +#include "http.h" +#include "misc.h" +#include "misc_json.h" +#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 ---------*/ + +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 int +streamurl_json_parse(struct input_metadata *metadata, const char *body) +{ + json_object *jresponse; + int i; + + jresponse = json_tokener_parse(body); + if (!jresponse) + 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); + } + } + + jparse_free(jresponse); + + return 0; +} + +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 int +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 -1; + } + + 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); + ret = -1; + 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) + { + 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); + 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; +} + + +/*---------------------------- 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) + 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 icy_timer; + int ret; + short flags; + + // We set "wanted" to 1 because the read size doesn't matter to us + // TODO optimize? + ret = transcode(source->evbuf, &icy_timer, 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; + } + + flags = (icy_timer && metadata_prepare(source) == 0) ? INPUT_FLAG_METADATA : 0; + + input_write(source->evbuf, &source->quality, flags); + + return 0; +} + +static int +seek(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 + // supposed to concern itself about this. + if (source->len_ms == 0) + return 0; + + return transcode_seek(source->input_ctx, seek_ms); +} + +static int +metadata_get(struct input_metadata *metadata, struct input_source *source) +{ + pthread_mutex_lock(&prepared_metadata.lock); + + *metadata = prepared_metadata.parsed; + + // Ownership transferred to caller, null all pointers in the struct + memset(&prepared_metadata.parsed, 0, sizeof(struct input_metadata)); + + pthread_mutex_unlock(&prepared_metadata.lock); + + return 0; +} + +static int +init(void) +{ + CHECK_ERR(L_PLAYER, mutex_init(&prepared_metadata.lock)); + return 0; +} + +static void +deinit(void) +{ + CHECK_ERR(L_PLAYER, pthread_mutex_destroy(&prepared_metadata.lock)); +} + +struct input_definition input_http = +{ + .name = "http", + .type = INPUT_TYPE_HTTP, + .disabled = 0, + .setup = setup, + .play = play, + .stop = stop, + .metadata_get = metadata_get, + .seek = seek, + .init = init, + .deinit = deinit, +}; 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