/* * Copyright (C) 2016 Christian Meffert * * 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 */ /* * To test with raw command input use: * echo "command" | nc -q0 localhost 6600 * Or interactive with: * socat - TCP:localhost:6600 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include // isdigit #include #include #include #include #include #include "artwork.h" #include "commands.h" #include "conffile.h" #include "db.h" #include "library.h" #include "listener.h" #include "logger.h" #include "misc.h" #include "player.h" #include "remote_pairing.h" #include "parsers/mpd_parser.h" // TODO // optimize queries (map albumartist/album groupings to songalbumid/artistid in db.c) // support for group in count, e.g. count group artist // empty group value is a negation // Command handlers should use this for returning errors to make sure both // ack_error and errmsg are correctly set #define RETURN_ERROR(r, ...) \ do { out->ack_error = (r); free(out->errmsg); out->errmsg = safe_asprintf(__VA_ARGS__); return -1; } while(0) // According to the mpd protocol send "OK MPD \n" to the client, where // version is the version of the supported mpd protocol and not the server version #define MPD_PROTOCOL_VERSION_OK "OK MPD 0.22.4\n" /** * from MPD source: * * * * The most we ever use is for search/find, and that limits it to the * * number of tags we can have. Add one for the command, and one extra * * to catch errors clients may send us * * * static constexpr std::size_t COMMAND_ARGV_MAX = 2 + TAG_NUM_OF_ITEM_TYPES * 2; * * https://github.com/MusicPlayerDaemon/MPD/blob/master/src/command/AllCommands.cxx */ #define MPD_COMMAND_ARGV_MAX 70 /** * config: * max_command_list_size KBYTES * * https://github.com/MusicPlayerDaemon/MPD/blob/master/src/client/Config.cxx */ #define MPD_MAX_COMMAND_LIST_SIZE (2048*1024) #define MPD_ALL_IDLE_LISTENER_EVENTS (LISTENER_PLAYER | LISTENER_QUEUE | LISTENER_VOLUME | LISTENER_SPEAKER | LISTENER_OPTIONS | LISTENER_DATABASE | LISTENER_UPDATE | LISTENER_STORED_PLAYLIST | LISTENER_RATING) #define MPD_RATING_FACTOR 10.0 #define MPD_BINARY_SIZE 8192 /* MPD MAX_BINARY_SIZE */ #define MPD_BINARY_SIZE_MIN 64 /* min size from MPD ClientCommands.cxx */ // MPD error codes (taken from ack.h) enum mpd_ack_error { ACK_ERROR_NONE = 0, ACK_ERROR_NOT_LIST = 1, ACK_ERROR_ARG = 2, ACK_ERROR_PASSWORD = 3, ACK_ERROR_PERMISSION = 4, ACK_ERROR_UNKNOWN = 5, ACK_ERROR_NO_EXIST = 50, ACK_ERROR_PLAYLIST_MAX = 51, ACK_ERROR_SYSTEM = 52, ACK_ERROR_PLAYLIST_LOAD = 53, ACK_ERROR_UPDATE_ALREADY = 54, ACK_ERROR_PLAYER_SYNC = 55, ACK_ERROR_EXIST = 56, }; enum command_list_type { COMMAND_LIST_NONE = 0, COMMAND_LIST_BEGIN, COMMAND_LIST_OK_BEGIN, COMMAND_LIST_END, COMMAND_LIST_OK_END, }; struct mpd_client_ctx { // True if the connection is already authenticated or does not need authentication bool authenticated; // The events the client needs to be notified of short events; // True if the client is waiting for idle events bool is_idle; // The events the client is waiting for (set by the idle command) short idle_events; // The current binary limit size unsigned int binarylimit; // The output buffer for the client (used to send data to the client) struct evbuffer *evbuffer; // Equals COMMAND_LIST_NONE unless command_list_begin or command_list_ok_begin // received. enum command_list_type cmd_list_type; // Current command list // When cmd_list_type is either COMMAND_LIST_BEGIN or COMMAND_LIST_OK_BEGIN // received commands are added to this buffer. When command_list_end is // received, the commands saved in this buffer are processed. struct evbuffer *cmd_list_buffer; // Set to true by handlers and command processing if we must cut the client // connection. bool must_disconnect; struct mpd_client_ctx *next; }; #define MPD_WANTS_NUM_ARGV_MIN 1 #define MPD_WANTS_NUM_ARGV_MAX 3 enum command_wants_num { MPD_WANTS_NUM_NONE = 0, MPD_WANTS_NUM_ARG1_IVAL = (1 << (0 + MPD_WANTS_NUM_ARGV_MIN)), MPD_WANTS_NUM_ARG2_IVAL = (1 << (1 + MPD_WANTS_NUM_ARGV_MIN)), MPD_WANTS_NUM_ARG3_IVAL = (1 << (2 + MPD_WANTS_NUM_ARGV_MIN)), MPD_WANTS_NUM_ARG1_UVAL = (1 << (0 + MPD_WANTS_NUM_ARGV_MIN + MPD_WANTS_NUM_ARGV_MAX)), MPD_WANTS_NUM_ARG2_UVAL = (1 << (1 + MPD_WANTS_NUM_ARGV_MIN + MPD_WANTS_NUM_ARGV_MAX)), MPD_WANTS_NUM_ARG3_UVAL = (1 << (2 + MPD_WANTS_NUM_ARGV_MIN + MPD_WANTS_NUM_ARGV_MAX)), }; struct mpd_command_input { // Raw argument line const char *args_raw; // Argument line unescaped and split char *args_split; char *argv[MPD_COMMAND_ARGV_MAX]; int argc; int has_num; int32_t argv_i32val[MPD_COMMAND_ARGV_MAX]; uint32_t argv_u32val[MPD_COMMAND_ARGV_MAX]; }; struct mpd_command_output { struct evbuffer *evbuf; char *errmsg; enum mpd_ack_error ack_error; }; struct mpd_command { const char *name; int (*handler)(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx); int min_argc; int wants_num; }; struct output { unsigned short shortid; uint64_t id; char *name; unsigned selected; }; struct output_get_param { unsigned short curid; unsigned short shortid; struct output *output; }; struct output_outputs_param { unsigned short nextid; struct evbuffer *buf; }; /* ---------------------------------- Globals ------------------------------- */ static pthread_t tid_mpd; static struct event_base *evbase_mpd; static struct commands_base *cmdbase; static struct evhttp *evhttpd; static struct evconnlistener *mpd_listener; static int mpd_sockfd; static bool mpd_plugin_httpd; // Virtual path to the default playlist directory static char *default_pl_dir; static bool allow_modifying_stored_playlists; // Forward static struct mpd_command mpd_handlers[]; // List of all connected mpd clients struct mpd_client_ctx *mpd_clients; /** * This lists for ffmpeg suffixes and mime types are taken from the ffmpeg decoder plugin from mpd * (FfmpegDecoderPlugin.cxx, git revision 9fb351a139a56fc7b1ece549894f8fc31fa887cd). * * The server does not support different decoders and always uses ffmpeg or libav for decoding. * Some clients rely on a response for the decoder commands (e.g. ncmpccp) therefor return something * valid for this command. */ static const char * const ffmpeg_suffixes[] = { "16sv", "3g2", "3gp", "4xm", "8svx", "aa3", "aac", "ac3", "afc", "aif", "aifc", "aiff", "al", "alaw", "amr", "anim", "apc", "ape", "asf", "atrac", "au", "aud", "avi", "avm2", "avs", "bap", "bfi", "c93", "cak", "cin", "cmv", "cpk", "daud", "dct", "divx", "dts", "dv", "dvd", "dxa", "eac3", "film", "flac", "flc", "fli", "fll", "flx", "flv", "g726", "gsm", "gxf", "iss", "m1v", "m2v", "m2t", "m2ts", "m4a", "m4b", "m4v", "mad", "mj2", "mjpeg", "mjpg", "mka", "mkv", "mlp", "mm", "mmf", "mov", "mp+", "mp1", "mp2", "mp3", "mp4", "mpc", "mpeg", "mpg", "mpga", "mpp", "mpu", "mve", "mvi", "mxf", "nc", "nsv", "nut", "nuv", "oga", "ogm", "ogv", "ogx", "oma", "ogg", "omg", "psp", "pva", "qcp", "qt", "r3d", "ra", "ram", "rl2", "rm", "rmvb", "roq", "rpl", "rvc", "shn", "smk", "snd", "sol", "son", "spx", "str", "swf", "tgi", "tgq", "tgv", "thp", "ts", "tsp", "tta", "xa", "xvid", "uv", "uv2", "vb", "vid", "vob", "voc", "vp6", "vmd", "wav", "webm", "wma", "wmv", "wsaud", "wsvga", "wv", "wve", NULL }; static const char * const ffmpeg_mime_types[] = { "application/flv", "application/m4a", "application/mp4", "application/octet-stream", "application/ogg", "application/x-ms-wmz", "application/x-ms-wmd", "application/x-ogg", "application/x-shockwave-flash", "application/x-shorten", "audio/8svx", "audio/16sv", "audio/aac", "audio/ac3", "audio/aiff", "audio/amr", "audio/basic", "audio/flac", "audio/m4a", "audio/mp4", "audio/mpeg", "audio/musepack", "audio/ogg", "audio/qcelp", "audio/vorbis", "audio/vorbis+ogg", "audio/x-8svx", "audio/x-16sv", "audio/x-aac", "audio/x-ac3", "audio/x-aiff", "audio/x-alaw", "audio/x-au", "audio/x-dca", "audio/x-eac3", "audio/x-flac", "audio/x-gsm", "audio/x-mace", "audio/x-matroska", "audio/x-monkeys-audio", "audio/x-mpeg", "audio/x-ms-wma", "audio/x-ms-wax", "audio/x-musepack", "audio/x-ogg", "audio/x-vorbis", "audio/x-vorbis+ogg", "audio/x-pn-realaudio", "audio/x-pn-multirate-realaudio", "audio/x-speex", "audio/x-tta", "audio/x-voc", "audio/x-wav", "audio/x-wma", "audio/x-wv", "video/anim", "video/quicktime", "video/msvideo", "video/ogg", "video/theora", "video/webm", "video/x-dv", "video/x-flv", "video/x-matroska", "video/x-mjpeg", "video/x-mpeg", "video/x-ms-asf", "video/x-msvideo", "video/x-ms-wmv", "video/x-ms-wvx", "video/x-ms-wm", "video/x-ms-wmx", "video/x-nut", "video/x-pva", "video/x-theora", "video/x-vid", "video/x-wmv", "video/x-xvid", /* special value for the "ffmpeg" input plugin: all streams by the "ffmpeg" input plugin shall be decoded by this plugin */ "audio/x-mpd-ffmpeg", NULL }; /* -------------------------------- Helpers --------------------------------- */ /* * Some MPD clients crash if the tag value includes the newline character. * While they should normally not be included in most ID3 tags, they sometimes * are, so we just change them to space. See #1613 for more details. */ static char * sanitize(char *strval) { char *ptr = strval; if (!strval) return ""; while (*ptr != '\0') { if (*ptr == '\n') *ptr = ' '; ptr++; } return strval; } static void client_ctx_free(struct mpd_client_ctx *client_ctx) { if (!client_ctx) return; evbuffer_free(client_ctx->cmd_list_buffer); free(client_ctx); } static struct mpd_client_ctx * client_ctx_new(void) { struct mpd_client_ctx *client_ctx; CHECK_NULL(L_MPD, client_ctx = calloc(1, sizeof(struct mpd_client_ctx))); CHECK_NULL(L_MPD, client_ctx->cmd_list_buffer = evbuffer_new()); return client_ctx; } static void client_ctx_remove(void *arg) { struct mpd_client_ctx *client_ctx = arg; struct mpd_client_ctx *client; struct mpd_client_ctx *prev; client = mpd_clients; prev = NULL; while (client) { if (client == client_ctx) { DPRINTF(E_DBG, L_MPD, "Removing mpd client\n"); if (prev) prev->next = client->next; else mpd_clients = client->next; break; } prev = client; client = client->next; } client_ctx_free(client_ctx); } static struct mpd_client_ctx * client_ctx_add(void) { struct mpd_client_ctx *client_ctx = client_ctx_new(); client_ctx->next = mpd_clients; mpd_clients = client_ctx; return client_ctx; } static void free_output(struct output *output) { if (!output) return; free(output->name); free(output); } /* * Creates a new string for the given path that starts with a '/'. * If 'path' already starts with a '/' the returned string is a duplicate * of 'path'. * * The returned string needs to be freed by the caller. */ static char * prepend_slash(const char *path) { char *result; if (path[0] == '/') result = strdup(path); else result = safe_asprintf("/%s", path); return result; } static void mpd_time(char *buffer, size_t bufferlen, time_t t) { struct tm tm; const struct tm *tm2 = gmtime_r(&t, &tm); if (tm2 == NULL) return; strftime(buffer, bufferlen, "%FT%TZ", tm2); } /* * Splits a range argument of the form START:END (the END item is not included in the range) * into its start and end position. * * @param start_pos set by this method to the start position * @param end_pos set by this method to the end postion * @param range the range argument * @return 0 on success, -1 on failure */ static int range_pos_from_arg(int *start_pos, int *end_pos, const char *range) { int ret; if (strchr(range, ':')) { ret = sscanf(range, "%d:%d", start_pos, end_pos); if (ret < 0) { DPRINTF(E_LOG, L_MPD, "Error splitting range argument '%s' (return code = %d)\n", range, ret); return -1; } } else { ret = safe_atoi32(range, start_pos); if (ret < 0) { DPRINTF(E_LOG, L_MPD, "Error spitting integer argument '%s' (return code = %d)\n", range, ret); return -1; } *end_pos = (*start_pos) + 1; } return 0; } /* * Converts a TO arg, which is either an absolute position "X", or a position * relative to currently playing song "+X"/"-X", into absolute queue pos. * * @param to_pos set by this method to absolute queue pos * @param to_arg string with an integer (abs) or prefixed with +/- (rel) * @return 0 on success, -1 on failure */ static int to_pos_from_arg(int *to_pos, const char *to_arg) { struct player_status status; struct db_queue_item *queue_item; int ret; *to_pos = -1; if (!to_arg) return 0; // If no to_arg, assume end of queue (to_pos = -1) ret = safe_atoi32(to_arg, to_pos); // +12 will become 12, -12 becomes -12 if (ret < 0) return -1; if (to_arg[0] != '+' && to_arg[0] != '-') return 0; ret = player_get_status(&status); if (ret < 0) return -1; queue_item = (status.status == PLAY_STOPPED) ? db_queue_fetch_bypos(0, status.shuffle) : db_queue_fetch_byitemid(status.item_id); if (!queue_item) return -1; *to_pos += queue_item->pos; free_queue_item(queue_item, 0); return 0; } /* * Returns the next unquoted string argument from the input string */ static char* mpd_next_unquoted(char **input) { char *arg; arg = *input; while (**input != 0) { if (**input == ' ') { **input = '\0'; (*input)++; return arg; } (*input)++; } return arg; } /* * Returns the next quoted string argument from the input string * with the quotes removed */ static char* mpd_next_quoted(char **input) { char *arg; char *src; char *dst; char ch; // skip double quote character (*input)++; src = dst = arg = *input; while ((ch = *src) != '"') { // A backslash character escapes the following character and should be removed if (ch == '\\') { ch = *(++src); } *dst++ = ch; if (ch == 0) { // Error handling for missing double quote at end of parameter DPRINTF(E_LOG, L_MPD, "Error missing closing double quote in argument\n"); *input = src; return NULL; } ++src; } *dst = '\0'; *input = ++src; return arg; } // Splits the argument string into an array of strings. Arguments are separated // by a whitespace character and may be wrapped in double quotes. static int mpd_split_args(char **argv, int argv_size, int *argc, char **split, const char *line) { char *ptr; int arg_count = 0; *split = safe_strdup(line); ptr = *split; while (*ptr != 0 && arg_count < argv_size) { // Ignore whitespace characters if (*ptr == ' ') { ptr++; continue; } // Check if the parameter is wrapped in double quotes if (*ptr == '"') argv[arg_count] = mpd_next_quoted(&ptr); else argv[arg_count] = mpd_next_unquoted(&ptr); if (!argv[arg_count]) goto error; arg_count++; } *argc = arg_count; // No args or too many args if (arg_count == 0 || (*ptr != 0 && arg_count == argv_size)) goto error; return 0; error: free(*split); *split = NULL; return -1; } /* * Adds the information (path, id, tags, etc.) for the given song to the given buffer * with additional information for the position of this song in the player queue. * * Example output: * file: foo/bar/song.mp3 * Last-Modified: 2013-07-14T06:57:59Z * Time: 172 * Artist: foo * AlbumArtist: foo * ArtistSort: foo * AlbumArtistSort: foo * Title: song * Album: bar * Track: 1/11 * Date: 2012-09-11 * Genre: Alternative * Disc: 1/1 * MUSICBRAINZ_ALBUMARTISTID: c5c2ea1c-4bde-4f4d-bd0b-47b200bf99d6 * MUSICBRAINZ_ARTISTID: c5c2ea1c-4bde-4f4d-bd0b-47b200bf99d6 * MUSICBRAINZ_ALBUMID: 812f4b87-8ad9-41bd-be79-38151f17a2b4 * MUSICBRAINZ_TRACKID: fde95c39-ee51-48f6-a7f9-b5631c2ed156 * Pos: 0 * Id: 1 * * @param evbuf the response event buffer * @param queue_item queue item information * @return the number of bytes added if successful, or -1 if an error occurred. */ static int mpd_add_db_queue_item(struct evbuffer *evbuf, struct db_queue_item *queue_item) { char modified[32]; int ret; mpd_time(modified, sizeof(modified), queue_item->time_modified); ret = evbuffer_add_printf(evbuf, "file: %s\n" "Last-Modified: %s\n" "Time: %d\n" "Artist: %s\n" "AlbumArtist: %s\n" "ArtistSort: %s\n" "AlbumArtistSort: %s\n" "Album: %s\n" "Title: %s\n" "Track: %d\n" "Date: %d\n" "Genre: %s\n" "Disc: %d\n" "Pos: %d\n" "Id: %d\n", (queue_item->virtual_path + 1), modified, (queue_item->song_length / 1000), sanitize(queue_item->artist), sanitize(queue_item->album_artist), sanitize(queue_item->artist_sort), sanitize(queue_item->album_artist_sort), sanitize(queue_item->album), sanitize(queue_item->title), queue_item->track, queue_item->year, sanitize(queue_item->genre), queue_item->disc, queue_item->pos, queue_item->id); return ret; } /* * Adds the information (path, id, tags, etc.) for the given song to the given buffer. * * Example output: * file: foo/bar/song.mp3 * Last-Modified: 2013-07-14T06:57:59Z * Time: 172 * Artist: foo * AlbumArtist: foo * ArtistSort: foo * AlbumArtistSort: foo * Title: song * Album: bar * Track: 1/11 * Date: 2012-09-11 * Genre: Alternative * Disc: 1/1 * MUSICBRAINZ_ALBUMARTISTID: c5c2ea1c-4bde-4f4d-bd0b-47b200bf99d6 * MUSICBRAINZ_ARTISTID: c5c2ea1c-4bde-4f4d-bd0b-47b200bf99d6 * MUSICBRAINZ_ALBUMID: 812f4b87-8ad9-41bd-be79-38151f17a2b4 * MUSICBRAINZ_TRACKID: fde95c39-ee51-48f6-a7f9-b5631c2ed156 * * @param evbuf the response event buffer * @param mfi media information * @return the number of bytes added if successful, or -1 if an error occurred. */ static int mpd_add_db_media_file_info(struct evbuffer *evbuf, struct db_media_file_info *dbmfi) { char modified[32]; uint32_t time_modified; uint32_t songlength; int ret; if (safe_atou32(dbmfi->time_modified, &time_modified) != 0) { DPRINTF(E_LOG, L_MPD, "Error converting time modified to uint32_t: %s\n", dbmfi->time_modified); return -1; } mpd_time(modified, sizeof(modified), time_modified); if (safe_atou32(dbmfi->song_length, &songlength) != 0) { DPRINTF(E_LOG, L_MPD, "Error converting song length to uint32_t: %s\n", dbmfi->song_length); return -1; } ret = evbuffer_add_printf(evbuf, "file: %s\n" "Last-Modified: %s\n" "Time: %d\n" "duration: %.3f\n" "Artist: %s\n" "AlbumArtist: %s\n" "ArtistSort: %s\n" "AlbumArtistSort: %s\n" "Album: %s\n" "Title: %s\n" "Track: %s\n" "Date: %s\n" "Genre: %s\n" "Disc: %s\n", (dbmfi->virtual_path + 1), modified, (songlength / 1000), ((float) songlength / 1000), sanitize(dbmfi->artist), sanitize(dbmfi->album_artist), sanitize(dbmfi->artist_sort), sanitize(dbmfi->album_artist_sort), sanitize(dbmfi->album), sanitize(dbmfi->title), dbmfi->track, dbmfi->year, sanitize(dbmfi->genre), dbmfi->disc); return ret; } static bool is_filter_end(const char *arg) { return (strcasecmp(arg, "sort") == 0 || strcasecmp(arg, "group") == 0 || strcasecmp(arg, "window") == 0); } // The bison/flex parser only works with the filter format used after version // 0.20 e.g. "(TAG == 'value') instead of TAG VALUE. static int args_reassemble(char *args, size_t args_size, int argc, char **argv) { const char *op; int filter_start; int filter_end; int i; // "list" has the filter as second arg, the rest have it as first filter_start = (strcmp(argv[0], "list") == 0) ? 2 : 1; op = strstr(argv[0], "search") ? " contains " : " == "; for (i = filter_start; i < argc && !is_filter_end(argv[i]); i++) ; filter_end = i; snprintf(args, args_size, "%s", argv[0]); for (i = 1; i < filter_start; i++) safe_snprintf_cat(args, args_size, " %s", argv[i]); for (i = filter_start; i < filter_end; i++) { if (*argv[i] == '(') { safe_snprintf_cat(args, args_size, " %s", argv[i]); } else if (i + 1 < filter_end) // Legacy filter format (0.20 and before), we will convert { safe_snprintf_cat(args, args_size, " %s%s%s\"%s\"%s", i == filter_start ? "((" : "AND (", argv[i], op, argv[i + 1], i + 2 == filter_end ? "))" : ")"); i++; } else if (filter_end == filter_start + 1) // Special case: a legacy single token is allowed if listing albums for an artist { safe_snprintf_cat(args, args_size, " (AlbumArtist%s\"%s\")", op, argv[i]); } } for (i = filter_end; i < argc; i++) safe_snprintf_cat(args, args_size, " %s", argv[i]); // Return an error if the buffer was filled and thus probably truncated return (strlen(args) + 1 < args_size) ? 0 : -1; } /* * Invokes a lexer/parser to read a supported command * * @param qp Must be initialized by caller (zeroed or set to default values) * @param pos Allocated string with TO position parameter, e.g. "+5" (or NULL) * @param type Allocated string with TYPE parameter, e.g. "albums" (or NULL) * @param in The command input */ static int parse_command(struct query_params *qp, char **pos, char **tagtype, struct mpd_command_input *in) { struct mpd_result result; char args_reassembled[8192]; int ret; if (pos) *pos = NULL; if (tagtype) *tagtype = NULL; if (in->argc < 2) return 0; // Nothing to parse ret = args_reassemble(args_reassembled, sizeof(args_reassembled), in->argc, in->argv); if (ret < 0) { DPRINTF(E_LOG, L_MPD, "Could not prepare '%s' for parsing\n", in->args_raw); return -1; } DPRINTF(E_DBG, L_MPD, "Parse mpd query input '%s'\n", args_reassembled); ret = mpd_lex_parse(&result, args_reassembled); if (ret != 0) { DPRINTF(E_LOG, L_MPD, "Could not parse '%s': %s\n", args_reassembled, result.errmsg); return -1; } qp->filter = safe_strdup(result.where); qp->order = safe_strdup(result.order); qp->group = safe_strdup(result.group); qp->limit = result.limit; qp->offset = result.offset; qp->idx_type = (qp->limit || qp->offset) ? I_SUB : I_NONE; if (pos) *pos = safe_strdup(result.position); if (tagtype) *tagtype = safe_strdup(result.tagtype); return 0; } static int notify_idle_client(struct mpd_client_ctx *client_ctx, short events, bool add_ok) { if (!client_ctx->is_idle) { client_ctx->events |= events; return 1; } if (!(client_ctx->idle_events & events)) { DPRINTF(E_DBG, L_MPD, "Client not listening for events: %d\n", events); return 1; } if (events & LISTENER_DATABASE) evbuffer_add(client_ctx->evbuffer, "changed: database\n", 18); if (events & LISTENER_UPDATE) evbuffer_add(client_ctx->evbuffer, "changed: update\n", 16); if (events & LISTENER_QUEUE) evbuffer_add(client_ctx->evbuffer, "changed: playlist\n", 18); if (events & LISTENER_PLAYER) evbuffer_add(client_ctx->evbuffer, "changed: player\n", 16); if (events & LISTENER_VOLUME) evbuffer_add(client_ctx->evbuffer, "changed: mixer\n", 15); if (events & LISTENER_SPEAKER) evbuffer_add(client_ctx->evbuffer, "changed: output\n", 16); if (events & LISTENER_OPTIONS) evbuffer_add(client_ctx->evbuffer, "changed: options\n", 17); if (events & LISTENER_STORED_PLAYLIST) evbuffer_add(client_ctx->evbuffer, "changed: stored_playlist\n", 25); if (events & LISTENER_RATING) evbuffer_add(client_ctx->evbuffer, "changed: sticker\n", 17); if (add_ok) evbuffer_add(client_ctx->evbuffer, "OK\n", 3); client_ctx->is_idle = false; client_ctx->idle_events = 0; client_ctx->events = 0; return 0; } /* ----------------------------- Command handlers --------------------------- */ static int mpd_command_currentsong(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { struct player_status status; struct db_queue_item *queue_item; int ret; player_get_status(&status); if (status.status == PLAY_STOPPED) queue_item = db_queue_fetch_bypos(0, status.shuffle); else queue_item = db_queue_fetch_byitemid(status.item_id); if (!queue_item) return 0; ret = mpd_add_db_queue_item(out->evbuf, queue_item); free_queue_item(queue_item, 0); if (ret < 0) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Error setting media info for file with id: %d", status.id); return 0; } /* * Example input: * idle "database" "mixer" "options" "output" "player" "playlist" "sticker" "update" */ static int mpd_command_idle(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { char *key; int i; ctx->idle_events = 0; ctx->is_idle = true; if (in->argc > 1) { for (i = 1; i < in->argc; i++) { key = in->argv[i]; if (0 == strcmp(key, "database")) ctx->idle_events |= LISTENER_DATABASE; else if (0 == strcmp(key, "update")) ctx->idle_events |= LISTENER_UPDATE; else if (0 == strcmp(key, "player")) ctx->idle_events |= LISTENER_PLAYER; else if (0 == strcmp(key, "playlist")) ctx->idle_events |= LISTENER_QUEUE; else if (0 == strcmp(key, "mixer")) ctx->idle_events |= LISTENER_VOLUME; else if (0 == strcmp(key, "output")) ctx->idle_events |= LISTENER_SPEAKER; else if (0 == strcmp(key, "options")) ctx->idle_events |= LISTENER_OPTIONS; else if (0 == strcmp(key, "stored_playlist")) ctx->idle_events |= LISTENER_STORED_PLAYLIST; else if (0 == strcmp(key, "sticker")) ctx->idle_events |= LISTENER_RATING; else DPRINTF(E_DBG, L_MPD, "Idle command for '%s' not supported\n", key); } } else ctx->idle_events = MPD_ALL_IDLE_LISTENER_EVENTS; // If events the client listens to occurred since the last idle call (or since the client connected, // if it is the first idle call), notify immediately. if (ctx->events & ctx->idle_events) notify_idle_client(ctx, ctx->events, false); return 0; } static int mpd_command_noidle(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { /* * The protocol specifies: "The idle command can be canceled by * sending the command noidle (no other commands are allowed). MPD * will then leave idle mode and print results immediately; might be * empty at this time." */ if (ctx->events) notify_idle_client(ctx, ctx->events, false); ctx->is_idle = false; return 0; } /* * Command handler function for 'status' * * Example output: * volume: -1 * repeat: 0 * random: 0 * single: 0 * consume: 0 * playlist: 2 * playlistlength: 34 * mixrampdb: 0.000000 * state: stop * song: 0 * songid: 1 * time: 28:306 * elapsed: 28.178 * bitrate: 278 * audio: 44100:f:2 * nextsong: 1 * nextsongid: 2 */ static int mpd_command_status(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { struct player_status status; uint32_t queue_length = 0; int queue_version = 0; char *state; uint32_t itemid = 0; struct db_queue_item *queue_item; player_get_status(&status); switch (status.status) { case PLAY_PAUSED: state = "pause"; break; case PLAY_PLAYING: state = "play"; break; default: state = "stop"; break; } db_admin_getint(&queue_version, DB_ADMIN_QUEUE_VERSION); db_queue_get_count(&queue_length); evbuffer_add_printf(out->evbuf, "volume: %d\n" "repeat: %d\n" "random: %d\n" "single: %d\n" "consume: %d\n" "playlist: %d\n" "playlistlength: %d\n" "mixrampdb: 0.000000\n" "state: %s\n", status.volume, (status.repeat == REPEAT_OFF ? 0 : 1), status.shuffle, (status.repeat == REPEAT_SONG ? 1 : 0), status.consume, queue_version, queue_length, state); if (status.status != PLAY_STOPPED) queue_item = db_queue_fetch_byitemid(status.item_id); else queue_item = db_queue_fetch_bypos(0, status.shuffle); if (queue_item) { evbuffer_add_printf(out->evbuf, "song: %d\n" "songid: %d\n", queue_item->pos, queue_item->id); itemid = queue_item->id; free_queue_item(queue_item, 0); } if (status.status != PLAY_STOPPED) { evbuffer_add_printf(out->evbuf, "time: %d:%d\n" "elapsed: %#.3f\n" "bitrate: 128\n" "audio: 44100:16:2\n", (status.pos_ms / 1000), (status.len_ms / 1000), (status.pos_ms / 1000.0)); } if (library_is_scanning()) { evbuffer_add(out->evbuf, "updating_db: 1\n", 15); } if (itemid > 0) { queue_item = db_queue_fetch_next(itemid, status.shuffle); if (queue_item) { evbuffer_add_printf(out->evbuf, "nextsong: %d\n" "nextsongid: %d\n", queue_item->pos, queue_item->id); free_queue_item(queue_item, 0); } } return 0; } /* * Command handler function for 'stats' */ static int mpd_command_stats(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { struct query_params qp = { .type = Q_COUNT_ITEMS }; struct filecount_info fci; double uptime; int64_t db_start = 0; int64_t db_update = 0; int ret; ret = db_filecount_get(&fci, &qp); if (ret < 0) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Could not start query"); db_admin_getint64(&db_start, DB_ADMIN_START_TIME); uptime = difftime(time(NULL), (time_t) db_start); db_admin_getint64(&db_update, DB_ADMIN_DB_UPDATE); //TODO [mpd] Implement missing stats attributes (playtime) evbuffer_add_printf(out->evbuf, "artists: %d\n" "albums: %d\n" "songs: %d\n" "uptime: %.f\n" //in seceonds "db_playtime: %" PRIi64 "\n" "db_update: %" PRIi64 "\n" "playtime: %d\n", fci.artist_count, fci.album_count, fci.count, uptime, (fci.length / 1000), db_update, 7); return 0; } /* * Command handler function for 'consume' * Sets the consume mode, expects argument argv[1] to be an integer with * 0 = disable consume * 1 = enable consume */ static int mpd_command_consume(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { player_consume_set(in->argv_u32val[1]); return 0; } /* * Command handler function for 'random' * Sets the shuffle mode, expects argument argv[1] to be an integer with * 0 = disable shuffle * 1 = enable shuffle */ static int mpd_command_random(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { player_shuffle_set(in->argv_u32val[1]); return 0; } /* * Command handler function for 'repeat' * Sets the repeat mode, expects argument argv[1] to be an integer with * 0 = repeat off * 1 = repeat all */ static int mpd_command_repeat(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { if (in->argv_u32val[1] == 0) player_repeat_set(REPEAT_OFF); else player_repeat_set(REPEAT_ALL); return 0; } /* * Command handler function for 'setvol' * Sets the volume, expects argument argv[1] to be an integer 0-100 */ static int mpd_command_setvol(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { player_volume_set(in->argv_u32val[1]); return 0; } /* * Command handler function for 'single' * Sets the repeat mode, expects argument argv[1] to be an integer or * "oneshot" for 0.21 protocol. * The server only allows single-mode in combination with repeat, therefore * the command single translates (depending on the current repeat mode) into: * a) if repeat off: * 0 = repeat off * 1 = repeat song * b) if repeat all: * 0 = repeat all * 1 = repeat song * c) if repeat song: * 0 = repeat all * 1 = repeat song * Thus "oneshot" is accepted, but ignored under all circumstances. */ static int mpd_command_single(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { bool has_enable = (in->has_num & MPD_WANTS_NUM_ARG1_UVAL); uint32_t enable = in->argv_u32val[1]; struct player_status status; // 0.21 protocol: accept "oneshot" mode if (strcmp(in->argv[1], "oneshot") == 0) return 0; if (!has_enable) RETURN_ERROR(ACK_ERROR_ARG, "Command 'single' expects integer or 'oneshot' argument"); player_get_status(&status); if (enable == 0 && status.repeat != REPEAT_OFF) player_repeat_set(REPEAT_ALL); else if (enable == 0) player_repeat_set(REPEAT_OFF); else player_repeat_set(REPEAT_SONG); return 0; } /* * Command handler function for 'replay_gain_status' * The server does not support replay gain, therefor this function returns always * "replay_gain_mode: off". */ static int mpd_command_replay_gain_status(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { evbuffer_add(out->evbuf, "replay_gain_mode: off\n", 22); return 0; } /* * Command handler function for 'volume' * Changes the volume by the given amount, expects argument argv[1] to be an integer */ static int mpd_command_volume(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { struct player_status status; int32_t volume = in->argv_i32val[1]; player_get_status(&status); volume += status.volume; player_volume_set(volume); return 0; } /* * Command handler function for 'next' * Skips to the next song in the playqueue */ static int mpd_command_next(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { int ret; ret = player_playback_next(); if (ret < 0) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to skip to next song"); ret = player_playback_start(); if (ret < 0) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Player returned an error for start after nextitem"); return 0; } /* * Command handler function for 'pause' * Toggles pause/play, if the optional argument argv[1] is present, it must be an integer with * 0 = play * 1 = pause */ static int mpd_command_pause(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { uint32_t pause; struct player_status status; int ret; player_get_status(&status); if (in->has_num & MPD_WANTS_NUM_ARG1_UVAL) pause = in->argv_u32val[1]; else pause = (status.status == PLAY_PLAYING) ? 1 : 0; // MPD ignores pause in stopped state if (pause == 1 && status.status == PLAY_PLAYING) ret = player_playback_pause(); else if (pause == 0 && status.status == PLAY_PAUSED) ret = player_playback_start(); else ret = 0; if (ret < 0) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to %s playback", (pause == 1) ? "pause" : "start"); return 0; } /* * Command handler function for 'play' * Starts playback, the optional argument argv[1] represents the position in the playqueue * where to start playback. */ static int mpd_command_play(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { bool has_songpos = (in->has_num & MPD_WANTS_NUM_ARG1_UVAL); uint32_t songpos = in->argv_u32val[1]; struct player_status status; struct db_queue_item *queue_item; int ret; player_get_status(&status); if (status.status == PLAY_PLAYING && !has_songpos) return 0; // Stop playback, if player is already playing and a valid song position is // given (it will be restarted for the given song position) if (status.status == PLAY_PLAYING) player_playback_stop(); if (has_songpos) { queue_item = db_queue_fetch_bypos(songpos, 0); if (!queue_item) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to start playback, unknown song position"); ret = player_playback_start_byitem(queue_item); free_queue_item(queue_item, 0); } else ret = player_playback_start(); if (ret < 0) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to start playback (queue empty?)"); return 0; } /* * Command handler function for 'playid' * Starts playback, the optional argument argv[1] represents the songid of the song * where to start playback. */ static int mpd_command_playid(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { bool has_id = (in->has_num & MPD_WANTS_NUM_ARG1_UVAL); uint32_t id = in->argv_u32val[1]; struct player_status status; struct db_queue_item *queue_item; int ret; player_get_status(&status); // Stop playback, if player is already playing and a valid item id is given // (it will be restarted for the given song) if (status.status == PLAY_PLAYING) player_playback_stop(); if (has_id) { queue_item = db_queue_fetch_byitemid(id); if (!queue_item) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to start playback, unknown song id"); ret = player_playback_start_byitem(queue_item); free_queue_item(queue_item, 0); } else ret = player_playback_start(); if (ret < 0) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to start playback (queue empty?)"); return 0; } /* * Command handler function for 'previous' * Skips to the previous song in the playqueue */ static int mpd_command_previous(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { int ret; ret = player_playback_prev(); if (ret < 0) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to skip to previous song"); ret = player_playback_start(); if (ret < 0) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Player returned an error for start after previtem"); return 0; } /* * Command handler function for 'seek' * Seeks to song at the given position in argv[1] to the position in seconds given in argument argv[2] * (fractions allowed). */ static int mpd_command_seek(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { float seek_target_sec; int seek_target_msec; int ret; //TODO Allow seeking in songs not currently playing seek_target_sec = strtof(in->argv[2], NULL); seek_target_msec = seek_target_sec * 1000; ret = player_playback_seek(seek_target_msec, PLAYER_SEEK_POSITION); if (ret < 0) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to seek current song to time %d msec", seek_target_msec); ret = player_playback_start(); if (ret < 0) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to start playback"); return 0; } /* * Command handler function for 'seekid' * Seeks to song with id given in argv[1] to the position in seconds given in argument argv[2] * (fractions allowed). */ static int mpd_command_seekid(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { struct player_status status; float seek_target_sec; int seek_target_msec; int ret; //TODO Allow seeking in songs not currently playing player_get_status(&status); if (status.item_id != in->argv_u32val[1]) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Given song is not the current playing one, seeking is not supported"); seek_target_sec = strtof(in->argv[2], NULL); seek_target_msec = seek_target_sec * 1000; ret = player_playback_seek(seek_target_msec, PLAYER_SEEK_POSITION); if (ret < 0) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to seek current song to time %d msec", seek_target_msec); ret = player_playback_start(); if (ret < 0) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to start playback"); return 0; } /* * Command handler function for 'seekcur' * Seeks the current song to the position in seconds given in argument argv[1] (fractions allowed). */ static int mpd_command_seekcur(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { float seek_target_sec; int seek_target_msec; int ret; seek_target_sec = strtof(in->argv[1], NULL); seek_target_msec = seek_target_sec * 1000; // TODO If prefixed by '+' or '-', then the time is relative to the current playing position. ret = player_playback_seek(seek_target_msec, PLAYER_SEEK_POSITION); if (ret < 0) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to seek current song to time %d msec", seek_target_msec); ret = player_playback_start(); if (ret < 0) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to start playback"); return 0; } /* * Command handler function for 'stop' * Stop playback. */ static int mpd_command_stop(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { int ret; ret = player_playback_stop(); if (ret != 0) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to stop playback"); return 0; } /* * Add media file item with given virtual path to the queue * * @param path The virtual path * @param exact_match If TRUE add only item with exact match, otherwise add all items virtual path start with the given path * @return The queue item id of the last inserted item or -1 on failure */ static int mpd_queue_add(char *path, bool exact_match, int position) { struct query_params qp = { .type = Q_ITEMS, .idx_type = I_NONE, .sort = S_ARTIST }; struct player_status status; int new_item_id; int ret; new_item_id = 0; if (exact_match) CHECK_NULL(L_MPD, qp.filter = db_mprintf("f.disabled = 0 AND f.virtual_path LIKE '/%q'", path)); else CHECK_NULL(L_MPD, qp.filter = db_mprintf("f.disabled = 0 AND f.virtual_path LIKE '/%q%%'", path)); player_get_status(&status); ret = db_queue_add_by_query(&qp, status.shuffle, status.item_id, position, NULL, &new_item_id); free_query_params(&qp, 1); if (ret == 0) return new_item_id; return ret; } /* * Command handler function for 'add' * Adds the all songs under the given path to the end of the playqueue (directories add recursively). * Expects argument argv[1] to be a path to a single file or directory. */ static int mpd_command_add(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { struct player_status status; int ret; ret = mpd_queue_add(in->argv[1], false, -1); if (ret < 0) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to add song '%s' to playlist", in->argv[1]); if (ret == 0) { player_get_status(&status); // Given path is not in the library, check if it is possible to add as a non-library queue item ret = library_queue_item_add(in->argv[1], -1, status.shuffle, status.item_id, NULL, NULL); if (ret != LIBRARY_OK) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to add song '%s' to playlist (unknown path)", in->argv[1]); } return 0; } /* * Command handler function for 'addid' * Adds the song under the given path to the end or to the given position of the playqueue. * Expects argument argv[1] to be a path to a single file. argv[2] is optional, if present * as int is the absolute new position, if present as "+x" og "-x" it is relative. */ static int mpd_command_addid(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { int to_pos = -1; struct player_status status; char *path = in->argv[1]; int ret; if (in->argc > 2) { ret = to_pos_from_arg(&to_pos, in->argv[2]); if (ret < 0) RETURN_ERROR(ACK_ERROR_ARG, "Invalid TO argument: '%s'", in->argv[2]); } ret = mpd_queue_add(path, true, to_pos); if (ret == 0) { player_get_status(&status); // Given path is not in the library, directly add it as a new queue item ret = library_queue_item_add(path, to_pos, status.shuffle, status.item_id, NULL, NULL); if (ret != LIBRARY_OK) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to add song '%s' to playlist (unknown path)", path); } if (ret < 0) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to add song '%s' to playlist", path); evbuffer_add_printf(out->evbuf, "Id: %d\n", ret); // mpd_queue_add returns the item_id of the last inserted queue item return 0; } /* * Command handler function for 'clear' * Stops playback and removes all songs from the playqueue */ static int mpd_command_clear(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { int ret; ret = player_playback_stop(); if (ret != 0) { DPRINTF(E_DBG, L_MPD, "Failed to stop playback\n"); } db_queue_clear(0); return 0; } /* * Command handler function for 'delete' * Removes songs from the playqueue. Expects argument argv[1] (optional) to be an integer or * an integer range {START:END} representing the position of the songs in the playlist, that * should be removed. */ static int mpd_command_delete(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { int start_pos; int end_pos; int count; int ret; // If argv[1] is omitted clear the whole queue if (in->argc < 2) { db_queue_clear(0); return 0; } // If argument argv[1] is present remove only the specified songs ret = range_pos_from_arg(&start_pos, &end_pos, in->argv[1]); if (ret < 0) RETURN_ERROR(ACK_ERROR_ARG, "Argument doesn't convert to integer or range: '%s'", in->argv[1]); count = end_pos - start_pos; ret = db_queue_delete_bypos(start_pos, count); if (ret < 0) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to remove %d songs starting at position %d", count, start_pos); return 0; } /* Command handler function for 'deleteid' * Removes the song with given id from the playqueue. Expects argument argv[1] to be an integer (song id). */ static int mpd_command_deleteid(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { uint32_t songid = in->argv_u32val[1]; int ret; ret = db_queue_delete_byitemid(songid); if (ret < 0) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to remove song with id '%s'", in->argv[1]); return 0; } // Moves the song at FROM or range of songs at START:END to TO in the playlist. static int mpd_command_move(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { int start_pos; int end_pos; int count; int to_pos; uint32_t queue_length; int ret; ret = range_pos_from_arg(&start_pos, &end_pos, in->argv[1]); if (ret < 0) RETURN_ERROR(ACK_ERROR_ARG, "Argument doesn't convert to integer or range: '%s'", in->argv[1]); count = end_pos - start_pos; ret = to_pos_from_arg(&to_pos, in->argv[2]); if (ret < 0) RETURN_ERROR(ACK_ERROR_ARG, "Invalid argument: '%s'", in->argv[2]); db_queue_get_count(&queue_length); // valid move pos and range is: // 0 <= start < queue_len // start < end <= queue_len // 0 <= to <= queue_len - count if (!(start_pos >= 0 && start_pos < queue_length && end_pos > start_pos && end_pos <= queue_length && to_pos >= 0 && to_pos <= queue_length - count)) RETURN_ERROR(ACK_ERROR_ARG, "Range too large for target position %d or bad song index (count %d, length %u)", to_pos, count, queue_length); ret = db_queue_move_bypos_range(start_pos, end_pos, to_pos); if (ret < 0) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to move song at position %d to %d", start_pos, to_pos); return 0; } static int mpd_command_moveid(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { uint32_t songid = in->argv_u32val[1]; int to_pos; int ret; ret = to_pos_from_arg(&to_pos, in->argv[2]); if (ret < 0) RETURN_ERROR(ACK_ERROR_ARG, "Invalid TO argument: '%s'", in->argv[2]); ret = db_queue_move_byitemid(songid, to_pos, 0); if (ret < 0) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to move song with id '%u' to index '%u'", songid, to_pos); return 0; } /* * Command handler function for 'playlistid' * Displays a list of all songs in the queue, or if the optional argument is given, displays information * only for the song with ID. * * The order of the songs is always the not shuffled order. */ static int mpd_command_playlistid(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { bool has_songid = (in->has_num & MPD_WANTS_NUM_ARG1_UVAL); uint32_t songid = in->argv_u32val[1]; struct query_params qp = { 0 }; struct db_queue_item queue_item; int ret; if (has_songid) qp.filter = db_mprintf("id = %d", songid); ret = db_queue_enum_start(&qp); if (ret < 0) { free_query_params(&qp, 1); RETURN_ERROR(ACK_ERROR_ARG, "Failed to start queue enum for command playlistid"); } while ((ret = db_queue_enum_fetch(&qp, &queue_item)) == 0 && queue_item.id > 0) { ret = mpd_add_db_queue_item(out->evbuf, &queue_item); if (ret < 0) { db_queue_enum_end(&qp); free_query_params(&qp, 1); RETURN_ERROR(ACK_ERROR_UNKNOWN, "Error adding media info for file"); } } db_queue_enum_end(&qp); free_query_params(&qp, 1); return 0; } /* * Command handler function for 'playlistinfo' * Displays a list of all songs in the queue, or if the optional argument is given, displays information * only for the song SONGPOS or the range of songs START:END given in argv[1]. * * The order of the songs is always the not shuffled order. */ static int mpd_command_playlistinfo(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { struct query_params qp = { 0 }; struct db_queue_item queue_item; int start_pos; int end_pos; int ret; start_pos = 0; end_pos = 0; if (in->argc > 1) { ret = range_pos_from_arg(&start_pos, &end_pos, in->argv[1]); if (ret < 0) RETURN_ERROR(ACK_ERROR_ARG, "Argument doesn't convert to integer or range: '%s'", in->argv[1]); if (start_pos < 0) DPRINTF(E_DBG, L_MPD, "Command 'playlistinfo' called with pos < 0 (arg = '%s'), ignore arguments and return whole queue\n", in->argv[1]); else qp.filter = db_mprintf("pos >= %d AND pos < %d", start_pos, end_pos); } ret = db_queue_enum_start(&qp); if (ret < 0) RETURN_ERROR(ACK_ERROR_ARG, "Failed to start queue enum for command playlistinfo: '%s'", in->argv[1]); while ((ret = db_queue_enum_fetch(&qp, &queue_item)) == 0 && queue_item.id > 0) { ret = mpd_add_db_queue_item(out->evbuf, &queue_item); if (ret < 0) { db_queue_enum_end(&qp); free_query_params(&qp, 1); RETURN_ERROR(ACK_ERROR_UNKNOWN, "Error adding media info"); } } db_queue_enum_end(&qp); free_query_params(&qp, 1); return 0; } /* * playlistfind {FILTER} [sort {TYPE}] [window {START:END}] * Searches for songs that match in the queue * TODO add support for window (currently not supported by db_queue_enum_x) */ static int mpd_command_playlistfind(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { struct query_params qp = { 0 }; struct db_queue_item queue_item; int ret; ret = parse_command(&qp, NULL, NULL, in); if (ret < 0) RETURN_ERROR(ACK_ERROR_ARG, "Unknown argument(s) for command 'playlistfind'"); ret = db_queue_enum_start(&qp); if (ret < 0) { free_query_params(&qp, 1); RETURN_ERROR(ACK_ERROR_ARG, "Failed to start queue enum for command playlistinfo: '%s'", in->argv[1]); } while ((ret = db_queue_enum_fetch(&qp, &queue_item)) == 0 && queue_item.id > 0) { ret = mpd_add_db_queue_item(out->evbuf, &queue_item); if (ret < 0) { db_queue_enum_end(&qp); free_query_params(&qp, 1); RETURN_ERROR(ACK_ERROR_UNKNOWN, "Error adding media info"); } } db_queue_enum_end(&qp); free_query_params(&qp, 1); return 0; } /* * playlistsearch {FILTER} [sort {TYPE}] [window {START:END}] */ static int mpd_command_playlistsearch(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { struct query_params qp = { 0 }; struct db_queue_item queue_item; int ret; ret = parse_command(&qp, NULL, NULL, in); if (ret < 0) RETURN_ERROR(ACK_ERROR_ARG, "Unknown argument(s) for command 'playlistsearch'"); ret = db_queue_enum_start(&qp); if (ret < 0) { free_query_params(&qp, 1); RETURN_ERROR(ACK_ERROR_ARG, "Failed to start queue enum for command playlistinfo: '%s'", in->argv[1]); } while ((ret = db_queue_enum_fetch(&qp, &queue_item)) == 0 && queue_item.id > 0) { ret = mpd_add_db_queue_item(out->evbuf, &queue_item); if (ret < 0) { db_queue_enum_end(&qp); free_query_params(&qp, 1); RETURN_ERROR(ACK_ERROR_UNKNOWN, "Error adding media info"); } } db_queue_enum_end(&qp); free_query_params(&qp, 1); return 0; } static int plchanges_build_queryparams(struct query_params *qp, uint32_t version, const char *range) { int start_pos; int end_pos; int ret; memset(qp, 0, sizeof(struct query_params)); start_pos = 0; end_pos = 0; if (range) { ret = range_pos_from_arg(&start_pos, &end_pos, range); if (ret < 0) return -1; if (start_pos < 0) DPRINTF(E_DBG, L_MPD, "Invalid range '%s', will return entire queue\n", range); } if (start_pos < 0 || end_pos <= 0) qp->filter = db_mprintf("(queue_version > %d)", version); else qp->filter = db_mprintf("(queue_version > %d AND pos >= %d AND pos < %d)", version, start_pos, end_pos); return 0; } /* * plchanges {VERSION} [START:END] * Lists all changed songs in the queue since the given playlist version in argv[1]. */ static int mpd_command_plchanges(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { uint32_t version = in->argv_u32val[1]; const char *range = (in->argc > 2) ? in->argv[2] : NULL; struct query_params qp; struct db_queue_item queue_item; int ret; ret = plchanges_build_queryparams(&qp, version, range); if (ret < 0) RETURN_ERROR(ACK_ERROR_ARG, "Invalid range '%s' provided to plchanges", range); ret = db_queue_enum_start(&qp); if (ret < 0) { free_query_params(&qp, 1); RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to start queue enum for command plchangesposid"); } while ((ret = db_queue_enum_fetch(&qp, &queue_item)) == 0 && queue_item.id > 0) { ret = mpd_add_db_queue_item(out->evbuf, &queue_item); if (ret < 0) { db_queue_enum_end(&qp); free_query_params(&qp, 1); RETURN_ERROR(ACK_ERROR_UNKNOWN, "Error adding media info"); } } db_queue_enum_end(&qp); free_query_params(&qp, 1); return 0; } /* * plchangesposid {VERSION} [START:END] * Lists all changed songs in the queue since the given playlist version in * argv[1] without metadata. */ static int mpd_command_plchangesposid(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { uint32_t version = in->argv_u32val[1]; const char *range = (in->argc > 2) ? in->argv[2] : NULL; struct query_params qp; struct db_queue_item queue_item; int ret; ret = plchanges_build_queryparams(&qp, version, range); if (ret < 0) RETURN_ERROR(ACK_ERROR_ARG, "Invalid range '%s' provided to plchangesposid", range); ret = db_queue_enum_start(&qp); if (ret < 0) { free_query_params(&qp, 1); RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to start queue enum for command plchangesposid"); } while ((ret = db_queue_enum_fetch(&qp, &queue_item)) == 0 && queue_item.id > 0) { evbuffer_add_printf(out->evbuf, "cpos: %d\n" "Id: %d\n", queue_item.pos, queue_item.id); } db_queue_enum_end(&qp); free_query_params(&qp, 1); return 0; } /* * Command handler function for 'listplaylist' * Lists all songs in the playlist given by virtual-path in argv[1]. */ static int mpd_command_listplaylist(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { char *path; struct playlist_info *pli; struct query_params qp = { .type = Q_PLITEMS, .idx_type = I_NONE }; struct db_media_file_info dbmfi; int ret; if (!default_pl_dir || strstr(in->argv[1], ":/")) { // Argument is a virtual path, make sure it starts with a '/' path = prepend_slash(in->argv[1]); } else { // Argument is a playlist name, prepend default playlist directory path = safe_asprintf("%s/%s", default_pl_dir, in->argv[1]); } pli = db_pl_fetch_byvirtualpath(path); free(path); if (!pli) RETURN_ERROR(ACK_ERROR_ARG, "Playlist not found for path '%s'", in->argv[1]); qp.id = pli->id; ret = db_query_start(&qp); if (ret < 0) { free_pli(pli, 0); RETURN_ERROR(ACK_ERROR_UNKNOWN, "Could not start query"); } while ((ret = db_query_fetch_file(&dbmfi, &qp)) == 0) { evbuffer_add_printf(out->evbuf, "file: %s\n", (dbmfi.virtual_path + 1)); } db_query_end(&qp); free_pli(pli, 0); return 0; } /* * Command handler function for 'listplaylistinfo' * Lists all songs in the playlist given by virtual-path in argv[1] with metadata. */ static int mpd_command_listplaylistinfo(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { char *path; struct playlist_info *pli; struct query_params qp = { .type = Q_PLITEMS, .idx_type = I_NONE }; struct db_media_file_info dbmfi; int ret; if (!default_pl_dir || strstr(in->argv[1], ":/")) { // Argument is a virtual path, make sure it starts with a '/' path = prepend_slash(in->argv[1]); } else { // Argument is a playlist name, prepend default playlist directory path = safe_asprintf("%s/%s", default_pl_dir, in->argv[1]); } pli = db_pl_fetch_byvirtualpath(path); free(path); if (!pli) RETURN_ERROR(ACK_ERROR_NO_EXIST, "Playlist not found for path '%s'", in->argv[1]); qp.id = pli->id; ret = db_query_start(&qp); if (ret < 0) { free_pli(pli, 0); RETURN_ERROR(ACK_ERROR_UNKNOWN, "Could not start query"); } while ((ret = db_query_fetch_file(&dbmfi, &qp)) == 0) { ret = mpd_add_db_media_file_info(out->evbuf, &dbmfi); if (ret < 0) { DPRINTF(E_LOG, L_MPD, "Error adding song to the evbuffer, song id: %s\n", dbmfi.id); } } db_query_end(&qp); free_pli(pli, 0); return 0; } /* * Command handler function for 'listplaylists' * Lists all playlists with their last modified date. */ static int mpd_command_listplaylists(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { struct query_params qp = { .type = Q_PL, .idx_type = I_NONE, .sort = S_PLAYLIST }; struct db_playlist_info dbpli; char modified[32]; uint32_t time_modified; int ret; qp.filter = db_mprintf("(f.type = %d OR f.type = %d)", PL_PLAIN, PL_SMART); ret = db_query_start(&qp); if (ret < 0) { free_query_params(&qp, 1); RETURN_ERROR(ACK_ERROR_UNKNOWN, "Could not start query"); } while (((ret = db_query_fetch_pl(&dbpli, &qp)) == 0) && (dbpli.id)) { if (safe_atou32(dbpli.db_timestamp, &time_modified) != 0) { db_query_end(&qp); free_query_params(&qp, 1); RETURN_ERROR(ACK_ERROR_UNKNOWN, "Error converting time modified to uint32_t"); } mpd_time(modified, sizeof(modified), time_modified); evbuffer_add_printf(out->evbuf, "playlist: %s\n" "Last-Modified: %s\n", (dbpli.virtual_path + 1), modified); } db_query_end(&qp); free_query_params(&qp, 1); return 0; } /* * Command handler function for 'load' * Adds the playlist given by virtual-path in argv[1] to the queue. */ static int mpd_command_load(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { char *path; struct playlist_info *pli; struct player_status status; struct query_params qp = { .type = Q_PLITEMS }; int ret; if (!default_pl_dir || strstr(in->argv[1], ":/")) { // Argument is a virtual path, make sure it starts with a '/' path = prepend_slash(in->argv[1]); } else { // Argument is a playlist name, prepend default playlist directory path = safe_asprintf("%s/%s", default_pl_dir, in->argv[1]); } pli = db_pl_fetch_byvirtualpath(path); free(path); if (!pli) RETURN_ERROR(ACK_ERROR_ARG, "Playlist not found for path '%s'", in->argv[1]); //TODO If a second parameter is given only add the specified range of songs to the playqueue qp.id = pli->id; free_pli(pli, 0); player_get_status(&status); ret = db_queue_add_by_query(&qp, status.shuffle, status.item_id, -1, NULL, NULL); if (ret < 0) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to add song '%s' to playlist", in->argv[1]); return 0; } static int mpd_command_playlistadd(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { char *vp_playlist; char *vp_item; int ret; if (!allow_modifying_stored_playlists) RETURN_ERROR(ACK_ERROR_PERMISSION, "Modifying stored playlists is not enabled"); if (!default_pl_dir || strstr(in->argv[1], ":/")) { // Argument is a virtual path, make sure it starts with a '/' vp_playlist = prepend_slash(in->argv[1]); } else { // Argument is a playlist name, prepend default playlist directory vp_playlist = safe_asprintf("%s/%s", default_pl_dir, in->argv[1]); } vp_item = prepend_slash(in->argv[2]); ret = library_playlist_item_add(vp_playlist, vp_item); free(vp_playlist); free(vp_item); if (ret < 0) RETURN_ERROR(ACK_ERROR_ARG, "Error adding item to file '%s'", in->argv[1]); return 0; } static int mpd_command_rm(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { char *virtual_path; int ret; if (!allow_modifying_stored_playlists) RETURN_ERROR(ACK_ERROR_PERMISSION, "Modifying stored playlists is not enabled"); if (!default_pl_dir || strstr(in->argv[1], ":/")) { // Argument is a virtual path, make sure it starts with a '/' virtual_path = prepend_slash(in->argv[1]); } else { // Argument is a playlist name, prepend default playlist directory virtual_path = safe_asprintf("%s/%s", default_pl_dir, in->argv[1]); } ret = library_playlist_remove(virtual_path); free(virtual_path); if (ret < 0) RETURN_ERROR(ACK_ERROR_ARG, "Error removing playlist '%s'", in->argv[1]); return 0; } static int mpd_command_save(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { char *virtual_path; int ret; if (!allow_modifying_stored_playlists) RETURN_ERROR(ACK_ERROR_PERMISSION, "Modifying stored playlists is not enabled"); if (!default_pl_dir || strstr(in->argv[1], ":/")) { // Argument is a virtual path, make sure it starts with a '/' virtual_path = prepend_slash(in->argv[1]); } else { // Argument is a playlist name, prepend default playlist directory virtual_path = safe_asprintf("%s/%s", default_pl_dir, in->argv[1]); } ret = library_queue_save(virtual_path); free(virtual_path); if (ret < 0) RETURN_ERROR(ACK_ERROR_ARG, "Error saving queue to file '%s'", in->argv[1]); return 0; } /* * count [FILTER] [group {GROUPTYPE}] * * TODO Support for groups (the db interface doesn't have method for this). Note * that mpd only supports one group. Mpd has filter as optional. Without filter, * but with group, mpd returns: * * Album: Welcome * songs: 1 * playtime: 249 */ static int mpd_command_count(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { struct query_params qp = { .type = Q_COUNT_ITEMS }; struct filecount_info fci; int ret; ret = parse_command(&qp, NULL, NULL, in); if (ret < 0) RETURN_ERROR(ACK_ERROR_ARG, "Unknown argument(s) for command 'count'"); ret = db_filecount_get(&fci, &qp); if (ret < 0) { free_query_params(&qp, 1); RETURN_ERROR(ACK_ERROR_UNKNOWN, "Could not start query"); } evbuffer_add_printf(out->evbuf, "songs: %d\n" "playtime: %" PRIu64 "\n", fci.count, (fci.length / 1000)); db_query_end(&qp); free_query_params(&qp, 1); return 0; } /* * find "albumartist" "Blue Foundation" "album" "Life Of A Ghost" "date" "2007" "window" "0:1" * search "(modified-since '2024-06-04T22:49:41Z')" * find "((Album == 'No Sign Of Bad') AND (AlbumArtist == 'Led Zeppelin'))" window 0:1 * find "(Artist == \"foo\\'bar\\\"\")" * * TODO not sure if we support this correctly: "An empty value string means: * match only if the given tag type does not exist at all; this implies that * negation with an empty value checks for the existence of the given tag type." * MaximumMPD (search function): * count "((Artist == \"Blue Foundation\") AND (album == \"\"))" */ static int mpd_command_find(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { struct query_params qp = { .type = Q_ITEMS, .idx_type = I_NONE, .sort = S_ARTIST }; struct db_media_file_info dbmfi; int ret; ret = parse_command(&qp, NULL, NULL, in); if (ret < 0) RETURN_ERROR(ACK_ERROR_ARG, "Unknown argument(s) in '%s'", in->args_raw); ret = db_query_start(&qp); if (ret < 0) { free_query_params(&qp, 1); RETURN_ERROR(ACK_ERROR_UNKNOWN, "Could not start query"); } while ((ret = db_query_fetch_file(&dbmfi, &qp)) == 0) { ret = mpd_add_db_media_file_info(out->evbuf, &dbmfi); if (ret < 0) { DPRINTF(E_LOG, L_MPD, "Error adding song to the evbuffer, song id: %s\n", dbmfi.id); } } db_query_end(&qp); free_query_params(&qp, 1); return 0; } /* * findadd "albumartist" "Blue Foundation" "album" "Life Of A Ghost" "date" "2007" "window" "3:4" position 0 */ static int mpd_command_findadd(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { struct query_params qp = { .type = Q_ITEMS, .idx_type = I_NONE, .sort = S_ARTIST }; char *pos = NULL; int to_pos; int ret; ret = parse_command(&qp, &pos, NULL, in); if (ret < 0) goto error; ret = to_pos_from_arg(&to_pos, pos); if (ret < 0) goto error; ret = db_queue_add_by_query(&qp, 0, 0, to_pos, NULL, NULL); if (ret < 0) goto error; free_query_params(&qp, 1); free(pos); return 0; error: free_query_params(&qp, 1); free(pos); RETURN_ERROR(ACK_ERROR_ARG, "Invalid arguments"); } static void groups_from_dbcols(struct mpd_tag_map *groups[], size_t sz, const char *dbcols) { char *copy = strdup(dbcols); char *saveptr; char *col; int i; for (col = strtok_r(copy, ",", &saveptr), i = 0; col && i < sz - 1; col = strtok_r(NULL, ",", &saveptr), i++) { trim(col); groups[i] = mpd_parser_tag_from_dbcol(col); } groups[i] = NULL; free(copy); } /* * list {TYPE} {FILTER} [group {GROUPTYPE} [group {GROUPTYPE} [...]]] * * Examples * Rygelian: list Album group Date group AlbumArtistSort group AlbumArtist * Plattenalbum: list "albumsort" "albumartist" "Bob Dylan" "group" "date" "group" "album" * list Album "(Artist starts_with \"K\")" group AlbumArtist * * TODO Note that the below repeats group tags like so: * AlbumArtist: Kasabian * Album: Empire * AlbumArtist: Kasabian * Album: West Ryder Pauper Lunatic Asylum * mpd doesn't repeat them: * AlbumArtist: Kasabian * Album: Empire * Album: West Ryder Pauper Lunatic Asylum */ static int mpd_command_list(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { struct query_params qp = { .type = Q_ITEMS, .sort = S_ARTIST }; struct db_media_file_info dbmfi; struct mpd_tag_map *groups[16]; char **strval; int ret; int i; ret = parse_command(&qp, NULL, NULL, in); if (ret < 0) RETURN_ERROR(ACK_ERROR_ARG, "Unknown argument(s) in: '%s'", in->args_raw); // qp.group should at least include the tag type field if (!qp.group) { free_query_params(&qp, 1); RETURN_ERROR(ACK_ERROR_UNKNOWN, "Bug! Unknown or unsupported tag type/groups: '%s'", in->args_raw); } groups_from_dbcols(groups, ARRAY_SIZE(groups), qp.group); ret = db_query_start(&qp); if (ret < 0) { free_query_params(&qp, 1); RETURN_ERROR(ACK_ERROR_UNKNOWN, "Could not start query"); } while ((ret = db_query_fetch_file(&dbmfi, &qp)) == 0) { for (i = 0; i < ARRAY_SIZE(groups) && groups[i]; i++) { strval = (char **) ((char *)&dbmfi + groups[i]->dbmfi_offset); if (!(*strval) || (**strval == '\0')) continue; evbuffer_add_printf(out->evbuf, "%s: %s\n", groups[i]->name, sanitize(*strval)); } } db_query_end(&qp); free_query_params(&qp, 1); return 0; } static int mpd_add_directory(struct mpd_command_output *out, int directory_id, int listall, int listinfo) { struct directory_info subdir; struct query_params qp = { .type = Q_PL, .idx_type = I_NONE, .sort = S_PLAYLIST }; struct directory_enum dir_enum; struct db_playlist_info dbpli; char modified[32]; uint32_t time_modified; struct db_media_file_info dbmfi; int ret; // Load playlists for dir-id qp.filter = db_mprintf("(f.directory_id = %d AND (f.type = %d OR f.type = %d))", directory_id, PL_PLAIN, PL_SMART); ret = db_query_start(&qp); if (ret < 0) { free_query_params(&qp, 1); RETURN_ERROR(ACK_ERROR_UNKNOWN, "Could not start query"); } while (((ret = db_query_fetch_pl(&dbpli, &qp)) == 0) && (dbpli.id)) { if (safe_atou32(dbpli.db_timestamp, &time_modified) != 0) { DPRINTF(E_LOG, L_MPD, "Error converting time modified to uint32_t: %s\n", dbpli.db_timestamp); } if (listinfo) { mpd_time(modified, sizeof(modified), time_modified); evbuffer_add_printf(out->evbuf, "playlist: %s\n" "Last-Modified: %s\n", (dbpli.virtual_path + 1), modified); } else { evbuffer_add_printf(out->evbuf, "playlist: %s\n", (dbpli.virtual_path + 1)); } } db_query_end(&qp); free_query_params(&qp, 1); // Load sub directories for dir-id memset(&dir_enum, 0, sizeof(struct directory_enum)); dir_enum.parent_id = directory_id; ret = db_directory_enum_start(&dir_enum); if (ret < 0) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to start directory enum"); while ((ret = db_directory_enum_fetch(&dir_enum, &subdir)) == 0 && subdir.id > 0) { if (listinfo) { evbuffer_add_printf(out->evbuf, "directory: %s\n" "Last-Modified: %s\n", (subdir.virtual_path + 1), "2015-12-01 00:00"); } else { evbuffer_add_printf(out->evbuf, "directory: %s\n", (subdir.virtual_path + 1)); } if (listall) { mpd_add_directory(out, subdir.id, listall, listinfo); } } db_directory_enum_end(&dir_enum); // Load files for dir-id memset(&qp, 0, sizeof(struct query_params)); qp.type = Q_ITEMS; qp.sort = S_ARTIST; qp.idx_type = I_NONE; qp.filter = db_mprintf("(f.directory_id = %d)", directory_id); ret = db_query_start(&qp); if (ret < 0) { free_query_params(&qp, 1); RETURN_ERROR(ACK_ERROR_UNKNOWN, "Could not start query"); } while ((ret = db_query_fetch_file(&dbmfi, &qp)) == 0) { if (listinfo) { ret = mpd_add_db_media_file_info(out->evbuf, &dbmfi); if (ret < 0) { DPRINTF(E_LOG, L_MPD, "Error adding song to the evbuffer, song id: %s\n", dbmfi.id); } } else { evbuffer_add_printf(out->evbuf, "file: %s\n", (dbmfi.virtual_path + 1)); } } db_query_end(&qp); free_query_params(&qp, 1); return 0; } static int mpd_command_listall(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { int dir_id; char parent[PATH_MAX]; int ret; if (in->argc < 2 || strlen(in->argv[1]) == 0 || (strncmp(in->argv[1], "/", 1) == 0 && strlen(in->argv[1]) == 1)) { ret = snprintf(parent, sizeof(parent), "/"); } else if (strncmp(in->argv[1], "/", 1) == 0) { ret = snprintf(parent, sizeof(parent), "%s/", in->argv[1]); } else { ret = snprintf(parent, sizeof(parent), "/%s", in->argv[1]); } if ((ret < 0) || (ret >= sizeof(parent))) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Parent path exceeds PATH_MAX"); // Load dir-id from db for parent-path dir_id = db_directory_id_byvirtualpath(parent); if (dir_id == 0) RETURN_ERROR(ACK_ERROR_NO_EXIST, "Directory info not found for virtual-path '%s'", parent); return mpd_add_directory(out, dir_id, 1, 0); } static int mpd_command_listallinfo(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { int dir_id; char parent[PATH_MAX]; int ret; if (in->argc < 2 || strlen(in->argv[1]) == 0 || (strncmp(in->argv[1], "/", 1) == 0 && strlen(in->argv[1]) == 1)) { ret = snprintf(parent, sizeof(parent), "/"); } else if (strncmp(in->argv[1], "/", 1) == 0) { ret = snprintf(parent, sizeof(parent), "%s/", in->argv[1]); } else { ret = snprintf(parent, sizeof(parent), "/%s", in->argv[1]); } if ((ret < 0) || (ret >= sizeof(parent))) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Parent path exceeds PATH_MAX"); // Load dir-id from db for parent-path dir_id = db_directory_id_byvirtualpath(parent); if (dir_id == 0) RETURN_ERROR(ACK_ERROR_NO_EXIST, "Directory info not found for virtual-path '%s'", parent); return mpd_add_directory(out, dir_id, 1, 1); } /* * Command handler function for 'lsinfo' * Lists the contents of the directory given in argv[1]. */ static int mpd_command_lsinfo(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { int dir_id; char parent[PATH_MAX]; int print_playlists; int ret; if (in->argc < 2 || strlen(in->argv[1]) == 0 || (strncmp(in->argv[1], "/", 1) == 0 && strlen(in->argv[1]) == 1)) { ret = snprintf(parent, sizeof(parent), "/"); } else if (strncmp(in->argv[1], "/", 1) == 0) { ret = snprintf(parent, sizeof(parent), "%s/", in->argv[1]); } else { ret = snprintf(parent, sizeof(parent), "/%s", in->argv[1]); } if ((ret < 0) || (ret >= sizeof(parent))) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Parent path exceeds PATH_MAX"); print_playlists = 0; if ((strncmp(parent, "/", 1) == 0 && strlen(parent) == 1)) { /* * Special handling necessary if the root directory '/' is given. * In this case additional to the directory contents the stored playlists will be returned. * This behavior is deprecated in the mpd protocol but clients like ncmpccp or ympd uses it. */ print_playlists = 1; } // Load dir-id from db for parent-path dir_id = db_directory_id_byvirtualpath(parent); if (dir_id == 0) RETURN_ERROR(ACK_ERROR_NO_EXIST, "Directory info not found for virtual-path '%s'", parent); ret = mpd_add_directory(out, dir_id, 0, 1); // If the root directory was passed as argument add the stored playlists to the response if (ret == 0 && print_playlists) { return mpd_command_listplaylists(out, in, ctx); } return ret; } /* * Command handler function for 'listfiles' * * This command should list all files including files that are not part of the library. We do not support this * and only report files in the library. */ static int mpd_command_listfiles(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { return mpd_command_lsinfo(out, in, ctx); } /* * Command handler function for 'update' * Initiates an init-rescan (scans for new files) */ static int mpd_command_update(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { if (in->argc > 1 && strlen(in->argv[1]) > 0) RETURN_ERROR(ACK_ERROR_ARG, "Update for specific uri not supported for command 'update'"); library_rescan(0); evbuffer_add(out->evbuf, "updating_db: 1\n", 15); return 0; } /* * sticker get song "/file:/path/to/song.wav" rating */ static int mpd_sticker_get(struct mpd_command_output *out, struct mpd_command_input *in, const char *virtual_path) { struct media_file_info *mfi; uint32_t rating; if (strcmp(in->argv[4], "rating") != 0) RETURN_ERROR(ACK_ERROR_NO_EXIST, "No such sticker"); mfi = db_file_fetch_byvirtualpath(virtual_path); if (!mfi) RETURN_ERROR(ACK_ERROR_ARG, "Unknown sticker domain"); if (mfi->rating > 0) { rating = mfi->rating / MPD_RATING_FACTOR; evbuffer_add_printf(out->evbuf, "sticker: rating=%d\n", rating); } free_mfi(mfi, 0); return 0; } /* * sticker set song "/file:/path/to/song.wav" rating 10 */ static int mpd_sticker_set(struct mpd_command_output *out, struct mpd_command_input *in, const char *virtual_path) { uint32_t rating; int id; int ret; if (strcmp(in->argv[4], "rating") != 0) RETURN_ERROR(ACK_ERROR_NO_EXIST, "No such sticker"); ret = safe_atou32(in->argv[5], &rating); if (ret < 0) RETURN_ERROR(ACK_ERROR_ARG, "Rating '%s' doesn't convert to integer", in->argv[5]); rating *= MPD_RATING_FACTOR; if (rating > DB_FILES_RATING_MAX) RETURN_ERROR(ACK_ERROR_ARG, "Rating '%s' is greater than maximum value allowed", in->argv[5]); id = db_file_id_byvirtualpath(virtual_path); if (id <= 0) RETURN_ERROR(ACK_ERROR_ARG, "Invalid path '%s'", virtual_path); library_item_attrib_save(id, LIBRARY_ATTRIB_RATING, rating); return 0; } static int mpd_sticker_delete(struct mpd_command_output *out, struct mpd_command_input *in, const char *virtual_path) { int id; if (strcmp(in->argv[4], "rating") != 0) RETURN_ERROR(ACK_ERROR_NO_EXIST, "No such sticker"); id = db_file_id_byvirtualpath(virtual_path); if (id <= 0) RETURN_ERROR(ACK_ERROR_ARG, "Invalid path '%s'", virtual_path); library_item_attrib_save(id, LIBRARY_ATTRIB_RATING, 0); return 0; } /* * sticker list song "/file:/path/to/song.wav" */ static int mpd_sticker_list(struct mpd_command_output *out, struct mpd_command_input *in, const char *virtual_path) { struct media_file_info *mfi; uint32_t rating; mfi = db_file_fetch_byvirtualpath(virtual_path); if (!mfi) RETURN_ERROR(ACK_ERROR_ARG, "Unknown sticker domain"); if (mfi->rating > 0) { rating = mfi->rating / MPD_RATING_FACTOR; evbuffer_add_printf(out->evbuf, "sticker: rating=%d\n", rating); } free_mfi(mfi, 0); /* |:todo:| real sticker implementation */ return 0; } /* * sticker find {TYPE} {URI} {NAME} [sort {SORTTYPE}] [window {START:END}] * sticker find {TYPE} {URI} {NAME} = {VALUE} [sort {SORTTYPE}] [window {START:END}] * * Example: * sticker find song "/file:/path" rating = 10 */ static int mpd_sticker_find(struct mpd_command_output *out, struct mpd_command_input *in, const char *virtual_path) { struct query_params qp = { .type = Q_ITEMS, .idx_type = I_NONE, .sort = S_VPATH }; struct db_media_file_info dbmfi; uint32_t rating = 0; uint32_t rating_arg = 0; const char *operator; int ret = 0; if (strcmp(in->argv[4], "rating") != 0) RETURN_ERROR(ACK_ERROR_NO_EXIST, "No such sticker"); if (in->argc > 6) { if (strcmp(in->argv[5], "=") != 0 && strcmp(in->argv[5], ">") != 0 && strcmp(in->argv[5], "<") != 0) RETURN_ERROR(ACK_ERROR_ARG, "Invalid operator '%s' given to 'sticker find'", in->argv[5]); operator = in->argv[5]; ret = safe_atou32(in->argv[6], &rating_arg); if (ret < 0) RETURN_ERROR(ACK_ERROR_ARG, "Rating '%s' doesn't convert to integer", in->argv[6]); rating_arg *= MPD_RATING_FACTOR; } else { operator = ">"; rating_arg = 0; } qp.filter = db_mprintf("(f.virtual_path LIKE '%s%%' AND f.rating > 0 AND f.rating %s %d)", virtual_path, operator, rating_arg); ret = db_query_start(&qp); if (ret < 0) { free_query_params(&qp, 1); RETURN_ERROR(ACK_ERROR_UNKNOWN, "Could not start query"); } while ((ret = db_query_fetch_file(&dbmfi, &qp)) == 0) { ret = safe_atou32(dbmfi.rating, &rating); if (ret < 0) { DPRINTF(E_LOG, L_MPD, "Error rating=%s doesn't convert to integer, song id: %s\n", dbmfi.rating, dbmfi.id); continue; } rating /= MPD_RATING_FACTOR; ret = evbuffer_add_printf(out->evbuf, "file: %s\n" "sticker: rating=%d\n", (dbmfi.virtual_path + 1), rating); if (ret < 0) DPRINTF(E_LOG, L_MPD, "Error adding song to the evbuffer, song id: %s\n", dbmfi.id); } db_query_end(&qp); free_query_params(&qp, 1); return 0; } struct mpd_sticker_command { const char *cmd; int (*handler)(struct mpd_command_output *out, struct mpd_command_input *in, const char *virtual_path); int need_args; }; static struct mpd_sticker_command mpd_sticker_handlers[] = { /* sticker command | handler function | minimum argument count */ { "get", mpd_sticker_get, 5 }, { "set", mpd_sticker_set, 6 }, { "delete", mpd_sticker_delete, 5 }, { "list", mpd_sticker_list, 4 }, { "find", mpd_sticker_find, 6 }, { NULL, NULL, 0 }, }; /* * Command handler function for 'sticker' * * sticker get "noth here" rating * ACK [2@0] {sticker} unknown sticker domain * * sticker get song "Al Cohn & Shorty Rogers/East Coast - West Coast Scene/04 Shorty Rogers - Cool Sunshine.flac" rating * ACK [50@0] {sticker} no such sticker * * sticker get song "Al Cohn & Shorty Rogers/East Coast - West Coast Scene/03 Al Cohn - Serenade For Kathy.flac" rating * sticker: rating=8 * OK * * From cantata: * sticker set song "file:/srv/music/VA/The Electro Swing Revolution Vol 3 1 - Hop, Hop, Hop/13 Mr. Hotcut - You Are.mp3" rating "6" * OK */ static int mpd_command_sticker(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { struct mpd_sticker_command *cmd_param = NULL; // Quell compiler warning about uninitialized use of cmd_param char *virtual_path; int ret; int i; if (strcmp(in->argv[2], "song") != 0) RETURN_ERROR(ACK_ERROR_ARG, "Unknown sticker domain"); for (i = 0; i < (sizeof(mpd_sticker_handlers) / sizeof(struct mpd_sticker_command)); ++i) { cmd_param = &mpd_sticker_handlers[i]; if (cmd_param->cmd && strcmp(in->argv[1], cmd_param->cmd) == 0) break; } if (!cmd_param->cmd) RETURN_ERROR(ACK_ERROR_ARG, "Bad request"); if (in->argc < cmd_param->need_args) RETURN_ERROR(ACK_ERROR_ARG, "Not enough arguments"); virtual_path = prepend_slash(in->argv[3]); ret = cmd_param->handler(out, in, virtual_path); free(virtual_path); return ret; } static int mpd_command_close(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { ctx->must_disconnect = true; return 0; } static int mpd_command_password(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { const char *required_password; const char *supplied_password = (in->argc > 1) ? in->argv[1] : ""; bool password_is_required; required_password = cfg_getstr(cfg_getsec(cfg, "library"), "password"); password_is_required = required_password && required_password[0] != '\0'; if (password_is_required && strcmp(supplied_password, required_password) != 0) RETURN_ERROR(ACK_ERROR_PASSWORD, "Wrong password. Authentication failed."); ctx->authenticated = true; return 0; } static int mpd_command_binarylimit(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { uint32_t size = in->argv_u32val[1]; if (size < MPD_BINARY_SIZE_MIN) RETURN_ERROR(ACK_ERROR_ARG, "Value too small"); ctx->binarylimit = size; return 0; } /* * Callback function for the 'player_speaker_enumerate' function. * Expect a struct output_get_param as argument and allocates a struct output if * the shortid of output_get_param matches the given speaker/output spk. */ static void output_get_cb(struct player_speaker_info *spk, void *arg) { struct output_get_param *param = arg; if (!param->output && param->shortid == param->curid) { CHECK_NULL(L_MPD, param->output = calloc(1, sizeof(struct output))); param->output->id = spk->id; param->output->shortid = param->shortid; param->output->name = strdup(spk->name); param->output->selected = spk->selected; param->curid++; DPRINTF(E_DBG, L_MPD, "Output found: shortid %d, id %" PRIu64 ", name '%s', selected %d\n", param->output->shortid, param->output->id, param->output->name, param->output->selected); } } /* * Command handler function for 'disableoutput' * Expects argument argv[1] to be the id of the speaker to disable. */ static int mpd_command_disableoutput(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { uint32_t num = in->argv_u32val[1]; struct output_get_param param = { .shortid = num }; int ret; player_speaker_enumerate(output_get_cb, ¶m); if (param.output && param.output->selected) { ret = player_speaker_disable(param.output->id); free_output(param.output); if (ret < 0) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Speakers deactivation failed: %d", num); } return 0; } /* * Command handler function for 'enableoutput' * Expects argument argv[1] to be the id of the speaker to enable. */ static int mpd_command_enableoutput(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { uint32_t num = in->argv_u32val[1]; struct output_get_param param = { .shortid = num }; int ret; player_speaker_enumerate(output_get_cb, ¶m); if (param.output && !param.output->selected) { ret = player_speaker_enable(param.output->id); free_output(param.output); if (ret < 0) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Speakers deactivation failed: %d", num); } return 0; } /* * Command handler function for 'toggleoutput' * Expects argument argv[1] to be the id of the speaker to enable/disable. */ static int mpd_command_toggleoutput(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { uint32_t num = in->argv_u32val[1]; struct output_get_param param = { .shortid = num }; int ret; player_speaker_enumerate(output_get_cb, ¶m); if (param.output) { if (param.output->selected) ret = player_speaker_disable(param.output->id); else ret = player_speaker_enable(param.output->id); free_output(param.output); if (ret < 0) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Toggle speaker failed: %d", num); } return 0; } /* * Callback function for the 'outputs' command. * Gets called for each available speaker and prints the speaker information to the evbuffer given in *arg. * * Example output: * outputid: 0 * outputname: Computer * plugin: alsa * outputenabled: 1 * outputvolume: 50 * https://mpd.readthedocs.io/en/latest/protocol.html#audio-output-devices */ static void speaker_enum_cb(struct player_speaker_info *spk, void *arg) { struct output_outputs_param *param = arg; struct evbuffer *evbuf = param->buf; char plugin[sizeof(spk->output_type)]; char *p; char *q; /* MPD outputs lowercase plugin (audio_output:type) so convert to * lowercase, convert spaces to underscores to make it a single word */ for (p = spk->output_type, q = plugin; *p != '\0'; p++, q++) { *q = tolower(*p); if (*q == ' ') *q = '_'; } *q = '\0'; evbuffer_add_printf(evbuf, "outputid: %u\n" "outputname: %s\n" "plugin: %s\n" "outputenabled: %d\n" "outputvolume: %d\n", param->nextid, spk->name, plugin, spk->selected, spk->absvol); param->nextid++; } /* * Command handler function for 'output' * Returns a lists with the avaiable speakers. */ static int mpd_command_outputs(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { struct output_outputs_param param = { 0 }; /* Reference: * https://mpd.readthedocs.io/en/latest/protocol.html#audio-output-devices * the ID returned by mpd may change between excutions, so what we do * is simply enumerate the speakers, and for get/set commands we count * ID times to the output referenced. */ param.buf = out->evbuf; player_speaker_enumerate(speaker_enum_cb, ¶m); /* streaming output is not in the speaker list, so add it as pseudo * element when configured to do so */ if (mpd_plugin_httpd) { evbuffer_add_printf(out->evbuf, "outputid: %u\n" "outputname: MP3 stream\n" "plugin: httpd\n" "outputenabled: 1\n", param.nextid); param.nextid++; } return 0; } static int outputvolume_set(uint32_t shortid, int volume) { struct output_get_param param = { .shortid = shortid }; int ret; player_speaker_enumerate(output_get_cb, ¶m); if (!param.output) return -1; ret = player_volume_setabs_speaker(param.output->id, volume); free_output(param.output); return ret; } static int mpd_command_outputvolume(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { uint32_t shortid = in->argv_u32val[1]; int32_t volume = in->argv_i32val[2]; int ret; ret = outputvolume_set(shortid, volume); if (ret < 0) RETURN_ERROR(ACK_ERROR_UNKNOWN, "Could not set volume for speaker with id: %d", shortid); return 0; } static void channel_outputvolume(const char *message) { uint32_t shortid; int volume; char *tmp; char *ptr; int ret; tmp = strdup(message); ptr = strrchr(tmp, ':'); if (!ptr) { free(tmp); DPRINTF(E_LOG, L_MPD, "Failed to parse output id and volume from message '%s' (expected format: \"output-id:volume\"\n", message); return; } *ptr = '\0'; ret = safe_atou32(tmp, &shortid); if (ret < 0) { free(tmp); DPRINTF(E_LOG, L_MPD, "Failed to parse output id from message: '%s'\n", message); return; } ret = safe_atoi32((ptr + 1), &volume); if (ret < 0) { free(tmp); DPRINTF(E_LOG, L_MPD, "Failed to parse volume from message: '%s'\n", message); return; } ret = outputvolume_set(shortid, volume); if (ret < 0) DPRINTF(E_LOG, L_MPD, "Failed to set output volume from message: '%s'\n", message); free(tmp); } static void channel_pairing(const char *message) { remote_pairing_kickoff((char **)&message); } static void channel_verification(const char *message) { player_raop_verification_kickoff((char **)&message); } struct mpd_channel { /* The channel name */ const char *channel; /* * The function to execute the sendmessage command for a specific channel * * @param message message received on this channel */ void (*handler)(const char *message); }; static struct mpd_channel mpd_channels[] = { /* channel | handler function */ { "outputvolume", channel_outputvolume }, { "pairing", channel_pairing }, { "verification", channel_verification }, { NULL, NULL }, }; /* * Finds the channel handler for the given channel name * * @param name channel name from sendmessage command * @return the channel or NULL if it is an unknown/unsupported channel */ static struct mpd_channel * mpd_find_channel(const char *name) { int i; for (i = 0; mpd_channels[i].handler; i++) { if (0 == strcmp(name, mpd_channels[i].channel)) { return &mpd_channels[i]; } } return NULL; } static int mpd_command_channels(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { int i; for (i = 0; mpd_channels[i].handler; i++) { evbuffer_add_printf(out->evbuf, "channel: %s\n", mpd_channels[i].channel); } return 0; } static int mpd_command_sendmessage(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { const char *channelname; const char *message; struct mpd_channel *channel; channelname = in->argv[1]; message = in->argv[2]; channel = mpd_find_channel(channelname); if (!channel) { // Just ignore the message, only log an error message DPRINTF(E_LOG, L_MPD, "Unsupported channel '%s'\n", channelname); return 0; } channel->handler(message); return 0; } /* * Dummy function to handle commands that are not supported and should * not raise an error. */ static int mpd_command_ignore(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { //do nothing DPRINTF(E_DBG, L_MPD, "Ignore command %s\n", in->argv[0]); return 0; } static int mpd_command_commands(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { int i; for (i = 0; mpd_handlers[i].handler; i++) { evbuffer_add_printf(out->evbuf, "command: %s\n", mpd_handlers[i].name); } return 0; } static void tagtypes_enum(struct mpd_tag_map *tag, void *arg) { struct evbuffer *evbuf = arg; if (tag->type != MPD_TYPE_SPECIAL) evbuffer_add_printf(evbuf, "tagtype: %s\n", tag->name); } /* * Command handler function for 'tagtypes' * Returns a lists with supported tags in the form: * tagtype: Artist */ static int mpd_command_tagtypes(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { mpd_parser_enum_tagtypes(tagtypes_enum, out->evbuf); return 0; } /* * Command handler function for 'urlhandlers' * Returns a lists with supported tags in the form: * handler: protocol:// */ static int mpd_command_urlhandlers(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { evbuffer_add_printf(out->evbuf, "handler: http://\n" // handlers supported by MPD 0.19.12 // "handler: https://\n" // "handler: mms://\n" // "handler: mmsh://\n" // "handler: mmst://\n" // "handler: mmsu://\n" // "handler: gopher://\n" // "handler: rtp://\n" // "handler: rtsp://\n" // "handler: rtmp://\n" // "handler: rtmpt://\n" // "handler: rtmps://\n" // "handler: smb://\n" // "handler: nfs://\n" // "handler: cdda://\n" // "handler: alsa://\n" ); return 0; } /* * Command handler function for 'decoders' * MPD returns the decoder plugins with their supported suffix and mime types. * * The server only uses libav/ffmepg for decoding and does not support decoder plugins, * therefor the function reports only ffmpeg as available. */ static int mpd_command_decoders(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { int i; evbuffer_add_printf(out->evbuf, "plugin: ffmpeg\n"); for (i = 0; ffmpeg_suffixes[i]; i++) { evbuffer_add_printf(out->evbuf, "suffix: %s\n", ffmpeg_suffixes[i]); } for (i = 0; ffmpeg_mime_types[i]; i++) { evbuffer_add_printf(out->evbuf, "mime_type: %s\n", ffmpeg_mime_types[i]); } return 0; } static int mpd_command_command_list_begin(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { ctx->cmd_list_type = COMMAND_LIST_BEGIN; return 0; } static int mpd_command_command_list_ok_begin(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { ctx->cmd_list_type = COMMAND_LIST_OK_BEGIN; return 0; } static int mpd_command_command_list_end(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { if (ctx->cmd_list_type == COMMAND_LIST_BEGIN) ctx->cmd_list_type = COMMAND_LIST_END; else if (ctx->cmd_list_type == COMMAND_LIST_OK_BEGIN) ctx->cmd_list_type = COMMAND_LIST_OK_END; else RETURN_ERROR(ACK_ERROR_ARG, "Got with command list end without preceeding list start"); return 0; } static struct mpd_command mpd_handlers[] = { /* commandname | handler function | min arg count | handler requires int args */ // Commands for querying status { "clearerror", mpd_command_ignore, -1 }, { "currentsong", mpd_command_currentsong, -1 }, { "idle", mpd_command_idle, -1 }, { "noidle", mpd_command_noidle, -1 }, { "status", mpd_command_status, -1 }, { "stats", mpd_command_stats, -1 }, // Playback options { "consume", mpd_command_consume, 2, MPD_WANTS_NUM_ARG1_UVAL }, { "crossfade", mpd_command_ignore, -1 }, { "mixrampdb", mpd_command_ignore, -1 }, { "mixrampdelay", mpd_command_ignore, -1 }, { "random", mpd_command_random, 2, MPD_WANTS_NUM_ARG1_UVAL }, { "repeat", mpd_command_repeat, 2, MPD_WANTS_NUM_ARG1_UVAL }, { "setvol", mpd_command_setvol, 2, MPD_WANTS_NUM_ARG1_UVAL }, { "single", mpd_command_single, 2 }, { "replay_gain_mode", mpd_command_ignore, -1 }, { "replay_gain_status", mpd_command_replay_gain_status, -1 }, { "volume", mpd_command_volume, 2, MPD_WANTS_NUM_ARG1_IVAL }, // Controlling playback { "next", mpd_command_next, -1 }, { "pause", mpd_command_pause, 1, MPD_WANTS_NUM_ARG1_UVAL }, { "play", mpd_command_play, 1, MPD_WANTS_NUM_ARG1_UVAL }, { "playid", mpd_command_playid, 1, MPD_WANTS_NUM_ARG1_UVAL }, { "previous", mpd_command_previous, -1 }, { "seek", mpd_command_seek, 3, MPD_WANTS_NUM_ARG1_UVAL }, { "seekid", mpd_command_seekid, 3, MPD_WANTS_NUM_ARG1_UVAL }, { "seekcur", mpd_command_seekcur, 2 }, { "stop", mpd_command_stop, -1 }, // The current playlist { "add", mpd_command_add, 2 }, { "addid", mpd_command_addid, 2 }, { "clear", mpd_command_clear, -1 }, { "delete", mpd_command_delete, -1 }, { "deleteid", mpd_command_deleteid, 2, MPD_WANTS_NUM_ARG1_UVAL }, { "move", mpd_command_move, 3 }, { "moveid", mpd_command_moveid, 3, MPD_WANTS_NUM_ARG1_UVAL }, { "playlist", mpd_command_playlistinfo, -1 }, // According to the mpd protocol the use of "playlist" is deprecated { "playlistfind", mpd_command_playlistfind, 2 }, { "playlistid", mpd_command_playlistid, 1, MPD_WANTS_NUM_ARG1_UVAL }, { "playlistinfo", mpd_command_playlistinfo, -1 }, { "playlistsearch", mpd_command_playlistsearch, 2 }, { "plchanges", mpd_command_plchanges, 2, MPD_WANTS_NUM_ARG1_UVAL }, { "plchangesposid", mpd_command_plchangesposid, 2, MPD_WANTS_NUM_ARG1_UVAL }, // { "prio", mpd_command_prio, -1 }, // { "prioid", mpd_command_prioid, -1 }, // { "rangeid", mpd_command_rangeid, -1 }, // { "shuffle", mpd_command_shuffle, -1 }, // { "swap", mpd_command_swap, -1 }, // { "swapid", mpd_command_swapid, -1 }, // { "addtagid", mpd_command_addtagid, -1 }, // { "cleartagid", mpd_command_cleartagid, -1 }, // Stored playlists { "listplaylist", mpd_command_listplaylist, 2 }, { "listplaylistinfo", mpd_command_listplaylistinfo, 2 }, { "listplaylists", mpd_command_listplaylists, -1 }, { "load", mpd_command_load, 2 }, { "playlistadd", mpd_command_playlistadd, 3 }, // { "playlistclear", mpd_command_playlistclear, -1 }, // { "playlistdelete", mpd_command_playlistdelete, -1 }, // { "playlistmove", mpd_command_playlistmove, -1 }, // { "rename", mpd_command_rename, -1 }, { "rm", mpd_command_rm, 2 }, { "save", mpd_command_save, 2 }, // The music database { "count", mpd_command_count, -1 }, { "find", mpd_command_find, 2 }, { "findadd", mpd_command_findadd, 2 }, { "search", mpd_command_find, 2 }, { "searchadd", mpd_command_findadd, 2 }, { "list", mpd_command_list, 2 }, { "listall", mpd_command_listall, -1 }, { "listallinfo", mpd_command_listallinfo, -1 }, { "listfiles", mpd_command_listfiles, -1 }, { "lsinfo", mpd_command_lsinfo, -1 }, // { "readcomments", mpd_command_readcomments, -1 }, // { "searchaddpl", mpd_command_searchaddpl, -1 }, { "update", mpd_command_update, -1 }, // { "rescan", mpd_command_rescan, -1 }, // Mounts and neighbors // { "mount", mpd_command_mount, -1 }, // { "unmount", mpd_command_unmount, -1 }, // { "listmounts", mpd_command_listmounts, -1 }, // { "listneighbors", mpd_command_listneighbors, -1 }, // Stickers { "sticker", mpd_command_sticker, 4 }, // Connection settings { "close", mpd_command_close, -1 }, // { "kill", mpd_command_kill, -1 }, { "password", mpd_command_password, -1 }, { "ping", mpd_command_ignore, -1 }, { "binarylimit", mpd_command_binarylimit, 2, MPD_WANTS_NUM_ARG1_UVAL }, // Audio output devices { "disableoutput", mpd_command_disableoutput, 2, MPD_WANTS_NUM_ARG1_UVAL }, { "enableoutput", mpd_command_enableoutput, 2, MPD_WANTS_NUM_ARG1_UVAL }, { "toggleoutput", mpd_command_toggleoutput, 2, MPD_WANTS_NUM_ARG1_UVAL }, { "outputs", mpd_command_outputs, -1 }, // Custom command outputvolume (not supported by mpd) { "outputvolume", mpd_command_outputvolume, 3, MPD_WANTS_NUM_ARG1_UVAL | MPD_WANTS_NUM_ARG2_IVAL }, // Client to client { "subscribe", mpd_command_ignore, -1 }, { "unsubscribe", mpd_command_ignore, -1 }, { "channels", mpd_command_channels, -1 }, { "readmessages", mpd_command_ignore, -1 }, { "sendmessage", mpd_command_sendmessage, 3 }, // Reflection // { "config", mpd_command_config, -1 }, { "commands", mpd_command_commands, -1 }, { "notcommands", mpd_command_ignore, -1 }, { "tagtypes", mpd_command_tagtypes, -1 }, { "urlhandlers", mpd_command_urlhandlers, -1 }, { "decoders", mpd_command_decoders, -1 }, // Command lists { "command_list_begin", mpd_command_command_list_begin, -1 }, { "command_list_ok_begin", mpd_command_command_list_ok_begin, -1 }, { "command_list_end", mpd_command_command_list_end, -1 }, // NULL command to terminate loop { NULL, NULL, -1 } }; /* * Finds the command handler for the given command name * * @param name the name of the command * @return the command or NULL if it is an unknown/unsupported command */ static struct mpd_command* mpd_find_command(const char *name) { int i; for (i = 0; mpd_handlers[i].handler; i++) { if (0 == strcmp(name, mpd_handlers[i].name)) { return &mpd_handlers[i]; } } return NULL; } static void mpd_command_input_free(struct mpd_command_input *input) { if (!input) return; free(input->args_split); free(input); } static int mpd_command_input_create(struct mpd_command_input **out, const char *line) { struct mpd_command_input *in; int ret; int i; CHECK_NULL(L_MPD, in = calloc(1, sizeof(struct mpd_command_input))); in->args_raw = line; ret = mpd_split_args(in->argv, sizeof(in->argv), &in->argc, &in->args_split, line); if (ret < 0) goto error; // Many of the handlers need numeric input. If you change this, then also // review command_has_num(). for (i = MPD_WANTS_NUM_ARGV_MIN; i < in->argc && i <= MPD_WANTS_NUM_ARGV_MAX; i++) { if (!isdigit(in->argv[i][0]) && in->argv[i][0] != '-') continue; // Save some cycles if clearly not a number if (safe_atoi32(in->argv[i], &in->argv_i32val[i]) == 0) in->has_num |= 1 << i; if (safe_atou32(in->argv[i], &in->argv_u32val[i]) == 0) in->has_num |= 1 << (i + MPD_WANTS_NUM_ARGV_MAX); } *out = in; return 0; error: mpd_command_input_free(in); return -1; } // Check if input has the numeric arguments required for the command, taking // into account that some commands have optional numeric args (purpose of mask) static bool command_has_num(int wants_num, int has_num, int argc) { int ival_mask = (1 << argc) - 1; // If argc == 2 becomes ...00000011 int uval_mask = (ival_mask << MPD_WANTS_NUM_ARGV_MAX); // If ..MAX == 3 becomes 00011000 int mask = (ival_mask | uval_mask); // becomes 00011011 return (wants_num & mask) == (has_num & wants_num & mask); } static bool mpd_must_process_command_now(const char *line, struct mpd_client_ctx *client_ctx) { size_t line_len = strlen(line); // We're in command list mode, just add command to buffer and return if ((client_ctx->cmd_list_type == COMMAND_LIST_BEGIN || client_ctx->cmd_list_type == COMMAND_LIST_OK_BEGIN) && strcmp(line, "command_list_end") != 0) { if (evbuffer_get_length(client_ctx->cmd_list_buffer) + line_len + 1 > MPD_MAX_COMMAND_LIST_SIZE) { DPRINTF(E_LOG, L_MPD, "Max command list size (%uKB) exceeded\n", (MPD_MAX_COMMAND_LIST_SIZE / 1024)); client_ctx->must_disconnect = true; return false; } evbuffer_add(client_ctx->cmd_list_buffer, line, line_len + 1); return false; } if (strcmp(line, "noidle") == 0 && !client_ctx->is_idle) { return false; // Just ignore, don't proceed to send an OK } return true; } static enum mpd_ack_error mpd_process_command_line(struct evbuffer *evbuf, const char *line, int cmd_num, struct mpd_client_ctx *client_ctx) { struct mpd_command_input *in = NULL; struct mpd_command_output out = { .evbuf = evbuf, .ack_error = ACK_ERROR_NONE }; struct mpd_command *command; const char *cmd_name = NULL; int ret; if (!mpd_must_process_command_now(line, client_ctx)) { return ACK_ERROR_NONE; } ret = mpd_command_input_create(&in, line); if (ret < 0) { client_ctx->must_disconnect = true; // This is what MPD does out.errmsg = safe_asprintf("Could not read command: '%s'", line); out.ack_error = ACK_ERROR_ARG; goto error; } cmd_name = in->argv[0]; if (strcmp(cmd_name, "noidle") != 0 && client_ctx->is_idle) { client_ctx->must_disconnect = true; out.errmsg = safe_asprintf("Only 'noidle' is allowed during idle"); out.ack_error = ACK_ERROR_ARG; goto error; } if (strcmp(cmd_name, "password") != 0 && !client_ctx->authenticated) { out.errmsg = safe_asprintf("Not authenticated"); out.ack_error = ACK_ERROR_PERMISSION; goto error; } command = mpd_find_command(cmd_name); if (!command) { out.errmsg = safe_asprintf("Unknown command"); out.ack_error = ACK_ERROR_UNKNOWN; goto error; } else if (command->min_argc > in->argc) { out.errmsg = safe_asprintf("Missing argument(s), expected %d, given %d", command->min_argc - 1, in->argc - 1); out.ack_error = ACK_ERROR_ARG; goto error; } else if (!command_has_num(command->wants_num, in->has_num, in->argc)) { out.errmsg = safe_asprintf("Missing or invalid numeric values in command: '%s'", line); out.ack_error = ACK_ERROR_ARG; goto error; } ret = command->handler(&out, in, client_ctx); if (ret < 0) { goto error; } if (client_ctx->cmd_list_type == COMMAND_LIST_NONE && !client_ctx->is_idle) evbuffer_add_printf(out.evbuf, "OK\n"); mpd_command_input_free(in); return out.ack_error; error: DPRINTF(E_LOG, L_MPD, "Error processing command '%s': %s\n", line, out.errmsg); if (cmd_name) evbuffer_add_printf(out.evbuf, "ACK [%d@%d] {%s} %s\n", out.ack_error, cmd_num, cmd_name, out.errmsg); mpd_command_input_free(in); free(out.errmsg); return out.ack_error; } // Process the commands that were added to client_ctx->cmd_list_buffer // From MPD documentation (https://mpd.readthedocs.io/en/latest/protocol.html#command-lists): // It does not execute any commands until the list has ended. The response is // a concatenation of all individual responses. // If a command fails, no more commands are executed and the appropriate ACK // error is returned. If command_list_ok_begin is used, list_OK is returned // for each successful command executed in the command list. // On success for all commands, OK is returned. static void mpd_process_command_list(struct evbuffer *evbuf, struct mpd_client_ctx *client_ctx) { char *line; enum mpd_ack_error ack_error = ACK_ERROR_NONE; int cmd_num = 0; while ((line = evbuffer_readln(client_ctx->cmd_list_buffer, NULL, EVBUFFER_EOL_NUL))) { ack_error = mpd_process_command_line(evbuf, line, cmd_num, client_ctx); cmd_num++; free(line); if (ack_error != ACK_ERROR_NONE) break; if (client_ctx->cmd_list_type == COMMAND_LIST_OK_END) evbuffer_add_printf(evbuf, "list_OK\n"); } if (ack_error == ACK_ERROR_NONE) evbuffer_add_printf(evbuf, "OK\n"); // Back to single-command mode evbuffer_drain(client_ctx->cmd_list_buffer, -1); client_ctx->cmd_list_type = COMMAND_LIST_NONE; } /* --------------------------- Server implementation ------------------------ */ /* * The read callback function is invoked if a complete command sequence was * received from the client (see mpd_input_filter function). * * @param bev the buffer event * @param ctx used for authentication */ static void mpd_read_cb(struct bufferevent *bev, void *arg) { struct mpd_client_ctx *client_ctx = arg; struct evbuffer *input; struct evbuffer *output; char *line; // Contains the command sequence received from the client input = bufferevent_get_input(bev); // Used to send the server response to the client output = bufferevent_get_output(bev); while ((line = evbuffer_readln(input, NULL, EVBUFFER_EOL_ANY))) { DPRINTF(E_DBG, L_MPD, "MPD message: '%s'\n", line); mpd_process_command_line(output, line, 0, client_ctx); free(line); if (client_ctx->cmd_list_type == COMMAND_LIST_END || client_ctx->cmd_list_type == COMMAND_LIST_OK_END) mpd_process_command_list(output, client_ctx); if (client_ctx->must_disconnect) goto disconnect; } return; disconnect: DPRINTF(E_DBG, L_MPD, "Disconnecting client\n"); // Freeing the bufferevent closes the connection, since it was opened with // BEV_OPT_CLOSE_ON_FREE. Bufferevents are internally reference-counted, so if // the bufferevent has pending deferred callbacks when you free it, it won’t // be deleted until the callbacks are done. bufferevent_setcb(bev, NULL, NULL, NULL, NULL); bufferevent_free(bev); } /* * Callback when an event occurs on the bufferevent */ static void mpd_event_cb(struct bufferevent *bev, short events, void *ctx) { if (events & BEV_EVENT_ERROR) { DPRINTF(E_LOG, L_MPD, "Error from bufferevent: %s\n", evutil_socket_error_to_string(EVUTIL_SOCKET_ERROR())); } if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) { bufferevent_free(bev); } } /* * The input filter buffer callback. * * Pass complete lines. * * @param src evbuffer to read data from (contains the data received from the client) * @param dst evbuffer to write data to (this is the evbuffer for the read callback) * @param lim the upper bound of bytes to add to destination * @param state write mode * @param ctx (not used) * @return BEV_OK if a complete command sequence was received otherwise BEV_NEED_MORE */ static enum bufferevent_filter_result mpd_input_filter(struct evbuffer *src, struct evbuffer *dst, ev_ssize_t lim, enum bufferevent_flush_mode state, void *ctx) { char *line; int ret; // Filter functions must return BEV_OK // if any data was successfully written to the destination buffer int output_count = 0; while ((line = evbuffer_readln(src, NULL, EVBUFFER_EOL_ANY))) { ret = evbuffer_add_printf(dst, "%s\n", line); if (ret < 0) { DPRINTF(E_LOG, L_MPD, "Error adding line to buffer: '%s'\n", line); free(line); return BEV_ERROR; } free(line); output_count += ret; } if (output_count == 0) { DPRINTF(E_DBG, L_MPD, "Message incomplete, waiting for more data\n"); return BEV_NEED_MORE; } return BEV_OK; } /* * The connection listener callback function is invoked when a new connection was received. * * @param listener the connection listener that received the connection * @param sock the new socket * @param address the address from which the connection was received * @param socklen the length of that address * @param ctx (not used) */ static void mpd_accept_conn_cb(struct evconnlistener *listener, evutil_socket_t sock, struct sockaddr *address, int socklen, void *ctx) { /* * For each new connection setup a new buffer event and wrap it around a filter event. * The filter event ensures, that the read callback on the buffer event is only invoked if a complete * command sequence from the client was received. */ struct event_base *base = evconnlistener_get_base(listener); struct bufferevent *bev = bufferevent_socket_new(base, sock, BEV_OPT_CLOSE_ON_FREE); struct mpd_client_ctx *client_ctx = client_ctx_add(); bev = bufferevent_filter_new(bev, mpd_input_filter, NULL, BEV_OPT_CLOSE_ON_FREE, client_ctx_remove, client_ctx); bufferevent_setcb(bev, mpd_read_cb, NULL, mpd_event_cb, client_ctx); bufferevent_enable(bev, EV_READ | EV_WRITE); client_ctx->evbuffer = bufferevent_get_output(bev); client_ctx->authenticated = !cfg_getstr(cfg_getsec(cfg, "library"), "password"); if (!client_ctx->authenticated) { client_ctx->authenticated = net_peer_address_is_trusted((union net_sockaddr *)address); } client_ctx->binarylimit = MPD_BINARY_SIZE; // No zero terminator (newline in the string is terminator) evbuffer_add(client_ctx->evbuffer, MPD_PROTOCOL_VERSION_OK, strlen(MPD_PROTOCOL_VERSION_OK)); DPRINTF(E_INFO, L_MPD, "New mpd client connection accepted\n"); } /* * Error callback that gets called whenever an accept() call fails on the listener * @param listener the connection listener that received the connection * @param ctx (not used) */ static void mpd_accept_error_cb(struct evconnlistener *listener, void *ctx) { int err; err = EVUTIL_SOCKET_ERROR(); DPRINTF(E_LOG, L_MPD, "Error occured %d (%s) on the listener.\n", err, evutil_socket_error_to_string(err)); } static enum command_state mpd_notify_idle(void *arg, int *retval) { short event_mask; struct mpd_client_ctx *client; int i; event_mask = *(short *)arg; DPRINTF(E_DBG, L_MPD, "Notify clients waiting for idle results: %d\n", event_mask); i = 0; client = mpd_clients; while (client) { DPRINTF(E_DBG, L_MPD, "Notify client #%d\n", i); notify_idle_client(client, event_mask, true); client = client->next; i++; } *retval = 0; return COMMAND_END; } static void mpd_listener_cb(short event_mask) { short *ptr; ptr = (short *)malloc(sizeof(short)); *ptr = event_mask; DPRINTF(E_DBG, L_MPD, "Asynchronous listener callback called with event type %d.\n", event_mask); commands_exec_async(cmdbase, mpd_notify_idle, ptr); } /* * 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 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_byvirtualpath_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, ART_DEFAULT_WIDTH, ART_DEFAULT_HEIGHT, 0); 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); } /* -------------------------------- Event loop ------------------------------ */ /* Thread: mpd */ static void * mpd(void *arg) { int ret; ret = db_perthread_init(); if (ret < 0) { DPRINTF(E_LOG, L_MPD, "Error: DB init failed\n"); pthread_exit(NULL); } event_base_dispatch(evbase_mpd); db_perthread_deinit(); pthread_exit(NULL); } /* -------------------------------- Init/deinit ----------------------------- */ /* Thread: main */ static int mpd_httpd_init(void) { unsigned short http_port; int ret; http_port = cfg_getint(cfg_getsec(cfg, "mpd"), "http_port"); if (http_port == 0) return 0; evhttpd = evhttp_new(evbase_mpd); if (!evhttpd) return -1; ret = net_evhttp_bind(evhttpd, http_port, "mpd artwork"); if (ret < 0) { evhttp_free(evhttpd); evhttpd = NULL; return -1; } evhttp_set_gencb(evhttpd, artwork_cb, NULL); return 0; } /* Thread: main */ static void mpd_httpd_deinit(void) { if (evhttpd) evhttp_free(evhttpd); evhttpd = NULL; } /* Thread: main */ int mpd_init(void) { unsigned short port; const char *pl_dir; int ret; port = cfg_getint(cfg_getsec(cfg, "mpd"), "port"); if (port <= 0) { DPRINTF(E_INFO, L_MPD, "MPD not enabled\n"); return 0; } CHECK_NULL(L_MPD, evbase_mpd = event_base_new()); CHECK_NULL(L_MPD, cmdbase = commands_base_new(evbase_mpd, NULL)); mpd_sockfd = net_bind(&port, SOCK_STREAM, "mpd"); if (mpd_sockfd < 0) { DPRINTF(E_LOG, L_MPD, "Could not bind mpd server to port %hu\n", port); goto bind_fail; } mpd_listener = evconnlistener_new(evbase_mpd, mpd_accept_conn_cb, NULL, 0, -1, mpd_sockfd); if (!mpd_listener) { DPRINTF(E_LOG, L_MPD, "Could not create connection listener for mpd clients on port %d\n", port); goto connew_fail; } evconnlistener_set_error_cb(mpd_listener, mpd_accept_error_cb); ret = mpd_httpd_init(); if (ret < 0) { DPRINTF(E_LOG, L_MPD, "Could not initialize HTTP artwork server\n"); goto httpd_fail; } allow_modifying_stored_playlists = cfg_getbool(cfg_getsec(cfg, "library"), "allow_modifying_stored_playlists"); pl_dir = cfg_getstr(cfg_getsec(cfg, "library"), "default_playlist_directory"); if (pl_dir) default_pl_dir = safe_asprintf("/file:%s", pl_dir); mpd_plugin_httpd = cfg_getbool(cfg_getsec(cfg, "mpd"), "enable_httpd_plugin"); /* Handle deprecated config options */ if (0 < cfg_opt_size(cfg_getopt(cfg_getsec(cfg, "mpd"), "allow_modifying_stored_playlists"))) { DPRINTF(E_LOG, L_MPD, "Found deprecated option 'allow_modifying_stored_playlists' in section 'mpd', please update configuration file (move option to section 'library').\n"); allow_modifying_stored_playlists = cfg_getbool(cfg_getsec(cfg, "mpd"), "allow_modifying_stored_playlists"); } if (0 < cfg_opt_size(cfg_getopt(cfg_getsec(cfg, "mpd"), "default_playlist_directory"))) { DPRINTF(E_LOG, L_MPD, "Found deprecated option 'default_playlist_directory' in section 'mpd', please update configuration file (move option to section 'library').\n"); free(default_pl_dir); pl_dir = cfg_getstr(cfg_getsec(cfg, "mpd"), "default_playlist_directory"); if (pl_dir) default_pl_dir = safe_asprintf("/file:%s", pl_dir); } DPRINTF(E_INFO, L_MPD, "mpd thread init\n"); ret = pthread_create(&tid_mpd, NULL, mpd, NULL); if (ret < 0) { DPRINTF(E_LOG, L_MPD, "Could not spawn MPD thread: %s\n", strerror(errno)); goto thread_fail; } thread_setname(tid_mpd, "mpd"); mpd_clients = NULL; listener_add(mpd_listener_cb, MPD_ALL_IDLE_LISTENER_EVENTS); return 0; thread_fail: mpd_httpd_deinit(); httpd_fail: evconnlistener_free(mpd_listener); connew_fail: close(mpd_sockfd); bind_fail: commands_base_free(cmdbase); event_base_free(evbase_mpd); evbase_mpd = NULL; return -1; } /* Thread: main */ void mpd_deinit(void) { unsigned short port; int ret; port = cfg_getint(cfg_getsec(cfg, "mpd"), "port"); if (port <= 0) { DPRINTF(E_INFO, L_MPD, "MPD not enabled\n"); return; } commands_base_destroy(cmdbase); ret = pthread_join(tid_mpd, NULL); if (ret != 0) { DPRINTF(E_FATAL, L_MPD, "Could not join MPD thread: %s\n", strerror(errno)); return; } listener_remove(mpd_listener_cb); while (mpd_clients) { client_ctx_remove(mpd_clients); } mpd_httpd_deinit(); evconnlistener_free(mpd_listener); close(mpd_sockfd); // Free event base (should free events too) event_base_free(evbase_mpd); free(default_pl_dir); }