static int get_query_params(struct evhttp_request *req, struct evkeyvalq *query, struct query_params *qp) { const char *param; int ret; qp->offset = 0; param = evhttp_find_header(query, "offset"); if (param) { ret = safe_atoi32(param, &qp->offset); if (ret < 0) { rsp_send_error(req, "Invalid offset"); return -1; } } qp->limit = 0; param = evhttp_find_header(query, "limit"); if (param) { ret = safe_atoi32(param, &qp->limit); if (ret < 0) { rsp_send_error(req, "Invalid limit"); return -1; } } if (qp->offset || qp->limit) qp->idx_type = I_SUB; else qp->idx_type = I_NONE; qp->sort = S_NONE; param = evhttp_find_header(query, "query"); if (param) { DPRINTF(E_DBG, L_RSP, "RSP browse query filter: %s\n", param); qp->filter = rsp_query_parse_sql(param); if (!qp->filter) DPRINTF(E_LOG, L_RSP, "Ignoring improper RSP query\n"); } return 0; }
static void rsp_stream(struct evhttp_request *req, char **uri, struct evkeyvalq *query) { int id; int ret; ret = safe_atoi32(uri[2], &id); if (ret < 0) evhttp_send_error(req, HTTP_BADREQUEST, "Bad Request"); else httpd_stream_file(req, id); }
int main(int argc, char **argv) { int option; char *configfile; int background; int mdns_no_rsp; int mdns_no_daap; int loglevel; char *logdomains; char *logfile; char *ffid; char *pidfile; const char *gcry_version; sigset_t sigs; int sigfd; #if defined(__FreeBSD__) || defined(__FreeBSD_kernel__) struct kevent ke_sigs[4]; #endif int ret; struct option option_map[] = { { "ffid", 1, NULL, 'b' }, { "debug", 1, NULL, 'd' }, { "logdomains", 1, NULL, 'D' }, { "foreground", 0, NULL, 'f' }, { "config", 1, NULL, 'c' }, { "pidfile", 1, NULL, 'P' }, { "version", 0, NULL, 'v' }, { "mdns-no-rsp", 0, NULL, 512 }, { "mdns-no-daap", 0, NULL, 513 }, { NULL, 0, NULL, 0 } }; configfile = CONFFILE; pidfile = PIDFILE; loglevel = -1; logdomains = NULL; logfile = NULL; background = 1; ffid = NULL; mdns_no_rsp = 0; mdns_no_daap = 0; while ((option = getopt_long(argc, argv, "D:d:c:P:fb:v", option_map, NULL)) != -1) { switch (option) { case 512: mdns_no_rsp = 1; break; case 513: mdns_no_daap = 1; break; case 'b': ffid = optarg; break; case 'd': ret = safe_atoi32(optarg, &option); if (ret < 0) fprintf(stderr, "Error: loglevel must be an integer in '-d %s'\n", optarg); else loglevel = option; break; case 'D': logdomains = optarg; break; case 'f': background = 0; break; case 'c': configfile = optarg; break; case 'P': pidfile = optarg; break; case 'v': version(); return EXIT_SUCCESS; break; default: usage(argv[0]); return EXIT_FAILURE; break; } } ret = logger_init(NULL, NULL, (loglevel < 0) ? E_LOG : loglevel); if (ret != 0) { fprintf(stderr, "Could not initialize log facility\n"); return EXIT_FAILURE; } ret = conffile_load(configfile); if (ret != 0) { DPRINTF(E_FATAL, L_MAIN, "Config file errors; please fix your config\n"); logger_deinit(); return EXIT_FAILURE; } logger_deinit(); /* Reinit log facility with configfile values */ if (loglevel < 0) loglevel = cfg_getint(cfg_getsec(cfg, "general"), "loglevel"); logfile = cfg_getstr(cfg_getsec(cfg, "general"), "logfile"); ret = logger_init(logfile, logdomains, loglevel); if (ret != 0) { fprintf(stderr, "Could not reinitialize log facility with config file settings\n"); conffile_unload(); return EXIT_FAILURE; } /* Set up libevent logging callback */ event_set_log_callback(logger_libevent); DPRINTF(E_LOG, L_MAIN, "Forked Media Server Version %s taking off\n", VERSION); ret = av_lockmgr_register(ffmpeg_lockmgr); if (ret < 0) { DPRINTF(E_FATAL, L_MAIN, "Could not register ffmpeg lock manager callback\n"); ret = EXIT_FAILURE; goto ffmpeg_init_fail; } av_register_all(); #if LIBAVFORMAT_VERSION_MAJOR >= 54 || (LIBAVFORMAT_VERSION_MAJOR == 53 && LIBAVFORMAT_VERSION_MINOR >= 13) avformat_network_init(); #endif av_log_set_callback(logger_ffmpeg); #ifdef LASTFM /* Initialize libcurl */ curl_global_init(CURL_GLOBAL_DEFAULT); #endif /* Initialize libgcrypt */ gcry_control(GCRYCTL_SET_THREAD_CBS, &gcry_threads_pthread); gcry_version = gcry_check_version(GCRYPT_VERSION); if (!gcry_version) { DPRINTF(E_FATAL, L_MAIN, "libgcrypt version mismatch\n"); ret = EXIT_FAILURE; goto gcrypt_init_fail; } /* We aren't handling anything sensitive, so give up on secure * memory, which is a scarce system resource. */ gcry_control(GCRYCTL_DISABLE_SECMEM, 0); gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0); DPRINTF(E_DBG, L_MAIN, "Initialized with gcrypt %s\n", gcry_version); /* Block signals for all threads except the main one */ sigemptyset(&sigs); sigaddset(&sigs, SIGINT); sigaddset(&sigs, SIGHUP); sigaddset(&sigs, SIGCHLD); sigaddset(&sigs, SIGTERM); sigaddset(&sigs, SIGPIPE); ret = pthread_sigmask(SIG_BLOCK, &sigs, NULL); if (ret != 0) { DPRINTF(E_LOG, L_MAIN, "Error setting signal set\n"); ret = EXIT_FAILURE; goto signal_block_fail; } /* Daemonize and drop privileges */ ret = daemonize(background, pidfile); if (ret < 0) { DPRINTF(E_LOG, L_MAIN, "Could not initialize server\n"); ret = EXIT_FAILURE; goto daemon_fail; } /* Initialize libevent (after forking) */ evbase_main = event_init(); DPRINTF(E_LOG, L_MAIN, "mDNS init\n"); ret = mdns_init(); if (ret != 0) { DPRINTF(E_FATAL, L_MAIN, "mDNS init failed\n"); ret = EXIT_FAILURE; goto mdns_fail; } /* Initialize the database before starting */ DPRINTF(E_INFO, L_MAIN, "Initializing database\n"); ret = db_init(); if (ret < 0) { DPRINTF(E_FATAL, L_MAIN, "Database init failed\n"); ret = EXIT_FAILURE; goto db_fail; } /* Open a DB connection for the main thread */ ret = db_perthread_init(); if (ret < 0) { DPRINTF(E_FATAL, L_MAIN, "Could not perform perthread DB init for main\n"); ret = EXIT_FAILURE; goto db_fail; } /* Spawn worker thread */ ret = worker_init(); if (ret != 0) { DPRINTF(E_FATAL, L_MAIN, "Worker thread failed to start\n"); ret = EXIT_FAILURE; goto worker_fail; } /* Spawn cache thread */ ret = cache_init(); if (ret != 0) { DPRINTF(E_FATAL, L_MAIN, "Cache thread failed to start\n"); ret = EXIT_FAILURE; goto cache_fail; } /* Spawn file scanner thread */ ret = filescanner_init(); if (ret != 0) { DPRINTF(E_FATAL, L_MAIN, "File scanner thread failed to start\n"); ret = EXIT_FAILURE; goto filescanner_fail; } #ifdef HAVE_SPOTIFY_H /* Spawn Spotify thread */ ret = spotify_init(); if (ret < 0) { DPRINTF(E_INFO, L_MAIN, "Spotify thread not started\n");; } #endif /* Spawn player thread */ ret = player_init(); if (ret != 0) { DPRINTF(E_FATAL, L_MAIN, "Player thread failed to start\n"); ret = EXIT_FAILURE; goto player_fail; } /* Spawn HTTPd thread */ ret = httpd_init(); if (ret != 0) { DPRINTF(E_FATAL, L_MAIN, "HTTPd thread failed to start\n"); ret = EXIT_FAILURE; goto httpd_fail; } #ifdef MPD /* Spawn MPD thread */ ret = mpd_init(); if (ret != 0) { DPRINTF(E_FATAL, L_MAIN, "MPD thread failed to start\n"); ret = EXIT_FAILURE; goto mpd_fail; } #endif /* Start Remote pairing service */ ret = remote_pairing_init(); if (ret != 0) { DPRINTF(E_FATAL, L_MAIN, "Remote pairing service failed to start\n"); ret = EXIT_FAILURE; goto remote_fail; } /* Register mDNS services */ ret = register_services(ffid, mdns_no_rsp, mdns_no_daap); if (ret < 0) { ret = EXIT_FAILURE; goto mdns_reg_fail; } #if defined(__linux__) /* Set up signal fd */ sigfd = signalfd(-1, &sigs, SFD_NONBLOCK | SFD_CLOEXEC); if (sigfd < 0) { DPRINTF(E_FATAL, L_MAIN, "Could not setup signalfd: %s\n", strerror(errno)); ret = EXIT_FAILURE; goto signalfd_fail; } event_set(&sig_event, sigfd, EV_READ, signal_signalfd_cb, NULL); #elif defined(__FreeBSD__) || defined(__FreeBSD_kernel__) sigfd = kqueue(); if (sigfd < 0) { DPRINTF(E_FATAL, L_MAIN, "Could not setup kqueue: %s\n", strerror(errno)); ret = EXIT_FAILURE; goto signalfd_fail; } EV_SET(&ke_sigs[0], SIGINT, EVFILT_SIGNAL, EV_ADD, 0, 0, NULL); EV_SET(&ke_sigs[1], SIGTERM, EVFILT_SIGNAL, EV_ADD, 0, 0, NULL); EV_SET(&ke_sigs[2], SIGHUP, EVFILT_SIGNAL, EV_ADD, 0, 0, NULL); EV_SET(&ke_sigs[3], SIGCHLD, EVFILT_SIGNAL, EV_ADD, 0, 0, NULL); ret = kevent(sigfd, ke_sigs, 4, NULL, 0, NULL); if (ret < 0) { DPRINTF(E_FATAL, L_MAIN, "Could not register signal events: %s\n", strerror(errno)); ret = EXIT_FAILURE; goto signalfd_fail; } event_set(&sig_event, sigfd, EV_READ, signal_kqueue_cb, NULL); #endif event_base_set(evbase_main, &sig_event); event_add(&sig_event, NULL); /* Run the loop */ event_base_dispatch(evbase_main); DPRINTF(E_LOG, L_MAIN, "Stopping gracefully\n"); ret = EXIT_SUCCESS; /* * On a clean shutdown, bring mDNS down first to give a chance * to the clients to perform a clean shutdown on their end */ DPRINTF(E_LOG, L_MAIN, "mDNS deinit\n"); mdns_deinit(); signalfd_fail: mdns_reg_fail: DPRINTF(E_LOG, L_MAIN, "Remote pairing deinit\n"); remote_pairing_deinit(); remote_fail: DPRINTF(E_LOG, L_MAIN, "HTTPd deinit\n"); httpd_deinit(); httpd_fail: DPRINTF(E_LOG, L_MAIN, "TCPd deinit\n"); #ifdef MPD DPRINTF(E_LOG, L_MAIN, "MPD deinit\n"); mpd_deinit(); mpd_fail: #endif DPRINTF(E_LOG, L_MAIN, "Player deinit\n"); player_deinit(); player_fail: #ifdef HAVE_SPOTIFY_H DPRINTF(E_LOG, L_MAIN, "Spotify deinit\n"); spotify_deinit(); #endif DPRINTF(E_LOG, L_MAIN, "File scanner deinit\n"); filescanner_deinit(); filescanner_fail: DPRINTF(E_LOG, L_MAIN, "Cache deinit\n"); cache_deinit(); cache_fail: DPRINTF(E_LOG, L_MAIN, "Worker deinit\n"); worker_deinit(); worker_fail: DPRINTF(E_LOG, L_MAIN, "Database deinit\n"); db_perthread_deinit(); db_deinit(); db_fail: if (ret == EXIT_FAILURE) { DPRINTF(E_LOG, L_MAIN, "mDNS deinit\n"); mdns_deinit(); } mdns_fail: daemon_fail: if (background) { ret = seteuid(0); if (ret < 0) DPRINTF(E_LOG, L_MAIN, "seteuid() failed: %s\n", strerror(errno)); else { ret = unlink(pidfile); if (ret < 0) DPRINTF(E_LOG, L_MAIN, "Could not unlink PID file %s: %s\n", pidfile, strerror(errno)); } } signal_block_fail: gcrypt_init_fail: #ifdef LASTFM curl_global_cleanup(); #endif #if LIBAVFORMAT_VERSION_MAJOR >= 54 || (LIBAVFORMAT_VERSION_MAJOR == 53 && LIBAVFORMAT_VERSION_MINOR >= 13) avformat_network_deinit(); #endif av_lockmgr_register(NULL); ffmpeg_init_fail: DPRINTF(E_LOG, L_MAIN, "Exiting.\n"); conffile_unload(); logger_deinit(); return ret; }
int bus_append_unit_property_assignment(sd_bus_message *m, const char *assignment) { const char *eq, *field; int r; assert(m); assert(assignment); eq = strchr(assignment, '='); if (!eq) { log_error("Not an assignment: %s", assignment); return -EINVAL; } field = strndupa(assignment, eq - assignment); eq ++; if (streq(field, "CPUQuota")) { if (isempty(eq)) { r = sd_bus_message_append_basic(m, SD_BUS_TYPE_STRING, "CPUQuotaPerSecUSec"); if (r < 0) return bus_log_create_error(r); r = sd_bus_message_append(m, "v", "t", USEC_INFINITY); } else if (endswith(eq, "%")) { double percent; if (sscanf(eq, "%lf%%", &percent) != 1 || percent <= 0) { log_error("CPU quota '%s' invalid.", eq); return -EINVAL; } r = sd_bus_message_append_basic(m, SD_BUS_TYPE_STRING, "CPUQuotaPerSecUSec"); if (r < 0) return bus_log_create_error(r); r = sd_bus_message_append(m, "v", "t", (usec_t) percent * USEC_PER_SEC / 100); } else { log_error("CPU quota needs to be in percent."); return -EINVAL; } if (r < 0) return bus_log_create_error(r); return 0; } r = sd_bus_message_append_basic(m, SD_BUS_TYPE_STRING, field); if (r < 0) return bus_log_create_error(r); if (STR_IN_SET(field, "CPUAccounting", "MemoryAccounting", "BlockIOAccounting", "SendSIGHUP", "SendSIGKILL")) { r = parse_boolean(eq); if (r < 0) { log_error("Failed to parse boolean assignment %s.", assignment); return -EINVAL; } r = sd_bus_message_append(m, "v", "b", r); } else if (streq(field, "MemoryLimit")) { off_t bytes; r = parse_size(eq, 1024, &bytes); if (r < 0) { log_error("Failed to parse bytes specification %s", assignment); return -EINVAL; } r = sd_bus_message_append(m, "v", "t", (uint64_t) bytes); } else if (STR_IN_SET(field, "CPUShares", "BlockIOWeight")) { uint64_t u; r = safe_atou64(eq, &u); if (r < 0) { log_error("Failed to parse %s value %s.", field, eq); return -EINVAL; } r = sd_bus_message_append(m, "v", "t", u); } else if (STR_IN_SET(field, "User", "Group", "DevicePolicy", "KillMode")) r = sd_bus_message_append(m, "v", "s", eq); else if (streq(field, "DeviceAllow")) { if (isempty(eq)) r = sd_bus_message_append(m, "v", "a(ss)", 0); else { const char *path, *rwm, *e; e = strchr(eq, ' '); if (e) { path = strndupa(eq, e - eq); rwm = e+1; } else { path = eq; rwm = ""; } if (!path_startswith(path, "/dev")) { log_error("%s is not a device file in /dev.", path); return -EINVAL; } r = sd_bus_message_append(m, "v", "a(ss)", 1, path, rwm); } } else if (STR_IN_SET(field, "BlockIOReadBandwidth", "BlockIOWriteBandwidth")) { if (isempty(eq)) r = sd_bus_message_append(m, "v", "a(st)", 0); else { const char *path, *bandwidth, *e; off_t bytes; e = strchr(eq, ' '); if (e) { path = strndupa(eq, e - eq); bandwidth = e+1; } else { log_error("Failed to parse %s value %s.", field, eq); return -EINVAL; } if (!path_startswith(path, "/dev")) { log_error("%s is not a device file in /dev.", path); return -EINVAL; } r = parse_size(bandwidth, 1000, &bytes); if (r < 0) { log_error("Failed to parse byte value %s.", bandwidth); return -EINVAL; } r = sd_bus_message_append(m, "v", "a(st)", 1, path, (uint64_t) bytes); } } else if (streq(field, "BlockIODeviceWeight")) { if (isempty(eq)) r = sd_bus_message_append(m, "v", "a(st)", 0); else { const char *path, *weight, *e; uint64_t u; e = strchr(eq, ' '); if (e) { path = strndupa(eq, e - eq); weight = e+1; } else { log_error("Failed to parse %s value %s.", field, eq); return -EINVAL; } if (!path_startswith(path, "/dev")) { log_error("%s is not a device file in /dev.", path); return -EINVAL; } r = safe_atou64(weight, &u); if (r < 0) { log_error("Failed to parse %s value %s.", field, weight); return -EINVAL; } r = sd_bus_message_append(m, "v", "a(st)", path, u); } } else if (rlimit_from_string(field) >= 0) { uint64_t rl; if (streq(eq, "infinity")) rl = (uint64_t) -1; else { r = safe_atou64(eq, &rl); if (r < 0) { log_error("Invalid resource limit: %s", eq); return -EINVAL; } } r = sd_bus_message_append(m, "v", "t", rl); } else if (streq(field, "Nice")) { int32_t i; r = safe_atoi32(eq, &i); if (r < 0) { log_error("Failed to parse %s value %s.", field, eq); return -EINVAL; } r = sd_bus_message_append(m, "v", "i", i); } else if (streq(field, "Environment")) { r = sd_bus_message_append(m, "v", "as", 1, eq); } else if (streq(field, "KillSignal")) { int sig; sig = signal_from_string_try_harder(eq); if (sig < 0) { log_error("Failed to parse %s value %s.", field, eq); return -EINVAL; } r = sd_bus_message_append(m, "v", "i", sig); } else { log_error("Unknown assignment %s.", assignment); return -EINVAL; } if (r < 0) return bus_log_create_error(r); return 0; }
static void rsp_reply_browse(struct evhttp_request *req, char **uri, struct evkeyvalq *query) { struct query_params qp; char *browse_item; mxml_node_t *reply; mxml_node_t *status; mxml_node_t *items; mxml_node_t *node; int records; int ret; memset(&qp, 0, sizeof(struct query_params)); if (strcmp(uri[3], "artist") == 0) qp.type = Q_BROWSE_ARTISTS; else if (strcmp(uri[3], "genre") == 0) qp.type = Q_BROWSE_GENRES; else if (strcmp(uri[3], "album") == 0) qp.type = Q_BROWSE_ALBUMS; else if (strcmp(uri[3], "composer") == 0) qp.type = Q_BROWSE_COMPOSERS; else { DPRINTF(E_LOG, L_RSP, "Unsupported browse type '%s'\n", uri[3]); rsp_send_error(req, "Unsupported browse type"); return; } ret = safe_atoi32(uri[2], &qp.id); if (ret < 0) { rsp_send_error(req, "Invalid playlist ID"); return; } ret = get_query_params(req, query, &qp); if (ret < 0) return; ret = db_query_start(&qp); if (ret < 0) { DPRINTF(E_LOG, L_RSP, "Could not start query\n"); rsp_send_error(req, "Could not start query"); if (qp.filter) free(qp.filter); return; } if (qp.offset > qp.results) records = 0; else if (qp.limit > (qp.results - qp.offset)) records = qp.results - qp.offset; else records = qp.limit; /* We'd use mxmlNewXML(), but then we can't put any attributes * on the root node and we need some. */ reply = mxmlNewElement(MXML_NO_PARENT, RSP_XML_ROOT); node = mxmlNewElement(reply, "response"); status = mxmlNewElement(node, "status"); items = mxmlNewElement(node, "items"); /* Status block */ node = mxmlNewElement(status, "errorcode"); mxmlNewText(node, 0, "0"); node = mxmlNewElement(status, "errorstring"); mxmlNewText(node, 0, ""); node = mxmlNewElement(status, "records"); mxmlNewTextf(node, 0, "%d", records); node = mxmlNewElement(status, "totalrecords"); mxmlNewTextf(node, 0, "%d", qp.results); /* Items block (all items) */ while (((ret = db_query_fetch_string(&qp, &browse_item)) == 0) && (browse_item)) { node = mxmlNewElement(items, "item"); mxmlNewText(node, 0, browse_item); } if (qp.filter) free(qp.filter); if (ret < 0) { DPRINTF(E_LOG, L_RSP, "Error fetching results\n"); mxmlDelete(reply); db_query_end(&qp); rsp_send_error(req, "Error fetching query results"); return; } /* HACK * Add a dummy empty string to the items element if there is no data * to return - this prevents mxml from sending out an empty <items/> * tag that the SoundBridge does not handle. It's hackish, but it works. */ if (qp.results == 0) mxmlNewText(items, 0, ""); db_query_end(&qp); rsp_send_reply(req, reply); }
static void rsp_reply_playlist(struct evhttp_request *req, char **uri, struct evkeyvalq *query) { struct query_params qp; struct db_media_file_info dbmfi; const char *param; char **strval; mxml_node_t *reply; mxml_node_t *status; mxml_node_t *items; mxml_node_t *item; mxml_node_t *node; int mode; int records; int transcode; int32_t bitrate; int i; int ret; memset(&qp, 0, sizeof(struct query_params)); ret = safe_atoi32(uri[2], &qp.id); if (ret < 0) { rsp_send_error(req, "Invalid playlist ID"); return; } if (qp.id == 0) qp.type = Q_ITEMS; else qp.type = Q_PLITEMS; mode = F_FULL; param = evhttp_find_header(query, "type"); if (param) { if (strcasecmp(param, "full") == 0) mode = F_FULL; else if (strcasecmp(param, "browse") == 0) mode = F_BROWSE; else if (strcasecmp(param, "id") == 0) mode = F_ID; else if (strcasecmp(param, "detailed") == 0) mode = F_DETAILED; else DPRINTF(E_LOG, L_RSP, "Unknown browse mode %s\n", param); } ret = get_query_params(req, query, &qp); if (ret < 0) return; ret = db_query_start(&qp); if (ret < 0) { DPRINTF(E_LOG, L_RSP, "Could not start query\n"); rsp_send_error(req, "Could not start query"); if (qp.filter) free(qp.filter); return; } if (qp.offset > qp.results) records = 0; else if (qp.limit > (qp.results - qp.offset)) records = qp.results - qp.offset; else records = qp.limit; /* We'd use mxmlNewXML(), but then we can't put any attributes * on the root node and we need some. */ reply = mxmlNewElement(MXML_NO_PARENT, RSP_XML_ROOT); node = mxmlNewElement(reply, "response"); status = mxmlNewElement(node, "status"); items = mxmlNewElement(node, "items"); /* Status block */ node = mxmlNewElement(status, "errorcode"); mxmlNewText(node, 0, "0"); node = mxmlNewElement(status, "errorstring"); mxmlNewText(node, 0, ""); node = mxmlNewElement(status, "records"); mxmlNewTextf(node, 0, "%d", records); node = mxmlNewElement(status, "totalrecords"); mxmlNewTextf(node, 0, "%d", qp.results); /* Items block (all items) */ while (((ret = db_query_fetch_file(&qp, &dbmfi)) == 0) && (dbmfi.id)) { transcode = transcode_needed(req->input_headers, dbmfi.codectype); /* Item block (one item) */ item = mxmlNewElement(items, "item"); for (i = 0; rsp_fields[i].field; i++) { if (!(rsp_fields[i].flags & mode)) continue; strval = (char **) ((char *)&dbmfi + rsp_fields[i].offset); if (!(*strval) || (strlen(*strval) == 0)) continue; node = mxmlNewElement(item, rsp_fields[i].field); if (!transcode) mxmlNewText(node, 0, *strval); else { switch (rsp_fields[i].offset) { case dbmfi_offsetof(type): mxmlNewText(node, 0, "wav"); break; case dbmfi_offsetof(bitrate): bitrate = 0; ret = safe_atoi32(dbmfi.samplerate, &bitrate); if ((ret < 0) || (bitrate == 0)) bitrate = 1411; else bitrate = (bitrate * 8) / 250; mxmlNewTextf(node, 0, "%d", bitrate); break; case dbmfi_offsetof(description): mxmlNewText(node, 0, "wav audio file"); break; case dbmfi_offsetof(codectype): mxmlNewText(node, 0, "wav"); node = mxmlNewElement(item, "original_codec"); mxmlNewText(node, 0, *strval); break; default: mxmlNewText(node, 0, *strval); break; } } } } if (qp.filter) free(qp.filter); if (ret < 0) { DPRINTF(E_LOG, L_RSP, "Error fetching results\n"); mxmlDelete(reply); db_query_end(&qp); rsp_send_error(req, "Error fetching query results"); return; } /* HACK * Add a dummy empty string to the items element if there is no data * to return - this prevents mxml from sending out an empty <items/> * tag that the SoundBridge does not handle. It's hackish, but it works. */ if (qp.results == 0) mxmlNewText(items, 0, ""); db_query_end(&qp); rsp_send_reply(req, reply); }