Merge branch 'streamurl1'

This commit is contained in:
ejurgensen 2020-05-19 23:08:26 +02:00
commit e132b2fd25
12 changed files with 625 additions and 151 deletions

View File

@ -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 = \

View File

@ -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

97
README_RADIO_STREAMS.md Normal file
View File

@ -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 <http://stream.url>` 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>, <Artist Name> - <Artist Title>`.
Length is -1 since it's a stream, `<Artist Name>` was left blank since
`StreamTitle` is accurate in the Metadata but `<Artist Title>` 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`.

View File

@ -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) \

View File

@ -26,6 +26,7 @@
#include <stdbool.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
@ -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;
}

View File

@ -9,6 +9,7 @@
#define ART_DEFAULT_WIDTH 600
#include <event2/buffer.h>
#include <stdbool.h>
/*
* 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__ */

View File

@ -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);
}

View File

@ -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;
};

View File

@ -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.)

View File

@ -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 <event2/buffer.h>
#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
};

421
src/inputs/http.c Normal file
View File

@ -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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <string.h> // strcasestr
#include <strings.h> // strcasecmp
#include <pthread.h> // mutex
#include <event2/buffer.h>
#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,
};

View File

@ -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