From 1f565b4d32b34be1702236a8df9adfaed8dcc684 Mon Sep 17 00:00:00 2001
From: Přemysl Janouch
Date: Sun, 9 Oct 2016 09:48:47 +0200
Subject: Make the Streams tab actually work
Clean up work is required now.
---
liberty | 2 +-
nncmpp.c | 364 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 363 insertions(+), 3 deletions(-)
diff --git a/liberty b/liberty
index ad143fd..0b77bde 160000
--- a/liberty
+++ b/liberty
@@ -1 +1 @@
-Subproject commit ad143fd8c06f2d46559e99c54fa2b611a25e3cc9
+Subproject commit 0b77bdeaf9d28c85d8c558bfe7bcd14117a3f943
diff --git a/nncmpp.c b/nncmpp.c
index 7dc0c93..37c14e4 100644
--- a/nncmpp.c
+++ b/nncmpp.c
@@ -61,6 +61,7 @@ enum
#define LIBERTY_WANT_POLLER
#define LIBERTY_WANT_ASYNC
+#define LIBERTY_WANT_PROTO_HTTP
#include "liberty/liberty.c"
#include
@@ -335,7 +336,7 @@ app_load_color (struct config_item *subtree, const char *name, int id)
struct str_vector v;
str_vector_init (&v);
- cstr_split_ignore_empty (value, ' ', &v);
+ cstr_split (value, " ", true, &v);
int colors = 0;
struct attrs attrs = { -1, -1, 0 };
@@ -1539,7 +1540,365 @@ app_process_termo_event (termo_key_t *event)
// --- Streams -----------------------------------------------------------------
-// TODO: play stream on Enter (just send a command, presumably)
+// TODO: either move to app_context or write a poller abstraction for cURL
+static struct
+{
+ CURLM *curl; ///< cURL multi handle
+ struct poller_timer timer; ///< cURL timer
+
+ struct poller poller; ///< Poller
+ bool polling; ///< Polling
+
+ char curl_error[CURL_ERROR_SIZE]; ///< cURL error info buffer
+ CURLcode result; ///< Transfer result
+}
+g_curl;
+
+static void
+app_curl_collect (curl_socket_t s, int ev_bitmask)
+{
+ int running = 0;
+ CURLMcode res;
+ // XXX: ignoring errors, in particular CURLM_CALL_MULTI_PERFORM
+ if ((res = curl_multi_socket_action (g_curl.curl, s, ev_bitmask, &running)))
+ print_debug ("cURL: %s", curl_multi_strerror (res));
+
+ CURLMsg *msg;
+ while ((msg = curl_multi_info_read (g_curl.curl, &running)))
+ {
+ // TODO: notify about completion
+ if (msg->msg == CURLMSG_DONE)
+ {
+ (void) msg->easy_handle;
+ g_curl.result = msg->data.result;
+ g_curl.polling = false;
+ }
+ }
+}
+
+static void
+app_curl_on_socket (const struct pollfd *pfd, void *user_data)
+{
+ (void) pfd;
+ (void) user_data;
+
+ int mask = 0;
+ if (pfd->revents & POLLIN) mask |= CURL_CSELECT_IN;
+ if (pfd->revents & POLLOUT) mask |= CURL_CSELECT_OUT;
+ if (pfd->revents & POLLERR) mask |= CURL_CSELECT_ERR;
+ app_curl_collect (pfd->fd, mask);
+}
+
+static int
+app_curl_on_socket_action (CURL *easy, curl_socket_t s, int what,
+ void *user_data, void *socket_data)
+{
+ (void) easy;
+ (void) user_data;
+
+ // TODO: when we move to the main poller, this should be a linked list
+ // so that we can be sure to free it all
+ struct poller_fd *fd;
+ if (!(fd = socket_data))
+ {
+ poller_fd_init ((fd = xmalloc (sizeof *fd)), &g_curl.poller, s);
+ fd->dispatcher = app_curl_on_socket;
+ curl_multi_assign (g_curl.curl, s, fd);
+ }
+ if (what == CURL_POLL_REMOVE)
+ {
+ poller_fd_reset (fd);
+ free (fd);
+ }
+ else
+ {
+ short events = 0;
+ if (what == CURL_POLL_IN) events = POLLIN;
+ if (what == CURL_POLL_OUT) events = POLLOUT;
+ if (what == CURL_POLL_INOUT) events = POLLIN | POLLOUT;
+ poller_fd_set (fd, events);
+ }
+ return 0;
+}
+
+static void
+app_curl_on_timer (void *user_data)
+{
+ (void) user_data;
+ app_curl_collect (CURL_SOCKET_TIMEOUT, 0);
+}
+
+static int
+app_curl_on_timer_change (CURLM *multi, long timeout_ms, void *user_data)
+{
+ (void) multi;
+ (void) user_data;
+
+ if (timeout_ms < 0)
+ poller_timer_reset (&g_curl.timer);
+ else
+ poller_timer_set (&g_curl.timer, timeout_ms);
+ return 0;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static CURL *
+app_curl_start (const char *uri, struct error **e)
+{
+ CURL *easy;
+ if (!(easy = curl_easy_init ()))
+ {
+ error_set (e, "cURL setup failed");
+ return NULL;
+ }
+
+ // We already take care of SIGPIPE, and native DNS timeouts are only
+ // a problem for people without the AsynchDNS feature.
+ //
+ // Unfortunately, cURL doesn't allow custom callbacks for DNS.
+ // The most we could try is parse out the hostname and provide an address
+ // override for it using CURLOPT_RESOLVE. Or be our own SOCKS4A/5 proxy.
+ CURLcode res;
+ if ((res = curl_easy_setopt (easy, CURLOPT_NOSIGNAL, 1L))
+ || (res = curl_easy_setopt (easy, CURLOPT_FOLLOWLOCATION, 1L))
+ || (res = curl_easy_setopt (easy, CURLOPT_NOPROGRESS, 1L))
+ // TODO: make the timeout a bit larger once we're asynchronous
+ || (res = curl_easy_setopt (easy, CURLOPT_TIMEOUT, 5L))
+ // TODO: the error needs to be one per "CURL *"
+ || (res = curl_easy_setopt (easy, CURLOPT_ERRORBUFFER, g_curl.curl_error))
+ // Not checking anything, we just want some data, any data
+ || (res = curl_easy_setopt (easy, CURLOPT_SSL_VERIFYPEER, 0L))
+ || (res = curl_easy_setopt (easy, CURLOPT_SSL_VERIFYHOST, 0L))
+ || (res = curl_easy_setopt (easy, CURLOPT_URL, uri)))
+ {
+ error_set (e, "%s", curl_easy_strerror (res));
+ curl_easy_cleanup (easy);
+ return NULL;
+ }
+
+ return easy;
+}
+
+static size_t
+write_callback (char *ptr, size_t size, size_t nmemb, void *user_data)
+{
+ struct str *buf = user_data;
+ str_append_data (buf, ptr, size * nmemb);
+
+ // Invoke CURLE_WRITE_ERROR when we've received enough data for a playlist
+ if (buf->len >= (1 << 16))
+ return 0;
+
+ return size * nmemb;
+}
+
+// TODO: don't block on this, move this somehow to the main event loop
+static bool
+app_download (const char *uri, struct str *buf, char **content_type,
+ struct error **e)
+{
+ bool result = false;
+ poller_init (&g_curl.poller);
+ poller_timer_init (&g_curl.timer, &g_curl.poller);
+ g_curl.timer.dispatcher = app_curl_on_timer;
+ g_curl.polling = true;
+
+ if (!(g_curl.curl = curl_multi_init ()))
+ {
+ error_set (e, "cURL setup failed");
+ goto error_1;
+ }
+
+ CURLMcode mres;
+ if ((mres = curl_multi_setopt (g_curl.curl,
+ CURLMOPT_SOCKETFUNCTION, app_curl_on_socket_action))
+ || (mres = curl_multi_setopt (g_curl.curl,
+ CURLMOPT_TIMERFUNCTION, app_curl_on_timer_change)))
+ {
+ error_set (e, "%s: %s",
+ "cURL setup failed", curl_multi_strerror (mres));
+ goto error_2;
+ }
+
+ CURL *easy;
+ if (!(easy = app_curl_start (uri, e)))
+ goto error_2;
+
+ CURLcode res;
+ if ((res = curl_easy_setopt (easy, CURLOPT_WRITEDATA, buf))
+ || (res = curl_easy_setopt (easy, CURLOPT_WRITEFUNCTION, write_callback)))
+ {
+ error_set (e, "%s: %s", "cURL setup failed", curl_easy_strerror (res));
+ goto error_3;
+ }
+
+ if ((mres = curl_multi_add_handle (g_curl.curl, easy)))
+ {
+ error_set (e, "%s: %s",
+ "cURL setup failed", curl_multi_strerror (mres));
+ goto error_3;
+ }
+
+ poller_timer_set (&g_curl.timer, 0);
+ while (g_curl.polling)
+ poller_run (&g_curl.poller);
+
+ if (g_curl.result
+ && g_curl.result != CURLE_WRITE_ERROR)
+ {
+ error_set (e, "%s: %s", "download failed", g_curl.curl_error);
+ goto error_4;
+ }
+
+ long code;
+ char *type;
+ if ((res = curl_easy_getinfo (easy, CURLINFO_RESPONSE_CODE, &code))
+ || (res = curl_easy_getinfo (easy, CURLINFO_CONTENT_TYPE, &type)))
+ {
+ error_set (e, "%s: %s",
+ "cURL info retrieval failed", curl_easy_strerror (res));
+ goto error_4;
+ }
+
+ if (code != 200)
+ {
+ error_set (e, "%s: %ld", "unexpected HTTP response code", code);
+ goto error_4;
+ }
+ if (type && content_type)
+ *content_type = xstrdup (type);
+
+ result = true;
+
+error_4:
+ curl_multi_remove_handle (g_curl.curl, easy);
+error_3:
+ curl_easy_cleanup (easy);
+error_2:
+ curl_multi_cleanup (g_curl.curl);
+error_1:
+ poller_free (&g_curl.poller);
+ return result;
+}
+
+static bool
+is_content_type (const char *content_type,
+ const char *expected_type, const char *expected_subtype)
+{
+ char *type = NULL, *subtype = NULL;
+ bool result = http_parse_media_type (content_type, &type, &subtype, NULL)
+ && !strcasecmp_ascii (type, expected_type)
+ && !strcasecmp_ascii (subtype, expected_subtype);
+ free (type);
+ free (subtype);
+ return result;
+}
+
+static void
+parse_playlist (const char *playlist, const char *content_type,
+ struct str_vector *out)
+{
+ // We accept a lot of very broken stuff because this is the real world
+ struct str_vector lines;
+ str_vector_init (&lines);
+ cstr_split (playlist, "\r\n", true, &lines);
+
+ // Since this excludes '"', it should even work for XMLs (w/o entities)
+ const char *extract_re =
+ "(https?://([][a-z0-9._~:/?#@!$&'()*+,;=-]|%[a-f0-9]{2})+)";
+ if ((lines.len && !strcasecmp_ascii (lines.vector[0], "[playlist]"))
+ || (content_type && is_content_type (content_type, "audio", "x-scpls")))
+ extract_re = "^File[^=]*=(.*)";
+ else if ((lines.len && !strcasecmp_ascii (lines.vector[0], "#EXTM3U"))
+ || (content_type && is_content_type (content_type, "audio", "x-mpegurl")))
+ extract_re = "^([^#].*)";
+
+ regex_t *re = regex_compile (extract_re, REG_EXTENDED, NULL);
+ hard_assert (re != NULL);
+
+ regmatch_t groups[2];
+ for (size_t i = 0; i < lines.len; i++)
+ {
+ if (regexec (re, lines.vector[i], 2, groups, 0) != REG_NOMATCH)
+ str_vector_add (out, xstrndup (lines.vector[i] + groups[1].rm_so,
+ groups[1].rm_eo - groups[1].rm_so));
+ }
+ regex_free (re);
+ str_vector_free (&lines);
+}
+
+static bool
+streams_extract_links (const char *uri, struct str_vector *out,
+ struct error **e)
+{
+ struct str buf;
+ str_init (&buf);
+
+ bool success;
+ char *content_type = NULL;
+ if (!(success = app_download (uri, &buf, &content_type, e)))
+ goto error;
+
+ // Since playlists are also "audio/*", this seems like a sane thing to do
+ bool is_binary = false;
+ for (size_t i = 0; i < buf.len; i++)
+ {
+ uint8_t c = buf.str[i];
+ is_binary |= (c < 32) & (c != '\t') & (c != '\r') & (c != '\n');
+ }
+ if (is_binary)
+ str_vector_add (out, uri);
+ else
+ parse_playlist (buf.str, content_type, out);
+ free (content_type);
+
+error:
+ str_free (&buf);
+ return success;
+}
+
+static bool
+streams_tab_on_action (enum user_action action)
+{
+ struct tab *self = g_ctx.active_tab;
+ if (self->item_selected < 0)
+ return false;
+
+ // For simplicity the URL is the string following the stream name
+ const char *uri = 1 + strchr (g_ctx.streams.vector[self->item_selected], 0);
+
+ struct mpd_client *c = &g_ctx.client;
+ bool result = true;
+ switch (action)
+ {
+ case USER_ACTION_MPD_REPLACE:
+ // FIXME: we also need to play it if we've been playing things already
+ MPD_SIMPLE ("clear")
+ case USER_ACTION_CHOOSE:
+ case USER_ACTION_MPD_ADD:
+ {
+ struct str_vector links;
+ str_vector_init (&links);
+
+ struct error *e = NULL;
+ if (!streams_extract_links (uri, &links, &e))
+ {
+ print_debug ("%s", e->message);
+ str_vector_add (&links, uri);
+ }
+
+ for (size_t i = 0; i < links.len; i++)
+ MPD_SIMPLE ("add", links.vector[i])
+
+ str_vector_free (&links);
+ break;
+ }
+ default:
+ result = false;
+ }
+ return result;
+}
static void
streams_tab_on_item_draw (size_t item_index, struct row_buffer *buffer,
@@ -1554,6 +1913,7 @@ streams_tab_init (void)
{
static struct tab super;
tab_init (&super, "Streams");
+ super.on_action = streams_tab_on_action;
super.on_item_draw = streams_tab_on_item_draw;
super.item_count = g_ctx.streams.len;
return &super;
--
cgit v1.2.3-70-g09d2