diff --git a/src/Makefile.am b/src/Makefile.am index 5f8a4c34..92c2ea4e 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -72,7 +72,7 @@ forked_daapd_SOURCES = main.c \ logger.c logger.h \ conffile.c conffile.h \ filescanner.c filescanner.h \ - filescanner_ffmpeg.c filescanner_m3u.c $(ITUNESSRC) \ + filescanner_ffmpeg.c filescanner_m3u.c filescanner_icy.c $(ITUNESSRC) \ mdns_avahi.c mdns.h \ remote_pairing.c remote_pairing.h \ evhttp/http.c evhttp/evhttp.h \ diff --git a/src/evhttp/evhttp.h b/src/evhttp/evhttp.h index d0d258a8..652e0932 100644 --- a/src/evhttp/evhttp.h +++ b/src/evhttp/evhttp.h @@ -239,6 +239,13 @@ struct { * the regular callback. */ void (*chunk_cb)(struct evhttp_request *, void *); + + /* + * Callback added for forked-daapd so we can collect ICY + * (shoutcast) metadata from the http header. If return + * int is negative the connection will be closed. + */ + int (*header_cb)(struct evhttp_request *, void *); }; /** diff --git a/src/evhttp/http.c b/src/evhttp/http.c index 54ebba23..67ae250a 100644 --- a/src/evhttp/http.c +++ b/src/evhttp/http.c @@ -1656,6 +1656,14 @@ evhttp_read_header(struct evhttp_connection *evcon, struct evhttp_request *req) return; } + /* Callback can shut down connection with negative return value */ + if (req->header_cb != NULL) { + if ((*req->header_cb)(req, req->cb_arg) < 0) { + evhttp_connection_fail(evcon, EVCON_HTTP_EOF); + return; + } + } + /* Done reading headers, do the real work */ switch (req->kind) { case EVHTTP_REQUEST: diff --git a/src/filescanner.c b/src/filescanner.c index 5e23647e..c74f2828 100644 --- a/src/filescanner.c +++ b/src/filescanner.c @@ -382,14 +382,18 @@ process_media_file(char *file, time_t mtime, off_t size, int compilation, int ur /* General case */ if (ret < 0) { - ret = scan_metadata_ffmpeg(file, &mfi); if (url == 0) - mfi.data_kind = 0; /* real file */ - if (url == 1) + { + mfi.data_kind = 0; /* real file */ + ret = scan_metadata_ffmpeg(file, &mfi); + } + else if (url == 1) { mfi.data_kind = 1; /* url/stream */ - mfi.url = file; + ret = scan_metadata_icy(file, &mfi); } + else + ret = -1; } if (ret < 0) diff --git a/src/filescanner.h b/src/filescanner.h index b3522ed4..3335fd50 100644 --- a/src/filescanner.h +++ b/src/filescanner.h @@ -17,6 +17,9 @@ process_media_file(char *file, time_t mtime, off_t size, int compilation, int ur int scan_metadata_ffmpeg(char *file, struct media_file_info *mfi); +int +scan_metadata_icy(char *url, struct media_file_info *mfi); + void scan_m3u_playlist(char *file, time_t mtime); diff --git a/src/filescanner_icy.c b/src/filescanner_icy.c new file mode 100644 index 00000000..bc4c6ce8 --- /dev/null +++ b/src/filescanner_icy.c @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2009-2010 Julien BLACHE + * + * Rewritten from mt-daapd code: + * Copyright (C) 2003 Ron Pedde (ron@pedde.com) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include "evhttp/evhttp.h" + +#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; +}; + +static void +free_icy(struct icy_ctx *ctx) +{ + if (ctx->url) + free(ctx->url); + + if (ctx) + free(ctx); +} + +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); + return -1; + } + + freeaddrinfo(result); + return 0; +} + +static void +scan_icy_request_cb(struct evhttp_request *req, void *arg) +{ + DPRINTF(E_DBG, L_SCAN, "ICY metadata request completed\n"); + + status = ICY_DONE; + return; +} + +/* 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 media_file_info *mfi; + const char *ptr; + + mfi = (struct media_file_info *)arg; + + if ( (ptr = evhttp_find_header(req->input_headers, "icy-name")) ) + { + mfi->title = strdup(ptr); + mfi->artist = strdup(ptr); + DPRINTF(E_DBG, L_SCAN, "Found ICY metadata, name (title/artist) is %s\n", mfi->title); + } + if ( (ptr = evhttp_find_header(req->input_headers, "icy-description")) ) + { + mfi->album = strdup(ptr); + DPRINTF(E_DBG, L_SCAN, "Found ICY metadata, description (album) is %s\n", mfi->album); + } + if ( (ptr = evhttp_find_header(req->input_headers, "icy-genre")) ) + { + mfi->genre = strdup(ptr); + DPRINTF(E_DBG, L_SCAN, "Found ICY metadata, genre is %s\n", mfi->genre); + } + + status = ICY_DONE; + return -1; +} + +int +scan_metadata_icy(char *url, struct media_file_info *mfi) +{ + struct evhttp_connection *evcon; + struct evhttp_request *req; + struct icy_ctx *ctx; + int ret; + int i; + + status = ICY_INIT; + + /* 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"); + + goto no_icy; + } + memset(ctx, 0, sizeof(struct icy_ctx)); + + ctx->url = evhttp_encode_uri(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->port < 0) + ctx->port = 80; + + /* 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); + evhttp_connection_set_timeout(evcon, ICY_TIMEOUT); + + /* Set up request */ + req = evhttp_request_new(scan_icy_request_cb, mfi); + if (!req) + { + DPRINTF(E_LOG, L_SCAN, "Could not create request to %s\n", ctx->hostname); + + evhttp_connection_free(evcon); + goto no_icy; + } + req->header_cb = scan_icy_header_cb; + evhttp_add_header(req->output_headers, "Host", ctx->hostname); + evhttp_add_header(req->output_headers, "Icy-MetaData", "1"); + + /* Make request */ + status = ICY_WAITING; + ret = evhttp_make_request(evcon, req, EVHTTP_REQ_GET, ctx->path); + if (ret < 0) + { + DPRINTF(E_LOG, L_SCAN, "Could not make request to %s\n", ctx->hostname); + + status = ICY_DONE; + evhttp_connection_free(evcon); + goto no_icy; + } + DPRINTF(E_INFO, L_SCAN, "Making request to %s asking for ICY (Shoutcast) metadata\n", url); + + /* Can't count on server support for ICY metadata, so + * while waiting for a reply make a parallel call to scan_metadata_ffmpeg. + * This call will also determine final return value. + */ + no_icy: + ret = scan_metadata_ffmpeg(url, mfi); + + /* Wait till ICY request completes or we reach timeout */ + for (i = 0; (status == ICY_WAITING) && (i <= ICY_TIMEOUT); i++) + sleep(1); + + free_icy(ctx); + + DPRINTF(E_DBG, L_SCAN, "scan_metadata_icy exiting with status %d after waiting %d sec\n", status, i); + + return ret; +}