Implement lyrics lookup
There is now a generic mechanism for loading lyrics, or any other arbitrary content related to songs.
This commit is contained in:
		@@ -104,6 +104,7 @@ foreach (extra m)
 | 
				
			|||||||
endforeach ()
 | 
					endforeach ()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Generate a configuration file
 | 
					# Generate a configuration file
 | 
				
			||||||
 | 
					include (GNUInstallDirs)
 | 
				
			||||||
configure_file (${PROJECT_SOURCE_DIR}/config.h.in
 | 
					configure_file (${PROJECT_SOURCE_DIR}/config.h.in
 | 
				
			||||||
	${PROJECT_BINARY_DIR}/config.h)
 | 
						${PROJECT_BINARY_DIR}/config.h)
 | 
				
			||||||
include_directories (${PROJECT_SOURCE_DIR} ${PROJECT_BINARY_DIR})
 | 
					include_directories (${PROJECT_SOURCE_DIR} ${PROJECT_BINARY_DIR})
 | 
				
			||||||
@@ -123,10 +124,10 @@ target_link_libraries (${PROJECT_NAME} ${Unistring_LIBRARIES}
 | 
				
			|||||||
add_threads (${PROJECT_NAME})
 | 
					add_threads (${PROJECT_NAME})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Installation
 | 
					# Installation
 | 
				
			||||||
include (GNUInstallDirs)
 | 
					 | 
				
			||||||
install (TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR})
 | 
					install (TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR})
 | 
				
			||||||
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
 | 
					install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
 | 
				
			||||||
