aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPřemysl Janouch <p.janouch@gmail.com>2016-10-10 10:28:40 +0200
committerPřemysl Janouch <p.janouch@gmail.com>2016-10-10 10:40:12 +0200
commite513102318d87ca31d64ef8c1196ce0fc62c3763 (patch)
treebb24ca6131ada84df3ecab8476558d8b93e99c3b
parent29bc035ecfed9e37ce63b42de982b95569753463 (diff)
downloadnncmpp-e513102318d87ca31d64ef8c1196ce0fc62c3763.tar.gz
nncmpp-e513102318d87ca31d64ef8c1196ce0fc62c3763.tar.xz
nncmpp-e513102318d87ca31d64ef8c1196ce0fc62c3763.zip
Rewrite Streams code to be more asynchronous
Also made cURL troubleshooting easier by adding a debug callback.
-rw-r--r--nncmpp.c321
1 files changed, 177 insertions, 144 deletions
diff --git a/nncmpp.c b/nncmpp.c
index 29a82b3..840d6e2 100644
--- a/nncmpp.c
+++ b/nncmpp.c
@@ -161,6 +161,42 @@ latin1_to_utf8 (const char *latin1)
return str_steal (&converted);
}
+static int
+print_curl_debug (CURL *easy, curl_infotype type, char *data, size_t len,
+ void *ud)
+{
+ (void) easy;
+ (void) ud;
+ (void) type;
+
+ char copy[len + 1];
+ for (size_t i = 0; i < len; i++)
+ {
+ uint8_t c = data[i];
+ copy[i] = c >= 32 || c == '\n' ? c : '.';
+ }
+ copy[len] = '\0';
+
+ char *next;
+ for (char *p = copy; p; p = next)
+ {
+ if ((next = strchr (p, '\n')))
+ *next++ = '\0';
+ if (!*p)
+ continue;
+
+ if (!utf8_validate (p, strlen (p)))
+ {
+ char *fixed = latin1_to_utf8 (p);
+ print_debug ("cURL: %s", fixed);
+ free (fixed);
+ }
+ else
+ print_debug ("cURL: %s", p);
+ }
+ return 0;
+}
+
// --- cURL async wrapper ------------------------------------------------------
// You are meant to subclass this structure, no user_data pointers needed
@@ -1767,109 +1803,11 @@ app_process_termo_event (termo_key_t *event)
struct stream_tab_task
{
struct poller_curl_task curl; ///< Superclass
+ struct str data; ///< Downloaded data
bool polling; ///< Still downloading
- CURLcode result; ///< Operation result
+ bool replace; ///< Should playlist be replaced?
};
-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;
-}
-
-static void
-app_download_on_done (CURLMsg *msg, struct poller_curl_task *task)
-{
- struct stream_tab_task *self =
- CONTAINER_OF (task, struct stream_tab_task, curl);
- self->polling = false;
- self->result = msg->data.result;
-}
-
-static bool
-app_download (const char *uri, struct str *buf, char **content_type,
- struct error **e)
-{
- struct poller poller;
- poller_init (&poller);
-
- struct poller_curl pc;
- hard_assert (poller_curl_init (&pc, &poller, NULL));
- struct stream_tab_task task;
- hard_assert (poller_curl_spawn (&task.curl, NULL));
-
- CURL *easy = task.curl.easy;
- bool result = false;
-
- CURLcode res;
- if ((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))
- // 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))
-
- || (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_1;
- }
-
- task.curl.on_done = app_download_on_done;
- hard_assert (poller_curl_add (&pc, task.curl.easy, NULL));
-
- // TODO: don't run a subloop, run the task fully asynchronously
- task.polling = true;
- while (task.polling)
- poller_run (&poller);
-
- if (task.result
- && task.result != CURLE_WRITE_ERROR)
- {
- error_set (e, "%s: %s", "download failed", task.curl.curl_error);
- goto error_2;
- }
-
- 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_2;
- }
-
- if (code != 200)
- {
- error_set (e, "%s: %ld", "unexpected HTTP response code", code);
- goto error_2;
- }
- if (type && content_type)
- *content_type = xstrdup (type);
-
- result = true;
-
-error_2:
- hard_assert (poller_curl_remove (&pc, task.curl.easy, NULL));
-error_1:
- curl_easy_cleanup (task.curl.easy);
- poller_curl_free (&pc);
-
- poller_free (&poller);
- return result;
-}
-
static bool
is_content_type (const char *content_type,
const char *expected_type, const char *expected_subtype)
@@ -1884,7 +1822,7 @@ is_content_type (const char *content_type,
}
static void
-parse_playlist (const char *playlist, const char *content_type,
+streams_tab_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
@@ -1924,33 +1862,145 @@ parse_playlist (const char *playlist, const char *content_type,
}
static bool
-streams_extract_links (const char *uri, struct str_vector *out,
- struct error **e)
+streams_tab_extract_links (struct str *data, const char *content_type,
+ struct str_vector *out)
{
- struct str buf;
- str_init (&buf);
+ // Since playlists are also "audio/*", this seems like a sane thing to do
+ for (size_t i = 0; i < data->len; i++)
+ {
+ uint8_t c = data->str[i];
+ if ((c < 32) & (c != '\t') & (c != '\r') & (c != '\n'))
+ return false;
+ }
- bool success;
- char *content_type = NULL;
- if (!(success = app_download (uri, &buf, &content_type, e)))
- goto error;
+ streams_tab_parse_playlist (data->str, content_type, out);
+ return true;
+}
- // 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++)
+static void
+streams_tab_on_downloaded (CURLMsg *msg, struct poller_curl_task *task)
+{
+ struct stream_tab_task *self =
+ CONTAINER_OF (task, struct stream_tab_task, curl);
+ self->polling = false;
+
+ if (msg->data.result
+ && msg->data.result != CURLE_WRITE_ERROR)
{
- uint8_t c = buf.str[i];
- is_binary |= (c < 32) & (c != '\t') & (c != '\r') & (c != '\n');
+ print_error ("%s: %s", "download failed", self->curl.curl_error);
+ return;
}
- if (is_binary)
- str_vector_add (out, uri);
- else
- parse_playlist (buf.str, content_type, out);
- free (content_type);
+
+ struct mpd_client *c = &g_ctx.client;
+ if (c->state != MPD_CONNECTED)
+ return;
+
+ CURL *easy = msg->easy_handle;
+ CURLcode res;
+
+ long code;
+ char *type, *uri;
+ if ((res = curl_easy_getinfo (easy, CURLINFO_RESPONSE_CODE, &code))
+ || (res = curl_easy_getinfo (easy, CURLINFO_CONTENT_TYPE, &type))
+ || (res = curl_easy_getinfo (easy, CURLINFO_EFFECTIVE_URL, &uri)))
+ {
+ print_error ("%s: %s",
+ "cURL info retrieval failed", curl_easy_strerror (res));
+ return;
+ }
+ // cURL is not willing to parse the ICY header, the code is zero then
+ if (code && code != 200)
+ {
+ print_error ("%s: %ld", "unexpected HTTP response code", code);
+ return;
+ }
+
+ mpd_client_list_begin (c);
+
+ // FIXME: we also need to play it if we've been playing things already
+ if (self->replace)
+ mpd_client_send_command (c, "clear", NULL);
+
+ struct str_vector links;
+ str_vector_init (&links);
+
+ if (!streams_tab_extract_links (&self->data, type, &links))
+ str_vector_add (&links, uri);
+ for (size_t i = 0; i < links.len; i++)
+ mpd_client_send_command (c, "add", links.vector[i], NULL);
+
+ str_vector_free (&links);
+ mpd_client_list_end (c);
+ mpd_client_add_task (c, NULL, NULL);
+ mpd_client_idle (c, 0);
+}
+
+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;
+}
+
+static bool
+streams_tab_process (const char *uri, bool replace, struct error **e)
+{
+ struct poller poller;
+ poller_init (&poller);
+
+ struct poller_curl pc;
+ hard_assert (poller_curl_init (&pc, &poller, NULL));
+ struct stream_tab_task task;
+ hard_assert (poller_curl_spawn (&task.curl, NULL));
+
+ CURL *easy = task.curl.easy;
+ str_init (&task.data);
+ task.replace = replace;
+ bool result = false;
+
+ CURLcode res;
+ if ((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))
+ // 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))
+
+ || (res = curl_easy_setopt (easy, CURLOPT_VERBOSE, (long) g_debug_mode))
+ || (res = curl_easy_setopt (easy, CURLOPT_DEBUGFUNCTION, print_curl_debug))
+ || (res = curl_easy_setopt (easy, CURLOPT_WRITEDATA, &task.data))
+ || (res = curl_easy_setopt (easy, CURLOPT_WRITEFUNCTION, write_callback)))
+ {
+ error_set (e, "%s: %s", "cURL setup failed", curl_easy_strerror (res));
+ goto error;
+ }
+
+ task.curl.on_done = streams_tab_on_downloaded;
+ hard_assert (poller_curl_add (&pc, task.curl.easy, NULL));
+
+ // TODO: don't run a subloop, run the task fully asynchronously
+ task.polling = true;
+ while (task.polling)
+ poller_run (&poller);
+
+ hard_assert (poller_curl_remove (&pc, task.curl.easy, NULL));
+ result = true;
error:
- str_free (&buf);
- return success;
+ curl_easy_cleanup (task.curl.easy);
+ str_free (&task.data);
+ poller_curl_free (&pc);
+
+ poller_free (&poller);
+ return result;
}
static bool
@@ -1963,36 +2013,19 @@ streams_tab_on_action (enum user_action action)
// 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;
+ // TODO: show any error to the user
switch (action)
{
case USER_ACTION_MPD_REPLACE:
- // FIXME: we also need to play it if we've been playing things already
- MPD_SIMPLE ("clear")
+ streams_tab_process (uri, true, NULL);
+ return true;
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;
- }
+ streams_tab_process (uri, false, NULL);
+ return true;
default:
- result = false;
+ return false;
}
- return result;
}
static void