Merge branch 'pl2'

Conflicts:
	src/db.c
This commit is contained in:
ejurgensen 2015-04-11 21:00:49 +02:00
commit 7982bca6f0
29 changed files with 2068 additions and 827 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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));

590
src/http.c Normal file
View File

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

96
src/http.h Normal file
View File

@ -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__ */

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) */

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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__ */

384
src/worker.c Normal file
View File

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

28
src/worker.h Normal file
View File

@ -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__ */