install (DIRECTORY contrib DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME})
 | 
					install (DIRECTORY contrib DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME})
 | 
				
			||||||
 | 
					install (DIRECTORY info DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Generate documentation from text markup
 | 
					# Generate documentation from text markup
 | 
				
			||||||
find_program (ASCIIDOCTOR_EXECUTABLE asciidoctor)
 | 
					find_program (ASCIIDOCTOR_EXECUTABLE asciidoctor)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										8
									
								
								NEWS
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								NEWS
									
									
									
									
									
								
							@@ -1,3 +1,11 @@
 | 
				
			|||||||
 | 
					Unreleased
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 * Added ability to look up song lyrics,
 | 
				
			||||||
 | 
					   using a new scriptable extension interface for the Info tab
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 * Added Readline-like M-u, M-l, M-c editor bindings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
2.0.0 (2022-09-03)
 | 
					2.0.0 (2022-09-03)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 * Added an optional X11 user interface
 | 
					 * Added an optional X11 user interface
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -40,7 +40,7 @@ Building
 | 
				
			|||||||
Build dependencies: CMake, pkg-config, asciidoctor or asciidoc,
 | 
					Build dependencies: CMake, pkg-config, asciidoctor or asciidoc,
 | 
				
			||||||
                    liberty (included), termo (included) +
 | 
					                    liberty (included), termo (included) +
 | 
				
			||||||
Runtime dependencies: ncursesw, libunistring, cURL +
 | 
					Runtime dependencies: ncursesw, libunistring, cURL +
 | 
				
			||||||
Optional runtime dependencies: fftw3, libpulse, x11, xft
 | 
					Optional runtime dependencies: fftw3, libpulse, x11, xft, Perl + cURL (lyrics)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 $ git clone --recursive https://git.janouch.name/p/nncmpp.git
 | 
					 $ git clone --recursive https://git.janouch.name/p/nncmpp.git
 | 
				
			||||||
 $ mkdir nncmpp/build
 | 
					 $ mkdir nncmpp/build
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,9 @@
 | 
				
			|||||||
#define PROGRAM_NAME "${PROJECT_NAME}"
 | 
					#define PROGRAM_NAME "${PROJECT_NAME}"
 | 
				
			||||||
#define PROGRAM_VERSION "${PROJECT_VERSION}"
 | 
					#define PROGRAM_VERSION "${PROJECT_VERSION}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// We use the XDG Base Directory Specification, but may be installed anywhere.
 | 
				
			||||||
 | 
					#define PROJECT_DATADIR "${CMAKE_INSTALL_FULL_DATADIR}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#cmakedefine HAVE_RESIZETERM
 | 
					#cmakedefine HAVE_RESIZETERM
 | 
				
			||||||
#cmakedefine WITH_FFTW
 | 
					#cmakedefine WITH_FFTW
 | 
				
			||||||
#cmakedefine WITH_PULSE
 | 
					#cmakedefine WITH_PULSE
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										43
									
								
								info/10-azlyrics.pl
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										43
									
								
								info/10-azlyrics.pl
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					#!/usr/bin/env perl
 | 
				
			||||||
 | 
					# 10-azlyrics.pl: nncmpp info plugin to fetch song lyrics on AZLyrics
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
 | 
				
			||||||
 | 
					# SPDX-License-Identifier: 0BSD
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Inspired by a similar ncmpc plugin.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use warnings;
 | 
				
			||||||
 | 
					use strict;
 | 
				
			||||||
 | 
					use utf8;
 | 
				
			||||||
 | 
					use open ':std', ':utf8';
 | 
				
			||||||
 | 
					unless (@ARGV) {
 | 
				
			||||||
 | 
						print "Look up on AZLyrics\n";
 | 
				
			||||||
 | 
						exit;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Encode;
 | 
				
			||||||
 | 
					my ($title, $artist, $album) = map {decode_utf8($_)} @ARGV;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# TODO: An upgrade would be transliteration with, e.g., Text::Unidecode.
 | 
				
			||||||
 | 
					use Unicode::Normalize;
 | 
				
			||||||
 | 
					$artist = lc(NFD($artist) =~ s/^the\s+//ir =~ s/[^A-Za-z0-9]//gr);
 | 
				
			||||||
 | 
					$title  = lc(NFD($title)  =~ s/\(.*?\)//gr =~ s/[^A-Za-z0-9]//gr);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# TODO: Consider caching the results in a location like
 | 
				
			||||||
 | 
					# $XDG_CACHE_HOME/nncmpp/info/azlyrics/$artist-$title
 | 
				
			||||||
 | 
					my $found = 0;
 | 
				
			||||||
 | 
					if ($title ne '') {
 | 
				
			||||||
 | 
						open(my $curl, '-|', 'curl', '-sA', 'nncmpp/2.0',
 | 
				
			||||||
 | 
							"https://www.azlyrics.com/lyrics/$artist/$title.html") or die $!;
 | 
				
			||||||
 | 
						while (<$curl>) {
 | 
				
			||||||
 | 
							next unless /^<div>/ .. /^<\/div>/; s/<!--.*?-->//g; s/\s+$//gs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							$found = 1;
 | 
				
			||||||
 | 
							s/<\/?b>/\x01/g; s/<\/?i>/\x02/g; s/<br>/\n/; s/<.+?>//g;
 | 
				
			||||||
 | 
							s/</</g; s/>/>/g; s/"/"/g; s/'/'/g; s/&/&/g;
 | 
				
			||||||
 | 
							print;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						close($curl) or die $?;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					print "No lyrics have been found.\n" unless $found;
 | 
				
			||||||
							
								
								
									
										468
									
								
								nncmpp.c
									
									
									
									
									
								
							
							
						
						
									
										468
									
								
								nncmpp.c
									
									
									
									
									
								
							@@ -75,10 +75,11 @@ enum
 | 
				
			|||||||
#define HAVE_LIBERTY
 | 
					#define HAVE_LIBERTY
 | 
				
			||||||
#include "line-editor.c"
 | 
					#include "line-editor.c"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include <math.h>
 | 
					#include <dirent.h>
 | 
				
			||||||
#include <locale.h>
 | 
					#include <locale.h>
 | 
				
			||||||
#include <termios.h>
 | 
					#include <math.h>
 | 
				
			||||||
#include <sys/ioctl.h>
 | 
					#include <sys/ioctl.h>
 | 
				
			||||||
 | 
					#include <termios.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ncurses is notoriously retarded for input handling, we need something
 | 
					// ncurses is notoriously retarded for input handling, we need something
 | 
				
			||||||
// different if only to receive mouse events reliably.
 | 
					// different if only to receive mouse events reliably.
 | 
				
			||||||
@@ -130,6 +131,20 @@ clock_msec (clockid_t clock)
 | 
				
			|||||||
	return (int64_t) tp.tv_sec * 1000 + (int64_t) tp.tv_nsec / 1000000;
 | 
						return (int64_t) tp.tv_sec * 1000 + (int64_t) tp.tv_nsec / 1000000;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					shell_quote (const char *str, struct str *output)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						// See SUSv3 Shell and Utilities, 2.2.3 Double-Quotes
 | 
				
			||||||
 | 
						str_append_c (output, '"');
 | 
				
			||||||
 | 
						for (const char *p = str; *p; p++)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							if (strchr ("`$\"\\", *p))
 | 
				
			||||||
 | 
								str_append_c (output, '\\');
 | 
				
			||||||
 | 
							str_append_c (output, *p);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						str_append_c (output, '"');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static bool
 | 
					static bool
 | 
				
			||||||
xstrtoul_map (const struct str_map *map, const char *key, unsigned long *out)
 | 
					xstrtoul_map (const struct str_map *map, const char *key, unsigned long *out)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@@ -164,6 +179,18 @@ latin1_to_utf8 (const char *latin1)
 | 
				
			|||||||
	return str_steal (&converted);
 | 
						return str_steal (&converted);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					str_enforce_utf8 (struct str *self)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						if (!utf8_validate (self->str, self->len))
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							char *sanitized = latin1_to_utf8 (self->str);
 | 
				
			||||||
 | 
							str_reset (self);
 | 
				
			||||||
 | 
							str_append (self, sanitized);
 | 
				
			||||||
 | 
							free (sanitized);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static void
 | 
					static void
 | 
				
			||||||
cstr_uncapitalize (char *s)
 | 
					cstr_uncapitalize (char *s)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@@ -318,6 +345,8 @@ poller_curl_on_socket_action (CURL *easy, curl_socket_t s, int what,
 | 
				
			|||||||
	struct poller_curl_fd *fd;
 | 
						struct poller_curl_fd *fd;
 | 
				
			||||||
	if (!(fd = socket_data))
 | 
						if (!(fd = socket_data))
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
 | 
							set_cloexec (s);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		fd = xmalloc (sizeof *fd);
 | 
							fd = xmalloc (sizeof *fd);
 | 
				
			||||||
		LIST_PREPEND (self->fds, fd);
 | 
							LIST_PREPEND (self->fds, fd);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -4089,68 +4118,456 @@ streams_tab_init (void)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// --- Info tab ----------------------------------------------------------------
 | 
					// --- Info tab ----------------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct info_tab_plugin
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						LIST_HEADER (struct info_tab_plugin)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						char *path;                         ///< Filesystem path to plugin
 | 
				
			||||||
 | 
						char *description;                  ///< What the plugin does
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static struct info_tab_plugin *
 | 
				
			||||||
 | 
					info_tab_plugin_load (const char *path)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						// Shell quoting is less annoying than process management.
 | 
				
			||||||
 | 
						struct str escaped = str_make ();
 | 
				
			||||||
 | 
						shell_quote (path, &escaped);
 | 
				
			||||||
 | 
						FILE *fp = popen (escaped.str, "r");
 | 
				
			||||||
 | 
						str_free (&escaped);
 | 
				
			||||||
 | 
						if (!fp)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							print_error ("%s: %s", path, strerror (errno));
 | 
				
			||||||
 | 
							return NULL;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						struct str description = str_make ();
 | 
				
			||||||
 | 
						char buf[BUFSIZ];
 | 
				
			||||||
 | 
						size_t len;
 | 
				
			||||||
 | 
						while ((len = fread (buf, 1, sizeof buf, fp)) == sizeof buf)
 | 
				
			||||||
 | 
							str_append_data (&description, buf, len);
 | 
				
			||||||
 | 
						str_append_data (&description, buf, len);
 | 
				
			||||||
 | 
						if (pclose (fp))
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							str_free (&description);
 | 
				
			||||||
 | 
							print_error ("%s: %s", path, strerror (errno));
 | 
				
			||||||
 | 
							return NULL;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						char *newline = strpbrk (description.str, "\r\n");
 | 
				
			||||||
 | 
						if (newline)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							description.len = newline - description.str;
 | 
				
			||||||
 | 
							*newline = '\0';
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						str_enforce_utf8 (&description);
 | 
				
			||||||
 | 
						if (!description.len)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							str_free (&description);
 | 
				
			||||||
 | 
							print_error ("%s: %s", path, "missing description");
 | 
				
			||||||
 | 
							return NULL;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						struct info_tab_plugin *plugin = xcalloc (1, sizeof *plugin);
 | 
				
			||||||
 | 
						plugin->path = xstrdup (path);
 | 
				
			||||||
 | 
						plugin->description = str_steal (&description);
 | 
				
			||||||
 | 
						return plugin;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					info_tab_plugin_load_dir (struct str_map *basename_to_path, const char *dirname)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						DIR *dir = opendir (dirname);
 | 
				
			||||||
 | 
						if (!dir)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							print_debug ("opendir: %s: %s", dirname, strerror (errno));
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						struct dirent *entry = NULL;
 | 
				
			||||||
 | 
						while ((entry = readdir (dir)))
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							struct stat st = {};
 | 
				
			||||||
 | 
							char *path = xstrdup_printf ("%s/%s", dirname, entry->d_name);
 | 
				
			||||||
 | 
							if (stat (path, &st) || !S_ISREG (st.st_mode))
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								free (path);
 | 
				
			||||||
 | 
								continue;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Empty files silently erase formerly found basenames.
 | 
				
			||||||
 | 
							if (!st.st_size)
 | 
				
			||||||
 | 
								cstr_set (&path, NULL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							str_map_set (basename_to_path, entry->d_name, path);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						closedir (dir);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static int
 | 
				
			||||||
 | 
					strv_sort_cb (const void *a, const void *b)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						return strcmp (*(const char **) a, *(const char **) b);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static struct info_tab_plugin *
 | 
				
			||||||
 | 
					info_tab_plugin_load_all (void)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						struct str_map basename_to_path = str_map_make (free);
 | 
				
			||||||
 | 
						struct strv paths = strv_make ();
 | 
				
			||||||
 | 
						get_xdg_data_dirs (&paths);
 | 
				
			||||||
 | 
						strv_append (&paths, PROJECT_DATADIR);
 | 
				
			||||||
 | 
						for (size_t i = paths.len; i--; )
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							char *dirname =
 | 
				
			||||||
 | 
								xstrdup_printf ("%s/" PROGRAM_NAME "/info", paths.vector[i]);
 | 
				
			||||||
 | 
							info_tab_plugin_load_dir (&basename_to_path, dirname);
 | 
				
			||||||
 | 
							free (dirname);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						strv_free (&paths);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						struct strv sorted = strv_make ();
 | 
				
			||||||
 | 
						struct str_map_iter iter = str_map_iter_make (&basename_to_path);
 | 
				
			||||||
 | 
						while (str_map_iter_next (&iter))
 | 
				
			||||||
 | 
							strv_append (&sorted, iter.link->key);
 | 
				
			||||||
 | 
						qsort (sorted.vector, sorted.len, sizeof *sorted.vector, strv_sort_cb);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						struct info_tab_plugin *result = NULL;
 | 
				
			||||||
 | 
						for (size_t i = sorted.len; i--; )
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							const char *path = str_map_find (&basename_to_path, sorted.vector[i]);
 | 
				
			||||||
 | 
							struct info_tab_plugin *plugin = info_tab_plugin_load (path);
 | 
				
			||||||
 | 
							if (plugin)
 | 
				
			||||||
 | 
								LIST_PREPEND (result, plugin);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						str_map_free (&basename_to_path);
 | 
				
			||||||
 | 
						strv_free (&sorted);
 | 
				
			||||||
 | 
						return result;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct info_tab_item
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						char *prefix;                       ///< Fixed-width prefix column or NULL
 | 
				
			||||||
 | 
						char *text;                         ///< Text or NULL
 | 
				
			||||||
 | 
						bool formatted;                     ///< Interpret inline formatting marks?
 | 
				
			||||||
 | 
						struct info_tab_plugin *plugin;     ///< Activatable plugin
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					info_tab_item_free (struct info_tab_item *self)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						cstr_set (&self->prefix, NULL);
 | 
				
			||||||
 | 
						cstr_set (&self->text, NULL);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static struct
 | 
					static struct
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	struct tab super;                   ///< Parent class
 | 
						struct tab super;                   ///< Parent class
 | 
				
			||||||
	struct strv keys;                   ///< Data keys
 | 
						struct info_tab_item *items;        ///< Items array
 | 
				
			||||||
	struct strv values;                 ///< Data values
 | 
						size_t items_alloc;                 ///< How many items are allocated
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						struct info_tab_plugin *plugins;    ///< Plugins
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						int plugin_songid;                  ///< Song ID or -1
 | 
				
			||||||
 | 
						pid_t plugin_pid;                   ///< Running plugin's process ID or -1
 | 
				
			||||||
 | 
						int plugin_stdout;                  ///< pid != -1: read end of stdout
 | 
				
			||||||
 | 
						struct poller_fd plugin_event;      ///< pid != -1: stdout is readable
 | 
				
			||||||
 | 
						struct str plugin_output;           ///< pid != -1: buffer, otherwise result
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
g_info_tab;
 | 
					g_info_tab;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static chtype
 | 
				
			||||||
 | 
					info_tab_format_decode_toggle (char c)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						switch (c)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
						case '\x01':
 | 
				
			||||||
 | 
							return A_BOLD;
 | 
				
			||||||
 | 
						case '\x02':
 | 
				
			||||||
 | 
							return A_ITALIC;
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return 0;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					info_tab_format (struct layout *l, const char *text)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						chtype attrs = 0;
 | 
				
			||||||
 | 
						for (const char *p = text; *p; p++)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							chtype toggled = info_tab_format_decode_toggle (*p);
 | 
				
			||||||
 | 
							if (!toggled)
 | 
				
			||||||
 | 
								continue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (p != text)
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								char *slice = xstrndup (text, p - text);
 | 
				
			||||||
 | 
								app_push (l, g.ui->label (attrs, slice));
 | 
				
			||||||
 | 
								free (slice);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							attrs ^= toggled;
 | 
				
			||||||
 | 
							text = p + 1;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (*text)
 | 
				
			||||||
 | 
							app_push (l, g.ui->label (attrs, text));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static struct layout
 | 
					static struct layout
 | 
				
			||||||
info_tab_on_item_layout (size_t item_index)
 | 
					info_tab_on_item_layout (size_t item_index)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	const char *key = g_info_tab.keys.vector[item_index];
 | 
						struct info_tab_item *item = &g_info_tab.items[item_index];
 | 
				
			||||||
	const char *value = g_info_tab.values.vector[item_index];
 | 
					 | 
				
			||||||
	struct layout l = {};
 | 
						struct layout l = {};
 | 
				
			||||||
 | 
						if (item->prefix)
 | 
				
			||||||
	char *prefix = xstrdup_printf ("%s:", key);
 | 
						{
 | 
				
			||||||
 | 
							char *prefix = xstrdup_printf ("%s:", item->prefix);
 | 
				
			||||||
		app_push (&l, g.ui->label (A_BOLD, prefix))
 | 
							app_push (&l, g.ui->label (A_BOLD, prefix))
 | 
				
			||||||
			->width = 8 * g.ui_hunit;
 | 
								->width = 8 * g.ui_hunit;
 | 
				
			||||||
		app_push (&l, g.ui->padding (0, 0.5, 1));
 | 
							app_push (&l, g.ui->padding (0, 0.5, 1));
 | 
				
			||||||
	app_push_fill (&l, g.ui->label (0, value));
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (item->plugin)
 | 
				
			||||||
 | 
							app_push (&l, g.ui->label (A_BOLD, item->plugin->description));
 | 
				
			||||||
 | 
						else if (!item->text || !*item->text)
 | 
				
			||||||
 | 
							app_push (&l, g.ui->padding (0, 1, 1));
 | 
				
			||||||
 | 
						else if (item->formatted)
 | 
				
			||||||
 | 
							info_tab_format (&l, item->text);
 | 
				
			||||||
 | 
						else
 | 
				
			||||||
 | 
							app_push (&l, g.ui->label (0, item->text));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (l.tail)
 | 
				
			||||||
 | 
							l.tail->width = -1;
 | 
				
			||||||
	return l;
 | 
						return l;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static struct info_tab_item *
 | 
				
			||||||
 | 
					info_tab_prepare (void)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						if (g_info_tab.super.item_count == g_info_tab.items_alloc)
 | 
				
			||||||
 | 
							g_info_tab.items = xreallocarray (g_info_tab.items,
 | 
				
			||||||
 | 
								sizeof *g_info_tab.items, (g_info_tab.items_alloc <<= 1));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						struct info_tab_item *item =
 | 
				
			||||||
 | 
							&g_info_tab.items[g_info_tab.super.item_count++];
 | 
				
			||||||
 | 
						memset (item, 0, sizeof *item);
 | 
				
			||||||
 | 
						return item;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static void
 | 
					static void
 | 
				
			||||||
info_tab_add (compact_map_t data, const char *field)
 | 
					info_tab_add (compact_map_t data, const char *field)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	const char *value = compact_map_find (data, field);
 | 
						struct info_tab_item *item = info_tab_prepare ();
 | 
				
			||||||
	if (!value) value = "";
 | 
						item->prefix = xstrdup (field);
 | 
				
			||||||
 | 
						item->text = xstrdup0 (compact_map_find (data, field));
 | 
				
			||||||
	strv_append (&g_info_tab.keys, field);
 | 
					 | 
				
			||||||
	strv_append (&g_info_tab.values, value);
 | 
					 | 
				
			||||||
	g_info_tab.super.item_count++;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static void
 | 
					static void
 | 
				
			||||||
info_tab_update (void)
 | 
					info_tab_update (void)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	strv_reset (&g_info_tab.keys);
 | 
						while (g_info_tab.super.item_count)
 | 
				
			||||||
	strv_reset (&g_info_tab.values);
 | 
							info_tab_item_free (&g_info_tab.items[--g_info_tab.super.item_count]);
 | 
				
			||||||
	g_info_tab.super.item_count = 0;
 | 
					
 | 
				
			||||||
 | 
						compact_map_t map = item_list_get (&g.playlist, g.song);
 | 
				
			||||||
 | 
						if (!map)
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	compact_map_t map;
 | 
					 | 
				
			||||||
	if ((map = item_list_get (&g.playlist, g.song)))
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
	info_tab_add (map, "Title");
 | 
						info_tab_add (map, "Title");
 | 
				
			||||||
	info_tab_add (map, "Artist");
 | 
						info_tab_add (map, "Artist");
 | 
				
			||||||
	info_tab_add (map, "Album");
 | 
						info_tab_add (map, "Album");
 | 
				
			||||||
	info_tab_add (map, "Track");
 | 
						info_tab_add (map, "Track");
 | 
				
			||||||
	info_tab_add (map, "Genre");
 | 
						info_tab_add (map, "Genre");
 | 
				
			||||||
		// Yes, it is "file", but this is also for display
 | 
						// We actually receive it as "file", but the key is also used for display
 | 
				
			||||||
	info_tab_add (map, "File");
 | 
						info_tab_add (map, "File");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (g_info_tab.plugins)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							(void) info_tab_prepare ();
 | 
				
			||||||
 | 
							LIST_FOR_EACH (struct info_tab_plugin, plugin, g_info_tab.plugins)
 | 
				
			||||||
 | 
								info_tab_prepare ()->plugin = plugin;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (g_info_tab.plugin_pid != -1)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							(void) info_tab_prepare ();
 | 
				
			||||||
 | 
							info_tab_prepare ()->text = xstrdup ("Processing...");
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const char *songid = compact_map_find (map, "Id");
 | 
				
			||||||
 | 
						if (songid && atoi (songid) == g_info_tab.plugin_songid
 | 
				
			||||||
 | 
						 && g_info_tab.plugin_output.len)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							struct strv lines = strv_make ();
 | 
				
			||||||
 | 
							cstr_split (g_info_tab.plugin_output.str, "\r\n", false, &lines);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							(void) info_tab_prepare ();
 | 
				
			||||||
 | 
							for (size_t i = 0; i < lines.len; i++)
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								struct info_tab_item *item = info_tab_prepare ();
 | 
				
			||||||
 | 
								item->formatted = true;
 | 
				
			||||||
 | 
								item->text = lines.vector[i];
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							free (lines.vector);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					info_tab_plugin_abort (void)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						if (g_info_tab.plugin_pid == -1)
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// XXX: our methods of killing are very crude, we hope to improve;
 | 
				
			||||||
 | 
						//   at least install a SIGCHLD handler to collect zombies
 | 
				
			||||||
 | 
						(void) kill (-g_info_tab.plugin_pid, SIGTERM);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						int status = 0;
 | 
				
			||||||
 | 
						while (waitpid (g_info_tab.plugin_pid, &status, WNOHANG) == -1
 | 
				
			||||||
 | 
							&& errno == EINTR)
 | 
				
			||||||
 | 
							;
 | 
				
			||||||
 | 
						if (WIFEXITED (status) && WEXITSTATUS (status) != EXIT_SUCCESS)
 | 
				
			||||||
 | 
							print_error ("plugin reported failure");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						g_info_tab.plugin_pid = -1;
 | 
				
			||||||
 | 
						poller_fd_reset (&g_info_tab.plugin_event);
 | 
				
			||||||
 | 
						xclose (g_info_tab.plugin_stdout);
 | 
				
			||||||
 | 
						g_info_tab.plugin_stdout = -1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					info_tab_on_plugin_stdout (const struct pollfd *fd, void *user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						(void) user_data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						struct str *buf = &g_info_tab.plugin_output;
 | 
				
			||||||
 | 
						switch (socket_io_try_read (fd->fd, buf))
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
						case SOCKET_IO_OK:
 | 
				
			||||||
 | 
							str_enforce_utf8 (buf);
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						case SOCKET_IO_ERROR:
 | 
				
			||||||
 | 
							print_error ("error reading from plugin: %s", strerror (errno));
 | 
				
			||||||
 | 
							// Fall-through
 | 
				
			||||||
 | 
						case SOCKET_IO_EOF:
 | 
				
			||||||
 | 
							info_tab_plugin_abort ();
 | 
				
			||||||
 | 
							info_tab_update ();
 | 
				
			||||||
 | 
							app_invalidate ();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					info_tab_plugin_run (struct info_tab_plugin *plugin, compact_map_t map)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						info_tab_plugin_abort ();
 | 
				
			||||||
 | 
						if (!map)
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const char *songid = compact_map_find (map, "Id");
 | 
				
			||||||
 | 
						const char *title  = compact_map_find (map, "Title");
 | 
				
			||||||
 | 
						const char *artist = compact_map_find (map, "Artist");
 | 
				
			||||||
 | 
						const char *album  = compact_map_find (map, "Album");
 | 
				
			||||||
 | 
						if (!songid || !title || !artist)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							print_error ("unknown song title or artist");
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						int stdout_pipe[2];
 | 
				
			||||||
 | 
						if (pipe (stdout_pipe))
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							print_error ("%s: %s", "pipe", strerror (errno));
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						enum { READ, WRITE };
 | 
				
			||||||
 | 
						set_cloexec (stdout_pipe[READ]);
 | 
				
			||||||
 | 
						set_cloexec (stdout_pipe[WRITE]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const char *argv[] =
 | 
				
			||||||
 | 
							{ xbasename (plugin->path), title, artist, album, NULL };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pid_t child = fork ();
 | 
				
			||||||
 | 
						switch (child)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
						case -1:
 | 
				
			||||||
 | 
							print_error ("%s: %s", "fork", strerror (errno));
 | 
				
			||||||
 | 
							xclose (stdout_pipe[READ]);
 | 
				
			||||||
 | 
							xclose (stdout_pipe[WRITE]);
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						case 0:
 | 
				
			||||||
 | 
							if (setpgid (0, 0) == -1 || !freopen ("/dev/null", "r", stdin)
 | 
				
			||||||
 | 
							 || dup2 (stdout_pipe[WRITE], STDOUT_FILENO) == -1
 | 
				
			||||||
 | 
							 || dup2 (stdout_pipe[WRITE], STDERR_FILENO) == -1)
 | 
				
			||||||
 | 
								_exit (EXIT_FAILURE);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							signal (SIGPIPE, SIG_DFL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							(void) execv (plugin->path, (char **) argv);
 | 
				
			||||||
 | 
							fprintf (stderr, "%s\n", strerror (errno));
 | 
				
			||||||
 | 
							_exit (EXIT_FAILURE);
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							// Resolve the race, even though it isn't critical for us
 | 
				
			||||||
 | 
							(void) setpgid (child, child);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							g_info_tab.plugin_songid = atoi (songid);
 | 
				
			||||||
 | 
							g_info_tab.plugin_pid = child;
 | 
				
			||||||
 | 
							set_blocking ((g_info_tab.plugin_stdout = stdout_pipe[READ]), false);
 | 
				
			||||||
 | 
							xclose (stdout_pipe[WRITE]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							struct poller_fd *event = &g_info_tab.plugin_event;
 | 
				
			||||||
 | 
							*event = poller_fd_make (&g.poller, g_info_tab.plugin_stdout);
 | 
				
			||||||
 | 
							event->dispatcher = info_tab_on_plugin_stdout;
 | 
				
			||||||
 | 
							str_reset (&g_info_tab.plugin_output);
 | 
				
			||||||
 | 
							poller_fd_set (&g_info_tab.plugin_event, POLLIN);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static bool
 | 
				
			||||||
 | 
					info_tab_on_action (enum action action)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						struct tab *tab = g.active_tab;
 | 
				
			||||||
 | 
						if (tab->item_selected < 0
 | 
				
			||||||
 | 
						 || tab->item_selected >= (int) tab->item_count)
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						struct info_tab_item *item = &g_info_tab.items[tab->item_selected];
 | 
				
			||||||
 | 
						if (!item->plugin)
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch (action)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
						case ACTION_DESCRIBE:
 | 
				
			||||||
 | 
							app_show_message (xstrdup ("Path: "), xstrdup (item->plugin->path));
 | 
				
			||||||
 | 
							return true;
 | 
				
			||||||
 | 
						case ACTION_CHOOSE:
 | 
				
			||||||
 | 
							info_tab_plugin_run (item->plugin, item_list_get (&g.playlist, g.song));
 | 
				
			||||||
 | 
							info_tab_update ();
 | 
				
			||||||
 | 
							app_invalidate ();
 | 
				
			||||||
 | 
							return true;
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static struct tab *
 | 
					static struct tab *
 | 
				
			||||||
info_tab_init (void)
 | 
					info_tab_init (void)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	g_info_tab.keys = strv_make ();
 | 
						g_info_tab.items =
 | 
				
			||||||
	g_info_tab.values = strv_make ();
 | 
							xcalloc ((g_info_tab.items_alloc = 16), sizeof *g_info_tab.items);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						g_info_tab.plugins = info_tab_plugin_load_all ();
 | 
				
			||||||
 | 
						g_info_tab.plugin_songid = -1;
 | 
				
			||||||
 | 
						g_info_tab.plugin_pid = -1;
 | 
				
			||||||
 | 
						g_info_tab.plugin_stdout = -1;
 | 
				
			||||||
 | 
						g_info_tab.plugin_output = str_make ();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	struct tab *super = &g_info_tab.super;
 | 
						struct tab *super = &g_info_tab.super;
 | 
				
			||||||
	tab_init (super, "Info");
 | 
						tab_init (super, "Info");
 | 
				
			||||||
 | 
						super->on_action = info_tab_on_action;
 | 
				
			||||||
	super->on_item_layout = info_tab_on_item_layout;
 | 
						super->on_item_layout = info_tab_on_item_layout;
 | 
				
			||||||
	return super;
 | 
						return super;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -5377,7 +5794,7 @@ tui_on_tty_readable (const struct pollfd *fd, void *user_data)
 | 
				
			|||||||
	poller_timer_reset (&g.tk_timer);
 | 
						poller_timer_reset (&g.tk_timer);
 | 
				
			||||||
	termo_advisereadable (g.tk);
 | 
						termo_advisereadable (g.tk);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	termo_key_t event;
 | 
						termo_key_t event = {};
 | 
				
			||||||
	int64_t event_ts = clock_msec (CLOCK_BEST);
 | 
						int64_t event_ts = clock_msec (CLOCK_BEST);
 | 
				
			||||||
	termo_result_t res;
 | 
						termo_result_t res;
 | 
				
			||||||
	while ((res = termo_getkey (g.tk, &event)) == TERMO_RES_KEY)
 | 
						while ((res = termo_getkey (g.tk, &event)) == TERMO_RES_KEY)
 | 
				
			||||||
@@ -6675,6 +7092,7 @@ app_log_handler (void *user_data, const char *quote, const char *fmt,
 | 
				
			|||||||
	str_append_vprintf (&message, fmt, ap);
 | 
						str_append_vprintf (&message, fmt, ap);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Show it prettified to the user, then maybe log it elsewhere as well.
 | 
						// Show it prettified to the user, then maybe log it elsewhere as well.
 | 
				
			||||||
 | 
						// TODO: Review locale encoding vs UTF-8 in the entire program.
 | 
				
			||||||
	message.str[0] = toupper_ascii (message.str[0]);
 | 
						message.str[0] = toupper_ascii (message.str[0]);
 | 
				
			||||||
	app_show_message (xstrndup (message.str, quote_len),
 | 
						app_show_message (xstrndup (message.str, quote_len),
 | 
				
			||||||
		xstrdup (message.str + quote_len));
 | 
							xstrdup (message.str + quote_len));
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user