From 6221e24f1b1880491a1faa149b3402471e244faa Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sat, 14 Mar 2015 21:42:53 +0100 Subject: [PATCH 01/39] Support for live ICY metadata for streams (incl. artwork) --- src/Makefile.am | 1 + src/artwork.c | 127 ++++++++++++++++++++++- src/db.c | 57 ++++++++-- src/db.h | 4 + src/filescanner_ffmpeg.c | 124 ++++++++-------------- src/icy.c | 217 +++++++++++++++++++++++++++++++++++++++ src/icy.h | 28 +++++ src/player.c | 135 ++++++++++++++++++++++-- src/player.h | 6 +- src/raop.c | 72 +++++++++++-- src/transcode.c | 39 +++++++ src/transcode.h | 7 ++ 12 files changed, 709 insertions(+), 108 deletions(-) create mode 100644 src/icy.c create mode 100644 src/icy.h diff --git a/src/Makefile.am b/src/Makefile.am index 822a80f5..821e214d 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -114,6 +114,7 @@ forked_daapd_SOURCES = main.c \ transcode.c transcode.h \ pipe.c pipe.h \ artwork.c artwork.h \ + icy.h icy.c \ misc.c misc.h \ rng.c rng.h \ rsp_query.c rsp_query.h \ diff --git a/src/artwork.c b/src/artwork.c index de8b4110..de96151e 100644 --- a/src/artwork.c +++ b/src/artwork.c @@ -38,6 +38,7 @@ #include "logger.h" #include "conffile.h" #include "cache.h" +#include "player.h" #if LIBAVFORMAT_VERSION_MAJOR >= 53 # include "avio_evbuffer.h" @@ -725,6 +726,127 @@ artwork_get(char *path, int max_w, int max_h, struct evbuffer *evbuf) return ret; } +static int +artwork_get_player_image(char *path, int max_w, int max_h, struct evbuffer *evbuf) +{ + AVFormatContext *src_ctx; + AVPacket pkt; + char *url; + int len; + int s; + int target_w; + int target_h; + int format_ok; + int ret; + + DPRINTF(E_DBG, L_ART, "Trying internet stream artwork in %s\n", path); + + player_icy_artwork_url(&url, path); + if (!url) + return 0; + + len = strlen(url); + if ((len < 14) || (len > PATH_MAX)) // Can't be shorter than http://a/1.jpg + return 0; + + src_ctx = NULL; + ret = avformat_open_input(&src_ctx, url, NULL, NULL); + if (ret < 0) + { + DPRINTF(E_WARN, L_ART, "Cannot open artwork file '%s': %s\n", url, strerror(AVUNERROR(ret))); + + return 0; + } + free(url); + + format_ok = 0; + for (s = 0; s < src_ctx->nb_streams; s++) + { + if (src_ctx->streams[s]->codec->codec_id == AV_CODEC_ID_PNG) + { + format_ok = ART_FMT_PNG; + break; + } + else if (src_ctx->streams[s]->codec->codec_id == AV_CODEC_ID_MJPEG) + { + format_ok = ART_FMT_JPEG; + break; + } + } + + if (s == src_ctx->nb_streams) + { + DPRINTF(E_LOG, L_ART, "Artwork file '%s' not a PNG or JPEG file\n", path); + + avformat_close_input(&src_ctx); + return 0; + } + + ret = 0; + while (av_read_frame(src_ctx, &pkt) == 0) + { + if (pkt.stream_index != s) + { + av_free_packet(&pkt); + continue; + } + + ret = 1; + break; + } + + if (!ret) + { + DPRINTF(E_LOG, L_ART, "Could not read artwork: '%s'\n", path); + + avformat_close_input(&src_ctx); + return 0; + } + + ret = rescale_needed(src_ctx->streams[s]->codec, max_w, max_h, &target_w, &target_h); + + /* Fastpath */ + if (!ret && format_ok) + { + DPRINTF(E_DBG, L_ART, "Artwork not too large, using original image\n"); + + ret = evbuffer_expand(evbuf, pkt.size); + if (ret < 0) + { + DPRINTF(E_LOG, L_ART, "Out of memory for artwork\n"); + + av_free_packet(&pkt); + avformat_close_input(&src_ctx); + return -1; + } + + ret = evbuffer_add(evbuf, pkt.data, pkt.size); + if (ret < 0) + { + DPRINTF(E_LOG, L_ART, "Could not add image to event buffer\n"); + } + else + ret = format_ok; + } + else + { + DPRINTF(E_DBG, L_ART, "Artwork too large, rescaling image\n"); + + ret = artwork_rescale(src_ctx, s, target_w, target_h, evbuf); + } + + av_free_packet(&pkt); + avformat_close_input(&src_ctx); + + if (ret < 0) + { + if (evbuffer_get_length(evbuf) > 0) + evbuffer_drain(evbuf, evbuffer_get_length(evbuf)); + } + + 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) @@ -786,7 +908,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; @@ -1062,6 +1184,9 @@ artwork_get_item_path(char *in_path, int artwork, int max_w, int max_h, char *ou ret = artwork_get_embedded_image(in_path, max_w, max_h, evbuf); break; #endif + case ARTWORK_HTTP: + ret = artwork_get_player_image(in_path, max_w, max_h, evbuf); + break; } return ret; diff --git a/src/db.c b/src/db.c index 7a976eb4..5c4e296f 100644 --- a/src/db.c +++ b/src/db.c @@ -62,6 +62,11 @@ struct db_unlock { pthread_mutex_t lck; }; +struct async_query { + char *query; + int delay; +}; + #define DB_TYPE_CHAR 1 #define DB_TYPE_INT 2 #define DB_TYPE_INT64 3 @@ -620,7 +625,7 @@ db_exec(const char *query, char **errmsg) static void * db_exec_thread(void *arg) { - char *query = arg; + struct async_query *async = arg; char *errmsg; time_t start, end; int ret; @@ -628,7 +633,7 @@ db_exec_thread(void *arg) // 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); + sleep(async->delay); ret = db_perthread_init(); if (ret < 0) @@ -637,19 +642,20 @@ db_exec_thread(void *arg) return NULL; } - DPRINTF(E_DBG, L_DB, "Running delayed query '%s'\n", query); + DPRINTF(E_DBG, L_DB, "Running delayed query '%s'\n", async->query); time(&start); - ret = db_exec(query, &errmsg); + ret = db_exec(async->query, &errmsg); if (ret != SQLITE_OK) - DPRINTF(E_LOG, L_DB, "Error running query '%s': %s\n", query, errmsg); + DPRINTF(E_LOG, L_DB, "Error running query '%s': %s\n", async->query, errmsg); time(&end); if (end - start > 1) - DPRINTF(E_LOG, L_DB, "Warning: Slow query detected '%s' - database performance problems?\n", query); + DPRINTF(E_LOG, L_DB, "Warning: Slow query detected '%s' - database performance problems?\n", async->query); sqlite3_free(errmsg); - sqlite3_free(query); + sqlite3_free(async->query); + free(async); db_perthread_deinit(); @@ -658,8 +664,9 @@ db_exec_thread(void *arg) // Creates a one-off thread to run a delayed, fire-and-forget, non-blocking query static void -db_exec_nonblock(char *query) +db_exec_nonblock(char *query, int delay) { + struct async_query *async; pthread_t tid; pthread_attr_t attr; int ret; @@ -671,8 +678,17 @@ db_exec_nonblock(char *query) return; } + async = malloc(sizeof(struct async_query)); + if (!async) + { + DPRINTF(E_LOG, L_DB, "Out of memory\n"); + return; + } + async->query = query; + async->delay = delay; + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); - ret = pthread_create(&tid, &attr, db_exec_thread, query); + ret = pthread_create(&tid, &attr, db_exec_thread, async); if (ret != 0) { DPRINTF(E_LOG, L_DB, "Error in db_exec_nonblock: Could not create thread\n"); @@ -2020,7 +2036,7 @@ db_file_inc_playcount(int id) } // Run the query non-blocking so we don't block playback if the update is slow - db_exec_nonblock(query); + db_exec_nonblock(query, 5); #undef Q_TMPL } @@ -2666,6 +2682,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_exec_nonblock(query, 0); +#undef Q_TMPL +} + void db_file_delete_bypath(char *path) { diff --git a/src/db.h b/src/db.h index 82f5e9e9..7a8fc1d7 100644 --- a/src/db.h +++ b/src/db.h @@ -49,6 +49,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, @@ -425,6 +426,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); diff --git a/src/filescanner_ffmpeg.c b/src/filescanner_ffmpeg.c index c70a2530..06560bfd 100644 --- a/src/filescanner_ffmpeg.c +++ b/src/filescanner_ffmpeg.c @@ -37,6 +37,7 @@ #include "logger.h" #include "filescanner.h" #include "misc.h" +#include "icy.h" /* Legacy format-specific scanners */ @@ -314,88 +315,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 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; @@ -425,7 +351,10 @@ scan_metadata_ffmpeg(char *file, struct media_file_info *mfi) # endif if (mfi->data_kind == 1) - av_dict_set(&options, "icy", "1", 0); + { + av_dict_set(&options, "icy", "1", 0); + mfi->artwork = ARTWORK_HTTP; + } ret = avformat_open_input(&ctx, file, NULL, &options); #else @@ -553,11 +482,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 = icy_metadata_get(ctx, 0); + if (icy_metadata && icy_metadata->name) + { + DPRINTF(E_DBG, L_SCAN, "libav/ffmpeg 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, "libav/ffmpeg 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, "libav/ffmpeg 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) + icy_metadata_free(icy_metadata); + } /* Get some more information on the audio stream */ if (audio_stream) diff --git a/src/icy.c b/src/icy.c new file mode 100644 index 00000000..81792e3a --- /dev/null +++ b/src/icy.c @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2015 Espen Jürgensen + * + * 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 +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "icy.h" +#include "logger.h" +#include "misc.h" + +#include + +static int +metadata_packet_get(struct 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 = 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 = 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 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 = strdup(ptr); + else if (strncmp(icy_token, "icy-description", strlen("icy-description")) == 0) + metadata->description = strdup(ptr); + else if (strncmp(icy_token, "icy-genre", strlen("icy-genre")) == 0) + metadata->genre = strdup(ptr); + + icy_token = strtok(NULL, "\r\n"); + } + av_free(buffer); + + return 0; +} + +void +icy_metadata_free(struct icy_metadata *metadata) +{ + 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); + + free(metadata); +} + +#if LIBAVFORMAT_VERSION_MAJOR >= 56 || (LIBAVFORMAT_VERSION_MAJOR == 55 && LIBAVFORMAT_VERSION_MINOR >= 13) +/* 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. + */ +struct icy_metadata * +icy_metadata_get(AVFormatContext *fmtctx, int packet_only) +{ + struct icy_metadata *metadata; + int got_packet; + int got_header; + + metadata = malloc(sizeof(struct icy_metadata)); + if (!metadata) + return NULL; + memset(metadata, 0, sizeof(struct 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_MISC, "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; +} +#else +struct icy_metadata * +icy_metadata_get(AVFormatContext *fmtctx, int packet_only) +{ + return NULL; +} +#endif + diff --git a/src/icy.h b/src/icy.h new file mode 100644 index 00000000..f909bf17 --- /dev/null +++ b/src/icy.h @@ -0,0 +1,28 @@ + +#ifndef __ICY_H__ +#define __ICY_H__ + +#include + +struct icy_metadata +{ + /* 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; +}; + +void +icy_metadata_free(struct icy_metadata *metadata); + +struct icy_metadata * +icy_metadata_get(AVFormatContext *fmtctx, int packet_only); + +#endif /* !__ICY_H__ */ diff --git a/src/player.c b/src/player.c index 448e937a..694a2640 100644 --- a/src/player.c +++ b/src/player.c @@ -65,6 +65,7 @@ /* These handle getting the media data */ #include "transcode.h" #include "pipe.h" +#include "icy.h" #ifdef HAVE_SPOTIFY_H # include "spotify.h" #endif @@ -77,6 +78,9 @@ #define MAX(a, b) ((a > b) ? a : b) #endif +/* Interval between ICY metadata polls for streams, in seconds */ +#define METADATA_ICY_POLL 5 + enum player_sync_source { PLAYER_SYNC_CLOCK, @@ -128,6 +132,12 @@ struct item_range uint32_t *id_ptr; }; +struct icy_artwork +{ + char *stream_url; + char *artwork_url; +}; + struct player_command { pthread_mutex_t lck; @@ -153,6 +163,7 @@ struct player_command int intval; int ps_pos[2]; struct item_range item_range; + struct icy_artwork icy_artwork; } arg; int ret; @@ -180,6 +191,7 @@ static int cmd_pipe[2]; static int player_exit; static struct event *exitev; static struct event *cmdev; +static struct event *metaev; static pthread_t tid_player; /* Player status */ @@ -661,6 +673,59 @@ metadata_send(struct player_source *ps, int startup) raop_metadata_send(ps->id, rtptime, offset, startup); } +static void +metadata_icy_poll_cb(int fd, short what, void *arg) +{ + struct timeval tv = { METADATA_ICY_POLL, 0 }; + struct icy_metadata *metadata; + int changed; + + /* Playback of stream has stopped, so stop polling */ + if (!cur_streaming || cur_streaming->type != SOURCE_HTTP || !cur_streaming->ctx) + { + if (metaev) + event_free(metaev); + + metaev = NULL; + return; + } + + transcode_metadata(cur_streaming->ctx, &metadata, &changed); + if (!metadata) + goto no_metadata; + + if (!changed) + goto no_update; + + /* Update db (async) and send status update to clients */ + db_file_update_icy(cur_streaming->id, metadata->artist, metadata->title); + status_update(player_state); + metadata_send(cur_streaming, 0); + + no_update: + icy_metadata_free(metadata); + + no_metadata: + evtimer_add(metaev, &tv); +} + +static void +metadata_icy_poll_start(void) +{ + struct timeval tv = { METADATA_ICY_POLL, 0 }; + + DPRINTF(E_DBG, L_PLAYER, "Starting ICY polling\n"); + + if (metaev) + return; + + metaev = evtimer_new(evbase_player, metadata_icy_poll_cb, NULL); + if (!metaev) + return; + + evtimer_add(metaev, &tv); +} + /* Audio sources */ /* Thread: httpd (DACP) */ static struct player_source * @@ -1059,7 +1124,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 +1153,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); @@ -1274,6 +1341,13 @@ 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 = transcode_setup(&ps->ctx, mfi, NULL, 0); + if (ret >= 0) + metadata_icy_poll_start(); + break; + case 2: ps->type = SOURCE_SPOTIFY; #ifdef HAVE_SPOTIFY_H @@ -1289,7 +1363,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); } @@ -1347,7 +1421,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); @@ -1585,7 +1659,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; @@ -1639,7 +1713,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; @@ -1726,7 +1800,8 @@ source_read(uint8_t *buf, int len, uint64_t rtptime) { switch (cur_streaming->type) { - case SOURCE_FFMPEG: + case SOURCE_FILE: + case SOURCE_HTTP: ret = transcode(cur_streaming->ctx, audio_buf, len - nbytes); break; @@ -2457,6 +2532,22 @@ now_playing(struct player_command *cmd) return 0; } +static int +artwork_url_get(struct player_command *cmd) +{ + DPRINTF(E_DBG, L_PLAYER, "ICY artwork url call\n"); + + cmd->arg.icy_artwork.artwork_url = NULL; + + /* Not playing a stream */ + if (!cur_playing || cur_playing->type != SOURCE_HTTP || !cur_playing->ctx) + return -1; + + transcode_metadata_artwork_url(cur_playing->ctx, &cmd->arg.icy_artwork.artwork_url, cmd->arg.icy_artwork.stream_url); + + return 0; +} + static int playback_stop(struct player_command *cmd) { @@ -2907,7 +2998,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 +3007,10 @@ playback_seek_bh(struct player_command *cmd) break; #endif case SOURCE_PIPE: + case SOURCE_HTTP: ret = 1; break; + default: ret = -1; } @@ -2964,7 +3057,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 +4203,30 @@ player_now_playing(uint32_t *id) return ret; } +int +player_icy_artwork_url(char **artwork_url, char *stream_url) +{ + struct player_command cmd; + int ret; + + command_init(&cmd); + + cmd.func = artwork_url_get; + cmd.func_bh = NULL; + cmd.arg.icy_artwork.stream_url = stream_url; + + if (pthread_self() != tid_player) + ret = sync_command(&cmd); + else + ret = artwork_url_get(&cmd); + + *artwork_url = cmd.arg.icy_artwork.artwork_url; + + command_deinit(&cmd); + + return ret; +} + /* * Starts/resumes playback * diff --git a/src/player.h b/src/player.h index 93ade6d1..c313efe5 100644 --- a/src/player.h +++ b/src/player.h @@ -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); +int +player_icy_artwork_url(char **artwork_url, char *stream_url); + void player_speaker_enumerate(spk_enum_cb cb, void *arg); diff --git a/src/raop.c b/src/raop.c index 2309eb71..745cf98b 100644 --- a/src/raop.c +++ b/src/raop.c @@ -61,6 +61,7 @@ #include "evrtsp/evrtsp.h" #include +#include #include "conffile.h" #include "logger.h" @@ -157,6 +158,14 @@ struct raop_metadata struct raop_metadata *next; }; +struct raop_metadata_send_ctx +{ + int id; + uint64_t rtptime; + uint64_t offset; + int startup; +}; + struct raop_service { int fd; @@ -2162,17 +2171,25 @@ raop_metadata_startup_send(struct raop_session *rs) } } -void -raop_metadata_send(int id, uint64_t rtptime, uint64_t offset, int startup) +static void * +raop_metadata_send_thread(void *arg) { + struct raop_metadata_send_ctx *ctx = arg; struct raop_session *rs; struct raop_metadata *rmd; uint32_t delay; int ret; - rmd = raop_metadata_prepare(id, rtptime); + ret = db_perthread_init(); + if (ret < 0) + { + DPRINTF(E_LOG, L_DB, "Error in raop_metadata_send_thread: Could not init thread\n"); + return NULL; + } + + rmd = raop_metadata_prepare(ctx->id, ctx->rtptime); if (!rmd) - return; + goto no_metadata; for (rs = sessions; rs; rs = rs->next) { @@ -2182,18 +2199,59 @@ raop_metadata_send(int id, uint64_t rtptime, uint64_t offset, int startup) if (!rs->wants_metadata) continue; - delay = (startup) ? RAOP_MD_DELAY_STARTUP : RAOP_MD_DELAY_SWITCH; + delay = (ctx->startup) ? RAOP_MD_DELAY_STARTUP : RAOP_MD_DELAY_SWITCH; - ret = raop_metadata_send_internal(rs, rmd, offset, delay); + ret = raop_metadata_send_internal(rs, rmd, ctx->offset, delay); if (ret < 0) { raop_session_failure(rs); - continue; } } + + no_metadata: + db_perthread_deinit(); + free(ctx); + + return NULL; } +void +raop_metadata_send(int id, uint64_t rtptime, uint64_t offset, int startup) +{ + struct raop_metadata_send_ctx *ctx; + pthread_t tid; + pthread_attr_t attr; + int ret; + + ctx = malloc(sizeof(struct raop_metadata_send_ctx)); + if (!ctx) + { + DPRINTF(E_LOG, L_RAOP, "Out of memory\n"); + return; + } + + ctx->id = id; + ctx->rtptime = rtptime; + ctx->offset = offset; + ctx->startup = startup; + + ret = pthread_attr_init(&attr); + if (ret != 0) + { + DPRINTF(E_LOG, L_DB, "Error in raop_metadata_send: Could not init attributes\n"); + return; + } + + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + ret = pthread_create(&tid, &attr, raop_metadata_send_thread, ctx); + if (ret != 0) + { + DPRINTF(E_LOG, L_DB, "Error in raop_metadata_send: Could not create thread\n"); + } + + pthread_attr_destroy(&attr); +} /* Volume handling */ static float diff --git a/src/transcode.c b/src/transcode.c index a1269c0b..678d5893 100644 --- a/src/transcode.c +++ b/src/transcode.c @@ -90,6 +90,7 @@ struct transcode_ctx { uint32_t duration; uint64_t samples; + uint32_t icy_hash; /* WAV header */ int wavhdr; @@ -894,3 +895,41 @@ transcode_needed(const char *user_agent, const char *client_codecs, char *file_c return 1; } + +void +transcode_metadata(struct transcode_ctx *ctx, struct icy_metadata **metadata, int *changed) +{ + struct icy_metadata *m; + + *metadata = NULL; + + if (!ctx->fmtctx) + return; + + m = icy_metadata_get(ctx->fmtctx, 1); + + *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, char *stream_url) +{ + struct icy_metadata *m; + + *artwork_url = NULL; + + if (!ctx->fmtctx || !ctx->fmtctx->filename) + return; + + if (strcmp(ctx->fmtctx->filename, stream_url) != 0) + return; + + m = icy_metadata_get(ctx->fmtctx, 1); + + if (m->artwork_url) + *artwork_url = strdup(m->artwork_url); +} diff --git a/src/transcode.h b/src/transcode.h index b266e9fb..e781802c 100644 --- a/src/transcode.h +++ b/src/transcode.h @@ -7,6 +7,7 @@ #else # include #endif +#include "icy.h" struct transcode_ctx; @@ -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 icy_metadata **metadata, int *changed); + +void +transcode_metadata_artwork_url(struct transcode_ctx *ctx, char **artwork_url, char *stream_url); + #endif /* !__TRANSCODE_H__ */ From 9fdb8a52474c629ca45952e0a14aae328011084e Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sat, 14 Mar 2015 22:00:57 +0100 Subject: [PATCH 02/39] Add DB and DAAP support for nested playlists --- src/db.c | 39 ++++++++++++++++++++++++++++++--------- src/db.h | 2 ++ src/httpd_daap.c | 7 ++++++- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src/db.c b/src/db.c index 5c4e296f..841129f5 100644 --- a/src/db.c +++ b/src/db.c @@ -159,6 +159,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 */ }; @@ -245,6 +246,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 */ }; @@ -4505,7 +4507,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 \ @@ -4601,14 +4604,11 @@ db_perthread_deinit(void) */ #define SCHEMA_VERSION_MAJOR 16 -#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 SCHEMA_VERSION_MINOR 01 #define Q_SCVER_MAJOR \ "INSERT INTO admin (key, value) VALUES ('schema_version_major', '16');" #define Q_SCVER_MINOR \ - "INSERT INTO admin (key, value) VALUES ('schema_version_minor', '00');" + "INSERT INTO admin (key, value) VALUES ('schema_version_minor', '01');" struct db_init_query { char *query; @@ -5681,8 +5681,6 @@ 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 U_V1600_SCVER_MAJOR \ "UPDATE admin SET value = '16' WHERE key = 'schema_version_major';" #define U_V1600_SCVER_MINOR \ @@ -5694,7 +5692,6 @@ 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" }, { U_V1600_SCVER_MAJOR, "set schema_version_major to 16" }, { U_V1600_SCVER_MINOR, "set schema_version_minor to 00" }, }; @@ -5809,6 +5806,25 @@ db_upgrade_v16(void) return 0; } +/* Upgrade from schema v16.00 to v16.01 */ +/* Expand data model to allow for nested playlists */ + +#define U_V1601_PL_PARENTID_ADD \ + "ALTER TABLE playlists ADD COLUMN parent_id INTEGER DEFAULT 0;" + +#define U_V1601_SCVER_MAJOR \ + "UPDATE admin SET value = '16' WHERE key = 'schema_version_major';" +#define U_V1601_SCVER_MINOR \ + "UPDATE admin SET value = '01' WHERE key = 'schema_version_minor';" + +static const struct db_init_query db_upgrade_v1601_queries[] = + { + { U_V1601_PL_PARENTID_ADD,"expanding table playlists with parent_id column" }, + + { U_V1601_SCVER_MAJOR, "set schema_version_major to 16" }, + { U_V1601_SCVER_MINOR, "set schema_version_minor to 01" }, + }; + static int db_upgrade(int db_ver) { @@ -5885,6 +5901,11 @@ db_upgrade(int db_ver) if (ret < 0) return -1; + /* FALLTHROUGH */ + + case 1600: + ret = db_generic_upgrade(db_upgrade_v1601_queries, sizeof(db_upgrade_v1601_queries) / sizeof(db_upgrade_v1601_queries[0])); + break; default: diff --git a/src/db.h b/src/db.h index 7a8fc1d7..f9d65b49 100644 --- a/src/db.h +++ b/src/db.h @@ -182,6 +182,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) @@ -198,6 +199,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) diff --git a/src/httpd_daap.c b/src/httpd_daap.c index f4a9ce38..29bbe63b 100644 --- a/src/httpd_daap.c +++ b/src/httpd_daap.c @@ -1516,6 +1516,7 @@ daap_reply_playlists(struct evhttp_request *req, struct evbuffer *evbuf, char ** int32_t plid; int32_t pltype; int32_t plitems; + int32_t plparent; int i; int ret; @@ -1665,7 +1666,11 @@ daap_reply_playlists(struct evhttp_request *req, struct evbuffer *evbuf, char ** 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) From e68c6c4932cc4ab09173a34ce000477547750466 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sat, 14 Mar 2015 22:34:03 +0100 Subject: [PATCH 03/39] Change db_pl_add/db_pl_update so input is playlist struct - so they work like db_file_add/update and can accept all the struct data (incl. parent_id) --- src/db.c | 38 +++++++++++++----------- src/db.h | 10 +++---- src/filescanner_itunes.c | 16 ++++++++++- src/filescanner_playlist.c | 59 ++++++++++++++++++++++---------------- src/spotify.c | 31 ++++++++++++++++---- 5 files changed, 101 insertions(+), 53 deletions(-) diff --git a/src/db.c b/src/db.c index 841129f5..88ebe511 100644 --- a/src/db.c +++ b/src/db.c @@ -158,8 +158,8 @@ static const struct col_type_map pli_cols_map[] = { pli_offsetof(path), DB_TYPE_STRING }, { 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 }, + { pli_offsetof(virtual_path), DB_TYPE_STRING }, /* items is computed on the fly */ }; @@ -245,8 +245,8 @@ static const ssize_t dbpli_cols_map[] = dbpli_offsetof(path), dbpli_offsetof(index), dbpli_offsetof(special_id), - dbpli_offsetof(virtual_path), dbpli_offsetof(parent_id), + dbpli_offsetof(virtual_path), /* items is computed on the fly */ }; @@ -3127,17 +3127,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"); @@ -3150,12 +3150,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"); @@ -3183,7 +3186,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; @@ -3216,13 +3219,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); @@ -4507,8 +4514,8 @@ db_perthread_deinit(void) " path VARCHAR(4096)," \ " idx INTEGER NOT NULL," \ " special_id INTEGER DEFAULT 0," \ - " virtual_path VARCHAR(4096)," \ - " parent_id INTEGER DEFAULT 0" \ + " parent_id INTEGER DEFAULT 0," \ + " virtual_path VARCHAR(4096)" \ ");" #define T_PLITEMS \ @@ -4638,7 +4645,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" }, }; diff --git a/src/db.h b/src/db.h index f9d65b49..acb1c699 100644 --- a/src/db.h +++ b/src/db.h @@ -165,7 +165,7 @@ struct media_file_info { #define mfi_offsetof(field) offsetof(struct media_file_info, field) enum pl_type { - PL_PLAIN, + PL_PLAIN = 0, PL_SMART, PL_MAX }; @@ -181,8 +181,8 @@ struct playlist_info { char *path; /* path of underlying playlist */ 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 */ + char *virtual_path; /* virtual path of underlying playlist */ }; #define pli_offsetof(field) offsetof(struct playlist_info, field) @@ -198,8 +198,8 @@ struct db_playlist_info { char *path; char *index; char *special_id; - char *virtual_path; char *parent_id; + char *virtual_path; }; #define dbpli_offsetof(field) offsetof(struct db_playlist_info, field) @@ -463,7 +463,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); @@ -475,7 +475,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); diff --git a/src/filescanner_itunes.c b/src/filescanner_itunes.c index 9a889977..24072fe7 100644 --- a/src/filescanner_itunes.c +++ b/src/filescanner_itunes.c @@ -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); diff --git a/src/filescanner_playlist.c b/src/filescanner_playlist.c index a21099d0..301f2ae9 100644 --- a/src/filescanner_playlist.c +++ b/src/filescanner_playlist.c @@ -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)); diff --git a/src/spotify.c b/src/spotify.c index 561d33e9..8ad2140e 100644 --- a/src/spotify.c +++ b/src/spotify.c @@ -645,8 +645,6 @@ 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" @@ -663,30 +661,51 @@ spotify_playlist_save(sp_playlist *pl) plid = pli->id; - free_pli(pli, 0); + free(pli->title); + pli->title = strdup(title); + 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(title); + pli->path = strdup(url); + pli->virtual_path = strdup(virtual_path); + + 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++) From 618e22d57e25ad51dd83073183c32597ef64e065 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Mon, 19 Jan 2015 21:32:24 +0100 Subject: [PATCH 04/39] Put Spotify playlists in a base playlist --- src/spotify.c | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/src/spotify.c b/src/spotify.c index 8ad2140e..ea74b723 100644 --- a/src/spotify.c +++ b/src/spotify.c @@ -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) */ @@ -647,13 +652,7 @@ spotify_playlist_save(sp_playlist *pl) 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) { @@ -662,7 +661,7 @@ spotify_playlist_save(sp_playlist *pl) plid = pli->id; free(pli->title); - pli->title = strdup(title); + pli->title = strdup(name); free(pli->virtual_path); pli->virtual_path = strdup(virtual_path); @@ -690,9 +689,11 @@ spotify_playlist_save(sp_playlist *pl) } memset(pli, 0, sizeof(struct playlist_info)); - pli->title = strdup(title); + + 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)) @@ -717,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); @@ -1246,6 +1247,8 @@ logged_in(sp_session *sess, sp_error error) { sp_playlist *pl; sp_playlistcontainer *pc; + struct playlist_info pli; + int ret; int i; if (SP_ERROR_OK != error) @@ -1261,6 +1264,17 @@ logged_in(sp_session *sess, sp_error error) pl = fptr_sp_session_starred_create(sess); fptr_sp_playlist_add_callbacks(pl, &pl_callbacks, NULL); + memset(&pli, 0, sizeof(struct playlist_info)); + pli.title = "Spotify"; + pli.path = "spotify:base_playlist"; + + ret = db_pl_add(&pli, &g_base_plid); + if (ret < 0) + { + DPRINTF(E_LOG, L_SPOTIFY, "Error adding base playlist\n"); + return; + } + pc = fptr_sp_session_playlistcontainer(sess); fptr_sp_playlistcontainer_add_callbacks(pc, &pc_callbacks, NULL); From 8359a9018d707340a61ba199a4bade95cd3e990b Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sat, 14 Mar 2015 23:35:19 +0100 Subject: [PATCH 05/39] Fixup parent playlist --- src/db.c | 11 +++++++---- src/db.h | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/db.c b/src/db.c index 88ebe511..19fdb9e0 100644 --- a/src/db.c +++ b/src/db.c @@ -158,8 +158,8 @@ static const struct col_type_map pli_cols_map[] = { pli_offsetof(path), DB_TYPE_STRING }, { pli_offsetof(index), DB_TYPE_INT }, { pli_offsetof(special_id), DB_TYPE_INT }, - { pli_offsetof(parent_id), DB_TYPE_INT }, { pli_offsetof(virtual_path), DB_TYPE_STRING }, + { pli_offsetof(parent_id), DB_TYPE_INT }, /* items is computed on the fly */ }; @@ -245,8 +245,8 @@ static const ssize_t dbpli_cols_map[] = dbpli_offsetof(path), dbpli_offsetof(index), dbpli_offsetof(special_id), - dbpli_offsetof(parent_id), dbpli_offsetof(virtual_path), + dbpli_offsetof(parent_id), /* items is computed on the fly */ }; @@ -4514,8 +4514,8 @@ db_perthread_deinit(void) " path VARCHAR(4096)," \ " idx INTEGER NOT NULL," \ " special_id INTEGER DEFAULT 0," \ - " parent_id INTEGER DEFAULT 0," \ - " virtual_path VARCHAR(4096)" \ + " virtual_path VARCHAR(4096)," \ + " parent_id INTEGER DEFAULT 0" \ ");" #define T_PLITEMS \ @@ -5687,6 +5687,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 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 \ @@ -5698,6 +5700,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" }, + { 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" }, }; diff --git a/src/db.h b/src/db.h index acb1c699..ae3bdc36 100644 --- a/src/db.h +++ b/src/db.h @@ -181,8 +181,8 @@ struct playlist_info { char *path; /* path of underlying playlist */ uint32_t index; /* index of playlist for paths with multiple playlists */ uint32_t special_id; /* iTunes identifies certain 'special' playlists with special meaning */ - uint32_t parent_id; /* Id of parent playlist if the playlist is nested */ 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) @@ -198,8 +198,8 @@ struct db_playlist_info { char *path; char *index; char *special_id; - char *parent_id; char *virtual_path; + char *parent_id; }; #define dbpli_offsetof(field) offsetof(struct db_playlist_info, field) From d7f7a746a293891fba3964ab6daa06e2ce7e9ed2 Mon Sep 17 00:00:00 2001 From: chme Date: Thu, 29 May 2014 09:29:23 +0200 Subject: [PATCH 06/39] Added item for "Radio" to DAAP-reply to "/databases" request --- src/httpd_daap.c | 68 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 16 deletions(-) diff --git a/src/httpd_daap.c b/src/httpd_daap.c index 29bbe63b..507a9b2a 100644 --- a/src/httpd_daap.c +++ b/src/httpd_daap.c @@ -862,7 +862,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,6 +1123,7 @@ 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; @@ -1144,29 +1145,64 @@ 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 (!content) + { + DPRINTF(E_LOG, L_DAAP, "Could not create evbuffer for DAAP dblist library item\n"); + dmap_send_error(req, "avdb", "Out of memory"); + return; + } + 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 = 2 + item = evbuffer_new(); + if (!content) + { + DPRINTF(E_LOG, L_DAAP, "Could not create evbuffer for DAAP dblist radio item\n"); + + dmap_send_error(req, "avdb", "Out of memory"); + return; + } + dmap_add_int(item, "miid", 2); + dmap_add_long(item, "mper", 2); + dmap_add_int(item, "mdbk", 0x64); + dmap_add_int(item, "aeCs", 0); + dmap_add_string(item, "minm", "Radio"); + count = 3; // TODO Get count of radio streams + 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); From cf6d94eda22dac92057c1411c953396869a656a0 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 15 Mar 2015 16:26:06 +0100 Subject: [PATCH 07/39] Fixup dblist for Radio item --- src/httpd_daap.c | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/httpd_daap.c b/src/httpd_daap.c index 507a9b2a..63b961f8 100644 --- a/src/httpd_daap.c +++ b/src/httpd_daap.c @@ -1147,13 +1147,14 @@ daap_reply_dblist(struct evhttp_request *req, struct evbuffer *evbuf, char **uri // Add db entry for library with dbid = 1 item = evbuffer_new(); - if (!content) - { - DPRINTF(E_LOG, L_DAAP, "Could not create evbuffer for DAAP dblist library item\n"); + 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_send_error(req, "avdb", "Out of memory"); - return; - } dmap_add_int(item, "miid", 1); dmap_add_long(item, "mper", 1); dmap_add_int(item, "mdbk", 1); @@ -1173,13 +1174,14 @@ daap_reply_dblist(struct evhttp_request *req, struct evbuffer *evbuf, char **uri // Add second db entry for radio with dbid = 2 item = evbuffer_new(); - if (!content) + 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; + return -1; } + dmap_add_int(item, "miid", 2); dmap_add_long(item, "mper", 2); dmap_add_int(item, "mdbk", 0x64); From 46e4214b39b2ca695b1337b4daefbff7b66468b4 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Mon, 16 Mar 2015 21:40:50 +0100 Subject: [PATCH 08/39] Adjust log severity --- src/artwork.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/artwork.c b/src/artwork.c index de96151e..a6ae3656 100644 --- a/src/artwork.c +++ b/src/artwork.c @@ -797,7 +797,7 @@ artwork_get_player_image(char *path, int max_w, int max_h, struct evbuffer *evbu if (!ret) { - DPRINTF(E_LOG, L_ART, "Could not read artwork: '%s'\n", path); + DPRINTF(E_WARN, L_ART, "Could not read artwork: '%s'\n", path); avformat_close_input(&src_ctx); return 0; From e5a1495b4934873b50aa911a69d797d83c9344a4 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Mon, 16 Mar 2015 23:33:42 +0100 Subject: [PATCH 09/39] Add DAAP logic for the Radio item --- src/db.c | 38 ++++++++++++++++++++++++++++---------- src/db.h | 5 ++++- src/httpd_daap.c | 35 +++++++++++++++++++++++++++++------ 3 files changed, 61 insertions(+), 17 deletions(-) diff --git a/src/db.c b/src/db.c index 19fdb9e0..0fbc05bd 100644 --- a/src/db.c +++ b/src/db.c @@ -296,7 +296,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); @@ -1696,6 +1696,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; @@ -1747,11 +1748,13 @@ db_query_fetch_pl(struct query_params *qp, struct db_playlist_info *dbpli) { case PL_PLAIN: 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: @@ -1759,13 +1762,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; @@ -2775,14 +2786,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) { @@ -2796,6 +2812,7 @@ db_pl_count_items(int id) return ret; +#undef Q_TMPL_STREAMS #undef Q_TMPL } @@ -3013,7 +3030,8 @@ db_pl_fetch_byquery(char *query) switch (pli->type) { case PL_PLAIN: - pli->items = db_pl_count_items(pli->id); + pli->items = db_pl_count_items(pli->id, 0); + pli->streams = db_pl_count_items(pli->id, 1); break; case PL_SMART: diff --git a/src/db.h b/src/db.h index ae3bdc36..b0c69de8 100644 --- a/src/db.h +++ b/src/db.h @@ -74,7 +74,8 @@ struct query_params { /* Private query context, keep out */ sqlite3_stmt *stmt; - char buf[32]; + char buf1[32]; + char buf2[32]; }; struct pairing_info { @@ -175,6 +176,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; @@ -192,6 +194,7 @@ struct db_playlist_info { char *title; char *type; char *items; + char *streams; char *query; char *db_timestamp; char *disabled; diff --git a/src/httpd_daap.c b/src/httpd_daap.c index 63b961f8..0a3809b8 100644 --- a/src/httpd_daap.c +++ b/src/httpd_daap.c @@ -69,6 +69,7 @@ extern struct event_base *evbase_httpd; /* Update requests refresh interval in seconds */ #define DAAP_UPDATE_REFRESH 0 +#define DAAP_DB_RADIO 2 struct uri_map { regex_t preg; @@ -1172,7 +1173,7 @@ daap_reply_dblist(struct evhttp_request *req, struct evbuffer *evbuf, char **uri evbuffer_add_buffer(content, item); evbuffer_free(item); - // Add second db entry for radio with dbid = 2 + // Add second db entry for radio with dbid = DAAP_DB_RADIO item = evbuffer_new(); if (!item) { @@ -1182,12 +1183,12 @@ daap_reply_dblist(struct evhttp_request *req, struct evbuffer *evbuf, char **uri return -1; } - dmap_add_int(item, "miid", 2); - dmap_add_long(item, "mper", 2); + 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", "Radio"); - count = 3; // TODO Get count of radio streams + 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) @@ -1549,11 +1550,13 @@ daap_reply_playlists(struct evhttp_request *req, struct evbuffer *evbuf, char ** const struct dmap_field **meta; const char *param; char **strval; + int database; int nmeta; int npls; int32_t plid; int32_t pltype; int32_t plitems; + int32_t plstreams; int32_t plparent; int i; int ret; @@ -1562,6 +1565,14 @@ 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; + } + ret = evbuffer_expand(evbuf, 61); if (ret < 0) { @@ -1654,8 +1665,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 ((database != DAAP_DB_RADIO) && (plstreams == plitems)) + continue; + + /* Don't add empty playlists */ + if ((plid > 1) && (plitems == 0)) continue; npls++; From d089eaf880fb74042d642f550d1b621d89eb91ed Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Tue, 17 Mar 2015 22:03:40 +0100 Subject: [PATCH 10/39] Some protection against mem leak --- src/icy.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/icy.c b/src/icy.c index 81792e3a..a5f1e81b 100644 --- a/src/icy.c +++ b/src/icy.c @@ -67,7 +67,7 @@ metadata_packet_get(struct icy_metadata *metadata, AVFormatContext *fmtctx) if (end) *end = '\0'; - if (strncmp(icy_token, "StreamTitle", strlen("StreamTitle")) == 0) + if ((strncmp(icy_token, "StreamTitle", strlen("StreamTitle")) == 0) && !metadata->title) { metadata->title = ptr; @@ -84,8 +84,10 @@ metadata_packet_get(struct icy_metadata *metadata, AVFormatContext *fmtctx) else metadata->title = strdup(metadata->title); } - else if (strncmp(icy_token, "StreamUrl", strlen("StreamUrl")) == 0) - metadata->artwork_url = strdup(ptr); + else if ((strncmp(icy_token, "StreamUrl", strlen("StreamUrl")) == 0) && !metadata->artwork_url) + { + metadata->artwork_url = strdup(ptr); + } if (end) *end = '\''; @@ -125,11 +127,11 @@ metadata_header_get(struct icy_metadata *metadata, AVFormatContext *fmtctx) if (ptr[0] == ' ') ptr++; - if (strncmp(icy_token, "icy-name", strlen("icy-name")) == 0) + 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) + 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) + else if ((strncmp(icy_token, "icy-genre", strlen("icy-genre")) == 0) && !metadata->genre) metadata->genre = strdup(ptr); icy_token = strtok(NULL, "\r\n"); From cde8441493b166e967f48b39f37a5634b4529acb Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Tue, 17 Mar 2015 22:04:07 +0100 Subject: [PATCH 11/39] Don't cache internet artwork --- src/artwork.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/artwork.c b/src/artwork.c index a6ae3656..3bff71fd 100644 --- a/src/artwork.c +++ b/src/artwork.c @@ -1355,7 +1355,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) @@ -1367,7 +1368,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; From b8d8df132b8e50b754f1e579c5b18f64d543717f Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 20 Mar 2015 23:40:42 +0100 Subject: [PATCH 12/39] Support for remote m3u playlists (ref pr #79) --- src/Makefile.am | 2 +- src/filescanner_ffmpeg.c | 24 +- src/http.c | 472 +++++++++++++++++++++++++++++++++++++++ src/http.h | 89 ++++++++ src/icy.c | 219 ------------------ src/icy.h | 28 --- src/logger.c | 2 +- src/logger.h | 43 ++-- src/player.c | 21 +- src/raop.c | 6 +- src/transcode.c | 10 +- src/transcode.h | 4 +- 12 files changed, 628 insertions(+), 292 deletions(-) create mode 100644 src/http.c create mode 100644 src/http.h delete mode 100644 src/icy.c delete mode 100644 src/icy.h diff --git a/src/Makefile.am b/src/Makefile.am index 821e214d..8685f87c 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -110,11 +110,11 @@ 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 \ artwork.c artwork.h \ - icy.h icy.c \ misc.c misc.h \ rng.c rng.h \ rsp_query.c rsp_query.h \ diff --git a/src/filescanner_ffmpeg.c b/src/filescanner_ffmpeg.c index 06560bfd..3c8637f9 100644 --- a/src/filescanner_ffmpeg.c +++ b/src/filescanner_ffmpeg.c @@ -37,7 +37,7 @@ #include "logger.h" #include "filescanner.h" #include "misc.h" -#include "icy.h" +#include "http.h" /* Legacy format-specific scanners */ @@ -321,7 +321,7 @@ scan_metadata_ffmpeg(char *file, struct media_file_info *mfi) AVFormatContext *ctx; AVDictionary *options; const struct metadata_map *extra_md_map; - struct icy_metadata *icy_metadata; + 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; @@ -333,12 +333,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 @@ -352,21 +354,29 @@ scan_metadata_ffmpeg(char *file, struct media_file_info *mfi) if (mfi->data_kind == 1) { + free(path); + ret = http_stream_setup(&path, file); + if (ret < 0) + return -1; + av_dict_set(&options, "icy", "1", 0); mfi->artwork = ARTWORK_HTTP; } - ret = avformat_open_input(&ctx, file, NULL, &options); + ret = avformat_open_input(&ctx, path, NULL, &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 @@ -485,7 +495,7 @@ scan_metadata_ffmpeg(char *file, struct media_file_info *mfi) /* Try to extract ICY metadata if url/stream */ if (mfi->data_kind == 1) { - icy_metadata = icy_metadata_get(ctx, 0); + icy_metadata = http_icy_metadata_get(ctx, 0); if (icy_metadata && icy_metadata->name) { DPRINTF(E_DBG, L_SCAN, "libav/ffmpeg found ICY metadata, name is '%s'\n", icy_metadata->name); @@ -520,7 +530,7 @@ scan_metadata_ffmpeg(char *file, struct media_file_info *mfi) mfi->genre = strdup(icy_metadata->genre); } if (icy_metadata) - icy_metadata_free(icy_metadata); + http_icy_metadata_free(icy_metadata); } /* Get some more information on the audio stream */ diff --git a/src/http.c b/src/http.c new file mode 100644 index 00000000..cb71fd39 --- /dev/null +++ b/src/http.c @@ -0,0 +1,472 @@ +/* + * Copyright (C) 2015 Espen Jürgensen + * + * 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 +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include + +#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 5 + +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; + + response_code = evhttp_request_get_response_code(req); + response_code_line = evhttp_request_get_response_code_line(req); + + if (req == NULL) + { + DPRINTF(E_LOG, L_HTTP, "Connection to %s failed: Connection timed out\n", ctx->url); + goto connection_error; + } + else 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; + } + + /* Async: we make a callback to caller, Sync: we move the body into the callers evbuf */ + ctx->ret = 0; + + if (ctx->async) + ctx->cb(req, arg); + else + evbuffer_add_buffer(ctx->evbuf, evhttp_request_get_input_buffer(req)); + + event_base_loopbreak(ctx->evbase); + + if (ctx->async) + free(ctx); + + return; + + connection_error: + + ctx->ret = -1; + + event_base_loopbreak(ctx->evbase); + + if (ctx->async) + free(ctx); + + return; +} + +static void * +request_make(void *arg) +{ + 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 = (struct http_client_ctx *)arg; + + 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 NULL; + } + + 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 NULL; + } + + 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 NULL; + } + + 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 NULL; + } + + 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); + + /* Make request */ + DPRINTF(E_INFO, L_HTTP, "Making request to %s asking for playlist\n", hostname); + + ret = evhttp_make_request(evcon, req, EVHTTP_REQ_GET, path); + if (ret < 0) + { + DPRINTF(E_LOG, L_HTTP, "Error making http request to %s\n", hostname); + + evhttp_connection_free(evcon); + event_base_free(ctx->evbase); + return NULL; + } + + event_base_dispatch(ctx->evbase); + + evhttp_connection_free(evcon); + event_base_free(ctx->evbase); + + return NULL; +} + +int +http_client_request(struct http_client_ctx *ctx) +{ + pthread_t tid; + pthread_attr_t attr; + int ret; + + /* If async make the request in a spawned thread, otherwise just make it */ + if (ctx->async) + { + ret = pthread_attr_init(&attr); + if (ret != 0) + { + DPRINTF(E_LOG, L_HTTP, "Error in http_client_request: Could not init attributes\n"); + return -1; + } + + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + ret = pthread_create(&tid, &attr, request_make, ctx); + if (ret != 0) + { + DPRINTF(E_LOG, L_HTTP, "Error in http_client_request: Could not create thread\n"); + return -1; + } + } + else + { + request_make(ctx); + ret = ctx->ret; + } + + return 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.async = 0; + ctx.url = url; + ctx.evbuf = 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.evbuf, 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.evbuf); + + 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 =============================*/ + +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; +} + +void +http_icy_metadata_free(struct http_icy_metadata *metadata) +{ + 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); + + free(metadata); +} + +#if LIBAVFORMAT_VERSION_MAJOR >= 56 || (LIBAVFORMAT_VERSION_MAJOR == 55 && LIBAVFORMAT_VERSION_MINOR >= 13) +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; +} +#else +struct http_icy_metadata * +http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only) +{ + return NULL; +} +#endif + diff --git a/src/http.h b/src/http.h new file mode 100644 index 00000000..70aa83f9 --- /dev/null +++ b/src/http.h @@ -0,0 +1,89 @@ + +#ifndef __HTTP_H__ +#define __HTTP_H__ + +#include +#include + +#include + +struct http_client_ctx +{ + int async; + const char *url; + int ret; + + /* For sync mode */ + struct evbuffer *evbuf; + + /* For async mode */ + void (*cb)(struct evhttp_request *, void *); + + /* Private */ + void *evbase; +}; + +struct http_icy_metadata +{ + /* 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 + * Can be called blocking or non-blocking. 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); + + +/* Frees a ICY metadata struct + * + * @param metadata struct to free + */ +void +http_icy_metadata_free(struct http_icy_metadata *metadata); + + +/* 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); + + +#endif /* !__HTTP_H__ */ diff --git a/src/icy.c b/src/icy.c deleted file mode 100644 index a5f1e81b..00000000 --- a/src/icy.c +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Copyright (C) 2015 Espen Jürgensen - * - * 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 -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "icy.h" -#include "logger.h" -#include "misc.h" - -#include - -static int -metadata_packet_get(struct 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 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; -} - -void -icy_metadata_free(struct icy_metadata *metadata) -{ - 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); - - free(metadata); -} - -#if LIBAVFORMAT_VERSION_MAJOR >= 56 || (LIBAVFORMAT_VERSION_MAJOR == 55 && LIBAVFORMAT_VERSION_MINOR >= 13) -/* 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. - */ -struct icy_metadata * -icy_metadata_get(AVFormatContext *fmtctx, int packet_only) -{ - struct icy_metadata *metadata; - int got_packet; - int got_header; - - metadata = malloc(sizeof(struct icy_metadata)); - if (!metadata) - return NULL; - memset(metadata, 0, sizeof(struct 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_MISC, "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; -} -#else -struct icy_metadata * -icy_metadata_get(AVFormatContext *fmtctx, int packet_only) -{ - return NULL; -} -#endif - diff --git a/src/icy.h b/src/icy.h deleted file mode 100644 index f909bf17..00000000 --- a/src/icy.h +++ /dev/null @@ -1,28 +0,0 @@ - -#ifndef __ICY_H__ -#define __ICY_H__ - -#include - -struct icy_metadata -{ - /* 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; -}; - -void -icy_metadata_free(struct icy_metadata *metadata); - -struct icy_metadata * -icy_metadata_get(AVFormatContext *fmtctx, int packet_only); - -#endif /* !__ICY_H__ */ diff --git a/src/logger.c b/src/logger.c index 80accc9c..273b4ad7 100644 --- a/src/logger.c +++ b/src/logger.c @@ -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" }; diff --git a/src/logger.h b/src/logger.h index e643f070..73e9424a 100644 --- a/src/logger.h +++ b/src/logger.h @@ -9,29 +9,30 @@ #define L_DAAP 1 #define L_DB 2 #define L_HTTPD 3 -#define L_MAIN 4 -#define L_MDNS 5 -#define L_MISC 6 -#define L_RSP 7 -#define L_SCAN 8 -#define L_XCODE 9 +#define L_HTTP 4 +#define L_MAIN 5 +#define L_MDNS 6 +#define L_MISC 7 +#define L_RSP 8 +#define L_SCAN 9 +#define L_XCODE 10 /* libevent logging */ -#define L_EVENT 10 -#define L_REMOTE 11 -#define L_DACP 12 -#define L_FFMPEG 13 -#define L_ART 14 -#define L_PLAYER 15 -#define L_RAOP 16 -#define L_LAUDIO 17 -#define L_DMAP 18 -#define L_DBPERF 19 -#define L_SPOTIFY 20 -#define L_LASTFM 21 -#define L_CACHE 22 -#define L_MPD 23 +#define L_EVENT 11 +#define L_REMOTE 12 +#define L_DACP 13 +#define L_FFMPEG 14 +#define L_ART 15 +#define L_PLAYER 16 +#define L_RAOP 17 +#define L_LAUDIO 18 +#define L_DMAP 19 +#define L_DBPERF 20 +#define L_SPOTIFY 21 +#define L_LASTFM 22 +#define L_CACHE 23 +#define L_MPD 24 -#define N_LOGDOMAINS 24 +#define N_LOGDOMAINS 25 /* Severities */ #define E_FATAL 0 diff --git a/src/player.c b/src/player.c index 694a2640..fbda3acc 100644 --- a/src/player.c +++ b/src/player.c @@ -65,7 +65,7 @@ /* These handle getting the media data */ #include "transcode.h" #include "pipe.h" -#include "icy.h" +#include "http.h" #ifdef HAVE_SPOTIFY_H # include "spotify.h" #endif @@ -677,7 +677,7 @@ static void metadata_icy_poll_cb(int fd, short what, void *arg) { struct timeval tv = { METADATA_ICY_POLL, 0 }; - struct icy_metadata *metadata; + struct http_icy_metadata *metadata; int changed; /* Playback of stream has stopped, so stop polling */ @@ -703,7 +703,7 @@ metadata_icy_poll_cb(int fd, short what, void *arg) metadata_send(cur_streaming, 0); no_update: - icy_metadata_free(metadata); + http_icy_metadata_free(metadata); no_metadata: evtimer_add(metaev, &tv); @@ -1312,6 +1312,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; @@ -1343,9 +1344,19 @@ source_open(struct player_source *ps, int no_md) { 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); - if (ret >= 0) - metadata_icy_poll_start(); + if (ret < 0) + break; + + metadata_icy_poll_start(); break; case 2: diff --git a/src/raop.c b/src/raop.c index 745cf98b..073c2ae9 100644 --- a/src/raop.c +++ b/src/raop.c @@ -2183,7 +2183,7 @@ raop_metadata_send_thread(void *arg) ret = db_perthread_init(); if (ret < 0) { - DPRINTF(E_LOG, L_DB, "Error in raop_metadata_send_thread: Could not init thread\n"); + DPRINTF(E_LOG, L_RAOP, "Error in raop_metadata_send_thread: Could not init thread\n"); return NULL; } @@ -2239,7 +2239,7 @@ raop_metadata_send(int id, uint64_t rtptime, uint64_t offset, int startup) ret = pthread_attr_init(&attr); if (ret != 0) { - DPRINTF(E_LOG, L_DB, "Error in raop_metadata_send: Could not init attributes\n"); + DPRINTF(E_LOG, L_RAOP, "Error in raop_metadata_send: Could not init attributes\n"); return; } @@ -2247,7 +2247,7 @@ raop_metadata_send(int id, uint64_t rtptime, uint64_t offset, int startup) ret = pthread_create(&tid, &attr, raop_metadata_send_thread, ctx); if (ret != 0) { - DPRINTF(E_LOG, L_DB, "Error in raop_metadata_send: Could not create thread\n"); + DPRINTF(E_LOG, L_RAOP, "Error in raop_metadata_send: Could not create thread\n"); } pthread_attr_destroy(&attr); diff --git a/src/transcode.c b/src/transcode.c index 678d5893..ee112f5d 100644 --- a/src/transcode.c +++ b/src/transcode.c @@ -897,16 +897,16 @@ transcode_needed(const char *user_agent, const char *client_codecs, char *file_c } void -transcode_metadata(struct transcode_ctx *ctx, struct icy_metadata **metadata, int *changed) +transcode_metadata(struct transcode_ctx *ctx, struct http_icy_metadata **metadata, int *changed) { - struct icy_metadata *m; + struct http_icy_metadata *m; *metadata = NULL; if (!ctx->fmtctx) return; - m = icy_metadata_get(ctx->fmtctx, 1); + m = http_icy_metadata_get(ctx->fmtctx, 1); *changed = (m->hash != ctx->icy_hash); @@ -918,7 +918,7 @@ transcode_metadata(struct transcode_ctx *ctx, struct icy_metadata **metadata, in void transcode_metadata_artwork_url(struct transcode_ctx *ctx, char **artwork_url, char *stream_url) { - struct icy_metadata *m; + struct http_icy_metadata *m; *artwork_url = NULL; @@ -928,7 +928,7 @@ transcode_metadata_artwork_url(struct transcode_ctx *ctx, char **artwork_url, ch if (strcmp(ctx->fmtctx->filename, stream_url) != 0) return; - m = icy_metadata_get(ctx->fmtctx, 1); + m = http_icy_metadata_get(ctx->fmtctx, 1); if (m->artwork_url) *artwork_url = strdup(m->artwork_url); diff --git a/src/transcode.h b/src/transcode.h index e781802c..5e1f8d9c 100644 --- a/src/transcode.h +++ b/src/transcode.h @@ -7,7 +7,7 @@ #else # include #endif -#include "icy.h" +#include "http.h" struct transcode_ctx; @@ -27,7 +27,7 @@ int transcode_needed(const char *user_agent, const char *client_codecs, char *file_codectype); void -transcode_metadata(struct transcode_ctx *ctx, struct icy_metadata **metadata, int *changed); +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, char *stream_url); From ea29a8d98884305977d658a9e25ef716bfbb501e Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 29 Mar 2015 00:29:06 +0100 Subject: [PATCH 13/39] Remove filescanner_icy.c and consolidate in http.c. Libav 9 does not support ICY metadata, so in that case we must be able to get it outselves. --- src/Makefile.am | 2 +- src/filescanner.c | 4 - src/filescanner_ffmpeg.c | 6 +- src/filescanner_icy.c | 348 --------------------------------------- src/http.c | 244 ++++++++++++++++++++++----- src/http.h | 29 ++-- 6 files changed, 229 insertions(+), 404 deletions(-) delete mode 100644 src/filescanner_icy.c diff --git a/src/Makefile.am b/src/Makefile.am index 8685f87c..ff779f86 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -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) \ diff --git a/src/filescanner.c b/src/filescanner.c index d4cb80b3..515d5f69 100644 --- a/src/filescanner.c +++ b/src/filescanner.c @@ -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) { diff --git a/src/filescanner_ffmpeg.c b/src/filescanner_ffmpeg.c index 3c8637f9..db28e47f 100644 --- a/src/filescanner_ffmpeg.c +++ b/src/filescanner_ffmpeg.c @@ -498,7 +498,7 @@ scan_metadata_ffmpeg(char *file, struct media_file_info *mfi) icy_metadata = http_icy_metadata_get(ctx, 0); if (icy_metadata && icy_metadata->name) { - DPRINTF(E_DBG, L_SCAN, "libav/ffmpeg found ICY metadata, name is '%s'\n", icy_metadata->name); + DPRINTF(E_DBG, L_SCAN, "Found ICY metadata, name is '%s'\n", icy_metadata->name); if (mfi->title) free(mfi->title); @@ -513,7 +513,7 @@ scan_metadata_ffmpeg(char *file, struct media_file_info *mfi) } if (icy_metadata && icy_metadata->description) { - DPRINTF(E_DBG, L_SCAN, "libav/ffmpeg found ICY metadata, description is '%s'\n", icy_metadata->description); + DPRINTF(E_DBG, L_SCAN, "Found ICY metadata, description is '%s'\n", icy_metadata->description); if (mfi->album) free(mfi->album); @@ -522,7 +522,7 @@ scan_metadata_ffmpeg(char *file, struct media_file_info *mfi) } if (icy_metadata && icy_metadata->genre) { - DPRINTF(E_DBG, L_SCAN, "libav/ffmpeg found ICY metadata, genre is '%s'\n", icy_metadata->genre); + DPRINTF(E_DBG, L_SCAN, "Found ICY metadata, genre is '%s'\n", icy_metadata->genre); if (mfi->genre) free(mfi->genre); diff --git a/src/filescanner_icy.c b/src/filescanner_icy.c deleted file mode 100644 index 23507963..00000000 --- a/src/filescanner_icy.c +++ /dev/null @@ -1,348 +0,0 @@ -/* - * Copyright (C) 2009-2010 Julien BLACHE - * - * 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 -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#if defined(__FreeBSD__) || defined(__FreeBSD_kernel__) -#include -#endif - -#include -#if defined HAVE_LIBEVENT2 -# include -#else -# include "evhttp/evhttp_compat.h" -#endif - -#include - -#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; -} diff --git a/src/http.c b/src/http.c index cb71fd39..ec8694f3 100644 --- a/src/http.c +++ b/src/http.c @@ -46,6 +46,38 @@ // Number of seconds the client will wait for a response before aborting #define HTTP_CLIENT_TIMEOUT 5 +/* 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", +}; + +/* 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) { @@ -55,15 +87,28 @@ request_cb(struct evhttp_request *req, void *arg) ctx = (struct http_client_ctx *)arg; - response_code = evhttp_request_get_response_code(req); - response_code_line = evhttp_request_get_response_code_line(req); + if (ctx->headers_only) + { + ctx->ret = 0; - if (req == NULL) + event_base_loopbreak(ctx->evbase); + + if (ctx->async) + free(ctx); + + return; + } + + if (!req) { DPRINTF(E_LOG, L_HTTP, "Connection to %s failed: Connection timed out\n", ctx->url); goto connection_error; } - else if (response_code == 0) + + response_code = evhttp_request_get_response_code(req); + response_code_line = evhttp_request_get_response_code_line(req); + + if (response_code == 0) { DPRINTF(E_LOG, L_HTTP, "Connection to %s failed: Connection refused\n", ctx->url); goto connection_error; @@ -77,10 +122,15 @@ request_cb(struct evhttp_request *req, void *arg) /* Async: we make a callback to caller, Sync: we move the body into the callers evbuf */ ctx->ret = 0; - if (ctx->async) - ctx->cb(req, arg); + if (!ctx->async) + { + 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)); + } else - evbuffer_add_buffer(ctx->evbuf, evhttp_request_get_input_buffer(req)); + ctx->cb(req, arg); event_base_loopbreak(ctx->evbase); @@ -101,6 +151,31 @@ request_cb(struct evhttp_request *req, void *arg) 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 + static void * request_make(void *arg) { @@ -165,19 +240,25 @@ request_make(void *arg) return NULL; } +#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 to %s asking for playlist\n", hostname); + DPRINTF(E_INFO, L_HTTP, "Making request to %s:%d\n", hostname, port); ret = evhttp_make_request(evcon, req, EVHTTP_REQ_GET, path); if (ret < 0) { - DPRINTF(E_LOG, L_HTTP, "Error making http request to %s\n", hostname); + DPRINTF(E_LOG, L_HTTP, "Error making http request to %s:%d\n", hostname, port); evhttp_connection_free(evcon); event_base_free(ctx->evbase); @@ -254,7 +335,7 @@ http_stream_setup(char **stream, const char *url) ctx.async = 0; ctx.url = url; - ctx.evbuf = evbuf; + ctx.body = evbuf; ret = http_client_request(&ctx); if (ret < 0) @@ -269,7 +350,7 @@ http_stream_setup(char **stream, const char *url) * nothing is found in the first 10 lines */ n = 0; - while ((line = evbuffer_readln(ctx.evbuf, NULL, EVBUFFER_EOL_ANY)) && (n < 10)) + while ((line = evbuffer_readln(ctx.body, NULL, EVBUFFER_EOL_ANY)) && (n < 10)) { n++; if (strncasecmp(line, "http://", strlen("http://")) == 0) @@ -283,7 +364,7 @@ http_stream_setup(char **stream, const char *url) free(line); } - evbuffer_free(ctx.evbuf); + evbuffer_free(ctx.body); if (n != -1) { @@ -300,6 +381,8 @@ http_stream_setup(char **stream, const char *url) /* ======================= 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) { @@ -404,31 +487,6 @@ metadata_header_get(struct http_icy_metadata *metadata, AVFormatContext *fmtctx) return 0; } -void -http_icy_metadata_free(struct http_icy_metadata *metadata) -{ - 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); - - free(metadata); -} - -#if LIBAVFORMAT_VERSION_MAJOR >= 56 || (LIBAVFORMAT_VERSION_MAJOR == 55 && LIBAVFORMAT_VERSION_MINOR >= 13) struct http_icy_metadata * http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only) { @@ -462,11 +520,121 @@ http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only) */ return metadata; } -#else + +#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! + * + * TODO: Get packet metadata from fmtctx->packet_buffer + */ +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 at the moment */ + if (packet_only) + return NULL; + + kv = keyval_alloc(); + if (!kv) + return NULL; + + memset(&ctx, 0, sizeof(struct http_client_ctx)); + ctx.async = 0; + 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) +{ + 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); + + free(metadata); +} diff --git a/src/http.h b/src/http.h index 70aa83f9..0f36da0f 100644 --- a/src/http.h +++ b/src/http.h @@ -4,6 +4,7 @@ #include #include +#include "misc.h" #include @@ -13,8 +14,17 @@ struct http_client_ctx const char *url; int ret; - /* For sync mode */ - struct evbuffer *evbuf; + /* For sync mode, 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; /* For async mode */ void (*cb)(struct evhttp_request *, void *); @@ -60,14 +70,6 @@ int http_stream_setup(char **stream, const char *url); -/* Frees a ICY metadata struct - * - * @param metadata struct to free - */ -void -http_icy_metadata_free(struct http_icy_metadata *metadata); - - /* Extracts ICY header and packet metadata (requires libav 10) * * example header metadata (standard http header format): @@ -86,4 +88,11 @@ struct http_icy_metadata * http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only); +/* Frees an ICY metadata struct + * + * @param metadata struct to free + */ +void +http_icy_metadata_free(struct http_icy_metadata *metadata); + #endif /* !__HTTP_H__ */ From 0b67ae397419ca01fcb236834008a5477ca6d1b8 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 29 Mar 2015 01:02:14 +0100 Subject: [PATCH 14/39] Some fixup for libav 0.8 --- src/artwork.c | 46 ++++++---------------------------------------- 1 file changed, 6 insertions(+), 40 deletions(-) diff --git a/src/artwork.c b/src/artwork.c index 3bff71fd..2582f7d2 100644 --- a/src/artwork.c +++ b/src/artwork.c @@ -53,6 +53,12 @@ # 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", @@ -220,7 +226,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 */ @@ -234,21 +239,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) @@ -560,19 +550,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; @@ -667,20 +649,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; @@ -885,20 +859,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; From 2b2883403e38f2e03f67bd47380035447210e2cf Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 29 Mar 2015 01:10:37 +0100 Subject: [PATCH 15/39] Fixup for libevent 2.0 --- src/http.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/http.c b/src/http.c index ec8694f3..bdc64036 100644 --- a/src/http.c +++ b/src/http.c @@ -106,7 +106,11 @@ request_cb(struct evhttp_request *req, void *arg) } 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) { From a529d788804e77d707e13be8d5a44b543e3b3b32 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 29 Mar 2015 20:16:56 +0200 Subject: [PATCH 16/39] Don't crash on no metadata... --- src/transcode.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/transcode.c b/src/transcode.c index ee112f5d..f3c1f170 100644 --- a/src/transcode.c +++ b/src/transcode.c @@ -907,6 +907,8 @@ transcode_metadata(struct transcode_ctx *ctx, struct http_icy_metadata **metadat return; m = http_icy_metadata_get(ctx->fmtctx, 1); + if (!m) + return; *changed = (m->hash != ctx->icy_hash); @@ -929,7 +931,11 @@ transcode_metadata_artwork_url(struct transcode_ctx *ctx, char **artwork_url, ch 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); } From 892fd9c40273c2e8c46c257e345ab641a0cb0f3d Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 29 Mar 2015 23:34:36 +0200 Subject: [PATCH 17/39] Send mpco in daap playlist reply, even when 0 (like iTunes) --- src/httpd_daap.c | 9 ++++----- src/spotify.c | 3 ++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/httpd_daap.c b/src/httpd_daap.c index 0a3809b8..df51684f 100644 --- a/src/httpd_daap.c +++ b/src/httpd_daap.c @@ -1674,11 +1674,11 @@ daap_reply_playlists(struct evhttp_request *req, struct evbuffer *evbuf, char ** */ if ((database == DAAP_DB_RADIO) && (plstreams == 0)) continue; - if ((database != DAAP_DB_RADIO) && (plstreams == plitems)) + if ((database != DAAP_DB_RADIO) && (plstreams > 0) && (plstreams == plitems)) continue; - /* Don't add empty playlists */ - if ((plid > 1) && (plitems == 0)) + /* Don't add empty Smart playlists */ + if ((plid > 1) && (plitems == 0) && (pltype == PL_SMART)) continue; npls++; @@ -1723,8 +1723,7 @@ 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) */ ret = safe_atoi32(dbpli.parent_id, &plparent); diff --git a/src/spotify.c b/src/spotify.c index ea74b723..680d3b6d 100644 --- a/src/spotify.c +++ b/src/spotify.c @@ -1266,7 +1266,8 @@ logged_in(sp_session *sess, sp_error error) memset(&pli, 0, sizeof(struct playlist_info)); pli.title = "Spotify"; - pli.path = "spotify:base_playlist"; + pli.type = PL_PLAIN; + pli.path = "spotify:playlistfolder"; ret = db_pl_add(&pli, &g_base_plid); if (ret < 0) From 33bc7227eadfb9f8e725a3536a793699d65422e9 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Mon, 30 Mar 2015 00:59:55 +0200 Subject: [PATCH 18/39] Make use of Spotify playlist folder configurable --- forked-daapd.conf | 5 +++++ src/conffile.c | 1 + src/spotify.c | 25 ++++++++++++++++--------- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/forked-daapd.conf b/forked-daapd.conf index b9891872..7b41e6e9 100644 --- a/forked-daapd.conf +++ b/forked-daapd.conf @@ -164,6 +164,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 diff --git a/src/conffile.c b/src/conffile.c index af94ee0a..c6801d20 100644 --- a/src/conffile.c +++ b/src/conffile.c @@ -113,6 +113,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), diff --git a/src/spotify.c b/src/spotify.c index 680d3b6d..9d658077 100644 --- a/src/spotify.c +++ b/src/spotify.c @@ -1245,6 +1245,7 @@ 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; @@ -1264,17 +1265,23 @@ logged_in(sp_session *sess, sp_error error) pl = fptr_sp_session_starred_create(sess); fptr_sp_playlist_add_callbacks(pl, &pl_callbacks, NULL); - memset(&pli, 0, sizeof(struct playlist_info)); - pli.title = "Spotify"; - pli.type = PL_PLAIN; - pli.path = "spotify:playlistfolder"; - - ret = db_pl_add(&pli, &g_base_plid); - if (ret < 0) + spotify_cfg = cfg_getsec(cfg, "spotify"); + if (! cfg_getbool(spotify_cfg, "base_playlist_disable")) { - DPRINTF(E_LOG, L_SPOTIFY, "Error adding base playlist\n"); - return; + 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); From 986b37ed29d6d520919b7ac72698147c000d8a59 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Mon, 30 Mar 2015 01:03:15 +0200 Subject: [PATCH 19/39] Adds a playlist folder type, it is needed for sorting since some clients (eg Retune) require that playlist folders are sent before their content. Playlist folders should, however, be sent after the base playlists, so the numbering is changed. At the same time makes the value of the smart playlist type a bit less hardcoded. --- src/db.c | 111 +++++++++++++++++++++++++++++------------------ src/db.h | 6 ++- src/httpd_daap.c | 2 +- 3 files changed, 73 insertions(+), 46 deletions(-) diff --git a/src/db.c b/src/db.c index 0fbc05bd..6af3d52a 100644 --- a/src/db.c +++ b/src/db.c @@ -723,7 +723,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; @@ -745,7 +745,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"); @@ -787,9 +787,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)) @@ -800,7 +800,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"); @@ -834,15 +834,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; @@ -860,6 +861,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 @@ -1205,6 +1228,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; @@ -1747,6 +1771,7 @@ 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, 0); nstreams = db_pl_count_items(id, 1); @@ -3030,6 +3055,7 @@ db_pl_fetch_byquery(char *query) switch (pli->type) { case PL_PLAIN: + case PL_FOLDER: pli->items = db_pl_count_items(pli->id, 0); pli->streams = db_pl_count_items(pli->id, 1); break; @@ -4600,27 +4626,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 @@ -4628,12 +4654,12 @@ db_perthread_deinit(void) " VALUES(8, 'Purchased', 0, 'media_kind = 1024', 0, '', 0, 8);" */ -#define SCHEMA_VERSION_MAJOR 16 -#define SCHEMA_VERSION_MINOR 01 +#define SCHEMA_VERSION_MAJOR 17 +#define SCHEMA_VERSION_MINOR 00 #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', '01');" + "INSERT INTO admin (key, value) VALUES ('schema_version_minor', '00');" struct db_init_query { char *query; @@ -5735,11 +5761,11 @@ db_upgrade_v16(void) char *title; int id; char *path; - int data_kind; + int type; char virtual_path[PATH_MAX]; int ret; - query = "SELECT id, album_artist, album, title, path, data_kind FROM files;"; + query = "SELECT id, album_artist, album, title, path FROM files;"; DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); @@ -5757,7 +5783,6 @@ db_upgrade_v16(void) album = (char *)sqlite3_column_text(stmt, 2); title = (char *)sqlite3_column_text(stmt, 3); path = (char *)sqlite3_column_text(stmt, 4); - data_kind = sqlite3_column_int(stmt, 5); if (strncmp(path, "http:", strlen("http:")) == 0) { @@ -5802,25 +5827,20 @@ db_upgrade_v16(void) id = sqlite3_column_int(stmt, 0); title = (char *)sqlite3_column_text(stmt, 1); path = (char *)sqlite3_column_text(stmt, 2); - data_kind = sqlite3_column_int(stmt, 3); + type = sqlite3_column_int(stmt, 3); - if (data_kind == 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); @@ -5833,23 +5853,28 @@ db_upgrade_v16(void) return 0; } -/* Upgrade from schema v16.00 to v16.01 */ -/* Expand data model to allow for nested playlists */ +/* Upgrade from schema v16.00 to v17.00 */ +/* Expand data model to allow for nested playlists and change default playlist + * enumeration + */ -#define U_V1601_PL_PARENTID_ADD \ +#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_V1601_SCVER_MAJOR \ - "UPDATE admin SET value = '16' WHERE key = 'schema_version_major';" -#define U_V1601_SCVER_MINOR \ - "UPDATE admin SET value = '01' WHERE key = 'schema_version_minor';" +#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_v1601_queries[] = +static const struct db_init_query db_upgrade_v17_queries[] = { - { U_V1601_PL_PARENTID_ADD,"expanding table playlists with parent_id column" }, + { 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_V1601_SCVER_MAJOR, "set schema_version_major to 16" }, - { U_V1601_SCVER_MINOR, "set schema_version_minor to 01" }, + { U_V17_SCVER_MAJOR, "set schema_version_major to 17" }, + { U_V17_SCVER_MINOR, "set schema_version_minor to 00" }, }; static int @@ -5931,7 +5956,7 @@ db_upgrade(int db_ver) /* FALLTHROUGH */ case 1600: - ret = db_generic_upgrade(db_upgrade_v1601_queries, sizeof(db_upgrade_v1601_queries) / sizeof(db_upgrade_v1601_queries[0])); + ret = db_generic_upgrade(db_upgrade_v17_queries, sizeof(db_upgrade_v17_queries) / sizeof(db_upgrade_v17_queries[0])); break; diff --git a/src/db.h b/src/db.h index b0c69de8..b653f7f9 100644 --- a/src/db.h +++ b/src/db.h @@ -165,10 +165,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 = 0, - PL_SMART, - PL_MAX + PL_FOLDER = 1, + PL_SMART = 2, + PL_MAX, }; struct playlist_info { diff --git a/src/httpd_daap.c b/src/httpd_daap.c index df51684f..1facff31 100644 --- a/src/httpd_daap.c +++ b/src/httpd_daap.c @@ -1692,7 +1692,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)) From 41f39ea5ca1ba5a88f7fd2c0bf301fe0ea5c70e0 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Mon, 30 Mar 2015 08:54:17 +0200 Subject: [PATCH 20/39] Sort playlists with ascending parent id, so that more nested playlists come after the less nested ones (required by Retune) --- src/db.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/db.c b/src/db.c index 6af3d52a..a7a07dcf 100644 --- a/src/db.c +++ b/src/db.c @@ -286,7 +286,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", }; From 48504231187f485abd46f29f043a59ba3e4d5c0f Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Mon, 30 Mar 2015 21:10:59 +0200 Subject: [PATCH 21/39] Make hiding internet streams from normal playlists configurable --- forked-daapd.conf | 11 +++++++++-- src/conffile.c | 2 ++ src/httpd_daap.c | 12 ++++++++++-- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/forked-daapd.conf b/forked-daapd.conf index 7b41e6e9..eac8c442 100644 --- a/forked-daapd.conf +++ b/forked-daapd.conf @@ -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 diff --git a/src/conffile.c b/src/conffile.c index c6801d20..e2781a91 100644 --- a/src/conffile.c +++ b/src/conffile.c @@ -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), diff --git a/src/httpd_daap.c b/src/httpd_daap.c index 1facff31..f125614d 100644 --- a/src/httpd_daap.c +++ b/src/httpd_daap.c @@ -69,6 +69,7 @@ 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 { @@ -1128,6 +1129,7 @@ daap_reply_dblist(struct evhttp_request *req, struct evbuffer *evbuf, char **uri struct daap_session *s; cfg_t *lib; char *name; + char *name_radio; int count; s = daap_session_find(req, query, evbuf); @@ -1136,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) @@ -1187,7 +1190,7 @@ daap_reply_dblist(struct evhttp_request *req, struct evbuffer *evbuf, char **uri 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", "Radio"); + 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); @@ -1545,12 +1548,14 @@ 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; @@ -1573,6 +1578,9 @@ daap_reply_playlists(struct evhttp_request *req, struct evbuffer *evbuf, char ** return -1; } + lib = cfg_getsec(cfg, "library"); + cfg_radiopl = cfg_getbool(lib, "radio_playlists"); + ret = evbuffer_expand(evbuf, 61); if (ret < 0) { @@ -1674,7 +1682,7 @@ daap_reply_playlists(struct evhttp_request *req, struct evbuffer *evbuf, char ** */ if ((database == DAAP_DB_RADIO) && (plstreams == 0)) continue; - if ((database != DAAP_DB_RADIO) && (plstreams > 0) && (plstreams == plitems)) + if (!cfg_radiopl && (database != DAAP_DB_RADIO) && (plstreams > 0) && (plstreams == plitems)) continue; /* Don't add empty Smart playlists */ From e5c8128c9db0253201e2b454b85dd696c2c72110 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Mon, 30 Mar 2015 23:10:33 +0200 Subject: [PATCH 22/39] Minor improvements for rescale_needed() --- src/artwork.c | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/artwork.c b/src/artwork.c index 2582f7d2..b7c606c6 100644 --- a/src/artwork.c +++ b/src/artwork.c @@ -111,27 +111,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); @@ -155,7 +149,7 @@ 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 From f947948a29905cf63a372cc50cf1bbb1e48364c8 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Mon, 30 Mar 2015 23:11:21 +0200 Subject: [PATCH 23/39] Avoid database locks when raop constructs metadata --- src/raop.c | 49 ++++++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/src/raop.c b/src/raop.c index 073c2ae9..324d03c9 100644 --- a/src/raop.c +++ b/src/raop.c @@ -781,6 +781,30 @@ raop_metadata_prepare(int id, uint64_t rtptime) memset(rmd, 0, sizeof(struct raop_metadata)); + /* Get artwork first (thus waiting a bit so that db metadata has a moment to + * update in case it is a live stream) + */ + 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 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; @@ -848,32 +872,11 @@ raop_metadata_prepare(int id, uint64_t rtptime) goto out_metadata; } + db_query_end(&qp); + 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; From 96200eb8080f8719c2bab0cac36eb5394366aad5 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Tue, 31 Mar 2015 00:08:29 +0200 Subject: [PATCH 24/39] Use own libevent http client for downloading artwork, ffmpeg seems a bit too slow --- src/artwork.c | 121 +++++++++++--------------------------------------- src/http.c | 7 +-- src/http.h | 2 +- 3 files changed, 31 insertions(+), 99 deletions(-) diff --git a/src/artwork.c b/src/artwork.c index b7c606c6..5b715723 100644 --- a/src/artwork.c +++ b/src/artwork.c @@ -39,6 +39,7 @@ #include "conffile.h" #include "cache.h" #include "player.h" +#include "http.h" #if LIBAVFORMAT_VERSION_MAJOR >= 53 # include "avio_evbuffer.h" @@ -697,14 +698,11 @@ artwork_get(char *path, int max_w, int max_h, struct evbuffer *evbuf) static int artwork_get_player_image(char *path, int max_w, int max_h, struct evbuffer *evbuf) { - AVFormatContext *src_ctx; - AVPacket pkt; + struct http_client_ctx ctx; + struct keyval *kv; + const char *content_type; char *url; int len; - int s; - int target_w; - int target_h; - int format_ok; int ret; DPRINTF(E_DBG, L_ART, "Trying internet stream artwork in %s\n", path); @@ -717,100 +715,33 @@ artwork_get_player_image(char *path, int max_w, int max_h, struct evbuffer *evbu if ((len < 14) || (len > PATH_MAX)) // Can't be shorter than http://a/1.jpg return 0; - src_ctx = NULL; - ret = avformat_open_input(&src_ctx, url, NULL, NULL); + kv = keyval_alloc(); + if (!kv) + return 0; + + memset(&ctx, 0, sizeof(ctx)); + ctx.url = url; + ctx.async = 0; + ctx.headers = kv; + ctx.body = evbuf; + + ret = http_client_request(&ctx); if (ret < 0) - { - DPRINTF(E_WARN, L_ART, "Cannot open artwork file '%s': %s\n", url, strerror(AVUNERROR(ret))); + return 0; - return 0; - } - free(url); - - format_ok = 0; - for (s = 0; s < src_ctx->nb_streams; s++) - { - if (src_ctx->streams[s]->codec->codec_id == AV_CODEC_ID_PNG) - { - format_ok = ART_FMT_PNG; - break; - } - else if (src_ctx->streams[s]->codec->codec_id == AV_CODEC_ID_MJPEG) - { - format_ok = ART_FMT_JPEG; - break; - } - } - - if (s == src_ctx->nb_streams) - { - DPRINTF(E_LOG, L_ART, "Artwork file '%s' not a PNG or JPEG file\n", path); - - avformat_close_input(&src_ctx); - return 0; - } - - ret = 0; - while (av_read_frame(src_ctx, &pkt) == 0) - { - if (pkt.stream_index != s) - { - av_free_packet(&pkt); - continue; - } - - ret = 1; - break; - } - - if (!ret) - { - DPRINTF(E_WARN, L_ART, "Could not read artwork: '%s'\n", path); - - avformat_close_input(&src_ctx); - return 0; - } - - ret = rescale_needed(src_ctx->streams[s]->codec, max_w, max_h, &target_w, &target_h); - - /* Fastpath */ - if (!ret && format_ok) - { - DPRINTF(E_DBG, L_ART, "Artwork not too large, using original image\n"); - - ret = evbuffer_expand(evbuf, pkt.size); - if (ret < 0) - { - DPRINTF(E_LOG, L_ART, "Out of memory for artwork\n"); - - av_free_packet(&pkt); - avformat_close_input(&src_ctx); - return -1; - } - - ret = evbuffer_add(evbuf, pkt.data, pkt.size); - if (ret < 0) - { - DPRINTF(E_LOG, L_ART, "Could not add image to event buffer\n"); - } - else - ret = format_ok; - } + content_type = keyval_get(kv, "Content-Type"); + if (strcmp(content_type, "image/jpeg") == 0) + ret = ART_FMT_JPEG; + else if (strcmp(content_type, "image/png") == 0) + ret = ART_FMT_PNG; else - { - DPRINTF(E_DBG, L_ART, "Artwork too large, rescaling image\n"); + ret = 0; - ret = artwork_rescale(src_ctx, s, target_w, target_h, evbuf); - } + if (ret) + DPRINTF(E_DBG, L_ART, "Found internet stream artwork in %s (%s)\n", url, content_type); - av_free_packet(&pkt); - avformat_close_input(&src_ctx); - - if (ret < 0) - { - if (evbuffer_get_length(evbuf) > 0) - evbuffer_drain(evbuf, evbuffer_get_length(evbuf)); - } + keyval_clear(kv); + free(kv); return ret; } diff --git a/src/http.c b/src/http.c index bdc64036..22aa65b1 100644 --- a/src/http.c +++ b/src/http.c @@ -44,7 +44,7 @@ /* ======================= libevent HTTP client =============================*/ // Number of seconds the client will wait for a response before aborting -#define HTTP_CLIENT_TIMEOUT 5 +#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 @@ -56,6 +56,7 @@ static char *header_list[] = "icy-description", "icy-metaint", "icy-genre", + "Content-Type", }; /* Copies headers we are searching for from one keyval struct to another @@ -257,12 +258,12 @@ request_make(void *arg) evhttp_add_header(headers, "Icy-MetaData", "1"); /* Make request */ - DPRINTF(E_INFO, L_HTTP, "Making request to %s:%d\n", hostname, port); + 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 http request to %s:%d\n", hostname, port); + 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); diff --git a/src/http.h b/src/http.h index 0f36da0f..a932201c 100644 --- a/src/http.h +++ b/src/http.h @@ -12,7 +12,6 @@ struct http_client_ctx { int async; const char *url; - int ret; /* For sync mode, a keyval/evbuf to store response headers and body * Can be set to NULL to ignore that part of the response @@ -30,6 +29,7 @@ struct http_client_ctx void (*cb)(struct evhttp_request *, void *); /* Private */ + int ret; void *evbase; }; From e49c941a00868642b17f085ca6b4f9d77175b823 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Tue, 31 Mar 2015 23:05:24 +0200 Subject: [PATCH 25/39] Add a worker thread to support async tasks from the player thread (and maybe others later) --- src/Makefile.am | 1 + src/db.c | 87 +-------- src/filescanner_ffmpeg.c | 2 +- src/http.c | 5 +- src/http.h | 5 +- src/main.c | 15 ++ src/player.c | 69 +++++-- src/raop.c | 72 +------- src/raop.h | 10 +- src/transcode.c | 2 +- src/worker.c | 386 +++++++++++++++++++++++++++++++++++++++ src/worker.h | 28 +++ 12 files changed, 511 insertions(+), 171 deletions(-) create mode 100644 src/worker.c create mode 100644 src/worker.h diff --git a/src/Makefile.am b/src/Makefile.am index ff779f86..43e15990 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -120,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) \ diff --git a/src/db.c b/src/db.c index a7a07dcf..cd887a5f 100644 --- a/src/db.c +++ b/src/db.c @@ -62,11 +62,6 @@ struct db_unlock { pthread_mutex_t lck; }; -struct async_query { - char *query; - int delay; -}; - #define DB_TYPE_CHAR 1 #define DB_TYPE_INT 2 #define DB_TYPE_INT64 3 @@ -623,83 +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) -{ - struct async_query *async = 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(async->delay); - - 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", async->query); - - time(&start); - ret = db_exec(async->query, &errmsg); - if (ret != SQLITE_OK) - DPRINTF(E_LOG, L_DB, "Error running query '%s': %s\n", async->query, errmsg); - - time(&end); - if (end - start > 1) - DPRINTF(E_LOG, L_DB, "Warning: Slow query detected '%s' - database performance problems?\n", async->query); - - sqlite3_free(errmsg); - sqlite3_free(async->query); - free(async); - - 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, int delay) -{ - struct async_query *async; - 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; - } - - async = malloc(sizeof(struct async_query)); - if (!async) - { - DPRINTF(E_LOG, L_DB, "Out of memory\n"); - return; - } - async->query = query; - async->delay = delay; - - pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); - ret = pthread_create(&tid, &attr, db_exec_thread, async); - 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) @@ -2073,8 +1991,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, 5); + db_query_run(query, 1, 0); #undef Q_TMPL } @@ -2737,7 +2654,7 @@ db_file_update_icy(int id, char *artist, char *album) return; } - db_exec_nonblock(query, 0); + db_query_run(query, 1, 0); #undef Q_TMPL } diff --git a/src/filescanner_ffmpeg.c b/src/filescanner_ffmpeg.c index db28e47f..148c5a9d 100644 --- a/src/filescanner_ffmpeg.c +++ b/src/filescanner_ffmpeg.c @@ -530,7 +530,7 @@ scan_metadata_ffmpeg(char *file, struct media_file_info *mfi) mfi->genre = strdup(icy_metadata->genre); } if (icy_metadata) - http_icy_metadata_free(icy_metadata); + http_icy_metadata_free(icy_metadata, 0); } /* Get some more information on the audio stream */ diff --git a/src/http.c b/src/http.c index 22aa65b1..24bea995 100644 --- a/src/http.c +++ b/src/http.c @@ -621,7 +621,7 @@ http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only) #endif void -http_icy_metadata_free(struct http_icy_metadata *metadata) +http_icy_metadata_free(struct http_icy_metadata *metadata, int content_only) { if (metadata->name) free(metadata->name); @@ -641,5 +641,6 @@ http_icy_metadata_free(struct http_icy_metadata *metadata) if (metadata->artwork_url) free(metadata->artwork_url); - free(metadata); + if (!content_only) + free(metadata); } diff --git a/src/http.h b/src/http.h index a932201c..69a76409 100644 --- a/src/http.h +++ b/src/http.h @@ -35,6 +35,8 @@ struct http_client_ctx struct http_icy_metadata { + uint32_t id; + /* Static stream metadata from icy_metadata_headers */ char *name; char *description; @@ -91,8 +93,9 @@ 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); +http_icy_metadata_free(struct http_icy_metadata *metadata, int content_only); #endif /* !__HTTP_H__ */ diff --git a/src/main.c b/src/main.c index 21df2baa..468e9469 100644 --- a/src/main.c +++ b/src/main.c @@ -65,6 +65,7 @@ GCRY_THREAD_OPTION_PTHREAD_IMPL; #include "mdns.h" #include "remote_pairing.h" #include "player.h" +#include "worker.h" #if LIBAVFORMAT_VERSION_MAJOR < 53 # include "ffmpeg_url_evbuffer.h" #endif @@ -676,6 +677,16 @@ main(int argc, char **argv) goto db_fail; } + /* Spawn worker thread */ + ret = worker_init(); + if (ret != 0) + { + DPRINTF(E_FATAL, L_MAIN, "Worker thread failed to start\n"); + + ret = EXIT_FAILURE; + goto worker_fail; + } + /* Spawn cache thread */ ret = cache_init(); if (ret != 0) @@ -848,6 +859,10 @@ main(int argc, char **argv) cache_deinit(); cache_fail: + DPRINTF(E_LOG, L_MAIN, "Worker deinit\n"); + worker_deinit(); + + worker_fail: DPRINTF(E_LOG, L_MAIN, "Database deinit\n"); db_perthread_deinit(); db_deinit(); diff --git a/src/player.c b/src/player.c index fbda3acc..cd18906e 100644 --- a/src/player.c +++ b/src/player.c @@ -57,6 +57,7 @@ #include "player.h" #include "raop.h" #include "laudio.h" +#include "worker.h" #ifdef LASTFM # include "lastfm.h" @@ -625,6 +626,32 @@ player_laudio_status_cb(enum laudio_state status) } } +/* Callbacks from the worker thread */ +static void +playcount_inc_cb(void *arg) +{ + int *id = arg; + + db_file_inc_playcount(*id); +} + +static void +metadata_send_cb(void *arg) +{ + struct raop_metadata_arg *rma = arg; + + raop_metadata_send(rma); +} + +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 @@ -642,35 +669,37 @@ metadata_purge(void) static void metadata_send(struct player_source *ps, int startup) { - uint64_t offset; - uint64_t rtptime; + struct raop_metadata_arg rma; - offset = 0; + rma.id = ps->id; + rma.offset = 0; + rma.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; + rma.offset = ps->output_start - ps->stream_start; + rma.rtptime = ps->stream_start; } else if (startup) { - rtptime = last_rtptime + AIRTUNES_V2_PACKET_SAMPLES; + rma.rtptime = last_rtptime + AIRTUNES_V2_PACKET_SAMPLES; } /* Generic case */ else if (cur_streaming && (cur_streaming->end)) { - rtptime = cur_streaming->end + 1; + rma.rtptime = cur_streaming->end + 1; } else { - rtptime = 0; + rma.rtptime = 0; DPRINTF(E_LOG, L_PLAYER, "PTOH! Unhandled song boundary case in metadata_send()\n"); } - raop_metadata_send(ps->id, rtptime, offset, startup); + /* Defer the actual work of sending the metadata to the worker thread */ + worker_execute(metadata_send_cb, &rma, sizeof(struct raop_metadata_arg), 0); } static void @@ -697,13 +726,23 @@ metadata_icy_poll_cb(int fd, short what, void *arg) if (!changed) goto no_update; - /* Update db (async) and send status update to clients */ - db_file_update_icy(cur_streaming->id, metadata->artist, metadata->title); + 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); + status_update(player_state); metadata_send(cur_streaming, 0); + /* Only free the struct, the content must be preserved for update_icy_cb */ + free(metadata); + + evtimer_add(metaev, &tv); + + return; + no_update: - http_icy_metadata_free(metadata); + http_icy_metadata_free(metadata, 0); no_metadata: evtimer_add(metaev, &tv); @@ -1623,6 +1662,7 @@ source_check(void) uint64_t pos; enum repeat_mode r_mode; int i; + int id; int ret; if (!cur_streaming) @@ -1699,9 +1739,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: diff --git a/src/raop.c b/src/raop.c index 324d03c9..81c8cb15 100644 --- a/src/raop.c +++ b/src/raop.c @@ -61,7 +61,6 @@ #include "evrtsp/evrtsp.h" #include -#include #include "conffile.h" #include "logger.h" @@ -158,14 +157,6 @@ struct raop_metadata struct raop_metadata *next; }; -struct raop_metadata_send_ctx -{ - int id; - uint64_t rtptime; - uint64_t offset; - int startup; -}; - struct raop_service { int fd; @@ -2174,25 +2165,17 @@ raop_metadata_startup_send(struct raop_session *rs) } } -static void * -raop_metadata_send_thread(void *arg) +void +raop_metadata_send(struct raop_metadata_arg *rma) { - struct raop_metadata_send_ctx *ctx = arg; struct raop_session *rs; struct raop_metadata *rmd; uint32_t delay; int ret; - ret = db_perthread_init(); - if (ret < 0) - { - DPRINTF(E_LOG, L_RAOP, "Error in raop_metadata_send_thread: Could not init thread\n"); - return NULL; - } - - rmd = raop_metadata_prepare(ctx->id, ctx->rtptime); + rmd = raop_metadata_prepare(rma->id, rma->rtptime); if (!rmd) - goto no_metadata; + return; for (rs = sessions; rs; rs = rs->next) { @@ -2202,58 +2185,15 @@ raop_metadata_send_thread(void *arg) if (!rs->wants_metadata) continue; - delay = (ctx->startup) ? RAOP_MD_DELAY_STARTUP : RAOP_MD_DELAY_SWITCH; + delay = (rma->startup) ? RAOP_MD_DELAY_STARTUP : RAOP_MD_DELAY_SWITCH; - ret = raop_metadata_send_internal(rs, rmd, ctx->offset, delay); + ret = raop_metadata_send_internal(rs, rmd, rma->offset, delay); if (ret < 0) { raop_session_failure(rs); continue; } } - - no_metadata: - db_perthread_deinit(); - free(ctx); - - return NULL; -} - -void -raop_metadata_send(int id, uint64_t rtptime, uint64_t offset, int startup) -{ - struct raop_metadata_send_ctx *ctx; - pthread_t tid; - pthread_attr_t attr; - int ret; - - ctx = malloc(sizeof(struct raop_metadata_send_ctx)); - if (!ctx) - { - DPRINTF(E_LOG, L_RAOP, "Out of memory\n"); - return; - } - - ctx->id = id; - ctx->rtptime = rtptime; - ctx->offset = offset; - ctx->startup = startup; - - ret = pthread_attr_init(&attr); - if (ret != 0) - { - DPRINTF(E_LOG, L_RAOP, "Error in raop_metadata_send: Could not init attributes\n"); - return; - } - - pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); - ret = pthread_create(&tid, &attr, raop_metadata_send_thread, ctx); - if (ret != 0) - { - DPRINTF(E_LOG, L_RAOP, "Error in raop_metadata_send: Could not create thread\n"); - } - - pthread_attr_destroy(&attr); } /* Volume handling */ diff --git a/src/raop.h b/src/raop.h index 1eb070f6..45dd9dd1 100644 --- a/src/raop.h +++ b/src/raop.h @@ -54,6 +54,14 @@ struct raop_device struct raop_device *next; }; +struct raop_metadata_arg +{ + int id; + uint64_t rtptime; + uint64_t offset; + int startup; +}; + /* RAOP session state */ /* Session is starting up */ @@ -115,7 +123,7 @@ raop_playback_stop(void); void -raop_metadata_send(int id, uint64_t rtptime, uint64_t offset, int startup); +raop_metadata_send(struct raop_metadata_arg *rma); int raop_set_volume_one(struct raop_session *rs, int volume, raop_status_cb cb); diff --git a/src/transcode.c b/src/transcode.c index f3c1f170..47ea6aca 100644 --- a/src/transcode.c +++ b/src/transcode.c @@ -937,5 +937,5 @@ transcode_metadata_artwork_url(struct transcode_ctx *ctx, char **artwork_url, ch if (m->artwork_url) *artwork_url = strdup(m->artwork_url); - http_icy_metadata_free(m); + http_icy_metadata_free(m, 0); } diff --git a/src/worker.c b/src/worker.c new file mode 100644 index 00000000..8196fca6 --- /dev/null +++ b/src/worker.c @@ -0,0 +1,386 @@ +/* + * Copyright (C) 2014 Espen Jürgensen + * + * 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 +#endif + +#include +#include +#include +#include +#include +#include +#include + #include +#include +#include + +#include + +#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; + +/* --------------------------------- HELPERS ------------------------------- */ + +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); +} + + +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_MISC, "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_MISC, "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_MISC, "Killing worker thread\n"); + + if (write(g_exit_pipe[1], &dummy, sizeof(dummy)) != sizeof(dummy)) + DPRINTF(E_LOG, L_MISC, "Could not write to exit fd: %s\n", strerror(errno)); +} + + + +/* --------------------------------- MAIN --------------------------------- */ +/* Thread: worker */ + +static void * +worker(void *arg) +{ + int ret; + + DPRINTF(E_DBG, L_MISC, "Worker loop initiating\n"); + + ret = db_perthread_init(); + if (ret < 0) + { + DPRINTF(E_LOG, L_MISC, "Error: DB init failed\n"); + pthread_exit(NULL); + } + + g_initialized = 1; + + event_base_dispatch(evbase_worker); + + if (g_initialized) + { + DPRINTF(E_LOG, L_MISC, "Worker event loop terminated ahead of time!\n"); + g_initialized = 0; + } + + db_perthread_deinit(); + + DPRINTF(E_DBG, L_MISC, "Worker loop terminating\n"); + + 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_MISC, "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_MISC, "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_MISC, "Got worker execute request\n"); + + cmd = (struct worker_command *)malloc(sizeof(struct worker_command)); + if (!cmd) + { + DPRINTF(E_LOG, L_MISC, "Could not allocate worker_command\n"); + return; + } + + memset(cmd, 0, sizeof(struct worker_command)); + + argcpy = malloc(arg_size); + if (!argcpy) + { + DPRINTF(E_LOG, L_MISC, "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_MISC, "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_MISC, "Could not create command pipe: %s\n", strerror(errno)); + goto cmd_fail; + } + + evbase_worker = event_base_new(); + if (!evbase_worker) + { + DPRINTF(E_LOG, L_MISC, "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_MISC, "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_MISC, "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_MISC, "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_MISC, "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]); +} diff --git a/src/worker.h b/src/worker.h new file mode 100644 index 00000000..125e9730 --- /dev/null +++ b/src/worker.h @@ -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__ */ From 19ba8fba6798caa2f8d7b09a60808cc6645a7afa Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Tue, 31 Mar 2015 23:27:18 +0200 Subject: [PATCH 26/39] Drop async mode from http client, not needed and probably not working --- src/artwork.c | 1 - src/http.c | 80 ++++++++------------------------------------------- src/http.h | 11 ++----- 3 files changed, 15 insertions(+), 77 deletions(-) diff --git a/src/artwork.c b/src/artwork.c index 5b715723..84533232 100644 --- a/src/artwork.c +++ b/src/artwork.c @@ -721,7 +721,6 @@ artwork_get_player_image(char *path, int max_w, int max_h, struct evbuffer *evbu memset(&ctx, 0, sizeof(ctx)); ctx.url = url; - ctx.async = 0; ctx.headers = kv; ctx.body = evbuf; diff --git a/src/http.c b/src/http.c index 24bea995..6b56c78c 100644 --- a/src/http.c +++ b/src/http.c @@ -31,8 +31,6 @@ #include #include -#include - #include #include @@ -94,9 +92,6 @@ request_cb(struct evhttp_request *req, void *arg) event_base_loopbreak(ctx->evbase); - if (ctx->async) - free(ctx); - return; } @@ -124,24 +119,15 @@ request_cb(struct evhttp_request *req, void *arg) goto connection_error; } - /* Async: we make a callback to caller, Sync: we move the body into the callers evbuf */ ctx->ret = 0; - if (!ctx->async) - { - 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)); - } - else - ctx->cb(req, arg); + 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); - if (ctx->async) - free(ctx); - return; connection_error: @@ -150,9 +136,6 @@ request_cb(struct evhttp_request *req, void *arg) event_base_loopbreak(ctx->evbase); - if (ctx->async) - free(ctx); - return; } @@ -181,10 +164,9 @@ request_header_cb(struct evhttp_request *req, void *arg) } #endif -static void * -request_make(void *arg) +int +http_client_request(struct http_client_ctx *ctx) { - struct http_client_ctx *ctx; struct evhttp_connection *evcon; struct evhttp_request *req; struct evkeyvalq *headers; @@ -194,8 +176,6 @@ request_make(void *arg) int port; int ret; - ctx = (struct http_client_ctx *)arg; - ctx->ret = -1; av_url_split(NULL, 0, NULL, 0, hostname, sizeof(hostname), &port, path, sizeof(path), ctx->url); @@ -203,7 +183,7 @@ request_make(void *arg) { DPRINTF(E_LOG, L_HTTP, "Error extracting hostname from URL: %s\n", ctx->url); - return NULL; + return ctx->ret; } if (port <= 0) @@ -220,7 +200,7 @@ request_make(void *arg) { DPRINTF(E_LOG, L_HTTP, "Could not create or find http client event base\n"); - return NULL; + return ctx->ret; } evcon = evhttp_connection_base_new(ctx->evbase, NULL, hostname, (unsigned short)port); @@ -229,7 +209,7 @@ request_make(void *arg) DPRINTF(E_LOG, L_HTTP, "Could not create connection to %s\n", hostname); event_base_free(ctx->evbase); - return NULL; + return ctx->ret; } evhttp_connection_set_timeout(evcon, HTTP_CLIENT_TIMEOUT); @@ -242,7 +222,7 @@ request_make(void *arg) evhttp_connection_free(evcon); event_base_free(ctx->evbase); - return NULL; + return ctx->ret; } #ifndef HAVE_LIBEVENT2_OLD @@ -267,7 +247,7 @@ request_make(void *arg) evhttp_connection_free(evcon); event_base_free(ctx->evbase); - return NULL; + return ctx->ret; } event_base_dispatch(ctx->evbase); @@ -275,41 +255,7 @@ request_make(void *arg) evhttp_connection_free(evcon); event_base_free(ctx->evbase); - return NULL; -} - -int -http_client_request(struct http_client_ctx *ctx) -{ - pthread_t tid; - pthread_attr_t attr; - int ret; - - /* If async make the request in a spawned thread, otherwise just make it */ - if (ctx->async) - { - ret = pthread_attr_init(&attr); - if (ret != 0) - { - DPRINTF(E_LOG, L_HTTP, "Error in http_client_request: Could not init attributes\n"); - return -1; - } - - pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); - ret = pthread_create(&tid, &attr, request_make, ctx); - if (ret != 0) - { - DPRINTF(E_LOG, L_HTTP, "Error in http_client_request: Could not create thread\n"); - return -1; - } - } - else - { - request_make(ctx); - ret = ctx->ret; - } - - return ret; + return ctx->ret; } int @@ -338,7 +284,6 @@ http_stream_setup(char **stream, const char *url) if (!evbuf) return -1; - ctx.async = 0; ctx.url = url; ctx.body = evbuf; @@ -560,7 +505,6 @@ http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only) return NULL; memset(&ctx, 0, sizeof(struct http_client_ctx)); - ctx.async = 0; ctx.url = fmtctx->filename; ctx.headers = kv; ctx.headers_only = 1; diff --git a/src/http.h b/src/http.h index 69a76409..62ba912b 100644 --- a/src/http.h +++ b/src/http.h @@ -10,11 +10,10 @@ struct http_client_ctx { - int async; const char *url; - /* For sync mode, a keyval/evbuf to store response headers and body - * Can be set to NULL to ignore that part of the response + /* 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; @@ -25,9 +24,6 @@ struct http_client_ctx */ int headers_only; - /* For async mode */ - void (*cb)(struct evhttp_request *, void *); - /* Private */ int ret; void *evbase; @@ -51,8 +47,7 @@ struct http_icy_metadata }; -/* Generic HTTP client - * Can be called blocking or non-blocking. No support for https. +/* Generic HTTP client. No support for https. * * @param ctx HTTP request params, see above * @return 0 if successful, -1 if an error occurred From e4c0c13a20a611807a8bba931218bacb24a0152f Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Tue, 31 Mar 2015 23:40:52 +0200 Subject: [PATCH 27/39] Remove irrelevant comment --- src/raop.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/raop.c b/src/raop.c index 81c8cb15..0d77cbaf 100644 --- a/src/raop.c +++ b/src/raop.c @@ -772,9 +772,7 @@ raop_metadata_prepare(int id, uint64_t rtptime) memset(rmd, 0, sizeof(struct raop_metadata)); - /* Get artwork first (thus waiting a bit so that db metadata has a moment to - * update in case it is a live stream) - */ + /* Get artwork */ rmd->artwork = evbuffer_new(); if (!rmd->artwork) { From e084eb71dbc51ffea7b3d2beb234e19df655cf49 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Wed, 1 Apr 2015 14:36:24 +0200 Subject: [PATCH 28/39] Polishing worker.c a bit --- src/worker.c | 46 ++++++++++++++++++++++------------------------ 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/src/worker.c b/src/worker.c index 8196fca6..a8601716 100644 --- a/src/worker.c +++ b/src/worker.c @@ -27,7 +27,7 @@ #include #include #include - #include +#include #include #include @@ -74,7 +74,8 @@ static int g_cmd_pipe[2]; static struct event *g_exitev; static struct event *g_cmdev; -/* --------------------------------- HELPERS ------------------------------- */ +/* ---------------------------- CALLBACK EXECUTION ------------------------- */ +/* Thread: worker */ static void execute_cb(int fd, short what, void *arg) @@ -85,6 +86,7 @@ execute_cb(int fd, short what, void *arg) event_free(cmd->arg.timer); free(cmd->arg.cb_arg); + free(cmd); } @@ -117,14 +119,14 @@ send_command(struct worker_command *cmd) if (!cmd->func) { - DPRINTF(E_LOG, L_MISC, "BUG: cmd->func is NULL!\n"); + 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_MISC, "Could not send command: %s\n", strerror(errno)); + DPRINTF(E_LOG, L_MAIN, "Could not send command: %s\n", strerror(errno)); return -1; } @@ -149,10 +151,10 @@ thread_exit(void) { int dummy = 42; - DPRINTF(E_DBG, L_MISC, "Killing worker thread\n"); + DPRINTF(E_DBG, L_MAIN, "Killing worker thread\n"); if (write(g_exit_pipe[1], &dummy, sizeof(dummy)) != sizeof(dummy)) - DPRINTF(E_LOG, L_MISC, "Could not write to exit fd: %s\n", strerror(errno)); + DPRINTF(E_LOG, L_MAIN, "Could not write to exit fd: %s\n", strerror(errno)); } @@ -165,12 +167,10 @@ worker(void *arg) { int ret; - DPRINTF(E_DBG, L_MISC, "Worker loop initiating\n"); - ret = db_perthread_init(); if (ret < 0) { - DPRINTF(E_LOG, L_MISC, "Error: DB init failed\n"); + DPRINTF(E_LOG, L_MAIN, "Error: DB init failed (worker thread)\n"); pthread_exit(NULL); } @@ -180,14 +180,12 @@ worker(void *arg) if (g_initialized) { - DPRINTF(E_LOG, L_MISC, "Worker event loop terminated ahead of time!\n"); + DPRINTF(E_LOG, L_MAIN, "Worker event loop terminated ahead of time!\n"); g_initialized = 0; } db_perthread_deinit(); - DPRINTF(E_DBG, L_MISC, "Worker loop terminating\n"); - pthread_exit(NULL); } @@ -199,7 +197,7 @@ exit_cb(int fd, short what, void *arg) ret = read(g_exit_pipe[0], &dummy, sizeof(dummy)); if (ret != sizeof(dummy)) - DPRINTF(E_LOG, L_MISC, "Error reading from exit pipe\n"); + DPRINTF(E_LOG, L_MAIN, "Error reading from exit pipe\n"); event_base_loopbreak(evbase_worker); @@ -217,7 +215,7 @@ command_cb(int fd, short what, void *arg) ret = read(g_cmd_pipe[0], &cmd, sizeof(cmd)); if (ret != sizeof(cmd)) { - DPRINTF(E_LOG, L_MISC, "Could not read command! (read %d): %s\n", ret, (ret < 0) ? strerror(errno) : "-no error-"); + DPRINTF(E_LOG, L_MAIN, "Could not read command! (read %d): %s\n", ret, (ret < 0) ? strerror(errno) : "-no error-"); goto readd; } @@ -252,12 +250,12 @@ worker_execute(void (*cb)(void *), void *cb_arg, size_t arg_size, int delay) struct worker_command *cmd; void *argcpy; - DPRINTF(E_DBG, L_MISC, "Got worker execute request\n"); + 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_MISC, "Could not allocate worker_command\n"); + DPRINTF(E_LOG, L_MAIN, "Could not allocate worker_command\n"); return; } @@ -266,7 +264,7 @@ worker_execute(void (*cb)(void *), void *cb_arg, size_t arg_size, int delay) argcpy = malloc(arg_size); if (!argcpy) { - DPRINTF(E_LOG, L_MISC, "Out of memory\n"); + DPRINTF(E_LOG, L_MAIN, "Out of memory\n"); return; } @@ -295,7 +293,7 @@ worker_init(void) # endif if (ret < 0) { - DPRINTF(E_LOG, L_MISC, "Could not create pipe: %s\n", strerror(errno)); + DPRINTF(E_LOG, L_MAIN, "Could not create pipe: %s\n", strerror(errno)); goto exit_fail; } @@ -306,28 +304,28 @@ worker_init(void) # endif if (ret < 0) { - DPRINTF(E_LOG, L_MISC, "Could not create command pipe: %s\n", strerror(errno)); + 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_MISC, "Could not create an event base\n"); + 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_MISC, "Could not create exit event\n"); + 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_MISC, "Could not create cmd event\n"); + DPRINTF(E_LOG, L_MAIN, "Could not create cmd event\n"); goto evnew_fail; } @@ -337,7 +335,7 @@ worker_init(void) ret = pthread_create(&tid_worker, NULL, worker, NULL); if (ret < 0) { - DPRINTF(E_LOG, L_MISC, "Could not spawn worker thread: %s\n", strerror(errno)); + DPRINTF(E_LOG, L_MAIN, "Could not spawn worker thread: %s\n", strerror(errno)); goto thread_fail; } @@ -371,7 +369,7 @@ worker_deinit(void) ret = pthread_join(tid_worker, NULL); if (ret != 0) { - DPRINTF(E_FATAL, L_MISC, "Could not join worker thread: %s\n", strerror(errno)); + DPRINTF(E_FATAL, L_MAIN, "Could not join worker thread: %s\n", strerror(errno)); return; } From 0f1a27641f1673df943b8147798b50d15a0edec9 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Wed, 1 Apr 2015 23:45:21 +0200 Subject: [PATCH 29/39] Fix minor mem leak in playlist scanner --- src/filescanner_playlist.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/filescanner_playlist.c b/src/filescanner_playlist.c index 301f2ae9..5963a1e9 100644 --- a/src/filescanner_playlist.c +++ b/src/filescanner_playlist.c @@ -296,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)); From be21482d7b5ed2fb30917e8380fd667505534953 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Thu, 2 Apr 2015 00:07:49 +0200 Subject: [PATCH 30/39] Fix memleak in filescanner_ffmpeg arising for ffmpeg versions without icy support --- src/filescanner_ffmpeg.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/filescanner_ffmpeg.c b/src/filescanner_ffmpeg.c index 148c5a9d..870f02ec 100644 --- a/src/filescanner_ffmpeg.c +++ b/src/filescanner_ffmpeg.c @@ -364,6 +364,9 @@ scan_metadata_ffmpeg(char *file, struct media_file_info *mfi) } ret = avformat_open_input(&ctx, path, NULL, &options); + + if (options) + av_dict_free(&options); #else ret = av_open_input_file(&ctx, path, NULL, 0, NULL); #endif From 56ece92209467c1bcf5893120fd856ad1c4376f6 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Thu, 2 Apr 2015 00:09:12 +0200 Subject: [PATCH 31/39] Remember to actually request ICY metadata in transcode.c --- src/transcode.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/transcode.c b/src/transcode.c index 47ea6aca..a0a0fe14 100644 --- a/src/transcode.c +++ b/src/transcode.c @@ -486,6 +486,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; @@ -498,6 +499,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 @@ -507,8 +510,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 From 50ef71054954916ac383196ff8d387b475dbebce Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Thu, 2 Apr 2015 00:40:10 +0200 Subject: [PATCH 32/39] Fix memleak in artwork.c --- src/artwork.c | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/artwork.c b/src/artwork.c index 84533232..bd0f4e62 100644 --- a/src/artwork.c +++ b/src/artwork.c @@ -707,41 +707,44 @@ artwork_get_player_image(char *path, int max_w, int max_h, struct evbuffer *evbu DPRINTF(E_DBG, L_ART, "Trying internet stream artwork in %s\n", path); + ret = 0; + player_icy_artwork_url(&url, path); if (!url) return 0; len = strlen(url); if ((len < 14) || (len > PATH_MAX)) // Can't be shorter than http://a/1.jpg - return 0; + goto out_url; kv = keyval_alloc(); if (!kv) - return 0; + goto out_url; memset(&ctx, 0, sizeof(ctx)); ctx.url = url; ctx.headers = kv; ctx.body = evbuf; - ret = http_client_request(&ctx); - if (ret < 0) - return 0; + if (http_client_request(&ctx) < 0) + goto out_kv; content_type = keyval_get(kv, "Content-Type"); - if (strcmp(content_type, "image/jpeg") == 0) + if (content_type && (strcmp(content_type, "image/jpeg") == 0)) ret = ART_FMT_JPEG; - else if (strcmp(content_type, "image/png") == 0) + else if (content_type && (strcmp(content_type, "image/png") == 0)) ret = ART_FMT_PNG; - else - ret = 0; 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; } From ad81e05ab4f317f2bc58977af13ad9d250e00ad2 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Tue, 7 Apr 2015 23:35:56 +0200 Subject: [PATCH 33/39] Better thread safety by making sure that the raop globals are only accessed through the player thread --- src/player.c | 82 ++++++++++++++++++++++++++++++++++++++++++++-------- src/raop.c | 52 +++++++++++++++++---------------- src/raop.h | 12 +++++--- 3 files changed, 105 insertions(+), 41 deletions(-) diff --git a/src/player.c b/src/player.c index cd18906e..eb68304f 100644 --- a/src/player.c +++ b/src/player.c @@ -154,6 +154,7 @@ struct player_command void *noarg; struct spk_enum *spk_enum; struct raop_device *rd; + struct raop_metadata *rmd; struct player_status *status; struct player_source *ps; player_status_handler status_handler; @@ -578,6 +579,9 @@ playback_abort(void); static int queue_clear(struct player_command *cmd); +static void +player_metadata_send(struct raop_metadata *rmd); + static void player_laudio_status_cb(enum laudio_state status) { @@ -626,7 +630,7 @@ player_laudio_status_cb(enum laudio_state status) } } -/* Callbacks from the worker thread */ +/* Callback from the worker thread (async operation as it may block) */ static void playcount_inc_cb(void *arg) { @@ -635,14 +639,23 @@ playcount_inc_cb(void *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_send_cb(void *arg) +metadata_prepare_cb(void *arg) { - struct raop_metadata_arg *rma = arg; + struct raop_metadata *rmd; - raop_metadata_send(rma); + rmd = raop_metadata_prepare(arg); + + if (rmd) + player_metadata_send(rmd); } +/* Callback from the worker thread (async operation as it may block) */ static void update_icy_cb(void *arg) { @@ -667,7 +680,7 @@ metadata_purge(void) } static void -metadata_send(struct player_source *ps, int startup) +metadata_trigger(struct player_source *ps, int startup) { struct raop_metadata_arg rma; @@ -695,11 +708,11 @@ metadata_send(struct player_source *ps, int startup) else { rma.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"); } - /* Defer the actual work of sending the metadata to the worker thread */ - worker_execute(metadata_send_cb, &rma, sizeof(struct raop_metadata_arg), 0); + /* Defer the actual work of preparing the metadata to the worker thread */ + worker_execute(metadata_prepare_cb, &rma, sizeof(struct raop_metadata_arg), 0); } static void @@ -732,7 +745,7 @@ metadata_icy_poll_cb(int fd, short what, void *arg) worker_execute(update_icy_cb, metadata, sizeof(struct http_icy_metadata), 0); status_update(player_state); - metadata_send(cur_streaming, 0); + metadata_trigger(cur_streaming, 0); /* Only free the struct, the content must be preserved for update_icy_cb */ free(metadata); @@ -1427,7 +1440,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; @@ -1480,7 +1493,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); @@ -2159,6 +2172,18 @@ device_remove_family(struct player_command *cmd) return 0; } +static int +metadata_send(struct player_command *cmd) +{ + struct raop_metadata *rmd; + + rmd = cmd->arg.rmd; + + raop_metadata_send(rmd); + + 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) @@ -2862,7 +2887,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 */ @@ -4869,6 +4894,39 @@ player_device_remove(struct raop_device *rd) } } +/* Thread: worker */ +static void +player_metadata_send(struct raop_metadata *rmd) +{ + struct player_command *cmd; + int ret; + + cmd = (struct player_command *)malloc(sizeof(struct player_command)); + if (!cmd) + { + DPRINTF(E_LOG, L_PLAYER, "Could not allocate player_command\n"); + + raop_metadata_free(rmd); + return; + } + + memset(cmd, 0, sizeof(struct player_command)); + + cmd->nonblock = 1; + + cmd->func = metadata_send; + cmd->arg.rmd = rmd; + + ret = nonblock_command(cmd); + if (ret < 0) + { + free(cmd); + raop_metadata_free(rmd); + + return; + } +} + /* RAOP devices discovery - mDNS callback */ /* Thread: main (mdns) */ diff --git a/src/raop.c b/src/raop.c index 0d77cbaf..f648257f 100644 --- a/src/raop.c +++ b/src/raop.c @@ -154,6 +154,9 @@ struct raop_metadata uint64_t start; uint64_t end; + uint64_t offset; + int startup; + struct raop_metadata *next; }; @@ -708,7 +711,7 @@ raop_crypt_encrypt_aes_key_base64(void) /* RAOP metadata */ -static void +void raop_metadata_free(struct raop_metadata *rmd) { evbuffer_free(rmd->metadata); @@ -751,8 +754,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(struct raop_metadata_arg *rma) { struct query_params qp; struct db_media_file_info dbmfi; @@ -781,10 +785,10 @@ raop_metadata_prepare(int id, uint64_t rtptime) goto skip_artwork; } - ret = artwork_get_item(id, 600, 600, rmd->artwork); + ret = artwork_get_item(rma->id, 600, 600, rmd->artwork); if (ret < 0) { - DPRINTF(E_INFO, L_RAOP, "Failed to retrieve artwork for file id %d; no artwork will be sent\n", id); + DPRINTF(E_INFO, L_RAOP, "Failed to retrieve artwork for file id %d; no artwork will be sent\n", rma->id); evbuffer_free(rmd->artwork); rmd->artwork = NULL; @@ -801,10 +805,10 @@ raop_metadata_prepare(int id, uint64_t rtptime) qp.sort = S_NONE; qp.filter = filter; - ret = snprintf(filter, sizeof(filter), "id = %d", id); + ret = snprintf(filter, sizeof(filter), "id = %d", rma->id); if ((ret < 0) || (ret >= sizeof(filter))) { - DPRINTF(E_LOG, L_RAOP, "Could not build filter for file id %d; metadata will not be sent\n", id); + DPRINTF(E_LOG, L_RAOP, "Could not build filter for file id %d; metadata will not be sent\n", rma->id); goto out_rmd; } @@ -820,7 +824,7 @@ raop_metadata_prepare(int id, uint64_t rtptime) ret = db_query_fetch_file(&qp, &dbmfi); if (ret < 0) { - DPRINTF(E_LOG, L_RAOP, "Couldn't fetch file id %d; metadata will not be sent\n", id); + DPRINTF(E_LOG, L_RAOP, "Couldn't fetch file id %d; metadata will not be sent\n", rma->id); goto out_query; } @@ -863,17 +867,11 @@ raop_metadata_prepare(int id, uint64_t rtptime) db_query_end(&qp); - rmd->start = rtptime; - rmd->end = rtptime + (duration * 44100UL) / 1000UL; + rmd->start = rma->rtptime; + rmd->end = rma->rtptime + (duration * 44100UL) / 1000UL; - /* Add rmd to metadata list */ - if (metadata_tail) - metadata_tail->next = rmd; - else - { - metadata_head = rmd; - metadata_tail = rmd; - } + rmd->startup = rma->startup; + rmd->offset = rma->offset; return rmd; @@ -2164,16 +2162,20 @@ raop_metadata_startup_send(struct raop_session *rs) } void -raop_metadata_send(struct raop_metadata_arg *rma) +raop_metadata_send(struct raop_metadata *rmd) { struct raop_session *rs; - struct raop_metadata *rmd; uint32_t delay; int ret; - rmd = raop_metadata_prepare(rma->id, rma->rtptime); - if (!rmd) - return; + /* 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) { @@ -2183,9 +2185,9 @@ raop_metadata_send(struct raop_metadata_arg *rma) if (!rs->wants_metadata) continue; - delay = (rma->startup) ? RAOP_MD_DELAY_STARTUP : RAOP_MD_DELAY_SWITCH; + delay = (rmd->startup) ? RAOP_MD_DELAY_STARTUP : RAOP_MD_DELAY_SWITCH; - ret = raop_metadata_send_internal(rs, rmd, rma->offset, delay); + ret = raop_metadata_send_internal(rs, rmd, rmd->offset, delay); if (ret < 0) { raop_session_failure(rs); diff --git a/src/raop.h b/src/raop.h index 45dd9dd1..7b17449b 100644 --- a/src/raop.h +++ b/src/raop.h @@ -26,6 +26,7 @@ enum raop_devtype { }; struct raop_session; +struct raop_metadata; struct raop_device { @@ -98,6 +99,8 @@ 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_free(struct raop_metadata *rmd); void raop_metadata_purge(void); @@ -105,6 +108,11 @@ raop_metadata_purge(void); void raop_metadata_prune(uint64_t rtptime); +struct raop_metadata * +raop_metadata_prepare(struct raop_metadata_arg *rma); + +void +raop_metadata_send(struct raop_metadata *rmd); int raop_device_probe(struct raop_device *rd, raop_status_cb cb); @@ -121,10 +129,6 @@ raop_playback_start(uint64_t next_pkt, struct timespec *ts); void raop_playback_stop(void); - -void -raop_metadata_send(struct raop_metadata_arg *rma); - int raop_set_volume_one(struct raop_session *rs, int volume, raop_status_cb cb); From f1931bfc1a96bd4659fe9cb4393807461fcdde34 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Thu, 9 Apr 2015 21:04:35 +0200 Subject: [PATCH 34/39] Abandon raop_metadata_arg, and defer metadata time setting --- src/player.c | 86 ++++++++++++++++++++++++++-------------------------- src/raop.c | 32 +++++++++---------- src/raop.h | 12 ++------ 3 files changed, 60 insertions(+), 70 deletions(-) diff --git a/src/player.c b/src/player.c index eb68304f..6843d883 100644 --- a/src/player.c +++ b/src/player.c @@ -139,6 +139,16 @@ struct icy_artwork 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; @@ -154,9 +164,9 @@ struct player_command void *noarg; struct spk_enum *spk_enum; struct raop_device *rd; - struct raop_metadata *rmd; 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; @@ -580,7 +590,7 @@ static int queue_clear(struct player_command *cmd); static void -player_metadata_send(struct raop_metadata *rmd); +player_metadata_send(struct player_metadata *pmd); static void player_laudio_status_cb(enum laudio_state status) @@ -647,12 +657,12 @@ playcount_inc_cb(void *arg) static void metadata_prepare_cb(void *arg) { - struct raop_metadata *rmd; + struct player_metadata *pmd = arg; - rmd = raop_metadata_prepare(arg); + pmd->rmd = raop_metadata_prepare(pmd->id); - if (rmd) - player_metadata_send(rmd); + if (pmd->rmd) + player_metadata_send(pmd); } /* Callback from the worker thread (async operation as it may block) */ @@ -682,37 +692,37 @@ metadata_purge(void) static void metadata_trigger(struct player_source *ps, int startup) { - struct raop_metadata_arg rma; + struct player_metadata pmd; - rma.id = ps->id; - rma.offset = 0; - rma.startup = startup; + 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) { - rma.offset = ps->output_start - ps->stream_start; - rma.rtptime = ps->stream_start; + pmd.offset = ps->output_start - ps->stream_start; + pmd.rtptime = ps->stream_start; } else if (startup) { - rma.rtptime = last_rtptime + AIRTUNES_V2_PACKET_SAMPLES; + /* Will be set later, right before sending */ } /* Generic case */ else if (cur_streaming && (cur_streaming->end)) { - rma.rtptime = cur_streaming->end + 1; + pmd.rtptime = cur_streaming->end + 1; } else { - rma.rtptime = 0; DPRINTF(E_LOG, L_PLAYER, "PTOH! Unhandled song boundary case in metadata_trigger()\n"); } /* Defer the actual work of preparing the metadata to the worker thread */ - worker_execute(metadata_prepare_cb, &rma, sizeof(struct raop_metadata_arg), 0); + worker_execute(metadata_prepare_cb, &pmd, sizeof(struct player_metadata), 0); } static void @@ -2175,11 +2185,17 @@ device_remove_family(struct player_command *cmd) static int metadata_send(struct player_command *cmd) { - struct raop_metadata *rmd; + struct player_metadata *pmd; - rmd = cmd->arg.rmd; + pmd = cmd->arg.pmd; - raop_metadata_send(rmd); + /* 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; } @@ -4896,35 +4912,19 @@ player_device_remove(struct raop_device *rd) /* Thread: worker */ static void -player_metadata_send(struct raop_metadata *rmd) +player_metadata_send(struct player_metadata *pmd) { - struct player_command *cmd; - int ret; + struct player_command cmd; - cmd = (struct player_command *)malloc(sizeof(struct player_command)); - if (!cmd) - { - DPRINTF(E_LOG, L_PLAYER, "Could not allocate player_command\n"); + command_init(&cmd); - raop_metadata_free(rmd); - return; - } + cmd.func = metadata_send; + cmd.func_bh = NULL; + cmd.arg.pmd = pmd; - memset(cmd, 0, sizeof(struct player_command)); + sync_command(&cmd); - cmd->nonblock = 1; - - cmd->func = metadata_send; - cmd->arg.rmd = rmd; - - ret = nonblock_command(cmd); - if (ret < 0) - { - free(cmd); - raop_metadata_free(rmd); - - return; - } + command_deinit(&cmd); } diff --git a/src/raop.c b/src/raop.c index f648257f..20d34366 100644 --- a/src/raop.c +++ b/src/raop.c @@ -154,9 +154,6 @@ struct raop_metadata uint64_t start; uint64_t end; - uint64_t offset; - int startup; - struct raop_metadata *next; }; @@ -756,7 +753,7 @@ raop_metadata_prune(uint64_t rtptime) /* Thread: worker */ struct raop_metadata * -raop_metadata_prepare(struct raop_metadata_arg *rma) +raop_metadata_prepare(int id) { struct query_params qp; struct db_media_file_info dbmfi; @@ -785,10 +782,10 @@ raop_metadata_prepare(struct raop_metadata_arg *rma) goto skip_artwork; } - ret = artwork_get_item(rma->id, 600, 600, rmd->artwork); + ret = artwork_get_item(id, 600, 600, rmd->artwork); if (ret < 0) { - DPRINTF(E_INFO, L_RAOP, "Failed to retrieve artwork for file id %d; no artwork will be sent\n", rma->id); + 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; @@ -805,10 +802,10 @@ raop_metadata_prepare(struct raop_metadata_arg *rma) qp.sort = S_NONE; qp.filter = filter; - ret = snprintf(filter, sizeof(filter), "id = %d", rma->id); + ret = snprintf(filter, sizeof(filter), "id = %d", id); if ((ret < 0) || (ret >= sizeof(filter))) { - DPRINTF(E_LOG, L_RAOP, "Could not build filter for file id %d; metadata will not be sent\n", rma->id); + DPRINTF(E_LOG, L_RAOP, "Could not build filter for file id %d; metadata will not be sent\n", id); goto out_rmd; } @@ -824,7 +821,7 @@ raop_metadata_prepare(struct raop_metadata_arg *rma) ret = db_query_fetch_file(&qp, &dbmfi); if (ret < 0) { - DPRINTF(E_LOG, L_RAOP, "Couldn't fetch file id %d; metadata will not be sent\n", rma->id); + DPRINTF(E_LOG, L_RAOP, "Couldn't fetch file id %d; metadata will not be sent\n", id); goto out_query; } @@ -867,11 +864,9 @@ raop_metadata_prepare(struct raop_metadata_arg *rma) db_query_end(&qp); - rmd->start = rma->rtptime; - rmd->end = rma->rtptime + (duration * 44100UL) / 1000UL; - - rmd->startup = rma->startup; - rmd->offset = rma->offset; + /* raop_metadata_send() will add rtptime to these */ + rmd->start = 0; + rmd->end = (duration * 44100UL) / 1000UL; return rmd; @@ -2162,12 +2157,15 @@ raop_metadata_startup_send(struct raop_session *rs) } void -raop_metadata_send(struct raop_metadata *rmd) +raop_metadata_send(struct raop_metadata *rmd, uint64_t rtptime, uint64_t offset, int startup) { struct raop_session *rs; uint32_t delay; int ret; + rmd->start += rtptime; + rmd->end += rtptime; + /* Add the rmd to the metadata list */ if (metadata_tail) metadata_tail->next = rmd; @@ -2185,9 +2183,9 @@ raop_metadata_send(struct raop_metadata *rmd) if (!rs->wants_metadata) continue; - delay = (rmd->startup) ? RAOP_MD_DELAY_STARTUP : RAOP_MD_DELAY_SWITCH; + delay = (startup) ? RAOP_MD_DELAY_STARTUP : RAOP_MD_DELAY_SWITCH; - ret = raop_metadata_send_internal(rs, rmd, rmd->offset, delay); + ret = raop_metadata_send_internal(rs, rmd, offset, delay); if (ret < 0) { raop_session_failure(rs); diff --git a/src/raop.h b/src/raop.h index 7b17449b..33e24579 100644 --- a/src/raop.h +++ b/src/raop.h @@ -55,14 +55,6 @@ struct raop_device struct raop_device *next; }; -struct raop_metadata_arg -{ - int id; - uint64_t rtptime; - uint64_t offset; - int startup; -}; - /* RAOP session state */ /* Session is starting up */ @@ -109,10 +101,10 @@ void raop_metadata_prune(uint64_t rtptime); struct raop_metadata * -raop_metadata_prepare(struct raop_metadata_arg *rma); +raop_metadata_prepare(int id); void -raop_metadata_send(struct raop_metadata *rmd); +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); From 94f94b03fed146fdeb63239af23a5d1b5cabda97 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Thu, 9 Apr 2015 21:23:20 +0200 Subject: [PATCH 35/39] Swap artwork arguments so destination evbuffer comes first --- src/artwork.c | 66 ++++++++++++++++++++++++------------------------ src/artwork.h | 4 +-- src/httpd_daap.c | 4 +-- src/httpd_dacp.c | 2 +- src/raop.c | 2 +- 5 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/artwork.c b/src/artwork.c index bd0f4e62..b8a4ccf1 100644 --- a/src/artwork.c +++ b/src/artwork.c @@ -66,7 +66,7 @@ static const char *cover_extension[] = }; static int -artwork_read(char *path, struct evbuffer *evbuf) +artwork_read(struct evbuffer *evbuf, char *path) { uint8_t buf[4096]; struct stat sb; @@ -154,7 +154,7 @@ rescale_needed(AVCodecContext *src, int max_w, int max_h, int *target_w, int *ta } 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; @@ -599,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; @@ -673,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); @@ -696,7 +696,7 @@ artwork_get(char *path, int max_w, int max_h, struct evbuffer *evbuf) } static int -artwork_get_player_image(char *path, int max_w, int max_h, struct evbuffer *evbuf) +artwork_get_player_image(struct evbuffer *evbuf, char *path) { struct http_client_ctx ctx; struct keyval *kv; @@ -750,7 +750,7 @@ artwork_get_player_image(char *path, int max_w, int max_h, struct evbuffer *evbu #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; @@ -839,7 +839,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); @@ -858,15 +858,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; @@ -916,7 +916,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); } /* @@ -925,15 +925,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]; @@ -1033,23 +1033,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; @@ -1064,7 +1064,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: @@ -1074,11 +1074,11 @@ 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(in_path, max_w, max_h, evbuf); + ret = artwork_get_player_image(evbuf, in_path); break; } @@ -1088,14 +1088,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; @@ -1113,7 +1113,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); @@ -1130,14 +1130,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; @@ -1186,7 +1186,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; @@ -1228,7 +1228,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; @@ -1270,14 +1270,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; @@ -1293,11 +1293,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); @@ -1313,14 +1313,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; @@ -1335,7 +1335,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); diff --git a/src/artwork.h b/src/artwork.h index b6b00a51..c97734db 100644 --- a/src/artwork.h +++ b/src/artwork.h @@ -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 diff --git a/src/httpd_daap.c b/src/httpd_daap.c index f125614d..5ecd4b25 100644 --- a/src/httpd_daap.c +++ b/src/httpd_daap.c @@ -2375,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) { diff --git a/src/httpd_dacp.c b/src/httpd_dacp.c index 6eba110f..63b9cb7c 100644 --- a/src/httpd_dacp.c +++ b/src/httpd_dacp.c @@ -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: diff --git a/src/raop.c b/src/raop.c index 20d34366..702ca90a 100644 --- a/src/raop.c +++ b/src/raop.c @@ -782,7 +782,7 @@ raop_metadata_prepare(int id) goto skip_artwork; } - ret = artwork_get_item(id, 600, 600, rmd->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); From e72447954a1a77bef68417e3b13af1a80dfd7ce5 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Thu, 9 Apr 2015 22:22:42 +0200 Subject: [PATCH 36/39] Some cleaning up of ICY artwork retrieval --- src/artwork.c | 19 ++++++++++++++++++- src/player.c | 27 +++++++++++++-------------- src/player.h | 4 ++-- src/transcode.c | 5 +---- src/transcode.h | 2 +- 5 files changed, 35 insertions(+), 22 deletions(-) diff --git a/src/artwork.c b/src/artwork.c index b8a4ccf1..05bd8e4f 100644 --- a/src/artwork.c +++ b/src/artwork.c @@ -695,6 +695,18 @@ artwork_get(struct evbuffer *evbuf, char *path, int max_w, int max_h) 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) { @@ -702,6 +714,7 @@ artwork_get_player_image(struct evbuffer *evbuf, char *path) struct keyval *kv; const char *content_type; char *url; + int id; int len; int ret; @@ -709,7 +722,11 @@ artwork_get_player_image(struct evbuffer *evbuf, char *path) ret = 0; - player_icy_artwork_url(&url, path); + id = db_file_id_bypath(path); + if (!id) + return 0; + + url = player_get_icy_artwork_url(id); if (!url) return 0; diff --git a/src/player.c b/src/player.c index 6843d883..c403c555 100644 --- a/src/player.c +++ b/src/player.c @@ -135,7 +135,7 @@ struct item_range struct icy_artwork { - char *stream_url; + uint32_t id; char *artwork_url; }; @@ -175,7 +175,7 @@ struct player_command int intval; int ps_pos[2]; struct item_range item_range; - struct icy_artwork icy_artwork; + struct icy_artwork icy; } arg; int ret; @@ -2628,15 +2628,13 @@ now_playing(struct player_command *cmd) static int artwork_url_get(struct player_command *cmd) { - DPRINTF(E_DBG, L_PLAYER, "ICY artwork url call\n"); + cmd->arg.icy.artwork_url = NULL; - cmd->arg.icy_artwork.artwork_url = NULL; - - /* Not playing a stream */ - if (!cur_playing || cur_playing->type != SOURCE_HTTP || !cur_playing->ctx) + /* Check that we are playing a viable stream, and that it has the requested id */ + if (!cur_streaming || cur_streaming->id != cmd->arg.icy.id || cur_streaming->type != SOURCE_HTTP || !cur_streaming->ctx) return -1; - transcode_metadata_artwork_url(cur_playing->ctx, &cmd->arg.icy_artwork.artwork_url, cmd->arg.icy_artwork.stream_url); + transcode_metadata_artwork_url(cur_streaming->ctx, &cmd->arg.icy.artwork_url); return 0; } @@ -4296,8 +4294,8 @@ player_now_playing(uint32_t *id) return ret; } -int -player_icy_artwork_url(char **artwork_url, char *stream_url) +char * +player_get_icy_artwork_url(uint32_t id) { struct player_command cmd; int ret; @@ -4306,18 +4304,19 @@ player_icy_artwork_url(char **artwork_url, char *stream_url) cmd.func = artwork_url_get; cmd.func_bh = NULL; - cmd.arg.icy_artwork.stream_url = stream_url; + cmd.arg.icy.id = id; if (pthread_self() != tid_player) ret = sync_command(&cmd); else ret = artwork_url_get(&cmd); - *artwork_url = cmd.arg.icy_artwork.artwork_url; - command_deinit(&cmd); - return ret; + if (ret < 0) + return NULL; + else + return cmd.arg.icy.artwork_url; } /* diff --git a/src/player.h b/src/player.h index c313efe5..a0a591e1 100644 --- a/src/player.h +++ b/src/player.h @@ -133,8 +133,8 @@ player_get_status(struct player_status *status); int player_now_playing(uint32_t *id); -int -player_icy_artwork_url(char **artwork_url, char *stream_url); +char * +player_get_icy_artwork_url(uint32_t id); void player_speaker_enumerate(spk_enum_cb cb, void *arg); diff --git a/src/transcode.c b/src/transcode.c index a0a0fe14..132f2ca6 100644 --- a/src/transcode.c +++ b/src/transcode.c @@ -926,7 +926,7 @@ transcode_metadata(struct transcode_ctx *ctx, struct http_icy_metadata **metadat } void -transcode_metadata_artwork_url(struct transcode_ctx *ctx, char **artwork_url, char *stream_url) +transcode_metadata_artwork_url(struct transcode_ctx *ctx, char **artwork_url) { struct http_icy_metadata *m; @@ -935,9 +935,6 @@ transcode_metadata_artwork_url(struct transcode_ctx *ctx, char **artwork_url, ch if (!ctx->fmtctx || !ctx->fmtctx->filename) return; - if (strcmp(ctx->fmtctx->filename, stream_url) != 0) - return; - m = http_icy_metadata_get(ctx->fmtctx, 1); if (!m) return; diff --git a/src/transcode.h b/src/transcode.h index 5e1f8d9c..77afa3bd 100644 --- a/src/transcode.h +++ b/src/transcode.h @@ -30,6 +30,6 @@ 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, char *stream_url); +transcode_metadata_artwork_url(struct transcode_ctx *ctx, char **artwork_url); #endif /* !__TRANSCODE_H__ */ From 0397b824a53d345fb63371344055ef5e12b12b44 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Thu, 9 Apr 2015 22:33:23 +0200 Subject: [PATCH 37/39] Fixup (select between cur_playing and cur_streaming) --- src/player.c | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/player.c b/src/player.c index c403c555..2f033bcb 100644 --- a/src/player.c +++ b/src/player.c @@ -2628,13 +2628,22 @@ now_playing(struct player_command *cmd) static int artwork_url_get(struct player_command *cmd) { + struct player_source *ps; + cmd->arg.icy.artwork_url = NULL; - /* Check that we are playing a viable stream, and that it has the requested id */ - if (!cur_streaming || cur_streaming->id != cmd->arg.icy.id || cur_streaming->type != SOURCE_HTTP || !cur_streaming->ctx) + if (cur_playing) + ps = cur_playing; + else if (cur_streaming) + ps = cur_streaming; + else return -1; - transcode_metadata_artwork_url(cur_streaming->ctx, &cmd->arg.icy.artwork_url); + /* 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; } From 99cda05dabd55dc12adc2ab9f6608c70273fab2e Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sat, 11 Apr 2015 20:30:31 +0200 Subject: [PATCH 38/39] Remove player metadata event timer (use the existing instead) --- src/httpd.c | 3 ++- src/player.c | 61 ++++++++++++------------------------------------- src/transcode.c | 5 +++- src/transcode.h | 2 +- 4 files changed, 22 insertions(+), 49 deletions(-) diff --git a/src/httpd.c b/src/httpd.c index 16529833..b7cc3815 100644 --- a/src/httpd.c +++ b/src/httpd.c @@ -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) diff --git a/src/player.c b/src/player.c index 2f033bcb..667adc93 100644 --- a/src/player.c +++ b/src/player.c @@ -79,9 +79,6 @@ #define MAX(a, b) ((a > b) ? a : b) #endif -/* Interval between ICY metadata polls for streams, in seconds */ -#define METADATA_ICY_POLL 5 - enum player_sync_source { PLAYER_SYNC_CLOCK, @@ -203,7 +200,6 @@ static int cmd_pipe[2]; static int player_exit; static struct event *exitev; static struct event *cmdev; -static struct event *metaev; static pthread_t tid_player; /* Player status */ @@ -725,26 +721,16 @@ metadata_trigger(struct player_source *ps, int startup) worker_execute(metadata_prepare_cb, &pmd, sizeof(struct player_metadata), 0); } -static void -metadata_icy_poll_cb(int fd, short what, void *arg) +/* Checks if there is new HTTP ICY metadata, and if so sends updates to clients */ +void +metadata_check_icy(void) { - struct timeval tv = { METADATA_ICY_POLL, 0 }; struct http_icy_metadata *metadata; int changed; - /* Playback of stream has stopped, so stop polling */ - if (!cur_streaming || cur_streaming->type != SOURCE_HTTP || !cur_streaming->ctx) - { - if (metaev) - event_free(metaev); - - metaev = NULL; - return; - } - transcode_metadata(cur_streaming->ctx, &metadata, &changed); if (!metadata) - goto no_metadata; + return; if (!changed) goto no_update; @@ -754,38 +740,18 @@ metadata_icy_poll_cb(int fd, short what, void *arg) /* Defer the database update to the worker thread */ worker_execute(update_icy_cb, metadata, sizeof(struct http_icy_metadata), 0); - status_update(player_state); + /* 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); - evtimer_add(metaev, &tv); + status_update(player_state); return; no_update: http_icy_metadata_free(metadata, 0); - - no_metadata: - evtimer_add(metaev, &tv); -} - -static void -metadata_icy_poll_start(void) -{ - struct timeval tv = { METADATA_ICY_POLL, 0 }; - - DPRINTF(E_DBG, L_PLAYER, "Starting ICY polling\n"); - - if (metaev) - return; - - metaev = evtimer_new(evbase_player, metadata_icy_poll_cb, NULL); - if (!metaev) - return; - - evtimer_add(metaev, &tv); } /* Audio sources */ @@ -1415,10 +1381,6 @@ source_open(struct player_source *ps, int no_md) mfi->path = url; ret = transcode_setup(&ps->ctx, mfi, NULL, 0); - if (ret < 0) - break; - - metadata_icy_poll_start(); break; case 2: @@ -1849,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; @@ -1875,9 +1838,15 @@ source_read(uint8_t *buf, int len, uint64_t rtptime) { switch (cur_streaming->type) { - case SOURCE_FILE: case SOURCE_HTTP: - ret = transcode(cur_streaming->ctx, audio_buf, len - nbytes); + 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 diff --git a/src/transcode.c b/src/transcode.c index 132f2ca6..9d2e1acc 100644 --- a/src/transcode.c +++ b/src/transcode.c @@ -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; @@ -153,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; @@ -393,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; } diff --git a/src/transcode.h b/src/transcode.h index 77afa3bd..10487027 100644 --- a/src/transcode.h +++ b/src/transcode.h @@ -12,7 +12,7 @@ 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); From 7d756230c27b61537591b254128b11ad83274d53 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sat, 11 Apr 2015 20:53:09 +0200 Subject: [PATCH 39/39] Make raop_metadata_free private again and change a comment in http.c --- src/http.c | 4 ++-- src/raop.c | 2 +- src/raop.h | 3 --- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/http.c b/src/http.c index 6b56c78c..5ccea129 100644 --- a/src/http.c +++ b/src/http.c @@ -484,7 +484,7 @@ http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only) * headers, so we must instead open the stream ourselves to get the metadata. * Sorry about the extra connections, you radio streaming people! * - * TODO: Get packet metadata from fmtctx->packet_buffer + * 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) @@ -496,7 +496,7 @@ http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only) int got_header; int ret; - /* Can only get header metadata at the moment */ + /* Can only get header metadata */ if (packet_only) return NULL; diff --git a/src/raop.c b/src/raop.c index 702ca90a..fb7ff0d0 100644 --- a/src/raop.c +++ b/src/raop.c @@ -708,7 +708,7 @@ raop_crypt_encrypt_aes_key_base64(void) /* RAOP metadata */ -void +static void raop_metadata_free(struct raop_metadata *rmd) { evbuffer_free(rmd->metadata); diff --git a/src/raop.h b/src/raop.h index 33e24579..e596dbb1 100644 --- a/src/raop.h +++ b/src/raop.h @@ -91,9 +91,6 @@ 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_free(struct raop_metadata *rmd); - void raop_metadata_purge(void);