Make the Streams tab actually work
Clean up work is required now.
This commit is contained in:
		
							parent
							
								
									4480d3c0cf
								
							
						
					
					
						commit
						1f565b4d32
					
				
							
								
								
									
										2
									
								
								liberty
									
									
									
									
									
								
							
							
								
								
								
								
								
								
									
									
								
							
						
						
									
										2
									
								
								liberty
									
									
									
									
									
								
							| @ -1 +1 @@ | ||||
| Subproject commit ad143fd8c06f2d46559e99c54fa2b611a25e3cc9 | ||||
| Subproject commit 0b77bdeaf9d28c85d8c558bfe7bcd14117a3f943 | ||||
							
								
								
									
										364
									
								
								nncmpp.c
									
									
									
									
									
								
							
							
						
						
									
										364
									
								
								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 <sys/un.h> | ||||
| @ -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; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user