mirror of
https://github.com/owntone/owntone-server.git
synced 2025-01-01 01:53:23 -05:00
4f5966c9ff
The previous solution would use subqueries to count the number of items and streams in each playlist, which means that response time gets pretty slow if there are many playlists. This commit also includes a number of lesser db code changes.
897 lines
22 KiB
C
897 lines
22 KiB
C
/*
|
|
* Copyright (C) 2009-2011 Julien BLACHE <jb@jblache.org>
|
|
*
|
|
* Adapted from mt-daapd:
|
|
* Copyright (C) 2006-2007 Ron Pedde <ron@pedde.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
*/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
# include <config.h>
|
|
#endif
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/queue.h>
|
|
#include <sys/types.h>
|
|
#include <limits.h>
|
|
|
|
#include <mxml.h>
|
|
|
|
#include "httpd_rsp.h"
|
|
#include "logger.h"
|
|
#include "db.h"
|
|
#include "conffile.h"
|
|
#include "misc.h"
|
|
#include "httpd.h"
|
|
#include "transcode.h"
|
|
#include "rsp_query.h"
|
|
|
|
#define RSP_VERSION "1.0"
|
|
#define RSP_XML_ROOT "?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\" ?"
|
|
|
|
|
|
#define F_FULL (1 << 0)
|
|
#define F_BROWSE (1 << 1)
|
|
#define F_ID (1 << 2)
|
|
#define F_DETAILED (1 << 3)
|
|
#define F_ALWAYS (F_FULL | F_BROWSE | F_ID | F_DETAILED)
|
|
|
|
struct field_map {
|
|
char *field;
|
|
size_t offset;
|
|
int flags;
|
|
};
|
|
|
|
static const struct field_map pl_fields[] =
|
|
{
|
|
{ "id", dbpli_offsetof(id), F_ALWAYS },
|
|
{ "title", dbpli_offsetof(title), F_FULL | F_BROWSE | F_DETAILED },
|
|
{ "type", dbpli_offsetof(type), F_DETAILED },
|
|
{ "items", dbpli_offsetof(items), F_FULL | F_BROWSE | F_DETAILED },
|
|
{ "query", dbpli_offsetof(query), F_DETAILED },
|
|
{ "db_timestamp", dbpli_offsetof(db_timestamp), F_DETAILED },
|
|
{ "path", dbpli_offsetof(path), F_DETAILED },
|
|
{ "index", dbpli_offsetof(index), F_DETAILED },
|
|
{ NULL, 0, 0 }
|
|
};
|
|
|
|
static const struct field_map rsp_fields[] =
|
|
{
|
|
{ "id", dbmfi_offsetof(id), F_ALWAYS },
|
|
{ "path", dbmfi_offsetof(path), F_DETAILED },
|
|
{ "fname", dbmfi_offsetof(fname), F_DETAILED },
|
|
{ "title", dbmfi_offsetof(title), F_ALWAYS },
|
|
{ "artist", dbmfi_offsetof(artist), F_DETAILED | F_FULL | F_BROWSE },
|
|
{ "album", dbmfi_offsetof(album), F_DETAILED | F_FULL | F_BROWSE },
|
|
{ "genre", dbmfi_offsetof(genre), F_DETAILED | F_FULL },
|
|
{ "comment", dbmfi_offsetof(comment), F_DETAILED | F_FULL },
|
|
{ "type", dbmfi_offsetof(type), F_ALWAYS },
|
|
{ "composer", dbmfi_offsetof(composer), F_DETAILED | F_FULL },
|
|
{ "orchestra", dbmfi_offsetof(orchestra), F_DETAILED | F_FULL },
|
|
{ "conductor", dbmfi_offsetof(conductor), F_DETAILED | F_FULL },
|
|
{ "url", dbmfi_offsetof(url), F_DETAILED | F_FULL },
|
|
{ "bitrate", dbmfi_offsetof(bitrate), F_DETAILED | F_FULL },
|
|
{ "samplerate", dbmfi_offsetof(samplerate), F_DETAILED | F_FULL },
|
|
{ "song_length", dbmfi_offsetof(song_length), F_DETAILED | F_FULL },
|
|
{ "file_size", dbmfi_offsetof(file_size), F_DETAILED | F_FULL },
|
|
{ "year", dbmfi_offsetof(year), F_DETAILED | F_FULL },
|
|
{ "track", dbmfi_offsetof(track), F_DETAILED | F_FULL | F_BROWSE },
|
|
{ "total_tracks", dbmfi_offsetof(total_tracks), F_DETAILED | F_FULL },
|
|
{ "disc", dbmfi_offsetof(disc), F_DETAILED | F_FULL | F_BROWSE },
|
|
{ "total_discs", dbmfi_offsetof(total_discs), F_DETAILED | F_FULL },
|
|
{ "bpm", dbmfi_offsetof(bpm), F_DETAILED | F_FULL },
|
|
{ "compilation", dbmfi_offsetof(compilation), F_DETAILED | F_FULL },
|
|
{ "rating", dbmfi_offsetof(rating), F_DETAILED | F_FULL },
|
|
{ "play_count", dbmfi_offsetof(play_count), F_DETAILED | F_FULL },
|
|
{ "skip_count", dbmfi_offsetof(skip_count), F_DETAILED | F_FULL },
|
|
{ "data_kind", dbmfi_offsetof(data_kind), F_DETAILED },
|
|
{ "item_kind", dbmfi_offsetof(item_kind), F_DETAILED },
|
|
{ "description", dbmfi_offsetof(description), F_DETAILED | F_FULL },
|
|
{ "time_added", dbmfi_offsetof(time_added), F_DETAILED | F_FULL },
|
|
{ "time_modified", dbmfi_offsetof(time_modified), F_DETAILED | F_FULL },
|
|
{ "time_played", dbmfi_offsetof(time_played), F_DETAILED | F_FULL },
|
|
{ "time_skipped", dbmfi_offsetof(time_skipped), F_DETAILED | F_FULL },
|
|
{ "db_timestamp", dbmfi_offsetof(db_timestamp), F_DETAILED },
|
|
{ "disabled", dbmfi_offsetof(disabled), F_ALWAYS },
|
|
{ "sample_count", dbmfi_offsetof(sample_count), F_DETAILED },
|
|
{ "codectype", dbmfi_offsetof(codectype), F_ALWAYS },
|
|
{ "idx", dbmfi_offsetof(idx), F_DETAILED },
|
|
{ "has_video", dbmfi_offsetof(has_video), F_DETAILED },
|
|
{ "contentrating", dbmfi_offsetof(contentrating), F_DETAILED },
|
|
{ NULL, 0, 0 }
|
|
};
|
|
|
|
|
|
/* -------------------------------- HELPERS --------------------------------- */
|
|
|
|
static struct evbuffer *
|
|
mxml_to_evbuf(mxml_node_t *tree)
|
|
{
|
|
struct evbuffer *evbuf;
|
|
char *xml;
|
|
int ret;
|
|
|
|
evbuf = evbuffer_new();
|
|
if (!evbuf)
|
|
{
|
|
DPRINTF(E_LOG, L_RSP, "Could not create evbuffer for RSP reply\n");
|
|
|
|
return NULL;
|
|
}
|
|
|
|
xml = mxmlSaveAllocString(tree, MXML_NO_CALLBACK);
|
|
if (!xml)
|
|
{
|
|
DPRINTF(E_LOG, L_RSP, "Could not finalize RSP reply\n");
|
|
|
|
evbuffer_free(evbuf);
|
|
return NULL;
|
|
}
|
|
|
|
ret = evbuffer_add(evbuf, xml, strlen(xml));
|
|
free(xml);
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_RSP, "Could not load evbuffer for RSP reply\n");
|
|
|
|
evbuffer_free(evbuf);
|
|
return NULL;
|
|
}
|
|
|
|
return evbuf;
|
|
}
|
|
|
|
static void
|
|
rsp_send_error(struct evhttp_request *req, char *errmsg)
|
|
{
|
|
struct evbuffer *evbuf;
|
|
struct evkeyvalq *headers;
|
|
mxml_node_t *reply;
|
|
mxml_node_t *status;
|
|
mxml_node_t *node;
|
|
|
|
/* 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");
|
|
|
|
/* Status block */
|
|
node = mxmlNewElement(status, "errorcode");
|
|
mxmlNewText(node, 0, "1");
|
|
|
|
node = mxmlNewElement(status, "errorstring");
|
|
mxmlNewText(node, 0, errmsg);
|
|
|
|
node = mxmlNewElement(status, "records");
|
|
mxmlNewText(node, 0, "0");
|
|
|
|
node = mxmlNewElement(status, "totalrecords");
|
|
mxmlNewText(node, 0, "0");
|
|
|
|
evbuf = mxml_to_evbuf(reply);
|
|
mxmlDelete(reply);
|
|
|
|
if (!evbuf)
|
|
{
|
|
httpd_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error");
|
|
|
|
return;
|
|
}
|
|
|
|
headers = evhttp_request_get_output_headers(req);
|
|
evhttp_add_header(headers, "Content-Type", "text/xml; charset=utf-8");
|
|
evhttp_add_header(headers, "Connection", "close");
|
|
|
|
httpd_send_reply(req, HTTP_OK, "OK", evbuf, HTTPD_SEND_NO_GZIP);
|
|
|
|
evbuffer_free(evbuf);
|
|
}
|
|
|
|
static int
|
|
query_params_set(struct query_params *qp, struct httpd_request *hreq)
|
|
{
|
|
const char *param;
|
|
int ret;
|
|
|
|
qp->offset = 0;
|
|
param = evhttp_find_header(hreq->query, "offset");
|
|
if (param)
|
|
{
|
|
ret = safe_atoi32(param, &qp->offset);
|
|
if (ret < 0)
|
|
{
|
|
rsp_send_error(hreq->req, "Invalid offset");
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
qp->limit = 0;
|
|
param = evhttp_find_header(hreq->query, "limit");
|
|
if (param)
|
|
{
|
|
ret = safe_atoi32(param, &qp->limit);
|
|
if (ret < 0)
|
|
{
|
|
rsp_send_error(hreq->req, "Invalid limit");
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
if (qp->offset || qp->limit)
|
|
qp->idx_type = I_SUB;
|
|
else
|
|
qp->idx_type = I_NONE;
|
|
|
|
param = evhttp_find_header(hreq->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_send_reply(struct evhttp_request *req, mxml_node_t *reply)
|
|
{
|
|
struct evbuffer *evbuf;
|
|
struct evkeyvalq *headers;
|
|
|
|
evbuf = mxml_to_evbuf(reply);
|
|
mxmlDelete(reply);
|
|
|
|
if (!evbuf)
|
|
{
|
|
rsp_send_error(req, "Could not finalize reply");
|
|
|
|
return;
|
|
}
|
|
|
|
headers = evhttp_request_get_output_headers(req);
|
|
evhttp_add_header(headers, "Content-Type", "text/xml; charset=utf-8");
|
|
evhttp_add_header(headers, "Connection", "close");
|
|
|
|
httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0);
|
|
|
|
evbuffer_free(evbuf);
|
|
}
|
|
|
|
static int
|
|
rsp_request_authorize(struct httpd_request *hreq)
|
|
{
|
|
char *passwd;
|
|
int ret;
|
|
|
|
if (peer_address_is_trusted(hreq->peer_address))
|
|
return 0;
|
|
|
|
passwd = cfg_getstr(cfg_getsec(cfg, "library"), "password");
|
|
if (!passwd)
|
|
return 0;
|
|
|
|
DPRINTF(E_DBG, L_RSP, "Checking authentication for library\n");
|
|
|
|
// We don't care about the username
|
|
ret = httpd_basic_auth(hreq->req, NULL, passwd, cfg_getstr(cfg_getsec(cfg, "library"), "name"));
|
|
if (ret != 0)
|
|
{
|
|
DPRINTF(E_LOG, L_RSP, "Unsuccessful library authorization attempt from '%s'\n", hreq->peer_address);
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
/* --------------------------- REPLY HANDLERS ------------------------------- */
|
|
|
|
static int
|
|
rsp_reply_info(struct httpd_request *hreq)
|
|
{
|
|
mxml_node_t *reply;
|
|
mxml_node_t *status;
|
|
mxml_node_t *info;
|
|
mxml_node_t *node;
|
|
cfg_t *lib;
|
|
char *library;
|
|
uint32_t songcount;
|
|
|
|
db_files_get_count(&songcount, NULL, NULL);
|
|
|
|
lib = cfg_getsec(cfg, "library");
|
|
library = cfg_getstr(lib, "name");
|
|
|
|
/* 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");
|
|
info = mxmlNewElement(node, "info");
|
|
|
|
/* Status block */
|
|
node = mxmlNewElement(status, "errorcode");
|
|
mxmlNewText(node, 0, "0");
|
|
|
|
node = mxmlNewElement(status, "errorstring");
|
|
mxmlNewText(node, 0, "");
|
|
|
|
node = mxmlNewElement(status, "records");
|
|
mxmlNewText(node, 0, "0");
|
|
|
|
node = mxmlNewElement(status, "totalrecords");
|
|
mxmlNewText(node, 0, "0");
|
|
|
|
/* Info block */
|
|
node = mxmlNewElement(info, "count");
|
|
mxmlNewTextf(node, 0, "%d", (int)songcount);
|
|
|
|
node = mxmlNewElement(info, "rsp-version");
|
|
mxmlNewText(node, 0, RSP_VERSION);
|
|
|
|
node = mxmlNewElement(info, "server-version");
|
|
mxmlNewText(node, 0, VERSION);
|
|
|
|
node = mxmlNewElement(info, "name");
|
|
mxmlNewText(node, 0, library);
|
|
|
|
rsp_send_reply(hreq->req, reply);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
rsp_reply_db(struct httpd_request *hreq)
|
|
{
|
|
struct query_params qp;
|
|
struct db_playlist_info dbpli;
|
|
char **strval;
|
|
mxml_node_t *reply;
|
|
mxml_node_t *status;
|
|
mxml_node_t *pls;
|
|
mxml_node_t *pl;
|
|
mxml_node_t *node;
|
|
int i;
|
|
int ret;
|
|
|
|
memset(&qp, 0, sizeof(struct query_params));
|
|
|
|
qp.type = Q_PL;
|
|
qp.idx_type = I_NONE;
|
|
|
|
ret = db_query_start(&qp);
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_RSP, "Could not start query\n");
|
|
|
|
rsp_send_error(hreq->req, "Could not start query");
|
|
return -1;
|
|
}
|
|
|
|
/* 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");
|
|
pls = mxmlNewElement(node, "playlists");
|
|
|
|
/* 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", qp.results);
|
|
|
|
node = mxmlNewElement(status, "totalrecords");
|
|
mxmlNewTextf(node, 0, "%d", qp.results);
|
|
|
|
/* Playlists block (all playlists) */
|
|
while (((ret = db_query_fetch_pl(&qp, &dbpli)) == 0) && (dbpli.id))
|
|
{
|
|
/* Playlist block (one playlist) */
|
|
pl = mxmlNewElement(pls, "playlist");
|
|
|
|
for (i = 0; pl_fields[i].field; i++)
|
|
{
|
|
if (pl_fields[i].flags & F_FULL)
|
|
{
|
|
strval = (char **) ((char *)&dbpli + pl_fields[i].offset);
|
|
|
|
node = mxmlNewElement(pl, pl_fields[i].field);
|
|
mxmlNewText(node, 0, *strval);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_RSP, "Error fetching results\n");
|
|
|
|
mxmlDelete(reply);
|
|
db_query_end(&qp);
|
|
rsp_send_error(hreq->req, "Error fetching query results");
|
|
return -1;
|
|
}
|
|
|
|
/* HACK
|
|
* Add a dummy empty string to the playlists element if there is no data
|
|
* to return - this prevents mxml from sending out an empty <playlists/>
|
|
* tag that the SoundBridge does not handle. It's hackish, but it works.
|
|
*/
|
|
if (qp.results == 0)
|
|
mxmlNewText(pls, 0, "");
|
|
|
|
db_query_end(&qp);
|
|
|
|
rsp_send_reply(hreq->req, reply);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
rsp_reply_playlist(struct httpd_request *hreq)
|
|
{
|
|
struct query_params qp;
|
|
struct db_media_file_info dbmfi;
|
|
struct evkeyvalq *headers;
|
|
const char *param;
|
|
const char *ua;
|
|
const char *client_codecs;
|
|
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(hreq->uri_parsed->path_parts[2], &qp.id);
|
|
if (ret < 0)
|
|
{
|
|
rsp_send_error(hreq->req, "Invalid playlist ID");
|
|
return -1;
|
|
}
|
|
|
|
if (qp.id == 0)
|
|
qp.type = Q_ITEMS;
|
|
else
|
|
qp.type = Q_PLITEMS;
|
|
|
|
qp.sort = S_NAME;
|
|
|
|
mode = F_FULL;
|
|
param = evhttp_find_header(hreq->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 = query_params_set(&qp, hreq);
|
|
if (ret < 0)
|
|
return -1;
|
|
|
|
ret = db_query_start(&qp);
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_RSP, "Could not start query\n");
|
|
|
|
rsp_send_error(hreq->req, "Could not start query");
|
|
|
|
if (qp.filter)
|
|
free(qp.filter);
|
|
return -1;
|
|
}
|
|
|
|
if (qp.offset > qp.results)
|
|
records = 0;
|
|
else
|
|
records = qp.results - qp.offset;
|
|
|
|
if (qp.limit && (records > qp.limit))
|
|
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))
|
|
{
|
|
headers = evhttp_request_get_input_headers(hreq->req);
|
|
|
|
ua = evhttp_find_header(headers, "User-Agent");
|
|
client_codecs = evhttp_find_header(headers, "Accept-Codecs");
|
|
|
|
transcode = transcode_needed(ua, client_codecs, 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(hreq->req, "Error fetching query results");
|
|
return -1;
|
|
}
|
|
|
|
/* 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(hreq->req, reply);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
rsp_reply_browse(struct httpd_request *hreq)
|
|
{
|
|
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(hreq->uri_parsed->path_parts[3], "artist") == 0)
|
|
{
|
|
qp.type = Q_BROWSE_ARTISTS;
|
|
}
|
|
else if (strcmp(hreq->uri_parsed->path_parts[3], "genre") == 0)
|
|
{
|
|
qp.type = Q_BROWSE_GENRES;
|
|
}
|
|
else if (strcmp(hreq->uri_parsed->path_parts[3], "album") == 0)
|
|
{
|
|
qp.type = Q_BROWSE_ALBUMS;
|
|
}
|
|
else if (strcmp(hreq->uri_parsed->path_parts[3], "composer") == 0)
|
|
{
|
|
qp.type = Q_BROWSE_COMPOSERS;
|
|
}
|
|
else
|
|
{
|
|
DPRINTF(E_LOG, L_RSP, "Unsupported browse type '%s'\n", hreq->uri_parsed->path_parts[3]);
|
|
|
|
rsp_send_error(hreq->req, "Unsupported browse type");
|
|
return -1;
|
|
}
|
|
|
|
ret = safe_atoi32(hreq->uri_parsed->path_parts[2], &qp.id);
|
|
if (ret < 0)
|
|
{
|
|
rsp_send_error(hreq->req, "Invalid playlist ID");
|
|
return -1;
|
|
}
|
|
|
|
ret = query_params_set(&qp, hreq);
|
|
if (ret < 0)
|
|
return -1;
|
|
|
|
ret = db_query_start(&qp);
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_RSP, "Could not start query\n");
|
|
|
|
rsp_send_error(hreq->req, "Could not start query");
|
|
|
|
if (qp.filter)
|
|
free(qp.filter);
|
|
return -1;
|
|
}
|
|
|
|
if (qp.offset > qp.results)
|
|
records = 0;
|
|
else
|
|
records = qp.results - qp.offset;
|
|
|
|
if (qp.limit && (records > qp.limit))
|
|
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(hreq->req, "Error fetching query results");
|
|
return -1;
|
|
}
|
|
|
|
/* 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(hreq->req, reply);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
rsp_stream(struct httpd_request *hreq)
|
|
{
|
|
int id;
|
|
int ret;
|
|
|
|
ret = safe_atoi32(hreq->uri_parsed->path_parts[2], &id);
|
|
if (ret < 0)
|
|
{
|
|
httpd_send_error(hreq->req, HTTP_BADREQUEST, "Bad Request");
|
|
return -1;
|
|
}
|
|
|
|
httpd_stream_file(hreq->req, id);
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
static struct httpd_uri_map rsp_handlers[] =
|
|
{
|
|
{
|
|
.regexp = "^/rsp/info$",
|
|
.handler = rsp_reply_info
|
|
},
|
|
{
|
|
.regexp = "^/rsp/db$",
|
|
.handler = rsp_reply_db
|
|
},
|
|
{
|
|
.regexp = "^/rsp/db/[[:digit:]]+$",
|
|
.handler = rsp_reply_playlist
|
|
},
|
|
{
|
|
.regexp = "^/rsp/db/[[:digit:]]+/[^/]+$",
|
|
.handler = rsp_reply_browse
|
|
},
|
|
{
|
|
.regexp = "^/rsp/stream/[[:digit:]]+$",
|
|
.handler = rsp_stream
|
|
},
|
|
{
|
|
.regexp = NULL,
|
|
.handler = NULL
|
|
}
|
|
};
|
|
|
|
|
|
/* -------------------------------- RSP API --------------------------------- */
|
|
|
|
void
|
|
rsp_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed)
|
|
{
|
|
struct httpd_request *hreq;
|
|
int ret;
|
|
|
|
DPRINTF(E_DBG, L_RSP, "RSP request: '%s'\n", uri_parsed->uri);
|
|
|
|
hreq = httpd_request_parse(req, uri_parsed, NULL, rsp_handlers);
|
|
if (!hreq)
|
|
{
|
|
DPRINTF(E_LOG, L_RSP, "Unrecognized path '%s' in RSP request: '%s'\n", uri_parsed->path, uri_parsed->uri);
|
|
|
|
rsp_send_error(req, "Server error");
|
|
return;
|
|
}
|
|
|
|
ret = rsp_request_authorize(hreq);
|
|
if (ret < 0)
|
|
{
|
|
rsp_send_error(req, "Access denied");
|
|
free(hreq);
|
|
return;
|
|
}
|
|
|
|
hreq->handler(hreq);
|
|
|
|
free(hreq);
|
|
}
|
|
|
|
int
|
|
rsp_is_request(const char *path)
|
|
{
|
|
if (strncmp(path, "/rsp/", strlen("/rsp/")) == 0)
|
|
return 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
rsp_init(void)
|
|
{
|
|
char buf[64];
|
|
int i;
|
|
int ret;
|
|
|
|
for (i = 0; rsp_handlers[i].handler; i++)
|
|
{
|
|
ret = regcomp(&rsp_handlers[i].preg, rsp_handlers[i].regexp, REG_EXTENDED | REG_NOSUB);
|
|
if (ret != 0)
|
|
{
|
|
regerror(ret, &rsp_handlers[i].preg, buf, sizeof(buf));
|
|
|
|
DPRINTF(E_FATAL, L_RSP, "RSP init failed; regexp error: %s\n", buf);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
rsp_deinit(void)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; rsp_handlers[i].handler; i++)
|
|
regfree(&rsp_handlers[i].preg);
|
|
}
|