mirror of
https://github.com/owntone/owntone-server.git
synced 2024-12-27 15:45:56 -05:00
Merge branch 'streamurl1'
This commit is contained in:
commit
e132b2fd25
@ -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 = \
|
||||
|
@ -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
97
README_RADIO_STREAMS.md
Normal 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`.
|
@ -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) \
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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__ */
|
||||
|
58
src/http.c
58
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);
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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.)
|
||||
|
@ -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
421
src/inputs/http.c
Normal 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,
|
||||
};
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user