diff --git a/src/Makefile.am b/src/Makefile.am index 085d69685c..f0d4ccbf9e 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -107,6 +107,7 @@ owntone_SOURCES = main.c \ artwork.c artwork.h \ misc.c misc.h \ misc_json.c misc_json.h \ + misc_xml.c misc_xml.h \ rng.c rng.h \ smartpl_query.c smartpl_query.h \ player.c player.h \ @@ -128,7 +129,6 @@ owntone_SOURCES = main.c \ $(MPD_SRC) \ listener.c listener.h \ commands.c commands.h \ - mxml-compat.h \ outputs/plist_wrap.h \ $(LIBWEBSOCKETS_SRC) \ $(GPERF_SRC) \ diff --git a/src/httpd_rsp.c b/src/httpd_rsp.c index 7bfce8f4cd..f8e1e212a0 100644 --- a/src/httpd_rsp.c +++ b/src/httpd_rsp.c @@ -30,13 +30,12 @@ #include #include -#include "mxml-compat.h" - #include "httpd_internal.h" #include "logger.h" #include "db.h" #include "conffile.h" #include "misc.h" +#include "misc_xml.h" #include "transcode.h" #include "parsers/rsp_parser.h" @@ -120,12 +119,12 @@ static const struct field_map rsp_fields[] = /* -------------------------------- HELPERS --------------------------------- */ static int -mxml_to_evbuf(struct evbuffer *evbuf, mxml_node_t *tree) +xml_to_evbuf(struct evbuffer *evbuf, xml_node *tree) { char *xml; int ret; - xml = mxmlSaveAllocString(tree, MXML_NO_CALLBACK); + xml = xml_to_string(tree); if (!xml) { DPRINTF(E_LOG, L_RSP, "Could not finalize RSP reply\n"); @@ -143,37 +142,33 @@ mxml_to_evbuf(struct evbuffer *evbuf, mxml_node_t *tree) return 0; } +static void +rsp_xml_response_new(xml_node **xml_ptr, xml_node **response_ptr, int errorcode, const char *errorstring, int records, int totalrecords) +{ + xml_node *xml = xml_new_node(NULL, RSP_XML_ROOT, NULL); + xml_node *response = xml_new_node(xml, "response", NULL); + xml_node *status = xml_new_node(response, "status", NULL); + + xml_new_node_textf(status, "errorcode", "%d", errorcode); + xml_new_node(status, "errorstring", errorstring); + xml_new_node_textf(status, "records", "%d", records); + xml_new_node_textf(status, "totalrecords", "%d", totalrecords); + + if (response_ptr) + *response_ptr = response; + if (xml_ptr) + *xml_ptr = xml; +} + static void rsp_send_error(struct httpd_request *hreq, char *errmsg) { - mxml_node_t *reply; - mxml_node_t *status; - mxml_node_t *node; + xml_node *xml; int ret; - /* 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"); - - ret = mxml_to_evbuf(hreq->out_body, reply); - mxmlDelete(reply); + rsp_xml_response_new(&xml, NULL, 1, errmsg, 0, 0); + ret = xml_to_evbuf(hreq->out_body, xml); + xml_free(xml); if (ret < 0) { @@ -259,12 +254,12 @@ query_params_set(struct query_params *qp, struct httpd_request *hreq) } static void -rsp_send_reply(struct httpd_request *hreq, mxml_node_t *reply) +rsp_send_reply(struct httpd_request *hreq, xml_node *reply) { int ret; - ret = mxml_to_evbuf(hreq->out_body, reply); - mxmlDelete(reply); + ret = xml_to_evbuf(hreq->out_body, reply); + xml_free(reply); if (ret < 0) { @@ -310,10 +305,9 @@ rsp_request_authorize(struct httpd_request *hreq) 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; + xml_node *xml; + xml_node *response; + xml_node *info; cfg_t *lib; char *library; uint32_t songcount; @@ -323,43 +317,16 @@ rsp_reply_info(struct httpd_request *hreq) 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); + rsp_xml_response_new(&xml, &response, 0, "", 0, 0); - node = mxmlNewElement(info, "server-version"); - mxmlNewText(node, 0, VERSION); + info = xml_new_node(response, "info", NULL); - node = mxmlNewElement(info, "name"); - mxmlNewText(node, 0, library); - - rsp_send_reply(hreq, reply); + xml_new_node_textf(info, "count", "%d", (int)songcount); + xml_new_node(info, "rsp-version", RSP_VERSION); + xml_new_node(info, "server-version", VERSION); + xml_new_node(info, "name", library); + rsp_send_reply(hreq, xml); return 0; } @@ -369,11 +336,10 @@ 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; + xml_node *xml; + xml_node *response; + xml_node *pls; + xml_node *pl; int i; int ret; @@ -391,27 +357,9 @@ rsp_reply_db(struct httpd_request *hreq) 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, ""); + rsp_xml_response_new(&xml, &response, 0, "", qp.results, qp.results); - node = mxmlNewElement(status, "records"); - mxmlNewTextf(node, 0, "%d", qp.results); - - node = mxmlNewElement(status, "totalrecords"); - mxmlNewTextf(node, 0, "%d", qp.results); + pls = xml_new_node(response, "playlists", NULL); /* Playlists block (all playlists) */ while (((ret = db_query_fetch_pl(&dbpli, &qp)) == 0) && (dbpli.id)) @@ -421,7 +369,7 @@ rsp_reply_db(struct httpd_request *hreq) continue; /* Playlist block (one playlist) */ - pl = mxmlNewElement(pls, "playlist"); + pl = xml_new_node(pls, "playlist", NULL); for (i = 0; pl_fields[i].field; i++) { @@ -429,8 +377,7 @@ rsp_reply_db(struct httpd_request *hreq) { strval = (char **) ((char *)&dbpli + pl_fields[i].offset); - node = mxmlNewElement(pl, pl_fields[i].field); - mxmlNewText(node, 0, *strval); + xml_new_node(pl, pl_fields[i].field, *strval); } } } @@ -439,7 +386,7 @@ rsp_reply_db(struct httpd_request *hreq) { DPRINTF(E_LOG, L_RSP, "Error fetching results\n"); - mxmlDelete(reply); + xml_free(xml); db_query_end(&qp); rsp_send_error(hreq, "Error fetching query results"); return -1; @@ -451,11 +398,11 @@ rsp_reply_db(struct httpd_request *hreq) * tag that the SoundBridge does not handle. It's hackish, but it works. */ if (qp.results == 0) - mxmlNewText(pls, 0, ""); + xml_new_text(pls, ""); db_query_end(&qp); - rsp_send_reply(hreq, reply); + rsp_send_reply(hreq, xml); return 0; } @@ -469,11 +416,10 @@ rsp_reply_playlist(struct httpd_request *hreq) 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; + xml_node *xml; + xml_node *response; + xml_node *items; + xml_node *item; int mode; int records; int transcode; @@ -537,27 +483,9 @@ rsp_reply_playlist(struct httpd_request *hreq) 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, ""); + rsp_xml_response_new(&xml, &response, 0, "", records, qp.results); - node = mxmlNewElement(status, "records"); - mxmlNewTextf(node, 0, "%d", records); - - node = mxmlNewElement(status, "totalrecords"); - mxmlNewTextf(node, 0, "%d", qp.results); + items = xml_new_node(response, "items", NULL); /* Items block (all items) */ while ((ret = db_query_fetch_file(&dbmfi, &qp)) == 0) @@ -568,7 +496,7 @@ rsp_reply_playlist(struct httpd_request *hreq) transcode = transcode_needed(ua, client_codecs, dbmfi.codectype); /* Item block (one item) */ - item = mxmlNewElement(items, "item"); + item = xml_new_node(items, "item", NULL); for (i = 0; rsp_fields[i].field; i++) { @@ -580,44 +508,41 @@ rsp_reply_playlist(struct httpd_request *hreq) 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; - } + xml_new_node(item, rsp_fields[i].field, *strval); + continue; + } + + switch (rsp_fields[i].offset) + { + case dbmfi_offsetof(type): + xml_new_node(item, rsp_fields[i].field, "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; + + xml_new_node_textf(item, rsp_fields[i].field, "%d", bitrate); + break; + + case dbmfi_offsetof(description): + xml_new_node(item, rsp_fields[i].field, "wav audio file"); + break; + + case dbmfi_offsetof(codectype): + xml_new_node(item, rsp_fields[i].field, "wav"); + xml_new_node(item, "original_codec", *strval); + break; + + default: + xml_new_node(item, rsp_fields[i].field, *strval); + break; } } } @@ -629,7 +554,7 @@ rsp_reply_playlist(struct httpd_request *hreq) { DPRINTF(E_LOG, L_RSP, "Error fetching results\n"); - mxmlDelete(reply); + xml_free(xml); db_query_end(&qp); rsp_send_error(hreq, "Error fetching query results"); return -1; @@ -641,11 +566,11 @@ rsp_reply_playlist(struct httpd_request *hreq) * tag that the SoundBridge does not handle. It's hackish, but it works. */ if (qp.results == 0) - mxmlNewText(items, 0, ""); + xml_new_text(items, ""); db_query_end(&qp); - rsp_send_reply(hreq, reply); + rsp_send_reply(hreq, xml); return 0; } @@ -655,10 +580,9 @@ 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; + xml_node *xml; + xml_node *response; + xml_node *items; int records; int ret; @@ -719,33 +643,14 @@ rsp_reply_browse(struct httpd_request *hreq) 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); + rsp_xml_response_new(&xml, &response, 0, "", records, qp.results); - node = mxmlNewElement(status, "totalrecords"); - mxmlNewTextf(node, 0, "%d", qp.results); + items = xml_new_node(response, "items", NULL); /* Items block (all items) */ while (((ret = db_query_fetch_string(&browse_item, &qp)) == 0) && (browse_item)) { - node = mxmlNewElement(items, "item"); - mxmlNewText(node, 0, browse_item); + xml_new_node(items, "item", browse_item); } if (qp.filter) @@ -755,7 +660,7 @@ rsp_reply_browse(struct httpd_request *hreq) { DPRINTF(E_LOG, L_RSP, "Error fetching results\n"); - mxmlDelete(reply); + xml_free(xml); db_query_end(&qp); rsp_send_error(hreq, "Error fetching query results"); return -1; @@ -767,11 +672,11 @@ rsp_reply_browse(struct httpd_request *hreq) * tag that the SoundBridge does not handle. It's hackish, but it works. */ if (qp.results == 0) - mxmlNewText(items, 0, ""); + xml_new_text(items, ""); db_query_end(&qp); - rsp_send_reply(hreq, reply); + rsp_send_reply(hreq, xml); return 0; } diff --git a/src/inputs/pipe.c b/src/inputs/pipe.c index d04b2f2ba5..861a57dafb 100644 --- a/src/inputs/pipe.c +++ b/src/inputs/pipe.c @@ -52,10 +52,9 @@ #include #include -#include "mxml-compat.h" - #include "input.h" #include "misc.h" +#include "misc_xml.h" #include "logger.h" #include "db.h" #include "conffile.h" @@ -531,35 +530,33 @@ log_incoming(int severity, const char *msg, uint32_t type, uint32_t code, int da DPRINTF(severity, L_PLAYER, "%s (type=%s, code=%s, len=%d)\n", msg, typestr, codestr, data_len); } +/* Example of xml item: + +73736e636d6473749 + +NDE5OTg3OTU0 +*/ static int parse_item_xml(uint32_t *type, uint32_t *code, uint8_t **data, int *data_len, const char *item) { - mxml_node_t *xml; - mxml_node_t *haystack; - mxml_node_t *needle; + xml_node *xml; const char *s; - xml = mxmlNewXML("1.0"); - if (!xml) - return -1; - -// DPRINTF(E_DBG, L_PLAYER, "Parsing %s\n", item); +// DPRINTF(E_DBG, L_PLAYER, "Got pipe metadata item: '%s'\n", item); - haystack = mxmlLoadString(xml, item, MXML_NO_CALLBACK); - if (!haystack) + xml = xml_from_string(item); + if (!xml) { - DPRINTF(E_LOG, L_PLAYER, "Could not parse pipe metadata: %s\n", item); + DPRINTF(E_LOG, L_PLAYER, "Could not parse pipe metadata item: %s\n", item); goto error; } *type = 0; - if ( (needle = mxmlFindElement(haystack, haystack, "type", NULL, NULL, MXML_DESCEND)) && - (s = mxmlGetText(needle, NULL)) ) + if ((s = xml_get_val(xml, "item/type"))) sscanf(s, "%8x", type); *code = 0; - if ( (needle = mxmlFindElement(haystack, haystack, "code", NULL, NULL, MXML_DESCEND)) && - (s = mxmlGetText(needle, NULL)) ) + if ((s = xml_get_val(xml, "item/code"))) sscanf(s, "%8x", code); if (*type == 0 || *code == 0) @@ -570,8 +567,7 @@ parse_item_xml(uint32_t *type, uint32_t *code, uint8_t **data, int *data_len, co *data = NULL; *data_len = 0; - if ( (needle = mxmlFindElement(haystack, haystack, "data", NULL, NULL, MXML_DESCEND)) && - (s = mxmlGetText(needle, NULL)) ) + if ((s = xml_get_val(xml, "item/data"))) { *data = b64_decode(data_len, s); if (*data == NULL) @@ -583,11 +579,11 @@ parse_item_xml(uint32_t *type, uint32_t *code, uint8_t **data, int *data_len, co log_incoming(E_SPAM, "Read Shairport metadata", *type, *code, *data_len); - mxmlDelete(xml); + xml_free(xml); return 0; error: - mxmlDelete(xml); + xml_free(xml); return -1; } diff --git a/src/lastfm.c b/src/lastfm.c index 2dafae450c..0a75c33f9b 100644 --- a/src/lastfm.c +++ b/src/lastfm.c @@ -34,14 +34,13 @@ #include #include -#include "mxml-compat.h" - #include "db.h" #include "conffile.h" #include "lastfm.h" #include "listener.h" #include "logger.h" #include "misc.h" +#include "misc_xml.h" #include "http.h" // LastFM becomes disabled if we get a scrobble, try initialising session, @@ -119,15 +118,44 @@ param_sign(struct keyval *kv) /* --------------------------------- MAIN --------------------------------- */ +/* Example responses + + + + + myname + dsfjDFDS22 + 0 + + + + + + Authentication Failed - You do not have permissions to access the service + + + + + + + Hard Place + My Artist + My Album + My Album Artist + 1699649816 + + + + +*/ + static int response_process(struct http_client_ctx *ctx, char **errmsg) { - mxml_node_t *tree; - mxml_node_t *s_node; - mxml_node_t *e_node; + xml_node *tree; + const char *error; char *body; char *sk; - int ret; // NULL-terminate the buffer evbuffer_add(ctx->input_body, "", 1); @@ -139,67 +167,55 @@ response_process(struct http_client_ctx *ctx, char **errmsg) return -1; } - tree = mxmlLoadString(NULL, body, MXML_OPAQUE_CALLBACK); + tree = xml_from_string(body); if (!tree) { DPRINTF(E_LOG, L_LASTFM, "Failed to parse LastFM response:\n%s\n", body); return -1; } - // Look for errors - e_node = mxmlFindElement(tree, tree, "error", NULL, NULL, MXML_DESCEND); - if (e_node) + error = xml_get_val(tree, "lfm/error"); + if (error) { - DPRINTF(E_LOG, L_LASTFM, "Request to LastFM failed: %s\n", mxmlGetOpaque(e_node)); + DPRINTF(E_LOG, L_LASTFM, "Request to LastFM failed: %s\n", error); DPRINTF(E_DBG, L_LASTFM, "LastFM response:\n%s\n", body); if (errmsg) - *errmsg = atrim(mxmlGetOpaque(e_node)); + *errmsg = atrim(error); - mxmlDelete(tree); + xml_free(tree); return -1; } DPRINTF(E_SPAM, L_LASTFM, "LastFM response:\n%s\n", body); // Was it a scrobble request? Then do nothing. TODO: Check for error messages - s_node = mxmlFindElement(tree, tree, "scrobbles", NULL, NULL, MXML_DESCEND); - if (s_node) + if (xml_get_node(tree, "lfm/scrobbles/scrobble")) { DPRINTF(E_DBG, L_LASTFM, "Scrobble callback\n"); - mxmlDelete(tree); + xml_free(tree); return 0; } // Otherwise an auth request, so get the session key - s_node = mxmlFindElement(tree, tree, "key", NULL, NULL, MXML_DESCEND); - if (!s_node) + sk = atrim(xml_get_val(tree, "lfm/session/key")); + if (!sk) { DPRINTF(E_LOG, L_LASTFM, "Session key not found\n"); - mxmlDelete(tree); + xml_free(tree); return -1; } - sk = atrim(mxmlGetOpaque(s_node)); - if (sk) - { - DPRINTF(E_LOG, L_LASTFM, "Got session key from LastFM: %s\n", sk); - db_admin_set(DB_ADMIN_LASTFM_SESSION_KEY, sk); + DPRINTF(E_INFO, L_LASTFM, "Got session key from LastFM: %s\n", sk); + db_admin_set(DB_ADMIN_LASTFM_SESSION_KEY, sk); - if (lastfm_session_key) - free(lastfm_session_key); + free(lastfm_session_key); - lastfm_session_key = sk; - lastfm_disabled = false; - ret = 0; - } - else - { - ret = -1; - } + lastfm_session_key = sk; + lastfm_disabled = false; - mxmlDelete(tree); - return ret; + xml_free(tree); + return 0; } /* diff --git a/src/library/rssscanner.c b/src/library/rssscanner.c index 820f73486f..8191c7c921 100644 --- a/src/library/rssscanner.c +++ b/src/library/rssscanner.c @@ -40,14 +40,13 @@ #include -#include "mxml-compat.h" - #include "conffile.h" #include "logger.h" #include "db.h" #include "http.h" #include "misc.h" #include "misc_json.h" +#include "misc_xml.h" #include "library.h" #include "library/filescanner.h" @@ -233,12 +232,12 @@ playlist_fetch(bool *is_new, const char *path) return NULL; } -static mxml_node_t * +static xml_node * rss_xml_get(const char *url) { struct http_client_ctx ctx = { 0 }; const char *raw = NULL; - mxml_node_t *xml = NULL; + xml_node *xml = NULL; char *feedurl; int ret; @@ -267,7 +266,7 @@ rss_xml_get(const char *url) raw = (const char*)evbuffer_pullup(ctx.input_body, -1); - xml = mxmlLoadString(NULL, raw, MXML_OPAQUE_CALLBACK); + xml = xml_from_string(raw); if (!xml) { DPRINTF(E_LOG, L_LIB, "Failed to parse RSS XML from '%s'\n", ctx.url); @@ -281,85 +280,39 @@ rss_xml_get(const char *url) } static int -rss_xml_parse_feed(const char **feed_title, const char **feed_author, const char **feed_artwork, mxml_node_t *xml) +feed_metadata_from_xml(const char **feed_title, const char **feed_author, const char **feed_artwork, xml_node *xml) { - mxml_node_t *channel; - mxml_node_t *node; - - channel = mxmlFindElement(xml, xml, "channel", NULL, NULL, MXML_DESCEND); + xml_node *channel = xml_get_node(xml, "rss/channel"); if (!channel) { DPRINTF(E_LOG, L_LIB, "Invalid RSS/xml, missing 'channel' node\n"); return -1; } - node = mxmlFindElement(channel, channel, "title", NULL, NULL, MXML_DESCEND_FIRST); - if (!node) + *feed_title = xml_get_val(channel, "title"); + if (!*feed_title) { DPRINTF(E_LOG, L_LIB, "Invalid RSS/xml, missing 'title' node\n"); return -1; } - *feed_title = mxmlGetOpaque(node); - - node = mxmlFindElement(channel, channel, "itunes:author", NULL, NULL, MXML_DESCEND_FIRST); - *feed_author = node ? mxmlGetOpaque(node) : NULL; - *feed_artwork = NULL; - node = mxmlFindElement(channel, channel, "image", NULL, NULL, MXML_DESCEND_FIRST); - if (node) - { - node = mxmlFindElement(node, node, "url", NULL, NULL, MXML_DESCEND_FIRST); - *feed_artwork = node ? mxmlGetOpaque(node) : NULL; - } + *feed_author = xml_get_val(channel, "itunes:author"); + *feed_artwork = xml_get_val(channel, "image/url"); return 0; } -static int -rss_xml_parse_item(struct rss_item_info *ri, mxml_node_t *xml, void **saveptr) +static void +ri_from_item(struct rss_item_info *ri, xml_node *item) { - mxml_node_t *item; - mxml_node_t *node; - const char *s; - - if (*saveptr) - { - item = (mxml_node_t *)(*saveptr); - while ( (item = mxmlGetNextSibling(item)) ) - { - s = mxmlGetElement(item); - if (s && strcmp(s, "item") == 0) - break; - } - *saveptr = item; - } - else - { - item = mxmlFindElement(xml, xml, "item", NULL, NULL, MXML_DESCEND); - *saveptr = item; - } - - if (!item) - return -1; // No more items - memset(ri, 0, sizeof(struct rss_item_info)); - node = mxmlFindElement(item, item, "title", NULL, NULL, MXML_DESCEND_FIRST); - ri->title = mxmlGetOpaque(node); - - node = mxmlFindElement(item, item, "pubDate", NULL, NULL, MXML_DESCEND_FIRST); - ri->pubdate = mxmlGetOpaque(node); + ri->title = xml_get_val(item, "title"); + ri->pubdate = xml_get_val(item, "pubDate"); + ri->link = xml_get_val(item, "link"); - node = mxmlFindElement(item, item, "link", NULL, NULL, MXML_DESCEND_FIRST); - ri->link = mxmlGetOpaque(node); - - node = mxmlFindElement(item, item, "enclosure", NULL, NULL, MXML_DESCEND_FIRST); - ri->url = mxmlElementGetAttr(node, "url"); - ri->type = mxmlElementGetAttr(node, "type"); - - DPRINTF(E_DBG, L_LIB, "RSS/xml item: title '%s' pubdate: '%s' link: '%s' url: '%s' type: '%s'\n", ri->title, ri->pubdate, ri->link, ri->url, ri->type); - - return 0; + ri->url = xml_get_attr(item, "enclosure", "url"); + ri->type = xml_get_attr(item, "enclosure", "type"); } // The RSS spec states: @@ -411,14 +364,14 @@ mfi_metadata_fixup(struct media_file_info *mfi, struct rss_item_info *ri, const static int rss_save(struct playlist_info *pli, int *count, enum rss_scan_type scan_type) { - mxml_node_t *xml; + xml_node *xml; + xml_node *item; const char *feed_title; const char *feed_author; const char *feed_artwork; struct media_file_info mfi = { 0 }; struct rss_item_info ri; uint32_t time_added; - void *ptr = NULL; int ret; xml = rss_xml_get(pli->path); @@ -428,11 +381,11 @@ rss_save(struct playlist_info *pli, int *count, enum rss_scan_type scan_type) return -1; } - ret = rss_xml_parse_feed(&feed_title, &feed_author, &feed_artwork, xml); + ret = feed_metadata_from_xml(&feed_title, &feed_author, &feed_artwork, xml); if (ret < 0) { DPRINTF(E_LOG, L_LIB, "Invalid RSS/xml received from '%s' (id %d)\n", pli->path, pli->id); - mxmlDelete(xml); + xml_free(xml); return -1; } @@ -455,21 +408,24 @@ rss_save(struct playlist_info *pli, int *count, enum rss_scan_type scan_type) *count = 0; db_transaction_begin(); db_pl_clear_items(pli->id); - while ((ret = rss_xml_parse_item(&ri, xml, &ptr)) == 0 && (*count < pli->query_limit)) + for (item = xml_get_node(xml, "rss/channel/item"); item && (*count < pli->query_limit); item = xml_get_next(xml, item)) { if (library_is_exiting()) { db_transaction_rollback(); - mxmlDelete(xml); + xml_free(xml); return -1; } + ri_from_item(&ri, item); if (!ri.url) { DPRINTF(E_WARN, L_LIB, "Missing URL for item '%s' (date %s) in RSS feed '%s'\n", ri.title, ri.pubdate, feed_title); continue; } + DPRINTF(E_DBG, L_LIB, "RSS/xml item: title '%s' pubdate: '%s' link: '%s' url: '%s' type: '%s'\n", ri.title, ri.pubdate, ri.link, ri.url, ri.type); + db_pl_add_item_bypath(pli->id, ri.url); (*count)++; @@ -499,7 +455,7 @@ rss_save(struct playlist_info *pli, int *count, enum rss_scan_type scan_type) } db_transaction_end(); - mxmlDelete(xml); + xml_free(xml); return 0; } diff --git a/src/misc_xml.c b/src/misc_xml.c new file mode 100644 index 0000000000..f343a2a0d3 --- /dev/null +++ b/src/misc_xml.c @@ -0,0 +1,400 @@ +/* + * Copyright (C) 2023 Espen Jurgensen + * + * 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 + * + * + * About pipe.c + * -------------- + * This module will read a PCM16 stream from a named pipe and write it to the + * input buffer. The user may start/stop playback from a pipe by selecting it + * through a client. If the user has configured pipe_autostart, then pipes in + * the library will also be watched for data, and playback will start/stop + * automatically. + * + * The module will also look for pipes with a .metadata suffix, and if found, + * the metadata will be parsed and fed to the player. The metadata must be in + * the format Shairport uses for this purpose. + * + */ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include // fopen +#include // va_* +#include + +#include + +typedef mxml_node_t xml_node; + + +/* ---------------- Compability with older versions of mxml ----------------- */ + +// mxml 2.10 has a memory leak in mxmlDelete, see https://github.com/michaelrsweet/mxml/issues/183 +// - and since this is the version in Ubuntu 18.04 LTS and Raspian Stretch, we +// fix it by including a fixed mxmlDelete here. It should be removed once the +// major distros no longer have 2.10. The below code is msweet's fixed mxml. +#if (MXML_MAJOR_VERSION == 2) && (MXML_MINOR_VERSION <= 10) + +#define mxmlDelete compat_mxmlDelete + +static void +compat_mxml_free(mxml_node_t *node) +{ + int i; + + switch (node->type) + { + case MXML_ELEMENT : + if (node->value.element.name) + free(node->value.element.name); + + if (node->value.element.num_attrs) + { + for (i = 0; i < node->value.element.num_attrs; i ++) + { + if (node->value.element.attrs[i].name) + free(node->value.element.attrs[i].name); + if (node->value.element.attrs[i].value) + free(node->value.element.attrs[i].value); + } + + free(node->value.element.attrs); + } + break; + case MXML_INTEGER : + break; + case MXML_OPAQUE : + if (node->value.opaque) + free(node->value.opaque); + break; + case MXML_REAL : + break; + case MXML_TEXT : + if (node->value.text.string) + free(node->value.text.string); + break; + case MXML_CUSTOM : + if (node->value.custom.data && + node->value.custom.destroy) + (*(node->value.custom.destroy))(node->value.custom.data); + break; + default : + break; + } + + free(node); +} + +__attribute__((unused)) static void +compat_mxmlDelete(mxml_node_t *node) +{ + mxml_node_t *current, + *next; + + if (!node) + return; + + mxmlRemove(node); + for (current = node->child; current; current = next) + { + if ((next = current->child) != NULL) + { + current->child = NULL; + continue; + } + + if ((next = current->next) == NULL) + { + if ((next = current->parent) == node) + next = NULL; + } + compat_mxml_free(current); + } + + compat_mxml_free(node); +} +#endif + +/* For compability with mxml 2.6 */ +#ifndef HAVE_MXMLGETTEXT +__attribute__((unused)) static const char * /* O - Text string or NULL */ +mxmlGetText(mxml_node_t *node, /* I - Node to get */ + int *whitespace) /* O - 1 if string is preceded by whitespace, 0 otherwise */ +{ + if (node->type == MXML_TEXT) + return (node->value.text.string); + else if (node->type == MXML_ELEMENT && + node->child && + node->child->type == MXML_TEXT) + return (node->child->value.text.string); + else + return (NULL); +} +#endif + +#ifndef HAVE_MXMLGETOPAQUE +__attribute__((unused)) static const char * /* O - Opaque string or NULL */ +mxmlGetOpaque(mxml_node_t *node) /* I - Node to get */ +{ + if (!node) + return (NULL); + + if (node->type == MXML_OPAQUE) + return (node->value.opaque); + else if (node->type == MXML_ELEMENT && + node->child && + node->child->type == MXML_OPAQUE) + return (node->child->value.opaque); + else + return (NULL); +} +#endif + +#ifndef HAVE_MXMLGETFIRSTCHILD +__attribute__((unused)) static mxml_node_t * /* O - First child or NULL */ +mxmlGetFirstChild(mxml_node_t *node) /* I - Node to get */ +{ + if (!node || node->type != MXML_ELEMENT) + return (NULL); + + return (node->child); +} +#endif + +#ifndef HAVE_MXMLGETTYPE +__attribute__((unused)) static mxml_type_t /* O - Type of node */ +mxmlGetType(mxml_node_t *node) /* I - Node to get */ +{ + return (node->type); +} +#endif + + +/* --------------------------------- Helpers -------------------------------- */ + +// We get values from mxml via GetOpaque, but that means they can whitespace, +// thus we trim them. A bit dirty, since the values are in principle const. +static const char * +trim(const char *str) +{ + char *term; + + if (!str) + return NULL; + + while (isspace(*str)) + str++; + + term = (char *)str + strlen(str); + while (term != str && isspace(*(term - 1))) + term--; + + // Dirty write to the const string from mxml + *term = '\0'; + + return str; +} + + +/* -------------------------- Wrapper implementation ------------------------ */ + +char * +xml_to_string(xml_node *top) +{ + return mxmlSaveAllocString(top, MXML_NO_CALLBACK); +} + +// This works both for well-formed xml strings (beginning with + // + // <![CDATA[Tissages]]> + // mxmlFindPath(top, "rss/channel") will return an OPAQUE node where the + // opaque value is just the whitespace. What we want is the ELEMENT parent, + // because that's the one we can use to search for children nodes ("title"). + node = mxmlFindPath(top, path); + type = mxmlGetType(node); + if (type == MXML_ELEMENT) + return node; + + return mxmlGetParent(node); +} + +xml_node * +xml_get_next(xml_node *top, xml_node *node) +{ + const char *name; + const char *s; + + name = mxmlGetElement(node); + if (!name) + return NULL; + + while ( (node = mxmlGetNextSibling(node)) ) + { + s = mxmlGetElement(node); + if (s && strcmp(s, name) == 0) + return node; + } + + return NULL; +} + +// Walks through the children of the "path" node until it finds one that is +// not just whitespace and returns a trimmed value (except for CDATA). Means +// that these variations will all give the same result: +// +// FOO FOO\nBAR BAR \n +// FOO FOO +// \nFOO FOO\n\n +const char * +xml_get_val(xml_node *top, const char *path) +{ + mxml_node_t *parent; + mxml_node_t *node; + mxml_type_t type; + const char *s = ""; + + parent = xml_get_node(top, path); + if (!parent) + return NULL; + + for (node = mxmlGetFirstChild(parent); node; node = mxmlGetNextSibling(node)) + { + type = mxmlGetType(node); + if (type == MXML_OPAQUE) + s = trim(mxmlGetOpaque(node)); + else if (type == MXML_ELEMENT) + s = mxmlGetCDATA(node); + + if (s && *s != '\0') + break; + } + + return s; +} + +const char * +xml_get_attr(xml_node *top, const char *path, const char *name) +{ + mxml_node_t *node = mxmlFindPath(top, path); + + return mxmlElementGetAttr(node, name); +} + +xml_node * +xml_new_node(xml_node *parent, const char *name, const char *val) +{ + if (!parent) + parent = MXML_NO_PARENT; + + mxml_node_t *node = mxmlNewElement(parent, name); + if (!val) + return node; // We're done, caller gets an ELEMENT to use as parent + + mxmlNewText(node, 0, val); + return node; +} + +xml_node * +xml_new_node_textf(xml_node *parent, const char *name, const char *format, ...) +{ + char *s = NULL; + va_list va; + mxml_node_t *node; + int ret; + + va_start(va, format); + ret = vasprintf(&s, format, va); + va_end(va); + + if (ret < 0) + return NULL; + + node = xml_new_node(parent, name, s); + + free(s); + + return node; +} + +void +xml_new_text(xml_node *parent, const char *val) +{ + mxmlNewText(parent, 0, val); +} diff --git a/src/misc_xml.h b/src/misc_xml.h new file mode 100644 index 0000000000..ac34edbdd8 --- /dev/null +++ b/src/misc_xml.h @@ -0,0 +1,56 @@ +#ifndef SRC_MISC_XML_H_ +#define SRC_MISC_XML_H_ + +// This wraps mxml and adds some convenience functions. This also means that +// callers don't need to concern themselves with changes and bugs in various +// versions of mxml. + +typedef void xml_node; + +// Wraps mxmlSaveAllocString. Returns NULL on error. +char * +xml_to_string(xml_node *top); + +// Wraps mxmlNewXML and mxmlLoadString, so creates an xml struct with the parsed +// content of string. Returns NULL on error. +xml_node * +xml_from_string(const char *string); + +// Wraps mxmlNewXML and mxmlLoadFile, so creates an xml struct with the parsed +// content of string. Returns NULL on error. +xml_node * +xml_from_file(const char *path); + +// Wraps mxmlDelete, which will free node + underlying nodes +void +xml_free(xml_node *top); + +// Wraps mxmlFindPath. +xml_node * +xml_get_node(xml_node *top, const char *path); + +// Wraps mxmlGetNextSibling, but only returns sibling nodes that have the same +// name as input node. +xml_node * +xml_get_next(xml_node *top, xml_node *node); + +// Wraps mxmlFindPath and mxmlGetOpaque + mxmlGetCDATA. Returns NULL if nothing +// can be found. +const char * +xml_get_val(xml_node *top, const char *path); + +// Wraps mxmlFindPath and mxmlElementGetAttr. Returns NULL if nothing can be +// found. +const char * +xml_get_attr(xml_node *top, const char *path, const char *name); + +xml_node * +xml_new_node(xml_node *parent, const char *name, const char *val); + +xml_node * +xml_new_node_textf(xml_node *parent, const char *name, const char *format, ...); + +void +xml_new_text(xml_node *parent, const char *val); + +#endif /* SRC_MISC_XML_H_ */ diff --git a/src/mxml-compat.h b/src/mxml-compat.h deleted file mode 100644 index 8fb7286626..0000000000 --- a/src/mxml-compat.h +++ /dev/null @@ -1,178 +0,0 @@ -#ifndef __MXML_COMPAT_H__ -#define __MXML_COMPAT_H__ - -#include - -// mxml 2.10 has a memory leak in mxmlDelete, see https://github.com/michaelrsweet/mxml/issues/183 -// - and since this is the version in Ubuntu 18.04 LTS and Raspian Stretch, we -// fix it by including a fixed mxmlDelete here. It should be removed once the -// major distros no longer have 2.10. The below code is msweet's fixed mxml. -#if (MXML_MAJOR_VERSION == 2) && (MXML_MINOR_VERSION <= 10) - -#define mxmlDelete compat_mxmlDelete - -static void -compat_mxml_free(mxml_node_t *node) -{ - int i; - - switch (node->type) - { - case MXML_ELEMENT : - if (node->value.element.name) - free(node->value.element.name); - - if (node->value.element.num_attrs) - { - for (i = 0; i < node->value.element.num_attrs; i ++) - { - if (node->value.element.attrs[i].name) - free(node->value.element.attrs[i].name); - if (node->value.element.attrs[i].value) - free(node->value.element.attrs[i].value); - } - - free(node->value.element.attrs); - } - break; - case MXML_INTEGER : - break; - case MXML_OPAQUE : - if (node->value.opaque) - free(node->value.opaque); - break; - case MXML_REAL : - break; - case MXML_TEXT : - if (node->value.text.string) - free(node->value.text.string); - break; - case MXML_CUSTOM : - if (node->value.custom.data && - node->value.custom.destroy) - (*(node->value.custom.destroy))(node->value.custom.data); - break; - default : - break; - } - - free(node); -} - -__attribute__((unused)) static void -compat_mxmlDelete(mxml_node_t *node) -{ - mxml_node_t *current, - *next; - - if (!node) - return; - - mxmlRemove(node); - for (current = node->child; current; current = next) - { - if ((next = current->child) != NULL) - { - current->child = NULL; - continue; - } - - if ((next = current->next) == NULL) - { - if ((next = current->parent) == node) - next = NULL; - } - compat_mxml_free(current); - } - - compat_mxml_free(node); -} -#endif - -// Debian 10.x amd64 w/mxml 2.12 has a mxmlNewTextf that causes segfault when -// mxmlSaveString or mxmlSaveAllocString is called, -// ref https://github.com/owntone/owntone-server/issues/938 -#if (MXML_MAJOR_VERSION == 2) && (MXML_MINOR_VERSION == 12) - -#include - -#define mxmlNewTextf compat_mxmlNewTextf - -__attribute__((unused)) static mxml_node_t * -compat_mxmlNewTextf(mxml_node_t *parent, int whitespace, const char *format, ...) -{ - char *s = NULL; - va_list va; - mxml_node_t *node; - int ret; - - va_start(va, format); - ret = vasprintf(&s, format, va); - va_end(va); - - if (ret < 0) - return NULL; - - node = mxmlNewText(parent, whitespace, s); - - free(s); - - return node; -} -#endif - -/* For compability with mxml 2.6 */ -#ifndef HAVE_MXMLGETTEXT -__attribute__((unused)) static const char * /* O - Text string or NULL */ -mxmlGetText(mxml_node_t *node, /* I - Node to get */ - int *whitespace) /* O - 1 if string is preceded by whitespace, 0 otherwise */ -{ - if (node->type == MXML_TEXT) - return (node->value.text.string); - else if (node->type == MXML_ELEMENT && - node->child && - node->child->type == MXML_TEXT) - return (node->child->value.text.string); - else - return (NULL); -} -#endif - -#ifndef HAVE_MXMLGETOPAQUE -__attribute__((unused)) static const char * /* O - Opaque string or NULL */ -mxmlGetOpaque(mxml_node_t *node) /* I - Node to get */ -{ - if (!node) - return (NULL); - - if (node->type == MXML_OPAQUE) - return (node->value.opaque); - else if (node->type == MXML_ELEMENT && - node->child && - node->child->type == MXML_OPAQUE) - return (node->child->value.opaque); - else - return (NULL); -} -#endif - -#ifndef HAVE_MXMLGETFIRSTCHILD -__attribute__((unused)) static mxml_node_t * /* O - First child or NULL */ -mxmlGetFirstChild(mxml_node_t *node) /* I - Node to get */ -{ - if (!node || node->type != MXML_ELEMENT) - return (NULL); - - return (node->child); -} -#endif - -#ifndef HAVE_MXMLGETTYPE -__attribute__((unused)) static mxml_type_t /* O - Type of node */ -mxmlGetType(mxml_node_t *node) /* I - Node to get */ -{ - return (node->type); -} -#endif - -#endif /* !__MXML_COMPAT_H__ */