commit
7982bca6f0
|
@ -78,14 +78,21 @@ library {
|
|||
# (changing this setting only takes effect after rescan, see the README)
|
||||
compilation_artist = "Various artists"
|
||||
|
||||
# There are 5 default playlists: "Library", "Music", "Movies", "TV Shows"
|
||||
# and "Podcasts". Here you can change the names of these playlists.
|
||||
# Internet streams in your playlists will by default be shown in the
|
||||
# "Radio" library, like iTunes does. However, some clients (like
|
||||
# TunesRemote+) won't show the "Radio" library. If you would also like
|
||||
# to have them shown like normal playlists, you can enable this option.
|
||||
# radio_playlists = false
|
||||
|
||||
# These are the default playlists. If you want them to have other names,
|
||||
# you can set it here.
|
||||
# name_library = "Library"
|
||||
# name_music = "Music"
|
||||
# name_movies = "Movies"
|
||||
# name_tvshows = "TV Shows"
|
||||
# name_podcasts = "Podcasts"
|
||||
# name_audiobooks = "Audiobooks"
|
||||
# name_radio = "Radio"
|
||||
|
||||
# Artwork file names (without file type extension)
|
||||
# forked-daapd will look for jpg and png files with these base names
|
||||
|
@ -164,6 +171,11 @@ spotify {
|
|||
# 0: No preference (default), 1: 96kbps, 2: 160kbps, 3: 320kbps
|
||||
# bitrate = 0
|
||||
|
||||
# Your Spotify playlists will by default be put in a "Spotify" playlist
|
||||
# folder. If you would rather have them together with your other
|
||||
# playlists you can set this option to true.
|
||||
# base_playlist_disable = false
|
||||
|
||||
# Spotify playlists usually have many artist, and if you don't want every
|
||||
# artist to be listed when artist browsing in Remote, you can set the
|
||||
# artist_override flag to true. This will use the compilation_artist as
|
||||
|
|
|
@ -101,7 +101,7 @@ forked_daapd_SOURCES = main.c \
|
|||
conffile.c conffile.h \
|
||||
cache.c cache.h \
|
||||
filescanner.c filescanner.h \
|
||||
filescanner_ffmpeg.c filescanner_playlist.c filescanner_icy.c $(ITUNES_SRC) \
|
||||
filescanner_ffmpeg.c filescanner_playlist.c $(ITUNES_SRC) \
|
||||
mdns_avahi.c mdns.h \
|
||||
remote_pairing.c remote_pairing.h \
|
||||
$(EVHTTP_SRC) \
|
||||
|
@ -110,6 +110,7 @@ forked_daapd_SOURCES = main.c \
|
|||
httpd_rsp.c httpd_rsp.h \
|
||||
httpd_daap.c httpd_daap.h \
|
||||
httpd_dacp.c httpd_dacp.h \
|
||||
http.c http.h \
|
||||
dmap_common.c dmap_common.h \
|
||||
transcode.c transcode.h \
|
||||
pipe.c pipe.h \
|
||||
|
@ -119,6 +120,7 @@ forked_daapd_SOURCES = main.c \
|
|||
rsp_query.c rsp_query.h \
|
||||
daap_query.c daap_query.h \
|
||||
player.c player.h \
|
||||
worker.c worker.h \
|
||||
$(ALSA_SRC) $(OSS4_SRC) laudio.h \
|
||||
raop.c raop.h \
|
||||
$(RTSP_SRC) \
|
||||
|
|
218
src/artwork.c
218
src/artwork.c
|
@ -38,6 +38,8 @@
|
|||
#include "logger.h"
|
||||
#include "conffile.h"
|
||||
#include "cache.h"
|
||||
#include "player.h"
|
||||
#include "http.h"
|
||||
|
||||
#if LIBAVFORMAT_VERSION_MAJOR >= 53
|
||||
# include "avio_evbuffer.h"
|
||||
|
@ -52,13 +54,19 @@
|
|||
# include "spotify.h"
|
||||
#endif
|
||||
|
||||
#if LIBAVCODEC_VERSION_MAJOR <= 53 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR <= 34)
|
||||
# define AV_CODEC_ID_MJPEG CODEC_ID_MJPEG
|
||||
# define AV_CODEC_ID_PNG CODEC_ID_PNG
|
||||
# define AV_CODEC_ID_NONE CODEC_ID_NONE
|
||||
#endif
|
||||
|
||||
static const char *cover_extension[] =
|
||||
{
|
||||
"jpg", "png",
|
||||
};
|
||||
|
||||
static int
|
||||
artwork_read(char *path, struct evbuffer *evbuf)
|
||||
artwork_read(struct evbuffer *evbuf, char *path)
|
||||
{
|
||||
uint8_t buf[4096];
|
||||
struct stat sb;
|
||||
|
@ -104,27 +112,21 @@ artwork_read(char *path, struct evbuffer *evbuf)
|
|||
static int
|
||||
rescale_needed(AVCodecContext *src, int max_w, int max_h, int *target_w, int *target_h)
|
||||
{
|
||||
int need_rescale;
|
||||
|
||||
DPRINTF(E_DBG, L_ART, "Original image dimensions: w %d h %d\n", src->width, src->height);
|
||||
|
||||
need_rescale = 1;
|
||||
*target_w = src->width;
|
||||
*target_h = src->height;
|
||||
|
||||
if ((max_w <= 0) || (max_h <= 0)) /* No valid dimensions, use original */
|
||||
{
|
||||
need_rescale = 0;
|
||||
if ((src->width == 0) || (src->height == 0)) /* Unknown source size, can't rescale */
|
||||
return 0;
|
||||
|
||||
*target_w = src->width;
|
||||
*target_h = src->height;
|
||||
}
|
||||
else if ((src->width <= max_w) && (src->height <= max_h)) /* Smaller than target */
|
||||
{
|
||||
need_rescale = 0;
|
||||
if ((max_w <= 0) || (max_h <= 0)) /* No valid target dimensions, use original */
|
||||
return 0;
|
||||
|
||||
*target_w = src->width;
|
||||
*target_h = src->height;
|
||||
}
|
||||
else if (src->width * max_h > src->height * max_w) /* Wider aspect ratio than target */
|
||||
if ((src->width <= max_w) && (src->height <= max_h)) /* Smaller than target */
|
||||
return 0;
|
||||
|
||||
if (src->width * max_h > src->height * max_w) /* Wider aspect ratio than target */
|
||||
{
|
||||
*target_w = max_w;
|
||||
*target_h = (double)max_w * ((double)src->height / (double)src->width);
|
||||
|
@ -148,11 +150,11 @@ rescale_needed(AVCodecContext *src, int max_w, int max_h, int *target_w, int *ta
|
|||
|
||||
DPRINTF(E_DBG, L_ART, "Destination width %d height %d\n", *target_w, *target_h);
|
||||
|
||||
return need_rescale;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
artwork_rescale(AVFormatContext *src_ctx, int s, int out_w, int out_h, struct evbuffer *evbuf)
|
||||
artwork_rescale(struct evbuffer *evbuf, AVFormatContext *src_ctx, int s, int out_w, int out_h)
|
||||
{
|
||||
uint8_t *buf;
|
||||
uint8_t *outbuf;
|
||||
|
@ -219,7 +221,6 @@ artwork_rescale(AVFormatContext *src_ctx, int s, int out_w, int out_h, struct ev
|
|||
goto out_close_src;
|
||||
}
|
||||
|
||||
#if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
|
||||
dst_fmt->video_codec = AV_CODEC_ID_NONE;
|
||||
|
||||
/* Try to keep same codec if possible */
|
||||
|
@ -233,21 +234,6 @@ artwork_rescale(AVFormatContext *src_ctx, int s, int out_w, int out_h, struct ev
|
|||
{
|
||||
dst_fmt->video_codec = AV_CODEC_ID_PNG;
|
||||
}
|
||||
#else
|
||||
dst_fmt->video_codec = CODEC_ID_NONE;
|
||||
|
||||
/* Try to keep same codec if possible */
|
||||
if (src->codec_id == CODEC_ID_PNG)
|
||||
dst_fmt->video_codec = CODEC_ID_PNG;
|
||||
else if (src->codec_id == CODEC_ID_MJPEG)
|
||||
dst_fmt->video_codec = CODEC_ID_MJPEG;
|
||||
|
||||
/* If not possible, select new codec */
|
||||
if (dst_fmt->video_codec == CODEC_ID_NONE)
|
||||
{
|
||||
dst_fmt->video_codec = CODEC_ID_PNG;
|
||||
}
|
||||
#endif
|
||||
|
||||
img_encoder = avcodec_find_encoder(dst_fmt->video_codec);
|
||||
if (!img_encoder)
|
||||
|
@ -559,19 +545,11 @@ artwork_rescale(AVFormatContext *src_ctx, int s, int out_w, int out_h, struct ev
|
|||
|
||||
switch (dst_fmt->video_codec)
|
||||
{
|
||||
#if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
|
||||
case AV_CODEC_ID_PNG:
|
||||
#else
|
||||
case CODEC_ID_PNG:
|
||||
#endif
|
||||
ret = ART_FMT_PNG;
|
||||
break;
|
||||
|
||||
#if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
|
||||
case AV_CODEC_ID_MJPEG:
|
||||
#else
|
||||
case CODEC_ID_MJPEG:
|
||||
#endif
|
||||
ret = ART_FMT_JPEG;
|
||||
break;
|
||||
|
||||
|
@ -621,7 +599,7 @@ artwork_rescale(AVFormatContext *src_ctx, int s, int out_w, int out_h, struct ev
|
|||
}
|
||||
|
||||
static int
|
||||
artwork_get(char *path, int max_w, int max_h, struct evbuffer *evbuf)
|
||||
artwork_get(struct evbuffer *evbuf, char *path, int max_w, int max_h)
|
||||
{
|
||||
AVFormatContext *src_ctx;
|
||||
int s;
|
||||
|
@ -666,20 +644,12 @@ artwork_get(char *path, int max_w, int max_h, struct evbuffer *evbuf)
|
|||
format_ok = 0;
|
||||
for (s = 0; s < src_ctx->nb_streams; s++)
|
||||
{
|
||||
#if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
|
||||
if (src_ctx->streams[s]->codec->codec_id == AV_CODEC_ID_PNG)
|
||||
#else
|
||||
if (src_ctx->streams[s]->codec->codec_id == CODEC_ID_PNG)
|
||||
#endif
|
||||
{
|
||||
format_ok = ART_FMT_PNG;
|
||||
break;
|
||||
}
|
||||
#if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
|
||||
else if (src_ctx->streams[s]->codec->codec_id == AV_CODEC_ID_MJPEG)
|
||||
#else
|
||||
else if (src_ctx->streams[s]->codec->codec_id == CODEC_ID_MJPEG)
|
||||
#endif
|
||||
{
|
||||
format_ok = ART_FMT_JPEG;
|
||||
break;
|
||||
|
@ -703,12 +673,12 @@ artwork_get(char *path, int max_w, int max_h, struct evbuffer *evbuf)
|
|||
/* Fastpath */
|
||||
if (!ret && format_ok)
|
||||
{
|
||||
ret = artwork_read(path, evbuf);
|
||||
ret = artwork_read(evbuf, path);
|
||||
if (ret == 0)
|
||||
ret = format_ok;
|
||||
}
|
||||
else
|
||||
ret = artwork_rescale(src_ctx, s, target_w, target_h, evbuf);
|
||||
ret = artwork_rescale(evbuf, src_ctx, s, target_w, target_h);
|
||||
|
||||
#if LIBAVFORMAT_VERSION_MAJOR >= 54 || (LIBAVFORMAT_VERSION_MAJOR == 53 && LIBAVFORMAT_VERSION_MINOR >= 21)
|
||||
avformat_close_input(&src_ctx);
|
||||
|
@ -725,9 +695,79 @@ artwork_get(char *path, int max_w, int max_h, struct evbuffer *evbuf)
|
|||
return ret;
|
||||
}
|
||||
|
||||
/*
|
||||
* 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. Notice: No
|
||||
* rescaling is done.
|
||||
*
|
||||
* @param evbuf the event buffer that will contain the (scaled) image
|
||||
* @param path path to the item we are getting artwork for
|
||||
* @return ART_FMT_* on success, 0 on error and nothing found
|
||||
*/
|
||||
static int
|
||||
artwork_get_player_image(struct evbuffer *evbuf, char *path)
|
||||
{
|
||||
struct http_client_ctx ctx;
|
||||
struct keyval *kv;
|
||||
const char *content_type;
|
||||
char *url;
|
||||
int id;
|
||||
int len;
|
||||
int ret;
|
||||
|
||||
DPRINTF(E_DBG, L_ART, "Trying internet stream artwork in %s\n", path);
|
||||
|
||||
ret = 0;
|
||||
|
||||
id = db_file_id_bypath(path);
|
||||
if (!id)
|
||||
return 0;
|
||||
|
||||
url = player_get_icy_artwork_url(id);
|
||||
if (!url)
|
||||
return 0;
|
||||
|
||||
len = strlen(url);
|
||||
if ((len < 14) || (len > PATH_MAX)) // Can't be shorter than http://a/1.jpg
|
||||
goto out_url;
|
||||
|
||||
kv = keyval_alloc();
|
||||
if (!kv)
|
||||
goto out_url;
|
||||
|
||||
memset(&ctx, 0, sizeof(ctx));
|
||||
ctx.url = url;
|
||||
ctx.headers = kv;
|
||||
ctx.body = evbuf;
|
||||
|
||||
if (http_client_request(&ctx) < 0)
|
||||
goto out_kv;
|
||||
|
||||
content_type = keyval_get(kv, "Content-Type");
|
||||
if (content_type && (strcmp(content_type, "image/jpeg") == 0))
|
||||
ret = ART_FMT_JPEG;
|
||||
else if (content_type && (strcmp(content_type, "image/png") == 0))
|
||||
ret = ART_FMT_PNG;
|
||||
|
||||
if (ret)
|
||||
DPRINTF(E_DBG, L_ART, "Found internet stream artwork in %s (%s)\n", url, content_type);
|
||||
|
||||
out_kv:
|
||||
keyval_clear(kv);
|
||||
free(kv);
|
||||
|
||||
out_url:
|
||||
free(url);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
#if LIBAVFORMAT_VERSION_MAJOR >= 55 || (LIBAVFORMAT_VERSION_MAJOR == 54 && LIBAVFORMAT_VERSION_MINOR >= 6)
|
||||
static int
|
||||
artwork_get_embedded_image(char *path, int max_w, int max_h, struct evbuffer *evbuf)
|
||||
artwork_get_embedded_image(struct evbuffer *evbuf, char *path, int max_w, int max_h)
|
||||
{
|
||||
AVFormatContext *src_ctx;
|
||||
AVStream *src_st;
|
||||
|
@ -763,20 +803,12 @@ artwork_get_embedded_image(char *path, int max_w, int max_h, struct evbuffer *ev
|
|||
{
|
||||
if (src_ctx->streams[s]->disposition & AV_DISPOSITION_ATTACHED_PIC)
|
||||
{
|
||||
#if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
|
||||
if (src_ctx->streams[s]->codec->codec_id == AV_CODEC_ID_PNG)
|
||||
#else
|
||||
if (src_ctx->streams[s]->codec->codec_id == CODEC_ID_PNG)
|
||||
#endif
|
||||
{
|
||||
format_ok = ART_FMT_PNG;
|
||||
break;
|
||||
}
|
||||
#if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
|
||||
else if (src_ctx->streams[s]->codec->codec_id == AV_CODEC_ID_MJPEG)
|
||||
#else
|
||||
else if (src_ctx->streams[s]->codec->codec_id == CODEC_ID_MJPEG)
|
||||
#endif
|
||||
{
|
||||
format_ok = ART_FMT_JPEG;
|
||||
break;
|
||||
|
@ -786,7 +818,7 @@ artwork_get_embedded_image(char *path, int max_w, int max_h, struct evbuffer *ev
|
|||
|
||||
if (s == src_ctx->nb_streams)
|
||||
{
|
||||
DPRINTF(E_SPAM, L_ART, "Did not find embedded artwork in '%s'\n", path);
|
||||
DPRINTF(E_DBG, L_ART, "Did not find embedded artwork in '%s'\n", path);
|
||||
|
||||
avformat_close_input(&src_ctx);
|
||||
return -1;
|
||||
|
@ -824,7 +856,7 @@ artwork_get_embedded_image(char *path, int max_w, int max_h, struct evbuffer *ev
|
|||
{
|
||||
DPRINTF(E_DBG, L_ART, "Artwork too large, rescaling image\n");
|
||||
|
||||
ret = artwork_rescale(src_ctx, s, target_w, target_h, evbuf);
|
||||
ret = artwork_rescale(evbuf, src_ctx, s, target_w, target_h);
|
||||
}
|
||||
|
||||
avformat_close_input(&src_ctx);
|
||||
|
@ -843,15 +875,15 @@ artwork_get_embedded_image(char *path, int max_w, int max_h, struct evbuffer *ev
|
|||
* Looks for basename(in_path).{png,jpg}, so if is in_path is /foo/bar.mp3 it
|
||||
* will look for /foo/bar.png and /foo/bar.jpg
|
||||
*
|
||||
* @param evbuf the event buffer that will contain the (scaled) image
|
||||
* @param in_path path to the item we are getting artwork for
|
||||
* @param max_w maximum image width
|
||||
* @param max_h maximum image height
|
||||
* @param out_path path to artwork, input must be either NULL or char[PATH_MAX]
|
||||
* @param evbuf the event buffer that will contain the (scaled) image
|
||||
* @return ART_FMT_* on success, 0 on nothing found, -1 on error
|
||||
*/
|
||||
static int
|
||||
artwork_get_own_image(char *in_path, int max_w, int max_h, char *out_path, struct evbuffer *evbuf)
|
||||
artwork_get_own_image(struct evbuffer *evbuf, char *in_path, int max_w, int max_h, char *out_path)
|
||||
{
|
||||
char path[PATH_MAX];
|
||||
char *ptr;
|
||||
|
@ -901,7 +933,7 @@ artwork_get_own_image(char *in_path, int max_w, int max_h, char *out_path, struc
|
|||
if (out_path)
|
||||
strcpy(out_path, path);
|
||||
|
||||
return artwork_get(path, max_w, max_h, evbuf);
|
||||
return artwork_get(evbuf, path, max_w, max_h);
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -910,15 +942,15 @@ artwork_get_own_image(char *in_path, int max_w, int max_h, char *out_path, struc
|
|||
* /foo/bar/cover.{png,jpg}, /foo/bar/artwork.{png,jpg} and also
|
||||
* /foo/bar/bar.{png,jpg} (so called parentdir artwork)
|
||||
*
|
||||
* @param evbuf the event buffer that will contain the (scaled) image
|
||||
* @param dir the directory to search
|
||||
* @param max_w maximum image width
|
||||
* @param max_h maximum image height
|
||||
* @param out_path path to artwork, input must be either NULL or char[PATH_MAX]
|
||||
* @param evbuf the event buffer that will contain the (scaled) image
|
||||
* @return ART_FMT_* on success, 0 on nothing found, -1 on error
|
||||
*/
|
||||
static int
|
||||
artwork_get_dir_image(char *dir, int max_w, int max_h, char *out_path, struct evbuffer *evbuf)
|
||||
artwork_get_dir_image(struct evbuffer *evbuf, char *dir, int max_w, int max_h, char *out_path)
|
||||
{
|
||||
char path[PATH_MAX];
|
||||
char parentdir[PATH_MAX];
|
||||
|
@ -1018,23 +1050,23 @@ artwork_get_dir_image(char *dir, int max_w, int max_h, char *out_path, struct ev
|
|||
if (out_path)
|
||||
strcpy(out_path, path);
|
||||
|
||||
return artwork_get(path, max_w, max_h, evbuf);
|
||||
return artwork_get(evbuf, path, max_w, max_h);
|
||||
}
|
||||
|
||||
/*
|
||||
* Given an artwork type (eg embedded, Spotify, own) this function will direct
|
||||
* to the appropriate handler
|
||||
*
|
||||
* @param evbuf the event buffer that will contain the (scaled) image
|
||||
* @param in_path path to the item we are getting artwork for
|
||||
* @param artwork type of the artwork
|
||||
* @param max_w maximum image width
|
||||
* @param max_h maximum image height
|
||||
* @param out_path path to artwork, input must be either NULL or char[PATH_MAX]
|
||||
* @param evbuf the event buffer that will contain the (scaled) image
|
||||
* @return ART_FMT_* on success, 0 on nothing found, -1 on error
|
||||
*/
|
||||
static int
|
||||
artwork_get_item_path(char *in_path, int artwork, int max_w, int max_h, char *out_path, struct evbuffer *evbuf)
|
||||
artwork_get_item_path(struct evbuffer *evbuf, char *in_path, int artwork, int max_w, int max_h, char *out_path)
|
||||
{
|
||||
int ret;
|
||||
|
||||
|
@ -1049,7 +1081,7 @@ artwork_get_item_path(char *in_path, int artwork, int max_w, int max_h, char *ou
|
|||
case ARTWORK_UNKNOWN:
|
||||
case ARTWORK_OWN:
|
||||
if (cfg_getbool(cfg_getsec(cfg, "library"), "artwork_individual"))
|
||||
ret = artwork_get_own_image(in_path, max_w, max_h, out_path, evbuf);
|
||||
ret = artwork_get_own_image(evbuf, in_path, max_w, max_h, out_path);
|
||||
break;
|
||||
#ifdef HAVE_SPOTIFY_H
|
||||
case ARTWORK_SPOTIFY:
|
||||
|
@ -1059,9 +1091,12 @@ artwork_get_item_path(char *in_path, int artwork, int max_w, int max_h, char *ou
|
|||
#endif
|
||||
#if LIBAVFORMAT_VERSION_MAJOR >= 55 || (LIBAVFORMAT_VERSION_MAJOR == 54 && LIBAVFORMAT_VERSION_MINOR >= 6)
|
||||
case ARTWORK_EMBEDDED:
|
||||
ret = artwork_get_embedded_image(in_path, max_w, max_h, evbuf);
|
||||
ret = artwork_get_embedded_image(evbuf, in_path, max_w, max_h);
|
||||
break;
|
||||
#endif
|
||||
case ARTWORK_HTTP:
|
||||
ret = artwork_get_player_image(evbuf, in_path);
|
||||
break;
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
@ -1070,14 +1105,14 @@ artwork_get_item_path(char *in_path, int artwork, int max_w, int max_h, char *ou
|
|||
/*
|
||||
* Get the artwork for the given media file and the given maxiumum width/height
|
||||
|
||||
* @param evbuf the event buffer that will contain the (scaled) image
|
||||
* @param mfi the media file structure for the file whose image should be returned
|
||||
* @param max_w maximum image width
|
||||
* @param max_h maximum image height
|
||||
* @param evbuf the event buffer that will contain the (scaled) image
|
||||
* @return ART_FMT_* on success, 0 on nothing found, -1 on error
|
||||
*/
|
||||
static int
|
||||
artwork_get_item_mfi(struct media_file_info *mfi, int max_w, int max_h, struct evbuffer *evbuf)
|
||||
artwork_get_item_mfi(struct evbuffer *evbuf, struct media_file_info *mfi, int max_w, int max_h)
|
||||
{
|
||||
char path[PATH_MAX];
|
||||
int cached;
|
||||
|
@ -1095,7 +1130,7 @@ artwork_get_item_mfi(struct media_file_info *mfi, int max_w, int max_h, struct e
|
|||
|
||||
if (mfi->data_kind == 0)
|
||||
{
|
||||
format = artwork_get_item_path(mfi->path, mfi->artwork, max_w, max_h, path, evbuf);
|
||||
format = artwork_get_item_path(evbuf, mfi->path, mfi->artwork, max_w, max_h, path);
|
||||
|
||||
if (format > 0)
|
||||
cache_artwork_add(CACHE_ARTWORK_INDIVIDUAL, mfi->id, max_w, max_h, format, path, evbuf);
|
||||
|
@ -1112,14 +1147,14 @@ artwork_get_item_mfi(struct media_file_info *mfi, int max_w, int max_h, struct e
|
|||
* The function first checks if there is a cache entry, if not it will first look for directory artwork files.
|
||||
* If no directory artwork files are found, it looks for individual artwork (embedded images or images from spotify).
|
||||
*
|
||||
* @param evbuf the event buffer that will contain the (scaled) image
|
||||
* @param persistentid persistent songalbumid or songartistid
|
||||
* @param max_w maximum image width
|
||||
* @param max_h maximum image height
|
||||
* @param evbuf the event buffer that will contain the (scaled) image
|
||||
* @return ART_FMT_* on success, 0 on nothing found, -1 on error
|
||||
*/
|
||||
static int
|
||||
artwork_get_group_persistentid(int64_t persistentid, int max_w, int max_h, struct evbuffer *evbuf)
|
||||
artwork_get_group_persistentid(struct evbuffer *evbuf, int64_t persistentid, int max_w, int max_h)
|
||||
{
|
||||
struct query_params qp;
|
||||
struct db_media_file_info dbmfi;
|
||||
|
@ -1168,7 +1203,7 @@ artwork_get_group_persistentid(int64_t persistentid, int max_w, int max_h, struc
|
|||
if (access(dir, F_OK) < 0)
|
||||
continue;
|
||||
|
||||
format = artwork_get_dir_image(dir, max_w, max_h, path, evbuf);
|
||||
format = artwork_get_dir_image(evbuf, dir, max_w, max_h, path);
|
||||
|
||||
if (format > 0)
|
||||
break;
|
||||
|
@ -1210,7 +1245,7 @@ artwork_get_group_persistentid(int64_t persistentid, int max_w, int max_h, struc
|
|||
if ((safe_atoi32(dbmfi.artwork, &artwork) != 0) && (safe_atou32(dbmfi.data_kind, &data_kind) != 0))
|
||||
continue;
|
||||
|
||||
format = artwork_get_item_path(dbmfi.path, artwork, max_w, max_h, path, evbuf);
|
||||
format = artwork_get_item_path(evbuf, dbmfi.path, artwork, max_w, max_h, path);
|
||||
|
||||
if (artwork == ARTWORK_SPOTIFY)
|
||||
got_spotifyitem = 1;
|
||||
|
@ -1230,7 +1265,8 @@ artwork_get_group_persistentid(int64_t persistentid, int max_w, int max_h, struc
|
|||
/* Found artwork, cache it and return */
|
||||
if (format > 0)
|
||||
{
|
||||
cache_artwork_add(CACHE_ARTWORK_GROUP, persistentid, max_w, max_h, format, path, evbuf);
|
||||
if (artwork != ARTWORK_HTTP)
|
||||
cache_artwork_add(CACHE_ARTWORK_GROUP, persistentid, max_w, max_h, format, path, evbuf);
|
||||
return format;
|
||||
}
|
||||
else if (format < 0)
|
||||
|
@ -1242,7 +1278,7 @@ artwork_get_group_persistentid(int64_t persistentid, int max_w, int max_h, struc
|
|||
DPRINTF(E_DBG, L_ART, "No artwork found for group %" PRIi64 "\n", persistentid);
|
||||
|
||||
/* Add cache entry for no artwork available */
|
||||
if (!got_spotifyitem)
|
||||
if ((artwork != ARTWORK_HTTP) && (!got_spotifyitem))
|
||||
cache_artwork_add(CACHE_ARTWORK_GROUP, persistentid, max_w, max_h, 0, "", evbuf);
|
||||
|
||||
return 0;
|
||||
|
@ -1251,14 +1287,14 @@ artwork_get_group_persistentid(int64_t persistentid, int max_w, int max_h, struc
|
|||
/*
|
||||
* Get the artwork image for the given item id and the given maximum width/height
|
||||
*
|
||||
* @param evbuf the event buffer that will contain the (scaled) image
|
||||
* @param id the mfi item id
|
||||
* @param max_w maximum image width
|
||||
* @param max_h maximum image height
|
||||
* @param evbuf the event buffer that will contain the (scaled) image
|
||||
* @return ART_FMT_* on success, -1 on error or no artwork found
|
||||
*/
|
||||
int
|
||||
artwork_get_item(int id, int max_w, int max_h, struct evbuffer *evbuf)
|
||||
artwork_get_item(struct evbuffer *evbuf, int id, int max_w, int max_h)
|
||||
{
|
||||
struct media_file_info *mfi;
|
||||
int format;
|
||||
|
@ -1274,11 +1310,11 @@ artwork_get_item(int id, int max_w, int max_h, struct evbuffer *evbuf)
|
|||
|
||||
format = 0;
|
||||
if (cfg_getbool(cfg_getsec(cfg, "library"), "artwork_individual"))
|
||||
format = artwork_get_item_mfi(mfi, max_w, max_h, evbuf);
|
||||
format = artwork_get_item_mfi(evbuf, mfi, max_w, max_h);
|
||||
|
||||
/* No individual artwork or individual artwork disabled, try group artwork */
|
||||
if (format <= 0)
|
||||
format = artwork_get_group_persistentid(mfi->songalbumid, max_w, max_h, evbuf);
|
||||
format = artwork_get_group_persistentid(evbuf, mfi->songalbumid, max_w, max_h);
|
||||
|
||||
free_mfi(mfi, 0);
|
||||
|
||||
|
@ -1294,14 +1330,14 @@ artwork_get_item(int id, int max_w, int max_h, struct evbuffer *evbuf)
|
|||
/*
|
||||
* Get the artwork image for the given group id and the given maximum width/height
|
||||
*
|
||||
* @param evbuf the event buffer that will contain the (scaled) image
|
||||
* @param id the group id (not the persistent id)
|
||||
* @param max_w maximum image width
|
||||
* @param max_h maximum image height
|
||||
* @param evbuf the event buffer that will contain the (scaled) image
|
||||
* @return ART_FMT_* on success, -1 on error or no artwork found
|
||||
*/
|
||||
int
|
||||
artwork_get_group(int id, int max_w, int max_h, struct evbuffer *evbuf)
|
||||
artwork_get_group(struct evbuffer *evbuf, int id, int max_w, int max_h)
|
||||
{
|
||||
int64_t persistentid;
|
||||
int format;
|
||||
|
@ -1316,7 +1352,7 @@ artwork_get_group(int id, int max_w, int max_h, struct evbuffer *evbuf)
|
|||
}
|
||||
|
||||
/* Load artwork image for the persistent id */
|
||||
format = artwork_get_group_persistentid(persistentid, max_w, max_h, evbuf);
|
||||
format = artwork_get_group_persistentid(evbuf, persistentid, max_w, max_h);
|
||||
if (format <= 0)
|
||||
{
|
||||
DPRINTF(E_DBG, L_ART, "No artwork found for group %d\n", id);
|
||||
|
|
|
@ -13,11 +13,11 @@
|
|||
|
||||
/* Get artwork for individual track */
|
||||
int
|
||||
artwork_get_item(int id, int max_w, int max_h, struct evbuffer *evbuf);
|
||||
artwork_get_item(struct evbuffer *evbuf, int id, int max_w, int max_h);
|
||||
|
||||
/* Get artwork for album or artist */
|
||||
int
|
||||
artwork_get_group(int id, int max_w, int max_h, struct evbuffer *evbuf);
|
||||
artwork_get_group(struct evbuffer *evbuf, int id, int max_w, int max_h);
|
||||
|
||||
/* Checks if the file is an artwork file */
|
||||
int
|
||||
|
|
|
@ -69,12 +69,14 @@ static cfg_opt_t sec_library[] =
|
|||
CFG_STR_LIST("audiobooks", NULL, CFGF_NONE),
|
||||
CFG_STR_LIST("compilations", NULL, CFGF_NONE),
|
||||
CFG_STR("compilation_artist", NULL, CFGF_NONE),
|
||||
CFG_BOOL("radio_playlists", cfg_false, CFGF_NONE),
|
||||
CFG_STR("name_library", "Library", CFGF_NONE),
|
||||
CFG_STR("name_music", "Music", CFGF_NONE),
|
||||
CFG_STR("name_movies", "Movies", CFGF_NONE),
|
||||
CFG_STR("name_tvshows", "TV Shows", CFGF_NONE),
|
||||
CFG_STR("name_podcasts", "Podcasts", CFGF_NONE),
|
||||
CFG_STR("name_audiobooks", "Audiobooks", CFGF_NONE),
|
||||
CFG_STR("name_radio", "Radio", CFGF_NONE),
|
||||
CFG_STR_LIST("artwork_basenames", "{artwork,cover,Folder}", CFGF_NONE),
|
||||
CFG_BOOL("artwork_individual", cfg_false, CFGF_NONE),
|
||||
CFG_STR_LIST("filetypes_ignore", "{.db,.ini,.db-journal,.pdf}", CFGF_NONE),
|
||||
|
@ -113,6 +115,7 @@ static cfg_opt_t sec_spotify[] =
|
|||
CFG_STR("settings_dir", STATEDIR "/cache/" PACKAGE "/libspotify", CFGF_NONE),
|
||||
CFG_STR("cache_dir", "/tmp", CFGF_NONE),
|
||||
CFG_INT("bitrate", 0, CFGF_NONE),
|
||||
CFG_BOOL("base_playlist_disable", cfg_false, CFGF_NONE),
|
||||
CFG_BOOL("artist_override", cfg_false, CFGF_NONE),
|
||||
CFG_BOOL("starred_artist_override", cfg_false, CFGF_NONE),
|
||||
CFG_BOOL("album_override", cfg_false, CFGF_NONE),
|
||||
|
|
274
src/db.c
274
src/db.c
|
@ -154,6 +154,7 @@ static const struct col_type_map pli_cols_map[] =
|
|||
{ pli_offsetof(index), DB_TYPE_INT },
|
||||
{ pli_offsetof(special_id), DB_TYPE_INT },
|
||||
{ pli_offsetof(virtual_path), DB_TYPE_STRING },
|
||||
{ pli_offsetof(parent_id), DB_TYPE_INT },
|
||||
|
||||
/* items is computed on the fly */
|
||||
};
|
||||
|
@ -240,6 +241,7 @@ static const ssize_t dbpli_cols_map[] =
|
|||
dbpli_offsetof(index),
|
||||
dbpli_offsetof(special_id),
|
||||
dbpli_offsetof(virtual_path),
|
||||
dbpli_offsetof(parent_id),
|
||||
|
||||
/* items is computed on the fly */
|
||||
};
|
||||
|
@ -279,7 +281,7 @@ static const char *sort_clause[] =
|
|||
"ORDER BY f.title_sort ASC",
|
||||
"ORDER BY f.album_sort ASC, f.disc ASC, f.track ASC",
|
||||
"ORDER BY f.album_artist_sort ASC",
|
||||
"ORDER BY f.type DESC, f.special_id ASC, f.title ASC",
|
||||
"ORDER BY f.type DESC, f.parent_id ASC, f.special_id ASC, f.title ASC",
|
||||
"ORDER BY f.year ASC",
|
||||
};
|
||||
|
||||
|
@ -289,7 +291,7 @@ static __thread sqlite3 *hdl;
|
|||
|
||||
/* Forward */
|
||||
static int
|
||||
db_pl_count_items(int id);
|
||||
db_pl_count_items(int id, int streams_only);
|
||||
|
||||
static int
|
||||
db_smartpl_count_items(const char *smartpl_query);
|
||||
|
@ -616,72 +618,6 @@ db_exec(const char *query, char **errmsg)
|
|||
}
|
||||
|
||||
|
||||
// This will run in its own shortlived, detached thread, created by db_exec_nonblock
|
||||
static void *
|
||||
db_exec_thread(void *arg)
|
||||
{
|
||||
char *query = arg;
|
||||
char *errmsg;
|
||||
time_t start, end;
|
||||
int ret;
|
||||
|
||||
// When switching tracks we update playcount and select the next track's
|
||||
// metadata. We want the update to run after the selects so it won't lock
|
||||
// the database.
|
||||
sleep(3);
|
||||
|
||||
ret = db_perthread_init();
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_DB, "Error in db_exec_thread: Could not init thread\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
DPRINTF(E_DBG, L_DB, "Running delayed query '%s'\n", query);
|
||||
|
||||
time(&start);
|
||||
ret = db_exec(query, &errmsg);
|
||||
if (ret != SQLITE_OK)
|
||||
DPRINTF(E_LOG, L_DB, "Error running query '%s': %s\n", query, errmsg);
|
||||
|
||||
time(&end);
|
||||
if (end - start > 1)
|
||||
DPRINTF(E_LOG, L_DB, "Warning: Slow query detected '%s' - database performance problems?\n", query);
|
||||
|
||||
sqlite3_free(errmsg);
|
||||
sqlite3_free(query);
|
||||
|
||||
db_perthread_deinit();
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Creates a one-off thread to run a delayed, fire-and-forget, non-blocking query
|
||||
static void
|
||||
db_exec_nonblock(char *query)
|
||||
{
|
||||
pthread_t tid;
|
||||
pthread_attr_t attr;
|
||||
int ret;
|
||||
|
||||
ret = pthread_attr_init(&attr);
|
||||
if (ret != 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_DB, "Error in db_exec_nonblock: Could not init attributes\n");
|
||||
return;
|
||||
}
|
||||
|
||||
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
|
||||
ret = pthread_create(&tid, &attr, db_exec_thread, query);
|
||||
if (ret != 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_DB, "Error in db_exec_nonblock: Could not create thread\n");
|
||||
}
|
||||
|
||||
pthread_attr_destroy(&attr);
|
||||
}
|
||||
|
||||
|
||||
/* Maintenance and DB hygiene */
|
||||
static void
|
||||
db_analyze(void)
|
||||
|
@ -705,7 +641,7 @@ db_analyze(void)
|
|||
static void
|
||||
db_set_cfg_names(void)
|
||||
{
|
||||
#define Q_TMPL "UPDATE playlists SET title = '%q' WHERE type = 1 AND special_id = %d;"
|
||||
#define Q_TMPL "UPDATE playlists SET title = '%q' WHERE type = %d AND special_id = %d;"
|
||||
char *cfg_item[6] = { "name_library", "name_music", "name_movies", "name_tvshows", "name_podcasts", "name_audiobooks" };
|
||||
char special_id[6] = { 0, 6, 4, 5, 1, 7 };
|
||||
cfg_t *lib;
|
||||
|
@ -727,7 +663,7 @@ db_set_cfg_names(void)
|
|||
continue;
|
||||
}
|
||||
|
||||
query = sqlite3_mprintf(Q_TMPL, title, special_id[i]);
|
||||
query = sqlite3_mprintf(Q_TMPL, title, PL_SMART, special_id[i]);
|
||||
if (!query)
|
||||
{
|
||||
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
|
||||
|
@ -769,9 +705,9 @@ db_purge_cruft(time_t ref)
|
|||
char *queries[3] = { NULL, NULL, NULL };
|
||||
char *queries_tmpl[3] =
|
||||
{
|
||||
"DELETE FROM playlistitems WHERE playlistid IN (SELECT id FROM playlists p WHERE p.type <> 1 AND p.db_timestamp < %" PRIi64 ");",
|
||||
"DELETE FROM playlists WHERE type <> 1 AND db_timestamp < %" PRIi64 ";",
|
||||
"DELETE FROM files WHERE db_timestamp < %" PRIi64 ";"
|
||||
"DELETE FROM playlistitems WHERE playlistid IN (SELECT id FROM playlists p WHERE p.type <> %d AND p.db_timestamp < %" PRIi64 ");",
|
||||
"DELETE FROM playlists WHERE type <> %d AND db_timestamp < %" PRIi64 ";",
|
||||
"DELETE FROM files WHERE -1 <> %d AND db_timestamp < %" PRIi64 ";"
|
||||
};
|
||||
|
||||
if (sizeof(queries) != sizeof(queries_tmpl))
|
||||
|
@ -782,7 +718,7 @@ db_purge_cruft(time_t ref)
|
|||
|
||||
for (i = 0; i < (sizeof(queries_tmpl) / sizeof(queries_tmpl[0])); i++)
|
||||
{
|
||||
queries[i] = sqlite3_mprintf(queries_tmpl[i], (int64_t)ref);
|
||||
queries[i] = sqlite3_mprintf(queries_tmpl[i], PL_SMART, (int64_t)ref);
|
||||
if (!queries[i])
|
||||
{
|
||||
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
|
||||
|
@ -816,15 +752,16 @@ db_purge_cruft(time_t ref)
|
|||
void
|
||||
db_purge_all(void)
|
||||
{
|
||||
#define Q_TMPL "DELETE FROM playlists WHERE type <> %d;"
|
||||
char *queries[5] =
|
||||
{
|
||||
"DELETE FROM inotify;",
|
||||
"DELETE FROM playlistitems;",
|
||||
"DELETE FROM playlists WHERE type <> 1;",
|
||||
"DELETE FROM files;",
|
||||
"DELETE FROM groups;",
|
||||
};
|
||||
char *errmsg;
|
||||
char *query;
|
||||
int i;
|
||||
int ret;
|
||||
|
||||
|
@ -842,6 +779,28 @@ db_purge_all(void)
|
|||
else
|
||||
DPRINTF(E_DBG, L_DB, "Purged %d rows\n", sqlite3_changes(hdl));
|
||||
}
|
||||
|
||||
query = sqlite3_mprintf(Q_TMPL, PL_SMART);
|
||||
if (!query)
|
||||
{
|
||||
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
|
||||
return;
|
||||
}
|
||||
|
||||
DPRINTF(E_DBG, L_DB, "Running purge query '%s'\n", query);
|
||||
|
||||
ret = db_exec(query, &errmsg);
|
||||
if (ret != SQLITE_OK)
|
||||
{
|
||||
DPRINTF(E_LOG, L_DB, "Purge query '%s' error: %s\n", query, errmsg);
|
||||
|
||||
sqlite3_free(errmsg);
|
||||
}
|
||||
else
|
||||
DPRINTF(E_DBG, L_DB, "Purged %d rows\n", sqlite3_changes(hdl));
|
||||
|
||||
sqlite3_free(query);
|
||||
#undef Q_TMPL
|
||||
}
|
||||
|
||||
static int
|
||||
|
@ -1187,6 +1146,7 @@ db_build_query_plitems(struct query_params *qp, char **q)
|
|||
break;
|
||||
|
||||
case PL_PLAIN:
|
||||
case PL_FOLDER:
|
||||
ret = db_build_query_plitems_plain(qp, q);
|
||||
break;
|
||||
|
||||
|
@ -1705,6 +1665,7 @@ db_query_fetch_pl(struct query_params *qp, struct db_playlist_info *dbpli)
|
|||
int id;
|
||||
int type;
|
||||
int nitems;
|
||||
int nstreams;
|
||||
int i;
|
||||
int ret;
|
||||
|
||||
|
@ -1755,12 +1716,15 @@ db_query_fetch_pl(struct query_params *qp, struct db_playlist_info *dbpli)
|
|||
switch (type)
|
||||
{
|
||||
case PL_PLAIN:
|
||||
case PL_FOLDER:
|
||||
id = sqlite3_column_int(qp->stmt, 0);
|
||||
nitems = db_pl_count_items(id);
|
||||
nitems = db_pl_count_items(id, 0);
|
||||
nstreams = db_pl_count_items(id, 1);
|
||||
break;
|
||||
|
||||
case PL_SMART:
|
||||
nitems = db_smartpl_count_items(dbpli->query);
|
||||
nstreams = 0;
|
||||
break;
|
||||
|
||||
default:
|
||||
|
@ -1768,13 +1732,21 @@ db_query_fetch_pl(struct query_params *qp, struct db_playlist_info *dbpli)
|
|||
return -1;
|
||||
}
|
||||
|
||||
dbpli->items = qp->buf;
|
||||
ret = snprintf(qp->buf, sizeof(qp->buf), "%d", nitems);
|
||||
if ((ret < 0) || (ret >= sizeof(qp->buf)))
|
||||
dbpli->items = qp->buf1;
|
||||
ret = snprintf(qp->buf1, sizeof(qp->buf1), "%d", nitems);
|
||||
if ((ret < 0) || (ret >= sizeof(qp->buf1)))
|
||||
{
|
||||
DPRINTF(E_LOG, L_DB, "Could not convert items, buffer too small\n");
|
||||
DPRINTF(E_LOG, L_DB, "Could not convert item count, buffer too small\n");
|
||||
|
||||
strcpy(qp->buf, "0");
|
||||
strcpy(qp->buf1, "0");
|
||||
}
|
||||
dbpli->streams = qp->buf2;
|
||||
ret = snprintf(qp->buf2, sizeof(qp->buf2), "%d", nstreams);
|
||||
if ((ret < 0) || (ret >= sizeof(qp->buf2)))
|
||||
{
|
||||
DPRINTF(E_LOG, L_DB, "Could not convert stream count, buffer too small\n");
|
||||
|
||||
strcpy(qp->buf2, "0");
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
@ -2083,8 +2055,7 @@ db_file_inc_playcount(int id)
|
|||
return;
|
||||
}
|
||||
|
||||
// Run the query non-blocking so we don't block playback if the update is slow
|
||||
db_exec_nonblock(query);
|
||||
db_query_run(query, 1, 0);
|
||||
#undef Q_TMPL
|
||||
}
|
||||
|
||||
|
@ -2730,6 +2701,27 @@ db_file_update(struct media_file_info *mfi)
|
|||
#undef Q_TMPL
|
||||
}
|
||||
|
||||
void
|
||||
db_file_update_icy(int id, char *artist, char *album)
|
||||
{
|
||||
#define Q_TMPL "UPDATE files SET artist = TRIM(%Q), album = TRIM(%Q) WHERE id = %d;"
|
||||
char *query;
|
||||
|
||||
if (id == 0)
|
||||
return;
|
||||
|
||||
query = sqlite3_mprintf(Q_TMPL, artist, album, id);
|
||||
if (!query)
|
||||
{
|
||||
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
db_query_run(query, 1, 0);
|
||||
#undef Q_TMPL
|
||||
}
|
||||
|
||||
void
|
||||
db_file_delete_bypath(char *path)
|
||||
{
|
||||
|
@ -2800,14 +2792,19 @@ db_pl_get_count(void)
|
|||
}
|
||||
|
||||
static int
|
||||
db_pl_count_items(int id)
|
||||
db_pl_count_items(int id, int streams_only)
|
||||
{
|
||||
#define Q_TMPL "SELECT COUNT(*) FROM playlistitems pi JOIN files f" \
|
||||
" ON pi.filepath = f.path WHERE f.disabled = 0 AND pi.playlistid = %d;"
|
||||
#define Q_TMPL_STREAMS "SELECT COUNT(*) FROM playlistitems pi JOIN files f" \
|
||||
" ON pi.filepath = f.path WHERE f.disabled = 0 AND f.data_kind = 1 AND pi.playlistid = %d;"
|
||||
char *query;
|
||||
int ret;
|
||||
|
||||
query = sqlite3_mprintf(Q_TMPL, id);
|
||||
if (!streams_only)
|
||||
query = sqlite3_mprintf(Q_TMPL, id);
|
||||
else
|
||||
query = sqlite3_mprintf(Q_TMPL_STREAMS, id);
|
||||
|
||||
if (!query)
|
||||
{
|
||||
|
@ -2821,6 +2818,7 @@ db_pl_count_items(int id)
|
|||
|
||||
return ret;
|
||||
|
||||
#undef Q_TMPL_STREAMS
|
||||
#undef Q_TMPL
|
||||
}
|
||||
|
||||
|
@ -3038,7 +3036,9 @@ db_pl_fetch_byquery(char *query)
|
|||
switch (pli->type)
|
||||
{
|
||||
case PL_PLAIN:
|
||||
pli->items = db_pl_count_items(pli->id);
|
||||
case PL_FOLDER:
|
||||
pli->items = db_pl_count_items(pli->id, 0);
|
||||
pli->streams = db_pl_count_items(pli->id, 1);
|
||||
break;
|
||||
|
||||
case PL_SMART:
|
||||
|
@ -3152,17 +3152,17 @@ db_pl_fetch_bytitlepath(char *title, char *path)
|
|||
}
|
||||
|
||||
int
|
||||
db_pl_add(char *title, char *path, char *virtual_path, int *id)
|
||||
db_pl_add(struct playlist_info *pli, int *id)
|
||||
{
|
||||
#define QDUP_TMPL "SELECT COUNT(*) FROM playlists p WHERE p.title = '%q' AND p.path = '%q';"
|
||||
#define QADD_TMPL "INSERT INTO playlists (title, type, query, db_timestamp, disabled, path, idx, special_id, virtual_path)" \
|
||||
" VALUES ('%q', 0, NULL, %" PRIi64 ", 0, '%q', 0, 0, '%q');"
|
||||
#define QDUP_TMPL "SELECT COUNT(*) FROM playlists p WHERE p.title = TRIM(%Q) AND p.path = '%q';"
|
||||
#define QADD_TMPL "INSERT INTO playlists (title, type, query, db_timestamp, disabled, path, idx, special_id, parent_id, virtual_path)" \
|
||||
" VALUES (TRIM(%Q), %d, NULL, %" PRIi64 ", %d, '%q', %d, %d, %d, '%q');"
|
||||
char *query;
|
||||
char *errmsg;
|
||||
int ret;
|
||||
|
||||
/* Check duplicates */
|
||||
query = sqlite3_mprintf(QDUP_TMPL, title, path);
|
||||
query = sqlite3_mprintf(QDUP_TMPL, pli->title, STR(pli->path));
|
||||
if (!query)
|
||||
{
|
||||
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
|
||||
|
@ -3175,12 +3175,15 @@ db_pl_add(char *title, char *path, char *virtual_path, int *id)
|
|||
|
||||
if (ret > 0)
|
||||
{
|
||||
DPRINTF(E_WARN, L_DB, "Duplicate playlist with title '%s' path '%s'\n", title, path);
|
||||
DPRINTF(E_WARN, L_DB, "Duplicate playlist with title '%s' path '%s'\n", pli->title, pli->path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Add */
|
||||
query = sqlite3_mprintf(QADD_TMPL, title, (int64_t)time(NULL), path, virtual_path);
|
||||
query = sqlite3_mprintf(QADD_TMPL,
|
||||
pli->title, pli->type, (int64_t)time(NULL), pli->disabled, STR(pli->path),
|
||||
pli->index, pli->special_id, pli->parent_id, pli->virtual_path);
|
||||
|
||||
if (!query)
|
||||
{
|
||||
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
|
||||
|
@ -3208,7 +3211,7 @@ db_pl_add(char *title, char *path, char *virtual_path, int *id)
|
|||
return -1;
|
||||
}
|
||||
|
||||
DPRINTF(E_DBG, L_DB, "Added playlist %s (path %s) with id %d\n", title, path, *id);
|
||||
DPRINTF(E_DBG, L_DB, "Added playlist %s (path %s) with id %d\n", pli->title, pli->path, *id);
|
||||
|
||||
return 0;
|
||||
|
||||
|
@ -3241,13 +3244,17 @@ db_pl_add_item_byid(int plid, int fileid)
|
|||
}
|
||||
|
||||
int
|
||||
db_pl_update(char *title, char *path, char *virtual_path, int id)
|
||||
db_pl_update(struct playlist_info *pli)
|
||||
{
|
||||
#define Q_TMPL "UPDATE playlists SET title = '%q', db_timestamp = %" PRIi64 ", disabled = 0, path = '%q', virtual_path = '%q' WHERE id = %d;"
|
||||
#define Q_TMPL "UPDATE playlists SET title = TRIM(%Q), type = %d, db_timestamp = %" PRIi64 ", disabled = %d, path = '%q', " \
|
||||
" idx = %d, special_id = %d, parent_id = %d, virtual_path = '%q' " \
|
||||
" WHERE id = %d;"
|
||||
char *query;
|
||||
int ret;
|
||||
|
||||
query = sqlite3_mprintf(Q_TMPL, title, (int64_t)time(NULL), path, virtual_path, id);
|
||||
query = sqlite3_mprintf(Q_TMPL,
|
||||
pli->title, pli->type, (int64_t)time(NULL), pli->disabled, STR(pli->path),
|
||||
pli->index, pli->special_id, pli->parent_id, pli->virtual_path, pli->id);
|
||||
|
||||
ret = db_query_run(query, 1, 0);
|
||||
|
||||
|
@ -4532,7 +4539,8 @@ db_perthread_deinit(void)
|
|||
" path VARCHAR(4096)," \
|
||||
" idx INTEGER NOT NULL," \
|
||||
" special_id INTEGER DEFAULT 0," \
|
||||
" virtual_path VARCHAR(4096)" \
|
||||
" virtual_path VARCHAR(4096)," \
|
||||
" parent_id INTEGER DEFAULT 0" \
|
||||
");"
|
||||
|
||||
#define T_PLITEMS \
|
||||
|
@ -4599,27 +4607,27 @@ db_perthread_deinit(void)
|
|||
|
||||
#define Q_PL1 \
|
||||
"INSERT INTO playlists (id, title, type, query, db_timestamp, path, idx, special_id)" \
|
||||
" VALUES(1, 'Library', 1, '1 = 1', 0, '', 0, 0);"
|
||||
" VALUES(1, 'Library', 2, '1 = 1', 0, '', 0, 0);"
|
||||
|
||||
#define Q_PL2 \
|
||||
"INSERT INTO playlists (id, title, type, query, db_timestamp, path, idx, special_id)" \
|
||||
" VALUES(2, 'Music', 1, 'f.media_kind = 1', 0, '', 0, 6);"
|
||||
" VALUES(2, 'Music', 2, 'f.media_kind = 1', 0, '', 0, 6);"
|
||||
|
||||
#define Q_PL3 \
|
||||
"INSERT INTO playlists (id, title, type, query, db_timestamp, path, idx, special_id)" \
|
||||
" VALUES(3, 'Movies', 1, 'f.media_kind = 2', 0, '', 0, 4);"
|
||||
" VALUES(3, 'Movies', 2, 'f.media_kind = 2', 0, '', 0, 4);"
|
||||
|
||||
#define Q_PL4 \
|
||||
"INSERT INTO playlists (id, title, type, query, db_timestamp, path, idx, special_id)" \
|
||||
" VALUES(4, 'TV Shows', 1, 'f.media_kind = 64', 0, '', 0, 5);"
|
||||
" VALUES(4, 'TV Shows', 2, 'f.media_kind = 64', 0, '', 0, 5);"
|
||||
|
||||
#define Q_PL5 \
|
||||
"INSERT INTO playlists (id, title, type, query, db_timestamp, path, idx, special_id)" \
|
||||
" VALUES(5, 'Podcasts', 1, 'f.media_kind = 4', 0, '', 0, 1);"
|
||||
" VALUES(5, 'Podcasts', 2, 'f.media_kind = 4', 0, '', 0, 1);"
|
||||
|
||||
#define Q_PL6 \
|
||||
"INSERT INTO playlists (id, title, type, query, db_timestamp, path, idx, special_id)" \
|
||||
" VALUES(6, 'Audiobooks', 1, 'f.media_kind = 8', 0, '', 0, 7);"
|
||||
" VALUES(6, 'Audiobooks', 2, 'f.media_kind = 8', 0, '', 0, 7);"
|
||||
|
||||
/* These are the remaining automatically-created iTunes playlists, but
|
||||
* their query is unknown
|
||||
|
@ -4627,13 +4635,10 @@ db_perthread_deinit(void)
|
|||
" VALUES(8, 'Purchased', 0, 'media_kind = 1024', 0, '', 0, 8);"
|
||||
*/
|
||||
|
||||
#define SCHEMA_VERSION_MAJOR 16
|
||||
#define SCHEMA_VERSION_MAJOR 17
|
||||
#define SCHEMA_VERSION_MINOR 00
|
||||
// Q_SCVER should be deprecated/removed at v16
|
||||
#define Q_SCVER \
|
||||
"INSERT INTO admin (key, value) VALUES ('schema_version', '16');"
|
||||
#define Q_SCVER_MAJOR \
|
||||
"INSERT INTO admin (key, value) VALUES ('schema_version_major', '16');"
|
||||
"INSERT INTO admin (key, value) VALUES ('schema_version_major', '17');"
|
||||
#define Q_SCVER_MINOR \
|
||||
"INSERT INTO admin (key, value) VALUES ('schema_version_minor', '00');"
|
||||
|
||||
|
@ -4665,7 +4670,6 @@ static const struct db_init_query db_init_table_queries[] =
|
|||
{ Q_PL5, "create default smart playlist 'Podcasts'" },
|
||||
{ Q_PL6, "create default smart playlist 'Audiobooks'" },
|
||||
|
||||
{ Q_SCVER, "set schema version" },
|
||||
{ Q_SCVER_MAJOR, "set schema version major" },
|
||||
{ Q_SCVER_MINOR, "set schema version minor" },
|
||||
};
|
||||
|
@ -5708,8 +5712,8 @@ static const struct db_init_query db_upgrade_v1501_queries[] =
|
|||
#define U_V16_ALTER_TBL_PL_ADD_COL \
|
||||
"ALTER TABLE playlists ADD COLUMN virtual_path VARCHAR(4096) DEFAULT NULL;"
|
||||
|
||||
#define U_V16_SCVER \
|
||||
"UPDATE admin SET value = '16' WHERE key = 'schema_version';"
|
||||
#define D_V1600_SCVER \
|
||||
"DELETE FROM admin WHERE key = 'schema_version';"
|
||||
#define U_V1600_SCVER_MAJOR \
|
||||
"UPDATE admin SET value = '16' WHERE key = 'schema_version_major';"
|
||||
#define U_V1600_SCVER_MINOR \
|
||||
|
@ -5721,7 +5725,7 @@ static const struct db_init_query db_upgrade_v16_queries[] =
|
|||
{ U_V16_ALTER_TBL_PL_ADD_COL, "alter table playlists add column virtual_path" },
|
||||
{ U_V16_CREATE_VIEW_FILELIST, "create new view filelist" },
|
||||
|
||||
{ U_V16_SCVER, "set schema_version to 16" },
|
||||
{ D_V1600_SCVER, "delete schema_version" },
|
||||
{ U_V1600_SCVER_MAJOR, "set schema_version_major to 16" },
|
||||
{ U_V1600_SCVER_MINOR, "set schema_version_minor to 00" },
|
||||
};
|
||||
|
@ -5806,23 +5810,18 @@ db_upgrade_v16(void)
|
|||
path = (char *)sqlite3_column_text(stmt, 2);
|
||||
type = sqlite3_column_int(stmt, 3);
|
||||
|
||||
if (type == 0) /* Excludes default playlists */
|
||||
if (type == PL_PLAIN) /* Excludes default/Smart playlists and playlist folders */
|
||||
{
|
||||
if (strncmp(path, "spotify:", strlen("spotify:")) == 0)
|
||||
{
|
||||
snprintf(virtual_path, PATH_MAX, "/spotify:/%s", title);
|
||||
}
|
||||
else
|
||||
{
|
||||
snprintf(virtual_path, PATH_MAX, "/file:%s", path);
|
||||
}
|
||||
snprintf(virtual_path, PATH_MAX, "/spotify:/%s", title);
|
||||
else
|
||||
snprintf(virtual_path, PATH_MAX, "/file:%s", path);
|
||||
|
||||
uquery = sqlite3_mprintf("UPDATE playlists SET virtual_path = '%q' WHERE id = %d;", virtual_path, id);
|
||||
|
||||
ret = sqlite3_exec(hdl, uquery, NULL, NULL, &errmsg);
|
||||
if (ret != SQLITE_OK)
|
||||
{
|
||||
DPRINTF(E_LOG, L_DB, "Error updating playlists: %s\n", errmsg);
|
||||
}
|
||||
|
||||
sqlite3_free(uquery);
|
||||
sqlite3_free(errmsg);
|
||||
|
@ -5835,6 +5834,30 @@ db_upgrade_v16(void)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* Upgrade from schema v16.00 to v17.00 */
|
||||
/* Expand data model to allow for nested playlists and change default playlist
|
||||
* enumeration
|
||||
*/
|
||||
|
||||
#define U_V17_PL_PARENTID_ADD \
|
||||
"ALTER TABLE playlists ADD COLUMN parent_id INTEGER DEFAULT 0;"
|
||||
#define U_V17_PL_TYPE_CHANGE \
|
||||
"UPDATE playlists SET type = 2 WHERE type = 1;"
|
||||
|
||||
#define U_V17_SCVER_MAJOR \
|
||||
"UPDATE admin SET value = '17' WHERE key = 'schema_version_major';"
|
||||
#define U_V17_SCVER_MINOR \
|
||||
"UPDATE admin SET value = '00' WHERE key = 'schema_version_minor';"
|
||||
|
||||
static const struct db_init_query db_upgrade_v17_queries[] =
|
||||
{
|
||||
{ U_V17_PL_PARENTID_ADD,"expanding table playlists with parent_id column" },
|
||||
{ U_V17_PL_TYPE_CHANGE, "changing numbering of default playlists 1 -> 2" },
|
||||
|
||||
{ U_V17_SCVER_MAJOR, "set schema_version_major to 17" },
|
||||
{ U_V17_SCVER_MINOR, "set schema_version_minor to 00" },
|
||||
};
|
||||
|
||||
static int
|
||||
db_upgrade(int db_ver)
|
||||
{
|
||||
|
@ -5911,6 +5934,11 @@ db_upgrade(int db_ver)
|
|||
if (ret < 0)
|
||||
return -1;
|
||||
|
||||
/* FALLTHROUGH */
|
||||
|
||||
case 1600:
|
||||
ret = db_generic_upgrade(db_upgrade_v17_queries, sizeof(db_upgrade_v17_queries) / sizeof(db_upgrade_v17_queries[0]));
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
|
|
23
src/db.h
23
src/db.h
|
@ -50,6 +50,7 @@ enum query_type {
|
|||
#define ARTWORK_DIR 4
|
||||
#define ARTWORK_PARENTDIR 5
|
||||
#define ARTWORK_SPOTIFY 6
|
||||
#define ARTWORK_HTTP 7
|
||||
|
||||
enum filelistitem_type {
|
||||
F_PLAYLIST = 1,
|
||||
|
@ -74,7 +75,8 @@ struct query_params {
|
|||
|
||||
/* Private query context, keep out */
|
||||
sqlite3_stmt *stmt;
|
||||
char buf[32];
|
||||
char buf1[32];
|
||||
char buf2[32];
|
||||
};
|
||||
|
||||
struct pairing_info {
|
||||
|
@ -164,10 +166,12 @@ struct media_file_info {
|
|||
|
||||
#define mfi_offsetof(field) offsetof(struct media_file_info, field)
|
||||
|
||||
/* PL_SMART value must be in sync with type value in Q_PL* in db.c */
|
||||
enum pl_type {
|
||||
PL_PLAIN,
|
||||
PL_SMART,
|
||||
PL_MAX
|
||||
PL_PLAIN = 0,
|
||||
PL_FOLDER = 1,
|
||||
PL_SMART = 2,
|
||||
PL_MAX,
|
||||
};
|
||||
|
||||
struct playlist_info {
|
||||
|
@ -175,6 +179,7 @@ struct playlist_info {
|
|||
char *title; /* playlist name as displayed in iTunes (minm) */
|
||||
enum pl_type type; /* see PL_ types */
|
||||
uint32_t items; /* number of items (mimc) */
|
||||
uint32_t streams; /* number of internet streams */
|
||||
char *query; /* where clause if type 1 (MSPS) */
|
||||
uint32_t db_timestamp; /* time last updated */
|
||||
uint32_t disabled;
|
||||
|
@ -182,6 +187,7 @@ struct playlist_info {
|
|||
uint32_t index; /* index of playlist for paths with multiple playlists */
|
||||
uint32_t special_id; /* iTunes identifies certain 'special' playlists with special meaning */
|
||||
char *virtual_path; /* virtual path of underlying playlist */
|
||||
uint32_t parent_id; /* Id of parent playlist if the playlist is nested */
|
||||
};
|
||||
|
||||
#define pli_offsetof(field) offsetof(struct playlist_info, field)
|
||||
|
@ -191,6 +197,7 @@ struct db_playlist_info {
|
|||
char *title;
|
||||
char *type;
|
||||
char *items;
|
||||
char *streams;
|
||||
char *query;
|
||||
char *db_timestamp;
|
||||
char *disabled;
|
||||
|
@ -198,6 +205,7 @@ struct db_playlist_info {
|
|||
char *index;
|
||||
char *special_id;
|
||||
char *virtual_path;
|
||||
char *parent_id;
|
||||
};
|
||||
|
||||
#define dbpli_offsetof(field) offsetof(struct db_playlist_info, field)
|
||||
|
@ -433,6 +441,9 @@ db_file_add(struct media_file_info *mfi);
|
|||
int
|
||||
db_file_update(struct media_file_info *mfi);
|
||||
|
||||
void
|
||||
db_file_update_icy(int id, char *artist, char *album);
|
||||
|
||||
void
|
||||
db_file_delete_bypath(char *path);
|
||||
|
||||
|
@ -465,7 +476,7 @@ struct playlist_info *
|
|||
db_pl_fetch_bytitlepath(char *title, char *path);
|
||||
|
||||
int
|
||||
db_pl_add(char *title, char *path, char *virtual_path, int *id);
|
||||
db_pl_add(struct playlist_info *pli, int *id);
|
||||
|
||||
int
|
||||
db_pl_add_item_bypath(int plid, char *path);
|
||||
|
@ -477,7 +488,7 @@ void
|
|||
db_pl_clear_items(int id);
|
||||
|
||||
int
|
||||
db_pl_update(char *title, char *path, char *virtual_path, int id);
|
||||
db_pl_update(struct playlist_info *pli);
|
||||
|
||||
void
|
||||
db_pl_delete(int id);
|
||||
|
|
|
@ -693,11 +693,7 @@ filescanner_process_media(char *path, time_t mtime, off_t size, int type, struct
|
|||
else if (type & F_SCAN_TYPE_URL)
|
||||
{
|
||||
mfi->data_kind = 1; /* url/stream */
|
||||
#if LIBAVFORMAT_VERSION_MAJOR >= 56 || (LIBAVFORMAT_VERSION_MAJOR == 55 && LIBAVFORMAT_VERSION_MINOR >= 13)
|
||||
ret = scan_metadata_ffmpeg(path, mfi);
|
||||
#else
|
||||
ret = scan_metadata_icy(path, mfi);
|
||||
#endif
|
||||
}
|
||||
else if (type & F_SCAN_TYPE_SPOTIFY)
|
||||
{
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
#include "logger.h"
|
||||
#include "filescanner.h"
|
||||
#include "misc.h"
|
||||
#include "http.h"
|
||||
|
||||
|
||||
/* Legacy format-specific scanners */
|
||||
|
@ -315,88 +316,13 @@ extract_metadata(struct media_file_info *mfi, AVFormatContext *ctx, AVStream *au
|
|||
return mdcount;
|
||||
}
|
||||
|
||||
#if LIBAVFORMAT_VERSION_MAJOR >= 56 || (LIBAVFORMAT_VERSION_MAJOR == 55 && LIBAVFORMAT_VERSION_MINOR >= 13)
|
||||
/* Extracts ICY metadata (requires libav 10) */
|
||||
static void
|
||||
extract_metadata_icy(struct media_file_info *mfi, AVFormatContext *ctx)
|
||||
{
|
||||
uint8_t *icy_meta;
|
||||
char *icy_token;
|
||||
char *icy_str;
|
||||
char *ptr;
|
||||
|
||||
icy_meta = NULL;
|
||||
// TODO Also get icy_metadata_packet to show current track
|
||||
av_opt_get(ctx, "icy_metadata_headers", AV_OPT_SEARCH_CHILDREN, &icy_meta);
|
||||
|
||||
if (!icy_meta)
|
||||
return;
|
||||
|
||||
icy_str = strdup((char *)icy_meta);
|
||||
icy_token = strtok(icy_str, "\r\n");
|
||||
|
||||
while (icy_token != NULL)
|
||||
{
|
||||
ptr = strchr(icy_token, ':');
|
||||
if (!ptr || (strlen(ptr) < 4))
|
||||
{
|
||||
icy_token = strtok(NULL, "\r\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
ptr++;
|
||||
if (ptr[0] == ' ')
|
||||
ptr++;
|
||||
|
||||
if (strstr(icy_token, "icy-name"))
|
||||
{
|
||||
DPRINTF(E_DBG, L_SCAN, "Libav/ffmpeg found ICY metadata, name is '%s'\n", ptr);
|
||||
|
||||
if (mfi->title)
|
||||
free(mfi->title);
|
||||
if (mfi->artist)
|
||||
free(mfi->artist);
|
||||
if (mfi->album_artist)
|
||||
free(mfi->album_artist);
|
||||
|
||||
mfi->title = strdup(ptr);
|
||||
mfi->artist = strdup(ptr);
|
||||
mfi->album_artist = strdup(ptr);
|
||||
}
|
||||
|
||||
if (strstr(icy_token, "icy-description"))
|
||||
{
|
||||
DPRINTF(E_DBG, L_SCAN, "Libav/ffmpeg found ICY metadata, description is '%s'\n", ptr);
|
||||
|
||||
if (mfi->album)
|
||||
free(mfi->album);
|
||||
|
||||
mfi->album = strdup(ptr);
|
||||
}
|
||||
|
||||
if (strstr(icy_token, "icy-genre"))
|
||||
{
|
||||
DPRINTF(E_DBG, L_SCAN, "Libav/ffmpeg found ICY metadata, genre is '%s'\n", ptr);
|
||||
|
||||
if (mfi->genre)
|
||||
free(mfi->genre);
|
||||
|
||||
mfi->genre = strdup(ptr);
|
||||
}
|
||||
|
||||
icy_token = strtok(NULL, "\r\n");
|
||||
}
|
||||
av_free(icy_meta);
|
||||
free(icy_str);
|
||||
}
|
||||
#endif
|
||||
|
||||
int
|
||||
scan_metadata_ffmpeg(char *file, struct media_file_info *mfi)
|
||||
{
|
||||
AVFormatContext *ctx;
|
||||
AVDictionary *options;
|
||||
const struct metadata_map *extra_md_map;
|
||||
struct http_icy_metadata *icy_metadata;
|
||||
#if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
|
||||
enum AVCodecID codec_id;
|
||||
enum AVCodecID video_codec_id;
|
||||
|
@ -408,12 +334,14 @@ scan_metadata_ffmpeg(char *file, struct media_file_info *mfi)
|
|||
#endif
|
||||
AVStream *video_stream;
|
||||
AVStream *audio_stream;
|
||||
char *path;
|
||||
int mdcount;
|
||||
int i;
|
||||
int ret;
|
||||
|
||||
ctx = NULL;
|
||||
options = NULL;
|
||||
path = strdup(file);
|
||||
|
||||
#if LIBAVFORMAT_VERSION_MAJOR >= 54 || (LIBAVFORMAT_VERSION_MAJOR == 53 && LIBAVFORMAT_VERSION_MINOR >= 3)
|
||||
# ifndef HAVE_FFMPEG
|
||||
|
@ -426,19 +354,33 @@ scan_metadata_ffmpeg(char *file, struct media_file_info *mfi)
|
|||
# endif
|
||||
|
||||
if (mfi->data_kind == 1)
|
||||
av_dict_set(&options, "icy", "1", 0);
|
||||
{
|
||||
free(path);
|
||||
ret = http_stream_setup(&path, file);
|
||||
if (ret < 0)
|
||||
return -1;
|
||||
|
||||
ret = avformat_open_input(&ctx, file, NULL, &options);
|
||||
av_dict_set(&options, "icy", "1", 0);
|
||||
mfi->artwork = ARTWORK_HTTP;
|
||||
}
|
||||
|
||||
ret = avformat_open_input(&ctx, path, NULL, &options);
|
||||
|
||||
if (options)
|
||||
av_dict_free(&options);
|
||||
#else
|
||||
ret = av_open_input_file(&ctx, file, NULL, 0, NULL);
|
||||
ret = av_open_input_file(&ctx, path, NULL, 0, NULL);
|
||||
#endif
|
||||
if (ret != 0)
|
||||
{
|
||||
DPRINTF(E_WARN, L_SCAN, "Cannot open media file '%s': %s\n", file, strerror(AVUNERROR(ret)));
|
||||
DPRINTF(E_WARN, L_SCAN, "Cannot open media file '%s': %s\n", path, strerror(AVUNERROR(ret)));
|
||||
|
||||
free(path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
free(path);
|
||||
|
||||
#if LIBAVFORMAT_VERSION_MAJOR >= 54 || (LIBAVFORMAT_VERSION_MAJOR == 53 && LIBAVFORMAT_VERSION_MINOR >= 3)
|
||||
ret = avformat_find_stream_info(ctx, NULL);
|
||||
#else
|
||||
|
@ -554,11 +496,46 @@ scan_metadata_ffmpeg(char *file, struct media_file_info *mfi)
|
|||
|
||||
DPRINTF(E_DBG, L_SCAN, "Duration %d ms, bitrate %d kbps\n", mfi->song_length, mfi->bitrate);
|
||||
|
||||
#if LIBAVFORMAT_VERSION_MAJOR >= 56 || (LIBAVFORMAT_VERSION_MAJOR == 55 && LIBAVFORMAT_VERSION_MINOR >= 13)
|
||||
/* Try to extract ICY metadata if url/stream */
|
||||
if (mfi->data_kind == 1)
|
||||
extract_metadata_icy(mfi, ctx);
|
||||
#endif
|
||||
{
|
||||
icy_metadata = http_icy_metadata_get(ctx, 0);
|
||||
if (icy_metadata && icy_metadata->name)
|
||||
{
|
||||
DPRINTF(E_DBG, L_SCAN, "Found ICY metadata, name is '%s'\n", icy_metadata->name);
|
||||
|
||||
if (mfi->title)
|
||||
free(mfi->title);
|
||||
if (mfi->artist)
|
||||
free(mfi->artist);
|
||||
if (mfi->album_artist)
|
||||
free(mfi->album_artist);
|
||||
|
||||
mfi->title = strdup(icy_metadata->name);
|
||||
mfi->artist = strdup(icy_metadata->name);
|
||||
mfi->album_artist = strdup(icy_metadata->name);
|
||||
}
|
||||
if (icy_metadata && icy_metadata->description)
|
||||
{
|
||||
DPRINTF(E_DBG, L_SCAN, "Found ICY metadata, description is '%s'\n", icy_metadata->description);
|
||||
|
||||
if (mfi->album)
|
||||
free(mfi->album);
|
||||
|
||||
mfi->album = strdup(icy_metadata->description);
|
||||
}
|
||||
if (icy_metadata && icy_metadata->genre)
|
||||
{
|
||||
DPRINTF(E_DBG, L_SCAN, "Found ICY metadata, genre is '%s'\n", icy_metadata->genre);
|
||||
|
||||
if (mfi->genre)
|
||||
free(mfi->genre);
|
||||
|
||||
mfi->genre = strdup(icy_metadata->genre);
|
||||
}
|
||||
if (icy_metadata)
|
||||
http_icy_metadata_free(icy_metadata, 0);
|
||||
}
|
||||
|
||||
/* Get some more information on the audio stream */
|
||||
if (audio_stream)
|
||||
|
|
|
@ -1,348 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2010 Julien BLACHE <jb@jblache.org>
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
# include <config.h>
|
||||
#endif
|
||||
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <limits.h>
|
||||
#include <sys/param.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include <netdb.h>
|
||||
#include <sys/socket.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <pthread.h>
|
||||
|
||||
#if defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
|
||||
#include <netinet/in.h>
|
||||
#endif
|
||||
|
||||
#include <event.h>
|
||||
#if defined HAVE_LIBEVENT2
|
||||
# include <event2/http.h>
|
||||
#else
|
||||
# include "evhttp/evhttp_compat.h"
|
||||
#endif
|
||||
|
||||
#include <libavformat/avformat.h>
|
||||
|
||||
#include "logger.h"
|
||||
#include "filescanner.h"
|
||||
#include "misc.h"
|
||||
|
||||
#define ICY_TIMEOUT 3
|
||||
|
||||
enum icy_request_status { ICY_INIT, ICY_WAITING, ICY_DONE };
|
||||
|
||||
static enum icy_request_status status;
|
||||
|
||||
/* TODO why doesn't evbase_scan work... */
|
||||
extern struct event_base *evbase_main;
|
||||
|
||||
struct icy_ctx
|
||||
{
|
||||
char *url;
|
||||
char address[INET6_ADDRSTRLEN];
|
||||
char hostname[PATH_MAX];
|
||||
char path[PATH_MAX];
|
||||
int port;
|
||||
|
||||
char *icy_name;
|
||||
char *icy_description;
|
||||
char *icy_genre;
|
||||
|
||||
pthread_mutex_t lck;
|
||||
pthread_cond_t cond;
|
||||
};
|
||||
|
||||
#ifndef HAVE_LIBEVENT2
|
||||
static int
|
||||
resolve_address(char *hostname, char *s, size_t maxlen)
|
||||
{
|
||||
struct addrinfo *result;
|
||||
int ret;
|
||||
|
||||
ret = getaddrinfo(hostname, NULL, NULL, &result);
|
||||
if (ret != 0)
|
||||
return -1;
|
||||
|
||||
switch(result->ai_addr->sa_family)
|
||||
{
|
||||
case AF_INET:
|
||||
inet_ntop(AF_INET, &(((struct sockaddr_in *)result->ai_addr)->sin_addr), s, maxlen);
|
||||
break;
|
||||
|
||||
case AF_INET6:
|
||||
inet_ntop(AF_INET6, &(((struct sockaddr_in6 *)result->ai_addr)->sin6_addr), s, maxlen);
|
||||
break;
|
||||
|
||||
default:
|
||||
strncpy(s, "Unknown AF", maxlen);
|
||||
freeaddrinfo(result);
|
||||
return -1;
|
||||
}
|
||||
|
||||
freeaddrinfo(result);
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef HAVE_LIBEVENT2_OLD
|
||||
static void
|
||||
scan_icy_request_cb(struct evhttp_request *req, void *arg)
|
||||
{
|
||||
struct icy_ctx *ctx;
|
||||
|
||||
ctx = (struct icy_ctx *)arg;
|
||||
|
||||
pthread_mutex_lock(&ctx->lck);
|
||||
|
||||
DPRINTF(E_DBG, L_SCAN, "ICY metadata request: Signal callback\n");
|
||||
|
||||
status = ICY_DONE;
|
||||
pthread_cond_signal(&ctx->cond);
|
||||
pthread_mutex_unlock(&ctx->lck);
|
||||
}
|
||||
|
||||
/* Will always return -1 to make evhttp close the connection - we only need the http headers */
|
||||
static int
|
||||
scan_icy_header_cb(struct evhttp_request *req, void *arg)
|
||||
{
|
||||
struct icy_ctx *ctx;
|
||||
struct evkeyvalq *headers;
|
||||
const char *ptr;
|
||||
|
||||
ctx = (struct icy_ctx *)arg;
|
||||
|
||||
DPRINTF(E_DBG, L_SCAN, "ICY metadata request: Headers received\n");
|
||||
|
||||
headers = evhttp_request_get_input_headers(req);
|
||||
if ( (ptr = evhttp_find_header(headers, "icy-name")) )
|
||||
{
|
||||
ctx->icy_name = strdup(ptr);
|
||||
DPRINTF(E_DBG, L_SCAN, "Found ICY metadata, name is %s\n", ctx->icy_name);
|
||||
}
|
||||
if ( (ptr = evhttp_find_header(headers, "icy-description")) )
|
||||
{
|
||||
ctx->icy_description = strdup(ptr);
|
||||
DPRINTF(E_DBG, L_SCAN, "Found ICY metadata, description is %s\n", ctx->icy_description);
|
||||
}
|
||||
if ( (ptr = evhttp_find_header(headers, "icy-genre")) )
|
||||
{
|
||||
ctx->icy_genre = strdup(ptr);
|
||||
DPRINTF(E_DBG, L_SCAN, "Found ICY metadata, genre is %s\n", ctx->icy_genre);
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
int
|
||||
scan_metadata_icy(char *url, struct media_file_info *mfi)
|
||||
{
|
||||
struct icy_ctx *ctx;
|
||||
struct evhttp_connection *evcon;
|
||||
#ifndef HAVE_LIBEVENT2_OLD
|
||||
struct evhttp_request *req;
|
||||
struct evkeyvalq *headers;
|
||||
char s[PATH_MAX];
|
||||
#endif
|
||||
time_t start;
|
||||
time_t end;
|
||||
int ret;
|
||||
|
||||
status = ICY_INIT;
|
||||
start = time(NULL);
|
||||
|
||||
/* We can set this straight away */
|
||||
mfi->url = strdup(url);
|
||||
|
||||
ctx = (struct icy_ctx *)malloc(sizeof(struct icy_ctx));
|
||||
if (!ctx)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Out of memory for ICY metadata context\n");
|
||||
|
||||
return -1;
|
||||
}
|
||||
memset(ctx, 0, sizeof(struct icy_ctx));
|
||||
|
||||
pthread_mutex_init(&ctx->lck, NULL);
|
||||
pthread_cond_init(&ctx->cond, NULL);
|
||||
|
||||
ctx->url = url;
|
||||
|
||||
/* TODO https */
|
||||
av_url_split(NULL, 0, NULL, 0, ctx->hostname, sizeof(ctx->hostname), &ctx->port, ctx->path, sizeof(ctx->path), ctx->url);
|
||||
if ((!ctx->hostname) || (strlen(ctx->hostname) == 0))
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Error extracting hostname from playlist URL: %s\n", ctx->url);
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (ctx->port < 0)
|
||||
ctx->port = 80;
|
||||
|
||||
if (strlen(ctx->path) == 0)
|
||||
{
|
||||
ctx->path[0] = '/';
|
||||
ctx->path[1] = '\0';
|
||||
}
|
||||
|
||||
#ifdef HAVE_LIBEVENT2
|
||||
evcon = evhttp_connection_base_new(evbase_main, NULL, ctx->hostname, (unsigned short)ctx->port);
|
||||
if (!evcon)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Could not create connection to %s\n", ctx->hostname);
|
||||
|
||||
goto no_icy;
|
||||
}
|
||||
#else
|
||||
/* Resolve IP address */
|
||||
ret = resolve_address(ctx->hostname, ctx->address, sizeof(ctx->address));
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Could not find IP address of %s\n", ctx->hostname);
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
DPRINTF(E_DBG, L_SCAN, "URL %s converted to hostname %s, port %d, path %s, IP %s\n", ctx->url, ctx->hostname, ctx->port, ctx->path, ctx->address);
|
||||
|
||||
/* Set up connection */
|
||||
evcon = evhttp_connection_new(ctx->address, (unsigned short)ctx->port);
|
||||
if (!evcon)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Could not create connection to %s\n", ctx->hostname);
|
||||
|
||||
goto no_icy;
|
||||
}
|
||||
evhttp_connection_set_base(evcon, evbase_main);
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_LIBEVENT2_OLD
|
||||
DPRINTF(E_LOG, L_SCAN, "Skipping Shoutcast metadata request for %s (requires libevent>=2.1.4 or libav 10)\n", ctx->hostname);
|
||||
#else
|
||||
evhttp_connection_set_timeout(evcon, ICY_TIMEOUT);
|
||||
|
||||
/* Set up request */
|
||||
req = evhttp_request_new(scan_icy_request_cb, ctx);
|
||||
if (!req)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Could not create request to %s\n", ctx->hostname);
|
||||
|
||||
goto no_icy;
|
||||
}
|
||||
|
||||
evhttp_request_set_header_cb(req, scan_icy_header_cb);
|
||||
|
||||
headers = evhttp_request_get_output_headers(req);
|
||||
snprintf(s, PATH_MAX, "%s:%d", ctx->hostname, ctx->port);
|
||||
evhttp_add_header(headers, "Host", s);
|
||||
evhttp_add_header(headers, "Icy-MetaData", "1");
|
||||
|
||||
/* Make request */
|
||||
DPRINTF(E_INFO, L_SCAN, "Making request to %s asking for ICY (Shoutcast) metadata\n", ctx->hostname);
|
||||
|
||||
status = ICY_WAITING;
|
||||
ret = evhttp_make_request(evcon, req, EVHTTP_REQ_GET, ctx->path);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Error making request to %s\n", ctx->hostname);
|
||||
|
||||
status = ICY_DONE;
|
||||
goto no_icy;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Can't count on server support for ICY metadata, so
|
||||
* while waiting for a reply make a parallel call to scan_metadata_ffmpeg.
|
||||
*/
|
||||
no_icy:
|
||||
ret = scan_metadata_ffmpeg(url, mfi);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Playlist URL is unavailable for probe/metadata, assuming MP3 encoding\n");
|
||||
mfi->type = strdup("mp3");
|
||||
mfi->codectype = strdup("mpeg");
|
||||
mfi->description = strdup("MPEG audio file");
|
||||
}
|
||||
|
||||
/* Wait for ICY request to complete or timeout */
|
||||
pthread_mutex_lock(&ctx->lck);
|
||||
|
||||
if (status == ICY_WAITING)
|
||||
pthread_cond_wait(&ctx->cond, &ctx->lck);
|
||||
|
||||
pthread_mutex_unlock(&ctx->lck);
|
||||
|
||||
/* Copy result to mfi */
|
||||
if (ctx->icy_name)
|
||||
{
|
||||
if (mfi->title)
|
||||
free(mfi->title);
|
||||
if (mfi->artist)
|
||||
free(mfi->artist);
|
||||
if (mfi->album_artist)
|
||||
free(mfi->album_artist);
|
||||
|
||||
mfi->title = strdup(ctx->icy_name);
|
||||
mfi->artist = strdup(ctx->icy_name);
|
||||
mfi->album_artist = strdup(ctx->icy_name);
|
||||
|
||||
free(ctx->icy_name);
|
||||
}
|
||||
|
||||
if (ctx->icy_description)
|
||||
{
|
||||
if (mfi->album)
|
||||
free(mfi->album);
|
||||
|
||||
mfi->album = ctx->icy_description;
|
||||
}
|
||||
|
||||
if (ctx->icy_genre)
|
||||
{
|
||||
if (mfi->genre)
|
||||
free(mfi->genre);
|
||||
|
||||
mfi->genre = ctx->icy_genre;
|
||||
}
|
||||
|
||||
/* Clean up */
|
||||
if (evcon)
|
||||
evhttp_connection_free(evcon);
|
||||
|
||||
pthread_cond_destroy(&ctx->cond);
|
||||
pthread_mutex_destroy(&ctx->lck);
|
||||
free(ctx);
|
||||
|
||||
end = time(NULL);
|
||||
|
||||
DPRINTF(E_DBG, L_SCAN, "ICY metadata scan of %s completed in %.f sec\n", url, difftime(end, start));
|
||||
|
||||
return 1;
|
||||
}
|
|
@ -759,8 +759,22 @@ process_pls(plist_t playlists, char *file)
|
|||
|
||||
if (pl_id == 0)
|
||||
{
|
||||
pli = (struct playlist_info *)malloc(sizeof(struct playlist_info));
|
||||
if (!pli)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Out of memory\n");
|
||||
|
||||
return;
|
||||
}
|
||||
memset(pli, 0, sizeof(struct playlist_info));
|
||||
|
||||
pli->title = strdup(name);
|
||||
pli->path = strdup(file);
|
||||
snprintf(virtual_path, PATH_MAX, "/file:%s", file);
|
||||
ret = db_pl_add(name, file, virtual_path, &pl_id);
|
||||
pli->virtual_path = strdup(virtual_path);
|
||||
|
||||
ret = db_pl_add(pli, &pl_id);
|
||||
free_pli(pli, 0);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Error adding iTunes playlist '%s' (%s)\n", name, file);
|
||||
|
|
|
@ -96,14 +96,6 @@ scan_playlist(char *file, time_t mtime)
|
|||
|
||||
DPRINTF(E_LOG, L_SCAN, "Processing static playlist: %s\n", file);
|
||||
|
||||
ret = stat(file, &sb);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Could not stat() '%s': %s\n", file, strerror(errno));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ptr = strrchr(file, '.');
|
||||
if (!ptr)
|
||||
return;
|
||||
|
@ -121,21 +113,13 @@ scan_playlist(char *file, time_t mtime)
|
|||
else
|
||||
filename++;
|
||||
|
||||
pli = db_pl_fetch_bypath(file);
|
||||
|
||||
if (pli)
|
||||
ret = stat(file, &sb);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_DBG, L_SCAN, "Playlist found, updating\n");
|
||||
DPRINTF(E_LOG, L_SCAN, "Could not stat() '%s': %s\n", file, strerror(errno));
|
||||
|
||||
pl_id = pli->id;
|
||||
|
||||
free_pli(pli, 0);
|
||||
|
||||
db_pl_ping(pl_id);
|
||||
db_pl_clear_items(pl_id);
|
||||
return;
|
||||
}
|
||||
else
|
||||
pl_id = 0;
|
||||
|
||||
fp = fopen(file, "r");
|
||||
if (!fp)
|
||||
|
@ -145,33 +129,58 @@ scan_playlist(char *file, time_t mtime)
|
|||
return;
|
||||
}
|
||||
|
||||
if (pl_id == 0)
|
||||
/* Fetch or create playlist */
|
||||
pli = db_pl_fetch_bypath(file);
|
||||
if (pli)
|
||||
{
|
||||
/* Get only the basename, to be used as the playlist name */
|
||||
DPRINTF(E_DBG, L_SCAN, "Found playlist '%s', updating\n", file);
|
||||
|
||||
pl_id = pli->id;
|
||||
|
||||
db_pl_ping(pl_id);
|
||||
db_pl_clear_items(pl_id);
|
||||
}
|
||||
else
|
||||
{
|
||||
pli = (struct playlist_info *)malloc(sizeof(struct playlist_info));
|
||||
if (!pli)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Out of memory\n");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
memset(pli, 0, sizeof(struct playlist_info));
|
||||
|
||||
/* Get only the basename, to be used as the playlist title */
|
||||
ptr = strrchr(filename, '.');
|
||||
if (ptr)
|
||||
*ptr = '\0';
|
||||
|
||||
/* Safe: filename is a subset of file which is <= PATH_MAX already */
|
||||
strncpy(buf, filename, sizeof(buf));
|
||||
pli->title = strdup(filename);
|
||||
|
||||
/* Restore the full filename */
|
||||
if (ptr)
|
||||
*ptr = '.';
|
||||
|
||||
pli->path = strdup(file);
|
||||
snprintf(virtual_path, PATH_MAX, "/file:%s", file);
|
||||
pli->virtual_path = strdup(virtual_path);
|
||||
|
||||
ret = db_pl_add(buf, file, virtual_path, &pl_id);
|
||||
ret = db_pl_add(pli, &pl_id);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Error adding playlist '%s'\n", file);
|
||||
|
||||
free_pli(pli, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
DPRINTF(E_INFO, L_SCAN, "Added playlist as id %d\n", pl_id);
|
||||
}
|
||||
|
||||
free_pli(pli, 0);
|
||||
|
||||
extinf = 0;
|
||||
memset(&mfi, 0, sizeof(struct media_file_info));
|
||||
|
||||
|
@ -287,6 +296,10 @@ scan_playlist(char *file, time_t mtime)
|
|||
free(filename);
|
||||
}
|
||||
|
||||
/* We had some extinf that we never got to use, free it now */
|
||||
if (extinf)
|
||||
free_mfi(&mfi, 1);
|
||||
|
||||
if (!feof(fp))
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Error reading playlist '%s': %s\n", file, strerror(errno));
|
||||
|
|
|
@ -0,0 +1,590 @@
|
|||
/*
|
||||
* Copyright (C) 2015 Espen Jürgensen <espenjurgensen@gmail.com>
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
# include <config.h>
|
||||
#endif
|
||||
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <limits.h>
|
||||
#include <sys/param.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include <libavutil/opt.h>
|
||||
|
||||
#include <event2/event.h>
|
||||
|
||||
#include "http.h"
|
||||
#include "logger.h"
|
||||
#include "misc.h"
|
||||
|
||||
/* ======================= libevent HTTP client =============================*/
|
||||
|
||||
// Number of seconds the client will wait for a response before aborting
|
||||
#define HTTP_CLIENT_TIMEOUT 8
|
||||
|
||||
/* The strict libevent api does not permit walking through an evkeyvalq and saving
|
||||
* all the http headers, so we predefine what we are looking for. You can add
|
||||
* extra headers here that you would like to save.
|
||||
*/
|
||||
static char *header_list[] =
|
||||
{
|
||||
"icy-name",
|
||||
"icy-description",
|
||||
"icy-metaint",
|
||||
"icy-genre",
|
||||
"Content-Type",
|
||||
};
|
||||
|
||||
/* Copies headers we are searching for from one keyval struct to another
|
||||
*
|
||||
*/
|
||||
static void
|
||||
headers_save(struct keyval *kv, struct evkeyvalq *headers)
|
||||
{
|
||||
const char *value;
|
||||
int i;
|
||||
|
||||
if (!kv || !headers)
|
||||
return;
|
||||
|
||||
for (i = 0; i < (sizeof(header_list) / sizeof(header_list[0])); i++)
|
||||
{
|
||||
if ( (value = evhttp_find_header(headers, header_list[i])) )
|
||||
keyval_add(kv, header_list[i], value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void
|
||||
request_cb(struct evhttp_request *req, void *arg)
|
||||
{
|
||||
struct http_client_ctx *ctx;
|
||||
const char *response_code_line;
|
||||
int response_code;
|
||||
|
||||
ctx = (struct http_client_ctx *)arg;
|
||||
|
||||
if (ctx->headers_only)
|
||||
{
|
||||
ctx->ret = 0;
|
||||
|
||||
event_base_loopbreak(ctx->evbase);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!req)
|
||||
{
|
||||
DPRINTF(E_LOG, L_HTTP, "Connection to %s failed: Connection timed out\n", ctx->url);
|
||||
goto connection_error;
|
||||
}
|
||||
|
||||
response_code = evhttp_request_get_response_code(req);
|
||||
#ifndef HAVE_LIBEVENT2_OLD
|
||||
response_code_line = evhttp_request_get_response_code_line(req);
|
||||
#else
|
||||
response_code_line = "no error text";
|
||||
#endif
|
||||
|
||||
if (response_code == 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_HTTP, "Connection to %s failed: Connection refused\n", ctx->url);
|
||||
goto connection_error;
|
||||
}
|
||||
else if (response_code != 200)
|
||||
{
|
||||
DPRINTF(E_LOG, L_HTTP, "Connection to %s failed: %s (error %d)\n", ctx->url, response_code_line, response_code);
|
||||
goto connection_error;
|
||||
}
|
||||
|
||||
ctx->ret = 0;
|
||||
|
||||
if (ctx->headers)
|
||||
headers_save(ctx->headers, evhttp_request_get_input_headers(req));
|
||||
if (ctx->body)
|
||||
evbuffer_add_buffer(ctx->body, evhttp_request_get_input_buffer(req));
|
||||
|
||||
event_base_loopbreak(ctx->evbase);
|
||||
|
||||
return;
|
||||
|
||||
connection_error:
|
||||
|
||||
ctx->ret = -1;
|
||||
|
||||
event_base_loopbreak(ctx->evbase);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/* This callback is only invoked if ctx->headers_only is set. Since that means
|
||||
* we only want headers, it will always return -1 to make evhttp close the
|
||||
* connection. The headers will be saved in a keyval struct in ctx, since we
|
||||
* cannot address the *evkeyvalq after the connection is free'd.
|
||||
*/
|
||||
#ifndef HAVE_LIBEVENT2_OLD
|
||||
static int
|
||||
request_header_cb(struct evhttp_request *req, void *arg)
|
||||
{
|
||||
struct http_client_ctx *ctx;
|
||||
|
||||
ctx = (struct http_client_ctx *)arg;
|
||||
|
||||
if (!ctx->headers)
|
||||
{
|
||||
DPRINTF(E_LOG, L_HTTP, "BUG: Header callback invoked but caller did not say where to save the headers\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
headers_save(ctx->headers, evhttp_request_get_input_headers(req));
|
||||
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
int
|
||||
http_client_request(struct http_client_ctx *ctx)
|
||||
{
|
||||
struct evhttp_connection *evcon;
|
||||
struct evhttp_request *req;
|
||||
struct evkeyvalq *headers;
|
||||
char hostname[PATH_MAX];
|
||||
char path[PATH_MAX];
|
||||
char s[PATH_MAX];
|
||||
int port;
|
||||
int ret;
|
||||
|
||||
ctx->ret = -1;
|
||||
|
||||
av_url_split(NULL, 0, NULL, 0, hostname, sizeof(hostname), &port, path, sizeof(path), ctx->url);
|
||||
if (strlen(hostname) == 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_HTTP, "Error extracting hostname from URL: %s\n", ctx->url);
|
||||
|
||||
return ctx->ret;
|
||||
}
|
||||
|
||||
if (port <= 0)
|
||||
port = 80;
|
||||
|
||||
if (strlen(path) == 0)
|
||||
{
|
||||
path[0] = '/';
|
||||
path[1] = '\0';
|
||||
}
|
||||
|
||||
ctx->evbase = event_base_new();
|
||||
if (!ctx->evbase)
|
||||
{
|
||||
DPRINTF(E_LOG, L_HTTP, "Could not create or find http client event base\n");
|
||||
|
||||
return ctx->ret;
|
||||
}
|
||||
|
||||
evcon = evhttp_connection_base_new(ctx->evbase, NULL, hostname, (unsigned short)port);
|
||||
if (!evcon)
|
||||
{
|
||||
DPRINTF(E_LOG, L_HTTP, "Could not create connection to %s\n", hostname);
|
||||
|
||||
event_base_free(ctx->evbase);
|
||||
return ctx->ret;
|
||||
}
|
||||
|
||||
evhttp_connection_set_timeout(evcon, HTTP_CLIENT_TIMEOUT);
|
||||
|
||||
/* Set up request */
|
||||
req = evhttp_request_new(request_cb, ctx);
|
||||
if (!req)
|
||||
{
|
||||
DPRINTF(E_LOG, L_HTTP, "Could not create request to %s\n", hostname);
|
||||
|
||||
evhttp_connection_free(evcon);
|
||||
event_base_free(ctx->evbase);
|
||||
return ctx->ret;
|
||||
}
|
||||
|
||||
#ifndef HAVE_LIBEVENT2_OLD
|
||||
if (ctx->headers_only)
|
||||
evhttp_request_set_header_cb(req, request_header_cb);
|
||||
#endif
|
||||
|
||||
headers = evhttp_request_get_output_headers(req);
|
||||
snprintf(s, PATH_MAX, "%s:%d", hostname, port);
|
||||
evhttp_add_header(headers, "Host", s);
|
||||
evhttp_add_header(headers, "Content-Length", "0");
|
||||
evhttp_add_header(headers, "User-Agent", "forked-daapd/" VERSION);
|
||||
evhttp_add_header(headers, "Icy-MetaData", "1");
|
||||
|
||||
/* Make request */
|
||||
DPRINTF(E_INFO, L_HTTP, "Making request for http://%s:%d%s\n", hostname, port, path);
|
||||
|
||||
ret = evhttp_make_request(evcon, req, EVHTTP_REQ_GET, path);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_HTTP, "Error making request for http://%s:%d%s\n", hostname, port, path);
|
||||
|
||||
evhttp_connection_free(evcon);
|
||||
event_base_free(ctx->evbase);
|
||||
return ctx->ret;
|
||||
}
|
||||
|
||||
event_base_dispatch(ctx->evbase);
|
||||
|
||||
evhttp_connection_free(evcon);
|
||||
event_base_free(ctx->evbase);
|
||||
|
||||
return ctx->ret;
|
||||
}
|
||||
|
||||
int
|
||||
http_stream_setup(char **stream, const char *url)
|
||||
{
|
||||
struct http_client_ctx ctx;
|
||||
struct evbuffer *evbuf;
|
||||
const char *ext;
|
||||
char *line;
|
||||
int ret;
|
||||
int n;
|
||||
|
||||
*stream = NULL;
|
||||
|
||||
ext = strrchr(url, '.');
|
||||
if (strcasecmp(ext, ".m3u") != 0)
|
||||
{
|
||||
*stream = strdup(url);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// It was a m3u playlist, so now retrieve it
|
||||
memset(&ctx, 0, sizeof(struct http_client_ctx));
|
||||
|
||||
evbuf = evbuffer_new();
|
||||
if (!evbuf)
|
||||
return -1;
|
||||
|
||||
ctx.url = url;
|
||||
ctx.body = evbuf;
|
||||
|
||||
ret = http_client_request(&ctx);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_HTTP, "Couldn't fetch internet playlist: %s\n", url);
|
||||
|
||||
evbuffer_free(evbuf);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Read the playlist until the first stream link is found, but give up if
|
||||
* nothing is found in the first 10 lines
|
||||
*/
|
||||
n = 0;
|
||||
while ((line = evbuffer_readln(ctx.body, NULL, EVBUFFER_EOL_ANY)) && (n < 10))
|
||||
{
|
||||
n++;
|
||||
if (strncasecmp(line, "http://", strlen("http://")) == 0)
|
||||
{
|
||||
DPRINTF(E_DBG, L_HTTP, "Found internet playlist stream (line %d): %s\n", n, line);
|
||||
|
||||
n = -1;
|
||||
break;
|
||||
}
|
||||
|
||||
free(line);
|
||||
}
|
||||
|
||||
evbuffer_free(ctx.body);
|
||||
|
||||
if (n != -1)
|
||||
{
|
||||
DPRINTF(E_LOG, L_HTTP, "Couldn't find stream in internet playlist: %s\n", url);
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
*stream = line;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* ======================= ICY metadata handling =============================*/
|
||||
|
||||
|
||||
#if LIBAVFORMAT_VERSION_MAJOR >= 56 || (LIBAVFORMAT_VERSION_MAJOR == 55 && LIBAVFORMAT_VERSION_MINOR >= 13)
|
||||
static int
|
||||
metadata_packet_get(struct http_icy_metadata *metadata, AVFormatContext *fmtctx)
|
||||
{
|
||||
uint8_t *buffer;
|
||||
char *icy_token;
|
||||
char *ptr;
|
||||
char *end;
|
||||
|
||||
av_opt_get(fmtctx, "icy_metadata_packet", AV_OPT_SEARCH_CHILDREN, &buffer);
|
||||
if (!buffer)
|
||||
return -1;
|
||||
|
||||
icy_token = strtok((char *)buffer, ";");
|
||||
while (icy_token != NULL)
|
||||
{
|
||||
ptr = strchr(icy_token, '=');
|
||||
if (!ptr || (ptr[1] == '\0'))
|
||||
{
|
||||
icy_token = strtok(NULL, ";");
|
||||
continue;
|
||||
}
|
||||
|
||||
ptr++;
|
||||
if (ptr[0] == '\'')
|
||||
ptr++;
|
||||
|
||||
end = strrchr(ptr, '\'');
|
||||
if (end)
|
||||
*end = '\0';
|
||||
|
||||
if ((strncmp(icy_token, "StreamTitle", strlen("StreamTitle")) == 0) && !metadata->title)
|
||||
{
|
||||
metadata->title = ptr;
|
||||
|
||||
/* Dash separates artist from title, if no dash assume all is title */
|
||||
ptr = strstr(ptr, " - ");
|
||||
if (ptr)
|
||||
{
|
||||
*ptr = '\0';
|
||||
metadata->title = strdup(metadata->title);
|
||||
*ptr = ' ';
|
||||
|
||||
metadata->artist = strdup(ptr + 3);
|
||||
}
|
||||
else
|
||||
metadata->title = strdup(metadata->title);
|
||||
}
|
||||
else if ((strncmp(icy_token, "StreamUrl", strlen("StreamUrl")) == 0) && !metadata->artwork_url)
|
||||
{
|
||||
metadata->artwork_url = strdup(ptr);
|
||||
}
|
||||
|
||||
if (end)
|
||||
*end = '\'';
|
||||
|
||||
icy_token = strtok(NULL, ";");
|
||||
}
|
||||
av_free(buffer);
|
||||
|
||||
if (metadata->title)
|
||||
metadata->hash = djb_hash(metadata->title, strlen(metadata->title));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
metadata_header_get(struct http_icy_metadata *metadata, AVFormatContext *fmtctx)
|
||||
{
|
||||
uint8_t *buffer;
|
||||
char *icy_token;
|
||||
char *ptr;
|
||||
|
||||
av_opt_get(fmtctx, "icy_metadata_headers", AV_OPT_SEARCH_CHILDREN, &buffer);
|
||||
if (!buffer)
|
||||
return -1;
|
||||
|
||||
icy_token = strtok((char *)buffer, "\r\n");
|
||||
while (icy_token != NULL)
|
||||
{
|
||||
ptr = strchr(icy_token, ':');
|
||||
if (!ptr || (ptr[1] == '\0'))
|
||||
{
|
||||
icy_token = strtok(NULL, "\r\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
ptr++;
|
||||
if (ptr[0] == ' ')
|
||||
ptr++;
|
||||
|
||||
if ((strncmp(icy_token, "icy-name", strlen("icy-name")) == 0) && !metadata->name)
|
||||
metadata->name = strdup(ptr);
|
||||
else if ((strncmp(icy_token, "icy-description", strlen("icy-description")) == 0) && !metadata->description)
|
||||
metadata->description = strdup(ptr);
|
||||
else if ((strncmp(icy_token, "icy-genre", strlen("icy-genre")) == 0) && !metadata->genre)
|
||||
metadata->genre = strdup(ptr);
|
||||
|
||||
icy_token = strtok(NULL, "\r\n");
|
||||
}
|
||||
av_free(buffer);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct http_icy_metadata *
|
||||
http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only)
|
||||
{
|
||||
struct http_icy_metadata *metadata;
|
||||
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));
|
||||
|
||||
got_packet = (metadata_packet_get(metadata, fmtctx) == 0);
|
||||
got_header = (!packet_only) && (metadata_header_get(metadata, fmtctx) == 0);
|
||||
|
||||
if (!got_packet && !got_header)
|
||||
{
|
||||
free(metadata);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* DPRINTF(E_DBG, L_HTTP, "Found ICY: N %s, D %s, G %s, T %s, A %s, U %s, I %" PRIu32 "\n",
|
||||
metadata->name,
|
||||
metadata->description,
|
||||
metadata->genre,
|
||||
metadata->title,
|
||||
metadata->artist,
|
||||
metadata->artwork_url,
|
||||
metadata->hash
|
||||
);
|
||||
*/
|
||||
return metadata;
|
||||
}
|
||||
|
||||
#elif defined(HAVE_LIBEVENT2_OLD)
|
||||
struct http_icy_metadata *
|
||||
http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only)
|
||||
{
|
||||
DPRINTF(E_INFO, L_HTTP, "Skipping Shoutcast metadata request for %s (requires libevent>=2.1.4 or libav 10)\n", fmtctx->filename);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
#else
|
||||
/* Earlier versions of ffmpeg/libav do not seem to allow access to the http
|
||||
* headers, so we must instead open the stream ourselves to get the metadata.
|
||||
* Sorry about the extra connections, you radio streaming people!
|
||||
*
|
||||
* It is not possible to get the packet metadata with these versions of ffmpeg
|
||||
*/
|
||||
struct http_icy_metadata *
|
||||
http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only)
|
||||
{
|
||||
struct http_icy_metadata *metadata;
|
||||
struct http_client_ctx ctx;
|
||||
struct keyval *kv;
|
||||
const char *value;
|
||||
int got_header;
|
||||
int ret;
|
||||
|
||||
/* Can only get header metadata */
|
||||
if (packet_only)
|
||||
return NULL;
|
||||
|
||||
kv = keyval_alloc();
|
||||
if (!kv)
|
||||
return NULL;
|
||||
|
||||
memset(&ctx, 0, sizeof(struct http_client_ctx));
|
||||
ctx.url = fmtctx->filename;
|
||||
ctx.headers = kv;
|
||||
ctx.headers_only = 1;
|
||||
ctx.body = NULL;
|
||||
|
||||
ret = http_client_request(&ctx);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_HTTP, "Error fetching %s\n", fmtctx->filename);
|
||||
|
||||
free(kv);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
metadata = malloc(sizeof(struct http_icy_metadata));
|
||||
if (!metadata)
|
||||
return NULL;
|
||||
memset(metadata, 0, sizeof(struct http_icy_metadata));
|
||||
|
||||
got_header = 0;
|
||||
if ( (value = keyval_get(ctx.headers, "icy-name")) )
|
||||
{
|
||||
metadata->name = strdup(value);
|
||||
got_header = 1;
|
||||
}
|
||||
if ( (value = keyval_get(ctx.headers, "icy-description")) )
|
||||
{
|
||||
metadata->description = strdup(value);
|
||||
got_header = 1;
|
||||
}
|
||||
if ( (value = keyval_get(ctx.headers, "icy-genre")) )
|
||||
{
|
||||
metadata->genre = strdup(value);
|
||||
got_header = 1;
|
||||
}
|
||||
|
||||
keyval_clear(kv);
|
||||
free(kv);
|
||||
|
||||
if (!got_header)
|
||||
{
|
||||
free(metadata);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* DPRINTF(E_DBG, L_HTTP, "Found ICY: N %s, D %s, G %s, T %s, A %s, U %s, I %" PRIu32 "\n",
|
||||
metadata->name,
|
||||
metadata->description,
|
||||
metadata->genre,
|
||||
metadata->title,
|
||||
metadata->artist,
|
||||
metadata->artwork_url,
|
||||
metadata->hash
|
||||
);*/
|
||||
|
||||
return metadata;
|
||||
}
|
||||
#endif
|
||||
|
||||
void
|
||||
http_icy_metadata_free(struct http_icy_metadata *metadata, int content_only)
|
||||
{
|
||||
if (metadata->name)
|
||||
free(metadata->name);
|
||||
|
||||
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);
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
|
||||
#ifndef __HTTP_H__
|
||||
#define __HTTP_H__
|
||||
|
||||
#include <event2/buffer.h>
|
||||
#include <event2/http.h>
|
||||
#include "misc.h"
|
||||
|
||||
#include <libavformat/avformat.h>
|
||||
|
||||
struct http_client_ctx
|
||||
{
|
||||
const char *url;
|
||||
|
||||
/* A keyval/evbuf to store response headers and body.
|
||||
* Can be set to NULL to ignore that part of the response.
|
||||
*/
|
||||
struct keyval *headers;
|
||||
struct evbuffer *body;
|
||||
|
||||
/* Cut the connection after the headers have been received
|
||||
* Used for getting Shoutcast/ICY headers for old versions of libav/ffmpeg
|
||||
* (requires libevent 1 or 2.1.4+)
|
||||
*/
|
||||
int headers_only;
|
||||
|
||||
/* Private */
|
||||
int ret;
|
||||
void *evbase;
|
||||
};
|
||||
|
||||
struct http_icy_metadata
|
||||
{
|
||||
uint32_t id;
|
||||
|
||||
/* Static stream metadata from icy_metadata_headers */
|
||||
char *name;
|
||||
char *description;
|
||||
char *genre;
|
||||
|
||||
/* Track specific, comes from icy_metadata_packet */
|
||||
char *title;
|
||||
char *artist;
|
||||
char *artwork_url;
|
||||
|
||||
uint32_t hash;
|
||||
};
|
||||
|
||||
|
||||
/* Generic HTTP client. No support for https.
|
||||
*
|
||||
* @param ctx HTTP request params, see above
|
||||
* @return 0 if successful, -1 if an error occurred
|
||||
*/
|
||||
int
|
||||
http_client_request(struct http_client_ctx *ctx);
|
||||
|
||||
|
||||
/* Returns a newly allocated string with the first stream in the m3u given in
|
||||
* url. If url is not a m3u, the string will be a copy of url.
|
||||
*
|
||||
* @param stream the newly allocated string with link to stream (NULL on error)
|
||||
* @param url link to either stream or m3u
|
||||
* @return 0 if successful, -1 if an error occurred
|
||||
*/
|
||||
int
|
||||
http_stream_setup(char **stream, const char *url);
|
||||
|
||||
|
||||
/* Extracts ICY header and packet metadata (requires libav 10)
|
||||
*
|
||||
* example header metadata (standard http header format):
|
||||
* icy-name: Rock On Radio
|
||||
* example packet metadata (track currently being played):
|
||||
* StreamTitle='Robert Miles - Black Rubber';StreamUrl='';
|
||||
*
|
||||
* The extraction is straight from the stream and done in the player thread, so
|
||||
* it must not produce significant delay.
|
||||
*
|
||||
* @param fmtctx the libav/ffmpeg AVFormatContext containing the stream
|
||||
* @param packet_only only get currently playing info (see struct above)
|
||||
* @return metadata struct if successful, NULL on error or nothing found
|
||||
*/
|
||||
struct http_icy_metadata *
|
||||
http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only);
|
||||
|
||||
|
||||
/* Frees an ICY metadata struct
|
||||
*
|
||||
* @param metadata struct to free
|
||||
* @param content_only just free content, not the struct
|
||||
*/
|
||||
void
|
||||
http_icy_metadata_free(struct http_icy_metadata *metadata, int content_only);
|
||||
|
||||
#endif /* !__HTTP_H__ */
|
|
@ -204,10 +204,11 @@ stream_chunk_xcode_cb(int fd, short event, void *arg)
|
|||
struct timeval tv;
|
||||
int xcoded;
|
||||
int ret;
|
||||
int dummy;
|
||||
|
||||
st = (struct stream_ctx *)arg;
|
||||
|
||||
xcoded = transcode(st->xcode, st->evbuf, STREAM_CHUNK_SIZE);
|
||||
xcoded = transcode(st->xcode, st->evbuf, STREAM_CHUNK_SIZE, &dummy);
|
||||
if (xcoded <= 0)
|
||||
{
|
||||
if (xcoded == 0)
|
||||
|
|
121
src/httpd_daap.c
121
src/httpd_daap.c
|
@ -69,6 +69,8 @@ extern struct event_base *evbase_httpd;
|
|||
/* Update requests refresh interval in seconds */
|
||||
#define DAAP_UPDATE_REFRESH 0
|
||||
|
||||
/* Database number for the Radio item */
|
||||
#define DAAP_DB_RADIO 2
|
||||
|
||||
struct uri_map {
|
||||
regex_t preg;
|
||||
|
@ -862,7 +864,7 @@ daap_reply_server_info(struct evhttp_request *req, struct evbuffer *evbuf, char
|
|||
dmap_add_char(content, "msix", 1); // dmap.supportsindex
|
||||
// dmap_add_char(content, "msrs", 1); // dmap.supportsresolve
|
||||
|
||||
dmap_add_int(content, "msdc", 1); // dmap.databasescount
|
||||
dmap_add_int(content, "msdc", 2); // dmap.databasescount
|
||||
|
||||
// dmap_add_int(content, "mstc", ); // dmap.utctime
|
||||
// dmap_add_int(content, "msto", ); // dmap.utcoffset
|
||||
|
@ -1123,9 +1125,11 @@ static int
|
|||
daap_reply_dblist(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua)
|
||||
{
|
||||
struct evbuffer *content;
|
||||
struct evbuffer *item;
|
||||
struct daap_session *s;
|
||||
cfg_t *lib;
|
||||
char *name;
|
||||
char *name_radio;
|
||||
int count;
|
||||
|
||||
s = daap_session_find(req, query, evbuf);
|
||||
|
@ -1134,6 +1138,7 @@ daap_reply_dblist(struct evhttp_request *req, struct evbuffer *evbuf, char **uri
|
|||
|
||||
lib = cfg_getsec(cfg, "library");
|
||||
name = cfg_getstr(lib, "name");
|
||||
name_radio = cfg_getstr(lib, "name_radio");
|
||||
|
||||
content = evbuffer_new();
|
||||
if (!content)
|
||||
|
@ -1144,29 +1149,66 @@ daap_reply_dblist(struct evhttp_request *req, struct evbuffer *evbuf, char **uri
|
|||
return -1;
|
||||
}
|
||||
|
||||
dmap_add_int(content, "miid", 1);
|
||||
dmap_add_long(content, "mper", 1);
|
||||
dmap_add_int(content, "mdbk", 1);
|
||||
dmap_add_int(content, "aeCs", 1);
|
||||
dmap_add_string(content, "minm", name);
|
||||
// Add db entry for library with dbid = 1
|
||||
item = evbuffer_new();
|
||||
if (!item)
|
||||
{
|
||||
DPRINTF(E_LOG, L_DAAP, "Could not create evbuffer for DAAP dblist library item\n");
|
||||
|
||||
dmap_send_error(req, "avdb", "Out of memory");
|
||||
return -1;
|
||||
}
|
||||
|
||||
dmap_add_int(item, "miid", 1);
|
||||
dmap_add_long(item, "mper", 1);
|
||||
dmap_add_int(item, "mdbk", 1);
|
||||
dmap_add_int(item, "aeCs", 1);
|
||||
dmap_add_string(item, "minm", name);
|
||||
count = db_files_get_count();
|
||||
dmap_add_int(content, "mimc", count);
|
||||
|
||||
dmap_add_int(item, "mimc", count);
|
||||
count = db_pl_get_count(); // TODO Don't count empty smart playlists, because they get excluded in aply
|
||||
dmap_add_int(content, "mctc", count);
|
||||
|
||||
dmap_add_int(item, "mctc", count);
|
||||
// dmap_add_int(content, "aeMk", 0x405); // com.apple.itunes.extended-media-kind (OR of all in library)
|
||||
dmap_add_int(content, "meds", 3);
|
||||
dmap_add_int(item, "meds", 3);
|
||||
|
||||
// Create container for library db
|
||||
dmap_add_container(content, "mlit", EVBUFFER_LENGTH(item));
|
||||
evbuffer_add_buffer(content, item);
|
||||
evbuffer_free(item);
|
||||
|
||||
// Add second db entry for radio with dbid = DAAP_DB_RADIO
|
||||
item = evbuffer_new();
|
||||
if (!item)
|
||||
{
|
||||
DPRINTF(E_LOG, L_DAAP, "Could not create evbuffer for DAAP dblist radio item\n");
|
||||
|
||||
dmap_send_error(req, "avdb", "Out of memory");
|
||||
return -1;
|
||||
}
|
||||
|
||||
dmap_add_int(item, "miid", DAAP_DB_RADIO);
|
||||
dmap_add_long(item, "mper", DAAP_DB_RADIO);
|
||||
dmap_add_int(item, "mdbk", 0x64);
|
||||
dmap_add_int(item, "aeCs", 0);
|
||||
dmap_add_string(item, "minm", name_radio);
|
||||
count = db_pl_get_count(); // TODO This counts too much, should only include stream playlists
|
||||
dmap_add_int(item, "mimc", count);
|
||||
dmap_add_int(item, "mctc", 0);
|
||||
dmap_add_int(item, "aeMk", 1); // com.apple.itunes.extended-media-kind (OR of all in library)
|
||||
dmap_add_int(item, "meds", 3);
|
||||
|
||||
// Create container for radio db
|
||||
dmap_add_container(content, "mlit", EVBUFFER_LENGTH(item));
|
||||
evbuffer_add_buffer(content, item);
|
||||
evbuffer_free(item);
|
||||
|
||||
// Create container
|
||||
dmap_add_container(evbuf, "avdb", EVBUFFER_LENGTH(content) + 61);
|
||||
dmap_add_container(evbuf, "avdb", EVBUFFER_LENGTH(content) + 53);
|
||||
dmap_add_int(evbuf, "mstt", 200); /* 12 */
|
||||
dmap_add_char(evbuf, "muty", 0); /* 9 */
|
||||
dmap_add_int(evbuf, "mtco", 1); /* 12 */
|
||||
dmap_add_int(evbuf, "mrco", 1); /* 12 */
|
||||
dmap_add_container(evbuf, "mlcl", EVBUFFER_LENGTH(content) + 8); /* 8 */
|
||||
dmap_add_container(evbuf, "mlit", EVBUFFER_LENGTH(content)); /* 8 */
|
||||
dmap_add_int(evbuf, "mtco", 2); /* 12 */
|
||||
dmap_add_int(evbuf, "mrco", 2); /* 12 */
|
||||
dmap_add_container(evbuf, "mlcl", EVBUFFER_LENGTH(content)); /* 8 */
|
||||
evbuffer_add_buffer(evbuf, content);
|
||||
evbuffer_free(content);
|
||||
|
||||
|
@ -1506,16 +1548,21 @@ daap_reply_playlists(struct evhttp_request *req, struct evbuffer *evbuf, char **
|
|||
struct daap_session *s;
|
||||
struct evbuffer *playlistlist;
|
||||
struct evbuffer *playlist;
|
||||
cfg_t *lib;
|
||||
const struct dmap_field_map *dfm;
|
||||
const struct dmap_field *df;
|
||||
const struct dmap_field **meta;
|
||||
const char *param;
|
||||
char **strval;
|
||||
int database;
|
||||
int cfg_radiopl;
|
||||
int nmeta;
|
||||
int npls;
|
||||
int32_t plid;
|
||||
int32_t pltype;
|
||||
int32_t plitems;
|
||||
int32_t plstreams;
|
||||
int32_t plparent;
|
||||
int i;
|
||||
int ret;
|
||||
|
||||
|
@ -1523,6 +1570,17 @@ daap_reply_playlists(struct evhttp_request *req, struct evbuffer *evbuf, char **
|
|||
if (!s)
|
||||
return -1;
|
||||
|
||||
ret = safe_atoi32(uri[1], &database);
|
||||
if (ret < 0)
|
||||
{
|
||||
dmap_send_error(req, "aply", "Invalid database ID");
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
lib = cfg_getsec(cfg, "library");
|
||||
cfg_radiopl = cfg_getbool(lib, "radio_playlists");
|
||||
|
||||
ret = evbuffer_expand(evbuf, 61);
|
||||
if (ret < 0)
|
||||
{
|
||||
|
@ -1615,8 +1673,20 @@ daap_reply_playlists(struct evhttp_request *req, struct evbuffer *evbuf, char **
|
|||
if (safe_atoi32(dbpli.items, &plitems) != 0)
|
||||
continue;
|
||||
|
||||
/* Don't add empty smart playlists */
|
||||
if ((plid > 1) && (pltype == 1) && (plitems == 0))
|
||||
plstreams = 0;
|
||||
if (safe_atoi32(dbpli.streams, &plstreams) != 0)
|
||||
continue;
|
||||
|
||||
/* Database DAAP_DB_RADIO is radio, so for that db skip playlists without
|
||||
* streams and for other databases skip playlists which are just streams
|
||||
*/
|
||||
if ((database == DAAP_DB_RADIO) && (plstreams == 0))
|
||||
continue;
|
||||
if (!cfg_radiopl && (database != DAAP_DB_RADIO) && (plstreams > 0) && (plstreams == plitems))
|
||||
continue;
|
||||
|
||||
/* Don't add empty Smart playlists */
|
||||
if ((plid > 1) && (plitems == 0) && (pltype == PL_SMART))
|
||||
continue;
|
||||
|
||||
npls++;
|
||||
|
@ -1630,7 +1700,7 @@ daap_reply_playlists(struct evhttp_request *req, struct evbuffer *evbuf, char **
|
|||
if (dfm == &dfm_dmap_mimc)
|
||||
continue;
|
||||
|
||||
/* com.apple.itunes.smart-playlist - type = 1 AND id != 1 */
|
||||
/* com.apple.itunes.smart-playlist - type = PL_SMART AND id != 1 */
|
||||
if (dfm == &dfm_dmap_aeSP)
|
||||
{
|
||||
if ((pltype == PL_SMART) && (plid != 1))
|
||||
|
@ -1661,11 +1731,14 @@ daap_reply_playlists(struct evhttp_request *req, struct evbuffer *evbuf, char **
|
|||
}
|
||||
|
||||
/* Item count (mimc) */
|
||||
if (plitems > 0)
|
||||
dmap_add_int(playlist, "mimc", plitems);
|
||||
dmap_add_int(playlist, "mimc", plitems);
|
||||
|
||||
/* Container ID (mpco) */
|
||||
dmap_add_int(playlist, "mpco", 0);
|
||||
ret = safe_atoi32(dbpli.parent_id, &plparent);
|
||||
if (ret == 0)
|
||||
dmap_add_int(playlist, "mpco", plparent);
|
||||
else
|
||||
dmap_add_int(playlist, "mpco", 0);
|
||||
|
||||
/* Base playlist (abpl), id = 1 */
|
||||
if (plid == 1)
|
||||
|
@ -2302,9 +2375,9 @@ daap_reply_extra_data(struct evhttp_request *req, struct evbuffer *evbuf, char *
|
|||
}
|
||||
|
||||
if (strcmp(uri[2], "groups") == 0)
|
||||
ret = artwork_get_group(id, max_w, max_h, evbuf);
|
||||
ret = artwork_get_group(evbuf, id, max_w, max_h);
|
||||
else if (strcmp(uri[2], "items") == 0)
|
||||
ret = artwork_get_item(id, max_w, max_h, evbuf);
|
||||
ret = artwork_get_item(evbuf, id, max_w, max_h);
|
||||
|
||||
switch (ret)
|
||||
{
|
||||
|
|
|
@ -1829,7 +1829,7 @@ dacp_reply_nowplayingartwork(struct evhttp_request *req, struct evbuffer *evbuf,
|
|||
if (ret < 0)
|
||||
goto no_artwork;
|
||||
|
||||
ret = artwork_get_item(id, max_w, max_h, evbuf);
|
||||
ret = artwork_get_item(evbuf, id, max_w, max_h);
|
||||
switch (ret)
|
||||
{
|
||||
case ART_FMT_PNG:
|
||||
|
|
|
@ -43,7 +43,7 @@ static int threshold;
|
|||
static int console;
|
||||
static char *logfilename;
|
||||
static FILE *logfile;
|
||||
static char *labels[] = { "config", "daap", "db", "httpd", "main", "mdns", "misc", "rsp", "scan", "xcode", "event", "remote", "dacp", "ffmpeg", "artwork", "player", "raop", "laudio", "dmap", "dbperf", "spotify", "lastfm", "cache", "mpd" };
|
||||
static char *labels[] = { "config", "daap", "db", "httpd", "http", "main", "mdns", "misc", "rsp", "scan", "xcode", "event", "remote", "dacp", "ffmpeg", "artwork", "player", "raop", "laudio", "dmap", "dbperf", "spotify", "lastfm", "cache", "mpd" };
|
||||
static char *severities[] = { "FATAL", "LOG", "WARN", "INFO", "DEBUG", "SPAM" };
|
||||
|
||||
|
||||
|
|
43
src/logger.h
43
src/logger.h
|
@ -9,29 +9,30 @@
|
|||
#define L_DAAP 1
|
||||
#define L_DB 2
|
||||
#define L_HTTPD 3
|
||||
#define L_MAIN 4
|
||||
#define L_MDNS 5
|
||||
#define L_MISC 6
|
||||
#define L_RSP 7
|
||||
#define L_SCAN 8
|
||||
#define L_XCODE 9
|
||||
#define L_HTTP 4
|
||||
#define L_MAIN 5
|
||||
#define L_MDNS 6
|
||||
#define L_MISC 7
|
||||
#define L_RSP 8
|
||||
#define L_SCAN 9
|
||||
#define L_XCODE 10
|
||||
/* libevent logging */
|
||||
#define L_EVENT 10
|
||||
#define L_REMOTE 11
|
||||
#define L_DACP 12
|
||||
#define L_FFMPEG 13
|
||||
#define L_ART 14
|
||||
#define L_PLAYER 15
|
||||
#define L_RAOP 16
|
||||
#define L_LAUDIO 17
|
||||
#define L_DMAP 18
|
||||
#define L_DBPERF 19
|
||||
#define L_SPOTIFY 20
|
||||
#define L_LASTFM 21
|
||||
#define L_CACHE 22
|
||||
#define L_MPD 23
|
||||
#define L_EVENT 11
|
||||
#define L_REMOTE 12
|
||||
#define L_DACP 13
|
||||
#define L_FFMPEG 14
|
||||
#define L_ART 15
|
||||
#define L_PLAYER 16
|
||||
#define L_RAOP 17
|
||||
#define L_LAUDIO 18
|
||||
#define L_DMAP 19
|
||||
#define L_DBPERF 20
|
||||
#define L_SPOTIFY 21
|
||||
#define L_LASTFM 22
|
||||
#define L_CACHE 23
|
||||
#define L_MPD 24
|
||||
|
||||
#define N_LOGDOMAINS 24
|
||||
#define N_LOGDOMAINS 25
|
||||
|
||||
/* Severities */
|
||||
#define E_FATAL 0
|
||||
|
|
15
src/main.c
15
src/main.c
|
@ -65,6 +65,7 @@ GCRY_THREAD_OPTION_PTHREAD_IMPL;
|
|||
#include "mdns.h"
|
||||
#include "remote_pairing.h"
|
||||
#include "player.h"
|
||||
#include "worker.h"
|
||||
#if LIBAVFORMAT_VERSION_MAJOR < 53
|
||||
# include "ffmpeg_url_evbuffer.h"
|
||||
#endif
|
||||
|
@ -676,6 +677,16 @@ main(int argc, char **argv)
|
|||
goto db_fail;
|
||||
}
|
||||
|
||||
/* Spawn worker thread */
|
||||
ret = worker_init();
|
||||
if (ret != 0)
|
||||
{
|
||||
DPRINTF(E_FATAL, L_MAIN, "Worker thread failed to start\n");
|
||||
|
||||
ret = EXIT_FAILURE;
|
||||
goto worker_fail;
|
||||
}
|
||||
|
||||
/* Spawn cache thread */
|
||||
ret = cache_init();
|
||||
if (ret != 0)
|
||||
|
@ -848,6 +859,10 @@ main(int argc, char **argv)
|
|||
cache_deinit();
|
||||
|
||||
cache_fail:
|
||||
DPRINTF(E_LOG, L_MAIN, "Worker deinit\n");
|
||||
worker_deinit();
|
||||
|
||||
worker_fail:
|
||||
DPRINTF(E_LOG, L_MAIN, "Database deinit\n");
|
||||
db_perthread_deinit();
|
||||
db_deinit();
|
||||
|
|
256
src/player.c
256
src/player.c
|
@ -57,6 +57,7 @@
|
|||
#include "player.h"
|
||||
#include "raop.h"
|
||||
#include "laudio.h"
|
||||
#include "worker.h"
|
||||
|
||||
#ifdef LASTFM
|
||||
# include "lastfm.h"
|
||||
|
@ -65,6 +66,7 @@
|
|||
/* These handle getting the media data */
|
||||
#include "transcode.h"
|
||||
#include "pipe.h"
|
||||
#include "http.h"
|
||||
#ifdef HAVE_SPOTIFY_H
|
||||
# include "spotify.h"
|
||||
#endif
|
||||
|
@ -128,6 +130,22 @@ struct item_range
|
|||
uint32_t *id_ptr;
|
||||
};
|
||||
|
||||
struct icy_artwork
|
||||
{
|
||||
uint32_t id;
|
||||
char *artwork_url;
|
||||
};
|
||||
|
||||
struct player_metadata
|
||||
{
|
||||
int id;
|
||||
uint64_t rtptime;
|
||||
uint64_t offset;
|
||||
int startup;
|
||||
|
||||
struct raop_metadata *rmd;
|
||||
};
|
||||
|
||||
struct player_command
|
||||
{
|
||||
pthread_mutex_t lck;
|
||||
|
@ -145,6 +163,7 @@ struct player_command
|
|||
struct raop_device *rd;
|
||||
struct player_status *status;
|
||||
struct player_source *ps;
|
||||
struct player_metadata *pmd;
|
||||
player_status_handler status_handler;
|
||||
uint32_t *id_ptr;
|
||||
uint64_t *raop_ids;
|
||||
|
@ -153,6 +172,7 @@ struct player_command
|
|||
int intval;
|
||||
int ps_pos[2];
|
||||
struct item_range item_range;
|
||||
struct icy_artwork icy;
|
||||
} arg;
|
||||
|
||||
int ret;
|
||||
|
@ -565,6 +585,9 @@ playback_abort(void);
|
|||
static int
|
||||
queue_clear(struct player_command *cmd);
|
||||
|
||||
static void
|
||||
player_metadata_send(struct player_metadata *pmd);
|
||||
|
||||
static void
|
||||
player_laudio_status_cb(enum laudio_state status)
|
||||
{
|
||||
|
@ -613,6 +636,41 @@ player_laudio_status_cb(enum laudio_state status)
|
|||
}
|
||||
}
|
||||
|
||||
/* Callback from the worker thread (async operation as it may block) */
|
||||
static void
|
||||
playcount_inc_cb(void *arg)
|
||||
{
|
||||
int *id = arg;
|
||||
|
||||
db_file_inc_playcount(*id);
|
||||
}
|
||||
|
||||
/* Callback from the worker thread
|
||||
* This prepares metadata in the worker thread, since especially the artwork
|
||||
* retrieval may take some time. raop_metadata_prepare() is thread safe. The
|
||||
* sending, however, must be done in the player thread.
|
||||
*/
|
||||
static void
|
||||
metadata_prepare_cb(void *arg)
|
||||
{
|
||||
struct player_metadata *pmd = arg;
|
||||
|
||||
pmd->rmd = raop_metadata_prepare(pmd->id);
|
||||
|
||||
if (pmd->rmd)
|
||||
player_metadata_send(pmd);
|
||||
}
|
||||
|
||||
/* Callback from the worker thread (async operation as it may block) */
|
||||
static void
|
||||
update_icy_cb(void *arg)
|
||||
{
|
||||
struct http_icy_metadata *metadata = arg;
|
||||
|
||||
db_file_update_icy(metadata->id, metadata->artist, metadata->title);
|
||||
|
||||
http_icy_metadata_free(metadata, 1);
|
||||
}
|
||||
|
||||
/* Metadata */
|
||||
static void
|
||||
|
@ -628,37 +686,72 @@ metadata_purge(void)
|
|||
}
|
||||
|
||||
static void
|
||||
metadata_send(struct player_source *ps, int startup)
|
||||
metadata_trigger(struct player_source *ps, int startup)
|
||||
{
|
||||
uint64_t offset;
|
||||
uint64_t rtptime;
|
||||
struct player_metadata pmd;
|
||||
|
||||
offset = 0;
|
||||
memset(&pmd, 0, sizeof(struct player_metadata));
|
||||
|
||||
pmd.id = ps->id;
|
||||
pmd.startup = startup;
|
||||
|
||||
/* Determine song boundaries, dependent on context */
|
||||
|
||||
/* Restart after pause/seek */
|
||||
if (ps->stream_start)
|
||||
{
|
||||
offset = ps->output_start - ps->stream_start;
|
||||
rtptime = ps->stream_start;
|
||||
pmd.offset = ps->output_start - ps->stream_start;
|
||||
pmd.rtptime = ps->stream_start;
|
||||
}
|
||||
else if (startup)
|
||||
{
|
||||
rtptime = last_rtptime + AIRTUNES_V2_PACKET_SAMPLES;
|
||||
/* Will be set later, right before sending */
|
||||
}
|
||||
/* Generic case */
|
||||
else if (cur_streaming && (cur_streaming->end))
|
||||
{
|
||||
rtptime = cur_streaming->end + 1;
|
||||
pmd.rtptime = cur_streaming->end + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
rtptime = 0;
|
||||
DPRINTF(E_LOG, L_PLAYER, "PTOH! Unhandled song boundary case in metadata_send()\n");
|
||||
DPRINTF(E_LOG, L_PLAYER, "PTOH! Unhandled song boundary case in metadata_trigger()\n");
|
||||
}
|
||||
|
||||
raop_metadata_send(ps->id, rtptime, offset, startup);
|
||||
/* Defer the actual work of preparing the metadata to the worker thread */
|
||||
worker_execute(metadata_prepare_cb, &pmd, sizeof(struct player_metadata), 0);
|
||||
}
|
||||
|
||||
/* Checks if there is new HTTP ICY metadata, and if so sends updates to clients */
|
||||
void
|
||||
metadata_check_icy(void)
|
||||
{
|
||||
struct http_icy_metadata *metadata;
|
||||
int changed;
|
||||
|
||||
transcode_metadata(cur_streaming->ctx, &metadata, &changed);
|
||||
if (!metadata)
|
||||
return;
|
||||
|
||||
if (!changed)
|
||||
goto no_update;
|
||||
|
||||
metadata->id = cur_streaming->id;
|
||||
|
||||
/* Defer the database update to the worker thread */
|
||||
worker_execute(update_icy_cb, metadata, sizeof(struct http_icy_metadata), 0);
|
||||
|
||||
/* Triggers preparing and sending RAOP metadata */
|
||||
metadata_trigger(cur_streaming, 0);
|
||||
|
||||
/* Only free the struct, the content must be preserved for update_icy_cb */
|
||||
free(metadata);
|
||||
|
||||
status_update(player_state);
|
||||
|
||||
return;
|
||||
|
||||
no_update:
|
||||
http_icy_metadata_free(metadata, 0);
|
||||
}
|
||||
|
||||
/* Audio sources */
|
||||
|
@ -1059,7 +1152,8 @@ source_free(struct player_source *ps)
|
|||
{
|
||||
switch (ps->type)
|
||||
{
|
||||
case SOURCE_FFMPEG:
|
||||
case SOURCE_FILE:
|
||||
case SOURCE_HTTP:
|
||||
if (ps->ctx)
|
||||
transcode_cleanup(ps->ctx);
|
||||
break;
|
||||
|
@ -1087,7 +1181,8 @@ source_stop(struct player_source *ps)
|
|||
{
|
||||
switch (ps->type)
|
||||
{
|
||||
case SOURCE_FFMPEG:
|
||||
case SOURCE_FILE:
|
||||
case SOURCE_HTTP:
|
||||
if (ps->ctx)
|
||||
{
|
||||
transcode_cleanup(ps->ctx);
|
||||
|
@ -1245,6 +1340,7 @@ static int
|
|||
source_open(struct player_source *ps, int no_md)
|
||||
{
|
||||
struct media_file_info *mfi;
|
||||
char *url;
|
||||
int ret;
|
||||
|
||||
ps->setup_done = 0;
|
||||
|
@ -1274,6 +1370,19 @@ source_open(struct player_source *ps, int no_md)
|
|||
// Setup the source type responsible for getting the audio
|
||||
switch (mfi->data_kind)
|
||||
{
|
||||
case 1:
|
||||
ps->type = SOURCE_HTTP;
|
||||
|
||||
ret = http_stream_setup(&url, mfi->path);
|
||||
if (ret < 0)
|
||||
break;
|
||||
|
||||
free(mfi->path);
|
||||
mfi->path = url;
|
||||
|
||||
ret = transcode_setup(&ps->ctx, mfi, NULL, 0);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
ps->type = SOURCE_SPOTIFY;
|
||||
#ifdef HAVE_SPOTIFY_H
|
||||
|
@ -1289,7 +1398,7 @@ source_open(struct player_source *ps, int no_md)
|
|||
break;
|
||||
|
||||
default:
|
||||
ps->type = SOURCE_FFMPEG;
|
||||
ps->type = SOURCE_FILE;
|
||||
ret = transcode_setup(&ps->ctx, mfi, NULL, 0);
|
||||
}
|
||||
|
||||
|
@ -1303,7 +1412,7 @@ source_open(struct player_source *ps, int no_md)
|
|||
}
|
||||
|
||||
if (!no_md)
|
||||
metadata_send(ps, (player_state == PLAY_PLAYING) ? 0 : 1);
|
||||
metadata_trigger(ps, (player_state == PLAY_PLAYING) ? 0 : 1);
|
||||
|
||||
ps->setup_done = 1;
|
||||
|
||||
|
@ -1347,7 +1456,7 @@ source_next(int force)
|
|||
if (!cur_streaming)
|
||||
break;
|
||||
|
||||
if ((cur_streaming->type == SOURCE_FFMPEG) && cur_streaming->ctx)
|
||||
if ((cur_streaming->type == SOURCE_FILE) && cur_streaming->ctx)
|
||||
{
|
||||
ret = transcode_seek(cur_streaming->ctx, 0);
|
||||
|
||||
|
@ -1356,7 +1465,7 @@ source_next(int force)
|
|||
* so we have to handle metadata ourselves here
|
||||
*/
|
||||
if (ret >= 0)
|
||||
metadata_send(cur_streaming, 0);
|
||||
metadata_trigger(cur_streaming, 0);
|
||||
}
|
||||
else
|
||||
ret = source_open(cur_streaming, force);
|
||||
|
@ -1538,6 +1647,7 @@ source_check(void)
|
|||
uint64_t pos;
|
||||
enum repeat_mode r_mode;
|
||||
int i;
|
||||
int id;
|
||||
int ret;
|
||||
|
||||
if (!cur_streaming)
|
||||
|
@ -1585,7 +1695,7 @@ source_check(void)
|
|||
|
||||
if (ps->setup_done)
|
||||
{
|
||||
if ((ps->type == SOURCE_FFMPEG) && ps->ctx)
|
||||
if ((ps->type == SOURCE_FILE) && ps->ctx)
|
||||
{
|
||||
transcode_cleanup(ps->ctx);
|
||||
ps->ctx = NULL;
|
||||
|
@ -1614,9 +1724,10 @@ source_check(void)
|
|||
{
|
||||
i++;
|
||||
|
||||
db_file_inc_playcount((int)cur_playing->id);
|
||||
id = (int)cur_playing->id;
|
||||
worker_execute(playcount_inc_cb, &id, sizeof(int), 5);
|
||||
#ifdef LASTFM
|
||||
lastfm_scrobble((int)cur_playing->id);
|
||||
lastfm_scrobble(id);
|
||||
#endif
|
||||
|
||||
/* Stop playback if:
|
||||
|
@ -1639,7 +1750,7 @@ source_check(void)
|
|||
|
||||
if (ps->setup_done)
|
||||
{
|
||||
if ((ps->type == SOURCE_FFMPEG) && ps->ctx)
|
||||
if ((ps->type == SOURCE_FILE) && ps->ctx)
|
||||
{
|
||||
transcode_cleanup(ps->ctx);
|
||||
ps->ctx = NULL;
|
||||
|
@ -1700,6 +1811,7 @@ source_read(uint8_t *buf, int len, uint64_t rtptime)
|
|||
int new;
|
||||
int ret;
|
||||
int nbytes;
|
||||
int icy_timer;
|
||||
|
||||
if (!cur_streaming)
|
||||
return 0;
|
||||
|
@ -1726,8 +1838,15 @@ source_read(uint8_t *buf, int len, uint64_t rtptime)
|
|||
{
|
||||
switch (cur_streaming->type)
|
||||
{
|
||||
case SOURCE_FFMPEG:
|
||||
ret = transcode(cur_streaming->ctx, audio_buf, len - nbytes);
|
||||
case SOURCE_HTTP:
|
||||
ret = transcode(cur_streaming->ctx, audio_buf, len - nbytes, &icy_timer);
|
||||
|
||||
if (icy_timer)
|
||||
metadata_check_icy();
|
||||
break;
|
||||
|
||||
case SOURCE_FILE:
|
||||
ret = transcode(cur_streaming->ctx, audio_buf, len - nbytes, &icy_timer);
|
||||
break;
|
||||
|
||||
#ifdef HAVE_SPOTIFY_H
|
||||
|
@ -2032,6 +2151,24 @@ device_remove_family(struct player_command *cmd)
|
|||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
metadata_send(struct player_command *cmd)
|
||||
{
|
||||
struct player_metadata *pmd;
|
||||
|
||||
pmd = cmd->arg.pmd;
|
||||
|
||||
/* Do the setting of rtptime which was deferred in metadata_trigger because we
|
||||
* wanted to wait until we had the actual last_rtptime
|
||||
*/
|
||||
if ((pmd->rtptime == 0) && (pmd->startup))
|
||||
pmd->rtptime = last_rtptime + AIRTUNES_V2_PACKET_SAMPLES;
|
||||
|
||||
raop_metadata_send(pmd->rmd, pmd->rtptime, pmd->offset, pmd->startup);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* RAOP callbacks executed in the player thread */
|
||||
static void
|
||||
device_streaming_cb(struct raop_device *dev, struct raop_session *rs, enum raop_session_state status)
|
||||
|
@ -2457,6 +2594,29 @@ now_playing(struct player_command *cmd)
|
|||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
artwork_url_get(struct player_command *cmd)
|
||||
{
|
||||
struct player_source *ps;
|
||||
|
||||
cmd->arg.icy.artwork_url = NULL;
|
||||
|
||||
if (cur_playing)
|
||||
ps = cur_playing;
|
||||
else if (cur_streaming)
|
||||
ps = cur_streaming;
|
||||
else
|
||||
return -1;
|
||||
|
||||
/* Check that we are playing a viable stream, and that it has the requested id */
|
||||
if (!ps->ctx || ps->type != SOURCE_HTTP || ps->id != cmd->arg.icy.id)
|
||||
return -1;
|
||||
|
||||
transcode_metadata_artwork_url(ps->ctx, &cmd->arg.icy.artwork_url);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
playback_stop(struct player_command *cmd)
|
||||
{
|
||||
|
@ -2719,7 +2879,7 @@ playback_start(struct player_command *cmd)
|
|||
* After a pause, the source is still open so source_open() doesn't get
|
||||
* called and we have to handle metadata ourselves.
|
||||
*/
|
||||
metadata_send(cur_streaming, 1);
|
||||
metadata_trigger(cur_streaming, 1);
|
||||
}
|
||||
|
||||
/* Start local audio if needed */
|
||||
|
@ -2907,7 +3067,7 @@ playback_seek_bh(struct player_command *cmd)
|
|||
/* Seek to commanded position */
|
||||
switch (ps->type)
|
||||
{
|
||||
case SOURCE_FFMPEG:
|
||||
case SOURCE_FILE:
|
||||
ret = transcode_seek(ps->ctx, ms);
|
||||
break;
|
||||
#ifdef HAVE_SPOTIFY_H
|
||||
|
@ -2916,8 +3076,10 @@ playback_seek_bh(struct player_command *cmd)
|
|||
break;
|
||||
#endif
|
||||
case SOURCE_PIPE:
|
||||
case SOURCE_HTTP:
|
||||
ret = 1;
|
||||
break;
|
||||
|
||||
default:
|
||||
ret = -1;
|
||||
}
|
||||
|
@ -2964,7 +3126,7 @@ playback_pause_bh(struct player_command *cmd)
|
|||
|
||||
switch (ps->type)
|
||||
{
|
||||
case SOURCE_FFMPEG:
|
||||
case SOURCE_FILE:
|
||||
ret = transcode_seek(ps->ctx, ms);
|
||||
break;
|
||||
#ifdef HAVE_SPOTIFY_H
|
||||
|
@ -4110,6 +4272,31 @@ player_now_playing(uint32_t *id)
|
|||
return ret;
|
||||
}
|
||||
|
||||
char *
|
||||
player_get_icy_artwork_url(uint32_t id)
|
||||
{
|
||||
struct player_command cmd;
|
||||
int ret;
|
||||
|
||||
command_init(&cmd);
|
||||
|
||||
cmd.func = artwork_url_get;
|
||||
cmd.func_bh = NULL;
|
||||
cmd.arg.icy.id = id;
|
||||
|
||||
if (pthread_self() != tid_player)
|
||||
ret = sync_command(&cmd);
|
||||
else
|
||||
ret = artwork_url_get(&cmd);
|
||||
|
||||
command_deinit(&cmd);
|
||||
|
||||
if (ret < 0)
|
||||
return NULL;
|
||||
else
|
||||
return cmd.arg.icy.artwork_url;
|
||||
}
|
||||
|
||||
/*
|
||||
* Starts/resumes playback
|
||||
*
|
||||
|
@ -4700,6 +4887,23 @@ player_device_remove(struct raop_device *rd)
|
|||
}
|
||||
}
|
||||
|
||||
/* Thread: worker */
|
||||
static void
|
||||
player_metadata_send(struct player_metadata *pmd)
|
||||
{
|
||||
struct player_command cmd;
|
||||
|
||||
command_init(&cmd);
|
||||
|
||||
cmd.func = metadata_send;
|
||||
cmd.func_bh = NULL;
|
||||
cmd.arg.pmd = pmd;
|
||||
|
||||
sync_command(&cmd);
|
||||
|
||||
command_deinit(&cmd);
|
||||
}
|
||||
|
||||
|
||||
/* RAOP devices discovery - mDNS callback */
|
||||
/* Thread: main (mdns) */
|
||||
|
|
|
@ -32,9 +32,10 @@ enum repeat_mode {
|
|||
};
|
||||
|
||||
enum source_type {
|
||||
SOURCE_FFMPEG = 0,
|
||||
SOURCE_FILE = 0,
|
||||
SOURCE_SPOTIFY,
|
||||
SOURCE_PIPE,
|
||||
SOURCE_HTTP,
|
||||
};
|
||||
|
||||
struct spk_flags {
|
||||
|
@ -132,6 +133,9 @@ player_get_status(struct player_status *status);
|
|||
int
|
||||
player_now_playing(uint32_t *id);
|
||||
|
||||
char *
|
||||
player_get_icy_artwork_url(uint32_t id);
|
||||
|
||||
void
|
||||
player_speaker_enumerate(spk_enum_cb cb, void *arg);
|
||||
|
||||
|
|
81
src/raop.c
81
src/raop.c
|
@ -751,8 +751,9 @@ raop_metadata_prune(uint64_t rtptime)
|
|||
}
|
||||
}
|
||||
|
||||
static struct raop_metadata *
|
||||
raop_metadata_prepare(int id, uint64_t rtptime)
|
||||
/* Thread: worker */
|
||||
struct raop_metadata *
|
||||
raop_metadata_prepare(int id)
|
||||
{
|
||||
struct query_params qp;
|
||||
struct db_media_file_info dbmfi;
|
||||
|
@ -772,6 +773,28 @@ raop_metadata_prepare(int id, uint64_t rtptime)
|
|||
|
||||
memset(rmd, 0, sizeof(struct raop_metadata));
|
||||
|
||||
/* Get artwork */
|
||||
rmd->artwork = evbuffer_new();
|
||||
if (!rmd->artwork)
|
||||
{
|
||||
DPRINTF(E_LOG, L_RAOP, "Out of memory for artwork evbuffer; no artwork will be sent\n");
|
||||
|
||||
goto skip_artwork;
|
||||
}
|
||||
|
||||
ret = artwork_get_item(rmd->artwork, id, 600, 600);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_INFO, L_RAOP, "Failed to retrieve artwork for file id %d; no artwork will be sent\n", id);
|
||||
|
||||
evbuffer_free(rmd->artwork);
|
||||
rmd->artwork = NULL;
|
||||
}
|
||||
|
||||
rmd->artwork_fmt = ret;
|
||||
|
||||
skip_artwork:
|
||||
|
||||
/* Get dbmfi */
|
||||
memset(&qp, 0, sizeof(struct query_params));
|
||||
qp.type = Q_ITEMS;
|
||||
|
@ -839,40 +862,11 @@ raop_metadata_prepare(int id, uint64_t rtptime)
|
|||
goto out_metadata;
|
||||
}
|
||||
|
||||
rmd->start = rtptime;
|
||||
rmd->end = rtptime + (duration * 44100UL) / 1000UL;
|
||||
|
||||
/* Get artwork */
|
||||
rmd->artwork = evbuffer_new();
|
||||
if (!rmd->artwork)
|
||||
{
|
||||
DPRINTF(E_LOG, L_RAOP, "Out of memory for artwork evbuffer; no artwork will be sent\n");
|
||||
|
||||
goto skip_artwork;
|
||||
}
|
||||
|
||||
ret = artwork_get_item(id, 600, 600, rmd->artwork);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_INFO, L_RAOP, "Failed to retrieve artwork for '%s' (%d); no artwork will be sent\n", dbmfi.title, id);
|
||||
|
||||
evbuffer_free(rmd->artwork);
|
||||
rmd->artwork = NULL;
|
||||
}
|
||||
|
||||
rmd->artwork_fmt = ret;
|
||||
|
||||
skip_artwork:
|
||||
db_query_end(&qp);
|
||||
|
||||
/* Add rmd to metadata list */
|
||||
if (metadata_tail)
|
||||
metadata_tail->next = rmd;
|
||||
else
|
||||
{
|
||||
metadata_head = rmd;
|
||||
metadata_tail = rmd;
|
||||
}
|
||||
/* raop_metadata_send() will add rtptime to these */
|
||||
rmd->start = 0;
|
||||
rmd->end = (duration * 44100UL) / 1000UL;
|
||||
|
||||
return rmd;
|
||||
|
||||
|
@ -2163,16 +2157,23 @@ raop_metadata_startup_send(struct raop_session *rs)
|
|||
}
|
||||
|
||||
void
|
||||
raop_metadata_send(int id, uint64_t rtptime, uint64_t offset, int startup)
|
||||
raop_metadata_send(struct raop_metadata *rmd, uint64_t rtptime, uint64_t offset, int startup)
|
||||
{
|
||||
struct raop_session *rs;
|
||||
struct raop_metadata *rmd;
|
||||
uint32_t delay;
|
||||
int ret;
|
||||
|
||||
rmd = raop_metadata_prepare(id, rtptime);
|
||||
if (!rmd)
|
||||
return;
|
||||
rmd->start += rtptime;
|
||||
rmd->end += rtptime;
|
||||
|
||||
/* Add the rmd to the metadata list */
|
||||
if (metadata_tail)
|
||||
metadata_tail->next = rmd;
|
||||
else
|
||||
{
|
||||
metadata_head = rmd;
|
||||
metadata_tail = rmd;
|
||||
}
|
||||
|
||||
for (rs = sessions; rs; rs = rs->next)
|
||||
{
|
||||
|
@ -2188,13 +2189,11 @@ raop_metadata_send(int id, uint64_t rtptime, uint64_t offset, int startup)
|
|||
if (ret < 0)
|
||||
{
|
||||
raop_session_failure(rs);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Volume handling */
|
||||
static float
|
||||
raop_volume_convert(int volume, char *name)
|
||||
|
|
11
src/raop.h
11
src/raop.h
|
@ -26,6 +26,7 @@ enum raop_devtype {
|
|||
};
|
||||
|
||||
struct raop_session;
|
||||
struct raop_metadata;
|
||||
|
||||
struct raop_device
|
||||
{
|
||||
|
@ -90,13 +91,17 @@ enum raop_session_state
|
|||
|
||||
typedef void (*raop_status_cb)(struct raop_device *dev, struct raop_session *rs, enum raop_session_state status);
|
||||
|
||||
|
||||
void
|
||||
raop_metadata_purge(void);
|
||||
|
||||
void
|
||||
raop_metadata_prune(uint64_t rtptime);
|
||||
|
||||
struct raop_metadata *
|
||||
raop_metadata_prepare(int id);
|
||||
|
||||
void
|
||||
raop_metadata_send(struct raop_metadata *rmd, uint64_t rtptime, uint64_t offset, int startup);
|
||||
|
||||
int
|
||||
raop_device_probe(struct raop_device *rd, raop_status_cb cb);
|
||||
|
@ -113,10 +118,6 @@ raop_playback_start(uint64_t next_pkt, struct timespec *ts);
|
|||
void
|
||||
raop_playback_stop(void);
|
||||
|
||||
|
||||
void
|
||||
raop_metadata_send(int id, uint64_t rtptime, uint64_t offset, int startup);
|
||||
|
||||
int
|
||||
raop_set_volume_one(struct raop_session *rs, int volume, raop_status_cb cb);
|
||||
|
||||
|
|
|
@ -140,6 +140,8 @@ static void *g_libhandle;
|
|||
static enum spotify_state g_state;
|
||||
/* (not used) Tells which commmand is currently being processed */
|
||||
static struct spotify_command *g_cmd;
|
||||
// The global base playlist id (parent of all Spotify playlists in the db)
|
||||
static int g_base_plid;
|
||||
|
||||
// Audio fifo
|
||||
static audio_fifo_t *g_audio_fifo;
|
||||
|
@ -465,7 +467,7 @@ thread_exit(void)
|
|||
/* Should only be called from within the spotify thread */
|
||||
|
||||
static int
|
||||
spotify_metadata_get(sp_track *track, struct media_file_info *mfi, char *pltitle)
|
||||
spotify_metadata_get(sp_track *track, struct media_file_info *mfi, const char *pltitle)
|
||||
{
|
||||
cfg_t *spotify_cfg;
|
||||
bool artist_override;
|
||||
|
@ -547,7 +549,7 @@ spotify_metadata_get(sp_track *track, struct media_file_info *mfi, char *pltitle
|
|||
}
|
||||
|
||||
static int
|
||||
spotify_track_save(int plid, sp_track *track, char *pltitle)
|
||||
spotify_track_save(int plid, sp_track *track, const char *pltitle)
|
||||
{
|
||||
struct media_file_info mfi;
|
||||
sp_link *link;
|
||||
|
@ -613,7 +615,6 @@ spotify_playlist_save(sp_playlist *pl)
|
|||
sp_link *link;
|
||||
char url[1024];
|
||||
const char *name;
|
||||
char title[512];
|
||||
int plid;
|
||||
int num_tracks;
|
||||
char virtual_path[PATH_MAX];
|
||||
|
@ -628,6 +629,10 @@ spotify_playlist_save(sp_playlist *pl)
|
|||
|
||||
name = fptr_sp_playlist_name(pl);
|
||||
|
||||
// The starred playlist has an empty name, set it manually to "Starred"
|
||||
if (*name == '\0')
|
||||
name = "Starred";
|
||||
|
||||
DPRINTF(E_INFO, L_SPOTIFY, "Saving playlist: '%s'\n", name);
|
||||
|
||||
/* Save playlist (playlists table) */
|
||||
|
@ -645,17 +650,9 @@ spotify_playlist_save(sp_playlist *pl)
|
|||
}
|
||||
fptr_sp_link_release(link);
|
||||
|
||||
// sleep(1); // Primitive way of preventing database locking (the mutex wasn't working)
|
||||
|
||||
pli = db_pl_fetch_bypath(url);
|
||||
|
||||
// The starred playlist has an empty name, set it manually to "Starred"
|
||||
if (*name == '\0')
|
||||
snprintf(title, sizeof(title), "[s] Starred");
|
||||
else
|
||||
snprintf(title, sizeof(title), "[s] %s", name);
|
||||
|
||||
snprintf(virtual_path, PATH_MAX, "/spotify:/%s", title);
|
||||
snprintf(virtual_path, PATH_MAX, "/spotify:/%s", name);
|
||||
|
||||
if (pli)
|
||||
{
|
||||
|
@ -663,30 +660,53 @@ spotify_playlist_save(sp_playlist *pl)
|
|||
|
||||
plid = pli->id;
|
||||
|
||||
free_pli(pli, 0);
|
||||
free(pli->title);
|
||||
pli->title = strdup(name);
|
||||
free(pli->virtual_path);
|
||||
pli->virtual_path = strdup(virtual_path);
|
||||
|
||||
ret = db_pl_update(title, url, virtual_path, plid);
|
||||
ret = db_pl_update(pli);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SPOTIFY, "Error updating playlist ('%s', link %s)\n", name, url);
|
||||
|
||||
free_pli(pli, 0);
|
||||
return -1;
|
||||
}
|
||||
|
||||
db_pl_ping(plid);
|
||||
db_pl_clear_items(plid);
|
||||
}
|
||||
else
|
||||
{
|
||||
DPRINTF(E_DBG, L_SPOTIFY, "Adding playlist ('%s', link %s)\n", name, url);
|
||||
|
||||
ret = db_pl_add(title, url, virtual_path, &plid);
|
||||
pli = (struct playlist_info *)malloc(sizeof(struct playlist_info));
|
||||
if (!pli)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Out of memory\n");
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(pli, 0, sizeof(struct playlist_info));
|
||||
|
||||
pli->title = strdup(name);
|
||||
pli->path = strdup(url);
|
||||
pli->virtual_path = strdup(virtual_path);
|
||||
pli->parent_id = g_base_plid;
|
||||
|
||||
ret = db_pl_add(pli, &plid);
|
||||
if ((ret < 0) || (plid < 1))
|
||||
{
|
||||
DPRINTF(E_LOG, L_SPOTIFY, "Error adding playlist ('%s', link %s, ret %d, plid %d)\n", name, url, ret, plid);
|
||||
|
||||
free_pli(pli, 0);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
free_pli(pli, 0);
|
||||
|
||||
/* Save tracks and playlistitems (files and playlistitems table) */
|
||||
num_tracks = fptr_sp_playlist_num_tracks(pl);
|
||||
for (i = 0; i < num_tracks; i++)
|
||||
|
@ -698,7 +718,7 @@ spotify_playlist_save(sp_playlist *pl)
|
|||
continue;
|
||||
}
|
||||
|
||||
ret = spotify_track_save(plid, track, title);
|
||||
ret = spotify_track_save(plid, track, name);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SPOTIFY, "Error saving track %d to playlist '%s' (id %d)\n", i, name, plid);
|
||||
|
@ -1225,8 +1245,11 @@ artwork_get(struct spotify_command *cmd)
|
|||
static void
|
||||
logged_in(sp_session *sess, sp_error error)
|
||||
{
|
||||
cfg_t *spotify_cfg;
|
||||
sp_playlist *pl;
|
||||
sp_playlistcontainer *pc;
|
||||
struct playlist_info pli;
|
||||
int ret;
|
||||
int i;
|
||||
|
||||
if (SP_ERROR_OK != error)
|
||||
|
@ -1242,6 +1265,24 @@ logged_in(sp_session *sess, sp_error error)
|
|||
pl = fptr_sp_session_starred_create(sess);
|
||||
fptr_sp_playlist_add_callbacks(pl, &pl_callbacks, NULL);
|
||||
|
||||
spotify_cfg = cfg_getsec(cfg, "spotify");
|
||||
if (! cfg_getbool(spotify_cfg, "base_playlist_disable"))
|
||||
{
|
||||
memset(&pli, 0, sizeof(struct playlist_info));
|
||||
pli.title = "Spotify";
|
||||
pli.type = PL_FOLDER;
|
||||
pli.path = "spotify:playlistfolder";
|
||||
|
||||
ret = db_pl_add(&pli, &g_base_plid);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SPOTIFY, "Error adding base playlist\n");
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
g_base_plid = 0;
|
||||
|
||||
pc = fptr_sp_session_playlistcontainer(sess);
|
||||
|
||||
fptr_sp_playlistcontainer_add_callbacks(pc, &pc_callbacks, NULL);
|
||||
|
|
|
@ -63,6 +63,8 @@
|
|||
# define XCODE_BUFFER_SIZE ((AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) / 2)
|
||||
#endif
|
||||
|
||||
/* Interval between ICY metadata checks for streams, in seconds */
|
||||
#define METADATA_ICY_INTERVAL 5
|
||||
|
||||
struct transcode_ctx {
|
||||
AVFormatContext *fmtctx;
|
||||
|
@ -90,6 +92,7 @@ struct transcode_ctx {
|
|||
|
||||
uint32_t duration;
|
||||
uint64_t samples;
|
||||
uint32_t icy_hash;
|
||||
|
||||
/* WAV header */
|
||||
int wavhdr;
|
||||
|
@ -152,7 +155,7 @@ make_wav_header(struct transcode_ctx *ctx, off_t *est_size)
|
|||
|
||||
|
||||
int
|
||||
transcode(struct transcode_ctx *ctx, struct evbuffer *evbuf, int wanted)
|
||||
transcode(struct transcode_ctx *ctx, struct evbuffer *evbuf, int wanted, int *icy_timer)
|
||||
{
|
||||
int16_t *buf;
|
||||
int buflen;
|
||||
|
@ -392,6 +395,7 @@ transcode(struct transcode_ctx *ctx, struct evbuffer *evbuf, int wanted)
|
|||
av_free(frame);
|
||||
#endif
|
||||
|
||||
*icy_timer = (ctx->offset % (METADATA_ICY_INTERVAL * 2 * 2 * 44100) < processed);
|
||||
return processed;
|
||||
}
|
||||
|
||||
|
@ -485,6 +489,7 @@ transcode_seek(struct transcode_ctx *ctx, int ms)
|
|||
int
|
||||
transcode_setup(struct transcode_ctx **nctx, struct media_file_info *mfi, off_t *est_size, int wavhdr)
|
||||
{
|
||||
AVDictionary *options;
|
||||
struct transcode_ctx *ctx;
|
||||
int ret;
|
||||
|
||||
|
@ -497,6 +502,8 @@ transcode_setup(struct transcode_ctx **nctx, struct media_file_info *mfi, off_t
|
|||
}
|
||||
memset(ctx, 0, sizeof(struct transcode_ctx));
|
||||
|
||||
options = NULL;
|
||||
|
||||
#if LIBAVFORMAT_VERSION_MAJOR >= 54 || (LIBAVFORMAT_VERSION_MAJOR == 53 && LIBAVFORMAT_VERSION_MINOR >= 3)
|
||||
# ifndef HAVE_FFMPEG
|
||||
// Without this, libav is slow to probe some internet streams, which leads to RAOP timeouts
|
||||
|
@ -506,8 +513,13 @@ transcode_setup(struct transcode_ctx **nctx, struct media_file_info *mfi, off_t
|
|||
ctx->fmtctx->probesize = 64000;
|
||||
}
|
||||
# endif
|
||||
if (mfi->data_kind == 1)
|
||||
av_dict_set(&options, "icy", "1", 0);
|
||||
|
||||
ret = avformat_open_input(&ctx->fmtctx, mfi->path, NULL, NULL);
|
||||
ret = avformat_open_input(&ctx->fmtctx, mfi->path, NULL, &options);
|
||||
|
||||
if (options)
|
||||
av_dict_free(&options);
|
||||
#else
|
||||
ret = av_open_input_file(&ctx->fmtctx, mfi->path, NULL, 0, NULL);
|
||||
#endif
|
||||
|
@ -894,3 +906,44 @@ transcode_needed(const char *user_agent, const char *client_codecs, char *file_c
|
|||
|
||||
return 1;
|
||||
}
|
||||
|
||||
void
|
||||
transcode_metadata(struct transcode_ctx *ctx, struct http_icy_metadata **metadata, int *changed)
|
||||
{
|
||||
struct http_icy_metadata *m;
|
||||
|
||||
*metadata = NULL;
|
||||
|
||||
if (!ctx->fmtctx)
|
||||
return;
|
||||
|
||||
m = http_icy_metadata_get(ctx->fmtctx, 1);
|
||||
if (!m)
|
||||
return;
|
||||
|
||||
*changed = (m->hash != ctx->icy_hash);
|
||||
|
||||
ctx->icy_hash = m->hash;
|
||||
|
||||
*metadata = m;
|
||||
}
|
||||
|
||||
void
|
||||
transcode_metadata_artwork_url(struct transcode_ctx *ctx, char **artwork_url)
|
||||
{
|
||||
struct http_icy_metadata *m;
|
||||
|
||||
*artwork_url = NULL;
|
||||
|
||||
if (!ctx->fmtctx || !ctx->fmtctx->filename)
|
||||
return;
|
||||
|
||||
m = http_icy_metadata_get(ctx->fmtctx, 1);
|
||||
if (!m)
|
||||
return;
|
||||
|
||||
if (m->artwork_url)
|
||||
*artwork_url = strdup(m->artwork_url);
|
||||
|
||||
http_icy_metadata_free(m, 0);
|
||||
}
|
||||
|
|
|
@ -7,11 +7,12 @@
|
|||
#else
|
||||
# include <event.h>
|
||||
#endif
|
||||
#include "http.h"
|
||||
|
||||
struct transcode_ctx;
|
||||
|
||||
int
|
||||
transcode(struct transcode_ctx *ctx, struct evbuffer *evbuf, int wanted);
|
||||
transcode(struct transcode_ctx *ctx, struct evbuffer *evbuf, int wanted, int *icy_timer);
|
||||
|
||||
int
|
||||
transcode_seek(struct transcode_ctx *ctx, int ms);
|
||||
|
@ -25,4 +26,10 @@ transcode_cleanup(struct transcode_ctx *ctx);
|
|||
int
|
||||
transcode_needed(const char *user_agent, const char *client_codecs, char *file_codectype);
|
||||
|
||||
void
|
||||
transcode_metadata(struct transcode_ctx *ctx, struct http_icy_metadata **metadata, int *changed);
|
||||
|
||||
void
|
||||
transcode_metadata_artwork_url(struct transcode_ctx *ctx, char **artwork_url);
|
||||
|
||||
#endif /* !__TRANSCODE_H__ */
|
||||
|
|
|
@ -0,0 +1,384 @@
|
|||
/*
|
||||
* Copyright (C) 2014 Espen Jürgensen <espenjurgensen@gmail.com>
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
# include <config.h>
|
||||
#endif
|
||||
|
||||
#include <stdio.h>
|
||||
#include <inttypes.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
|
||||
#include <event2/event.h>
|
||||
|
||||
#include "db.h"
|
||||
#include "logger.h"
|
||||
#include "worker.h"
|
||||
|
||||
|
||||
struct worker_command;
|
||||
|
||||
typedef int (*cmd_func)(struct worker_command *cmd);
|
||||
|
||||
struct worker_command
|
||||
{
|
||||
pthread_mutex_t lck;
|
||||
pthread_cond_t cond;
|
||||
|
||||
cmd_func func;
|
||||
|
||||
int nonblock;
|
||||
|
||||
struct {
|
||||
void (*cb)(void *);
|
||||
void *cb_arg;
|
||||
int delay;
|
||||
struct event *timer;
|
||||
} arg;
|
||||
|
||||
int ret;
|
||||
};
|
||||
|
||||
|
||||
/* --- Globals --- */
|
||||
// worker thread
|
||||
static pthread_t tid_worker;
|
||||
|
||||
// Event base, pipes and events
|
||||
struct event_base *evbase_worker;
|
||||
static int g_initialized;
|
||||
static int g_exit_pipe[2];
|
||||
static int g_cmd_pipe[2];
|
||||
static struct event *g_exitev;
|
||||
static struct event *g_cmdev;
|
||||
|
||||
/* ---------------------------- CALLBACK EXECUTION ------------------------- */
|
||||
/* Thread: worker */
|
||||
|
||||
static void
|
||||
execute_cb(int fd, short what, void *arg)
|
||||
{
|
||||
struct worker_command *cmd = arg;
|
||||
|
||||
cmd->arg.cb(cmd->arg.cb_arg);
|
||||
|
||||
event_free(cmd->arg.timer);
|
||||
free(cmd->arg.cb_arg);
|
||||
free(cmd);
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
execute(struct worker_command *cmd)
|
||||
{
|
||||
struct timeval tv = { cmd->arg.delay, 0 };
|
||||
|
||||
if (cmd->arg.delay)
|
||||
{
|
||||
cmd->arg.timer = evtimer_new(evbase_worker, execute_cb, cmd);
|
||||
evtimer_add(cmd->arg.timer, &tv);
|
||||
|
||||
return 1; // Not done yet, ask caller not to free cmd
|
||||
}
|
||||
|
||||
cmd->arg.cb(cmd->arg.cb_arg);
|
||||
free(cmd->arg.cb_arg);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------------- COMMAND EXECUTION -------------------------- */
|
||||
|
||||
static int
|
||||
send_command(struct worker_command *cmd)
|
||||
{
|
||||
int ret;
|
||||
|
||||
if (!cmd->func)
|
||||
{
|
||||
DPRINTF(E_LOG, L_MAIN, "BUG: cmd->func is NULL!\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
ret = write(g_cmd_pipe[1], &cmd, sizeof(cmd));
|
||||
if (ret != sizeof(cmd))
|
||||
{
|
||||
DPRINTF(E_LOG, L_MAIN, "Could not send command: %s\n", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
nonblock_command(struct worker_command *cmd)
|
||||
{
|
||||
int ret;
|
||||
|
||||
ret = send_command(cmd);
|
||||
if (ret < 0)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Thread: main */
|
||||
static void
|
||||
thread_exit(void)
|
||||
{
|
||||
int dummy = 42;
|
||||
|
||||
DPRINTF(E_DBG, L_MAIN, "Killing worker thread\n");
|
||||
|
||||
if (write(g_exit_pipe[1], &dummy, sizeof(dummy)) != sizeof(dummy))
|
||||
DPRINTF(E_LOG, L_MAIN, "Could not write to exit fd: %s\n", strerror(errno));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* --------------------------------- MAIN --------------------------------- */
|
||||
/* Thread: worker */
|
||||
|
||||
static void *
|
||||
worker(void *arg)
|
||||
{
|
||||
int ret;
|
||||
|
||||
ret = db_perthread_init();
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_MAIN, "Error: DB init failed (worker thread)\n");
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
|
||||
g_initialized = 1;
|
||||
|
||||
event_base_dispatch(evbase_worker);
|
||||
|
||||
if (g_initialized)
|
||||
{
|
||||
DPRINTF(E_LOG, L_MAIN, "Worker event loop terminated ahead of time!\n");
|
||||
g_initialized = 0;
|
||||
}
|
||||
|
||||
db_perthread_deinit();
|
||||
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
|
||||
static void
|
||||
exit_cb(int fd, short what, void *arg)
|
||||
{
|
||||
int dummy;
|
||||
int ret;
|
||||
|
||||
ret = read(g_exit_pipe[0], &dummy, sizeof(dummy));
|
||||
if (ret != sizeof(dummy))
|
||||
DPRINTF(E_LOG, L_MAIN, "Error reading from exit pipe\n");
|
||||
|
||||
event_base_loopbreak(evbase_worker);
|
||||
|
||||
g_initialized = 0;
|
||||
|
||||
event_add(g_exitev, NULL);
|
||||
}
|
||||
|
||||
static void
|
||||
command_cb(int fd, short what, void *arg)
|
||||
{
|
||||
struct worker_command *cmd;
|
||||
int ret;
|
||||
|
||||
ret = read(g_cmd_pipe[0], &cmd, sizeof(cmd));
|
||||
if (ret != sizeof(cmd))
|
||||
{
|
||||
DPRINTF(E_LOG, L_MAIN, "Could not read command! (read %d): %s\n", ret, (ret < 0) ? strerror(errno) : "-no error-");
|
||||
goto readd;
|
||||
}
|
||||
|
||||
if (cmd->nonblock)
|
||||
{
|
||||
ret = cmd->func(cmd);
|
||||
|
||||
if (ret == 0)
|
||||
free(cmd);
|
||||
goto readd;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&cmd->lck);
|
||||
|
||||
ret = cmd->func(cmd);
|
||||
cmd->ret = ret;
|
||||
|
||||
pthread_cond_signal(&cmd->cond);
|
||||
pthread_mutex_unlock(&cmd->lck);
|
||||
|
||||
readd:
|
||||
event_add(g_cmdev, NULL);
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------------- Our worker API --------------------------- */
|
||||
|
||||
/* Thread: player */
|
||||
void
|
||||
worker_execute(void (*cb)(void *), void *cb_arg, size_t arg_size, int delay)
|
||||
{
|
||||
struct worker_command *cmd;
|
||||
void *argcpy;
|
||||
|
||||
DPRINTF(E_DBG, L_MAIN, "Got worker execute request\n");
|
||||
|
||||
cmd = (struct worker_command *)malloc(sizeof(struct worker_command));
|
||||
if (!cmd)
|
||||
{
|
||||
DPRINTF(E_LOG, L_MAIN, "Could not allocate worker_command\n");
|
||||
return;
|
||||
}
|
||||
|
||||
memset(cmd, 0, sizeof(struct worker_command));
|
||||
|
||||
argcpy = malloc(arg_size);
|
||||
if (!argcpy)
|
||||
{
|
||||
DPRINTF(E_LOG, L_MAIN, "Out of memory\n");
|
||||
return;
|
||||
}
|
||||
|
||||
memcpy(argcpy, cb_arg, arg_size);
|
||||
|
||||
cmd->nonblock = 1;
|
||||
cmd->func = execute;
|
||||
cmd->arg.cb = cb;
|
||||
cmd->arg.cb_arg = argcpy;
|
||||
cmd->arg.delay = delay;
|
||||
|
||||
nonblock_command(cmd);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
int
|
||||
worker_init(void)
|
||||
{
|
||||
int ret;
|
||||
|
||||
# if defined(__linux__)
|
||||
ret = pipe2(g_exit_pipe, O_CLOEXEC);
|
||||
# else
|
||||
ret = pipe(g_exit_pipe);
|
||||
# endif
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_MAIN, "Could not create pipe: %s\n", strerror(errno));
|
||||
goto exit_fail;
|
||||
}
|
||||
|
||||
# if defined(__linux__)
|
||||
ret = pipe2(g_cmd_pipe, O_CLOEXEC);
|
||||
# else
|
||||
ret = pipe(g_cmd_pipe);
|
||||
# endif
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_MAIN, "Could not create command pipe: %s\n", strerror(errno));
|
||||
goto cmd_fail;
|
||||
}
|
||||
|
||||
evbase_worker = event_base_new();
|
||||
if (!evbase_worker)
|
||||
{
|
||||
DPRINTF(E_LOG, L_MAIN, "Could not create an event base\n");
|
||||
goto evbase_fail;
|
||||
}
|
||||
|
||||
g_exitev = event_new(evbase_worker, g_exit_pipe[0], EV_READ, exit_cb, NULL);
|
||||
if (!g_exitev)
|
||||
{
|
||||
DPRINTF(E_LOG, L_MAIN, "Could not create exit event\n");
|
||||
goto evnew_fail;
|
||||
}
|
||||
|
||||
g_cmdev = event_new(evbase_worker, g_cmd_pipe[0], EV_READ, command_cb, NULL);
|
||||
if (!g_cmdev)
|
||||
{
|
||||
DPRINTF(E_LOG, L_MAIN, "Could not create cmd event\n");
|
||||
goto evnew_fail;
|
||||
}
|
||||
|
||||
event_add(g_exitev, NULL);
|
||||
event_add(g_cmdev, NULL);
|
||||
|
||||
ret = pthread_create(&tid_worker, NULL, worker, NULL);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_MAIN, "Could not spawn worker thread: %s\n", strerror(errno));
|
||||
|
||||
goto thread_fail;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
thread_fail:
|
||||
evnew_fail:
|
||||
event_base_free(evbase_worker);
|
||||
evbase_worker = NULL;
|
||||
|
||||
evbase_fail:
|
||||
close(g_cmd_pipe[0]);
|
||||
close(g_cmd_pipe[1]);
|
||||
|
||||
cmd_fail:
|
||||
close(g_exit_pipe[0]);
|
||||
close(g_exit_pipe[1]);
|
||||
|
||||
exit_fail:
|
||||
return -1;
|
||||
}
|
||||
|
||||
void
|
||||
worker_deinit(void)
|
||||
{
|
||||
int ret;
|
||||
|
||||
thread_exit();
|
||||
|
||||
ret = pthread_join(tid_worker, NULL);
|
||||
if (ret != 0)
|
||||
{
|
||||
DPRINTF(E_FATAL, L_MAIN, "Could not join worker thread: %s\n", strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
// Free event base (should free events too)
|
||||
event_base_free(evbase_worker);
|
||||
|
||||
// Close pipes
|
||||
close(g_cmd_pipe[0]);
|
||||
close(g_cmd_pipe[1]);
|
||||
close(g_exit_pipe[0]);
|
||||
close(g_exit_pipe[1]);
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
|
||||
#ifndef __WORKER_H__
|
||||
#define __WORKER_H__
|
||||
|
||||
/* The worker thread is made for running asyncronous tasks from a real time
|
||||
* thread, mainly the player thread.
|
||||
|
||||
* The worker_execute() function will trigger a callback from the worker thread.
|
||||
* Before returning the function will copy the argument given, so the caller
|
||||
* does not need to preserve them. However, if the argument contains pointers to
|
||||
* data, the caller must either make sure that the data remains valid until the
|
||||
* callback (which can free it), or make sure the callback does not refer to it.
|
||||
*
|
||||
* @param cb the function to call from the worker thread
|
||||
* @param cb_arg arguments for callback
|
||||
* @param arg_size size of the arguments given
|
||||
* @param delay how much in seconds to delay the execution
|
||||
*/
|
||||
void
|
||||
worker_execute(void (*cb)(void *), void *cb_arg, size_t arg_size, int delay);
|
||||
|
||||
int
|
||||
worker_init(void);
|
||||
|
||||
void
|
||||
worker_deinit(void);
|
||||
|
||||
#endif /* !__WORKER_H__ */
|
Loading…
Reference in New Issue