diff --git a/forked-daapd.conf b/forked-daapd.conf index 2a5cf0ec..9112f166 100644 --- a/forked-daapd.conf +++ b/forked-daapd.conf @@ -203,6 +203,18 @@ spotify { # starred_album_override = false } +# MPD configuration (only have effect if MPD enabled - see README/INSTALL) +mpd { + # TCP port to listen on for MPD client requests. + # Default port is 6600, set to 0 to disable MPD support. +# port = 6600 + + # HTTP port to listen for artwork requests (only supported by some MPD clients + # and will need additional configuration in the MPD client to work). + # Set to 0 to disable serving artwork over http. +# http_port = 0 +} + # SQLite configuration (allows to modify the operation of the SQLite databases) # Make sure to read the SQLite documentation for the corresponding PRAGMA statements as # changing them from the defaults may increase the possibility of database corruptions! diff --git a/src/conffile.c b/src/conffile.c index ad3f398e..5b965abe 100644 --- a/src/conffile.c +++ b/src/conffile.c @@ -139,6 +139,7 @@ static cfg_opt_t sec_sqlite[] = static cfg_opt_t sec_mpd[] = { CFG_INT("port", 6600, CFGF_NONE), + CFG_INT("http_port", 0, CFGF_NONE), CFG_END() }; diff --git a/src/db.c b/src/db.c index b5bc446e..b4cdfdb1 100644 --- a/src/db.c +++ b/src/db.c @@ -2315,6 +2315,30 @@ db_file_id_byurl(char *url) #undef Q_TMPL } +int +db_file_id_by_virtualpath_match(char *path) +{ +#define Q_TMPL "SELECT f.id FROM files f WHERE f.virtual_path LIKE '%%%q%%';" + char *query; + int ret; + + query = sqlite3_mprintf(Q_TMPL, path); + if (!query) + { + DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); + + return 0; + } + + ret = db_file_id_byquery(query); + + sqlite3_free(query); + + return ret; + +#undef Q_TMPL +} + void db_file_stamp_bypath(char *path, time_t *stamp, int *id) { diff --git a/src/db.h b/src/db.h index 6d3e7092..e5f5c660 100644 --- a/src/db.h +++ b/src/db.h @@ -452,6 +452,9 @@ db_file_id_byfile(char *filename); int db_file_id_byurl(char *url); +int +db_file_id_by_virtualpath_match(char *path); + void db_file_stamp_bypath(char *path, time_t *stamp, int *id); diff --git a/src/mpd.c b/src/mpd.c index 42947c7e..a481e6a0 100644 --- a/src/mpd.c +++ b/src/mpd.c @@ -37,6 +37,7 @@ # include # include # include +#include # include #if defined(HAVE_SYS_EVENTFD_H) && defined(HAVE_EVENTFD) @@ -53,6 +54,7 @@ #include "conffile.h" #include "misc.h" #include "listener.h" +#include "artwork.h" #include "player.h" #include "filescanner.h" @@ -67,6 +69,8 @@ static struct event *g_exitev; static int g_cmd_pipe[2]; static struct event *g_cmdev; +static struct evhttp *evhttpd; + struct mpd_command; typedef int (*cmd_func)(struct mpd_command *cmd); @@ -4274,6 +4278,118 @@ command_cb(int fd, short what, void *arg) event_add(g_cmdev, NULL); } +/* + * Callback function that handles http requests for artwork files + * + * Some MPD clients allow retrieval of local artwork by making http request for artwork + * files. + * + * A request for the artwork of an item with virtual path "file:/path/to/example.mp3" looks + * like: + * GET http://:/path/to/cover.jpg + * + * Artwork is found by taking the uri and removing everything after the last '/'. The first + * item in the library with a virtual path that matches *path/to* is used to read the artwork + * file through the default forked-daapd artwork logic. + */ +static void +artwork_cb(struct evhttp_request *req, void *arg) +{ + struct evbuffer *evbuffer; + struct evhttp_uri *decoded; + const char *uri; + const char *path; + char *decoded_path; + char *last_slash; + int itemid; + int format; + + if (evhttp_request_get_command(req) != EVHTTP_REQ_GET) + { + DPRINTF(E_LOG, L_MPD, "Unsupported request type for artwork\n"); + evhttp_send_error(req, HTTP_BADMETHOD, "Method not allowed"); + return; + } + + uri = evhttp_request_get_uri(req); + DPRINTF(E_DBG, L_MPD, "Got artwork request with uri '%s'\n", uri); + + decoded = evhttp_uri_parse(uri); + if (!decoded) + { + DPRINTF(E_LOG, L_MPD, "Bad artwork request with uri '%s'\n", uri); + evhttp_send_error(req, HTTP_BADREQUEST, 0); + return; + } + + path = evhttp_uri_get_path(decoded); + if (!path) + { + DPRINTF(E_LOG, L_MPD, "Invalid path from artwork request with uri '%s'\n", uri); + evhttp_send_error(req, HTTP_BADREQUEST, 0); + evhttp_uri_free(decoded); + return; + } + + decoded_path = evhttp_uridecode(path, 0, NULL); + if (!decoded_path) + { + DPRINTF(E_LOG, L_MPD, "Error decoding path from artwork request with uri '%s'\n", uri); + evhttp_send_error(req, HTTP_BADREQUEST, 0); + evhttp_uri_free(decoded); + return; + } + + last_slash = strrchr(decoded_path, '/'); + if (last_slash) + *last_slash = '\0'; + + DPRINTF(E_DBG, L_MPD, "Artwork request for path: %s\n", decoded_path); + + itemid = db_file_id_by_virtualpath_match(decoded_path); + if (!itemid) + { + DPRINTF(E_WARN, L_MPD, "No item found for path '%s' from request uri '%s'\n", decoded_path, uri); + evhttp_send_error(req, HTTP_NOTFOUND, "Document was not found"); + evhttp_uri_free(decoded); + free(decoded_path); + return; + } + + evbuffer = evbuffer_new(); + if (!evbuffer) + { + DPRINTF(E_LOG, L_MPD, "Could not allocate an evbuffer for artwork request\n"); + evhttp_send_error(req, HTTP_INTERNAL, "Document was not found"); + evhttp_uri_free(decoded); + free(decoded_path); + return; + } + + format = artwork_get_item(evbuffer, itemid, 600, 600); + if (format < 0) + { + evhttp_send_error(req, HTTP_NOTFOUND, "Document was not found"); + } + else + { + switch (format) + { + case ART_FMT_PNG: + evhttp_add_header(evhttp_request_get_output_headers(req), "Content-Type", "image/png"); + break; + + default: + evhttp_add_header(evhttp_request_get_output_headers(req), "Content-Type", "image/jpeg"); + break; + } + evhttp_send_reply(req, HTTP_OK, "OK", evbuffer); + } + + evbuffer_free(evbuffer); + evhttp_uri_free(decoded); + free(decoded_path); +} /* Thread: main */ int mpd_init(void) @@ -4284,6 +4400,8 @@ int mpd_init(void) struct sockaddr_in sin; struct sockaddr_in6 sin6; unsigned short port; + unsigned short http_port; + const char *http_addr; int v6enabled; int ret; @@ -4380,6 +4498,33 @@ int mpd_init(void) } evconnlistener_set_error_cb(listener, mpd_accept_error_cb); + http_port = cfg_getint(cfg_getsec(cfg, "mpd"), "http_port"); + if (http_port > 0) + { + evhttpd = evhttp_new(evbase_mpd); + if (!evhttpd) + { + DPRINTF(E_LOG, L_MPD, "Could not create HTTP artwork server\n"); + + goto evhttp_fail; + } + + evhttp_set_gencb(evhttpd, artwork_cb, NULL); + + if (v6enabled) + http_addr = "::"; + else + http_addr = "0.0.0.0"; + + ret = evhttp_bind_socket(evhttpd, http_addr, http_port); + if (ret < 0) + { + DPRINTF(E_FATAL, L_MPD, "Could not bind HTTP artwork server at %s:%d\n", http_addr, port); + + goto bind_fail; + } + } + DPRINTF(E_INFO, L_MPD, "mpd thread init\n"); ret = pthread_create(&tid_mpd, NULL, mpd, NULL); @@ -4397,6 +4542,10 @@ int mpd_init(void) thread_fail: + bind_fail: + if (http_port > 0) + evhttp_free(evhttpd); + evhttp_fail: connew_fail: evnew_fail: event_base_free(evbase_mpd); @@ -4419,6 +4568,7 @@ void mpd_deinit(void) { struct idle_client *temp; unsigned short port; + unsigned short http_port; int ret; port = cfg_getint(cfg_getsec(cfg, "mpd"), "port"); @@ -4446,6 +4596,10 @@ void mpd_deinit(void) free(temp); } + http_port = cfg_getint(cfg_getsec(cfg, "mpd"), "http_port"); + if (http_port > 0) + evhttp_free(evhttpd); + // Free event base (should free events too) event_base_free(evbase_mpd);