17 Commits

Author SHA1 Message Date
61a141203b Bump liberty and version, update NEWS 2021-12-21 05:46:28 +01:00
48482ef2e5 Make incremental search more useful
Make it unanchored, as well as case-insensitive.
2021-12-21 05:46:22 +01:00
840c69767c Prepare a NEWS entry for the next release 2021-12-08 19:05:58 +01:00
a14a907b18 Indicate that a stream download is in progress 2021-12-08 18:58:03 +01:00
333049de01 Do not run cURL fully synchronously
The improvement is very minor in character.
2021-12-08 18:23:30 +01:00
4e3596db35 Add rudimentary incremental search facility 2021-12-08 17:23:25 +01:00
5aa07fd8af Clean up mpd_process_info() better 2021-12-07 20:38:02 +01:00
2060da4a8e Do not jump to beginning after unqueueing
Instead, assume that the whole previously selected range
has been removed, and try to go after or before it accordingly.
2021-12-07 20:34:32 +01:00
f5b5cec340 Clean up unreadable code 2021-12-07 20:10:35 +01:00
1a671dfad5 Document PulseAudio integration 2021-11-16 05:17:15 +01:00
587a02fa15 Indent man page snippets with spaces 2021-11-16 05:16:51 +01:00
227b8e0fa2 Do not show both volumes if unnecessary
Also, make it apparent which value comes from where.
2021-11-16 04:48:52 +01:00
e66e9f249a Rename an action to be shorter
Also, fix make dependencies to include the source file for actions.
2021-11-16 04:48:52 +01:00
32203f8117 Fix the comment for settings.pulseaudio 2021-11-08 07:23:08 +01:00
6b871898d8 Fix build on macOS and other non-GNU systems 2021-11-08 06:36:01 +01:00
4598c45d2f Generate actions from a text file
Mostly because I wanted to nest preprocessing.

This makes the build more complex and slightly less portable,
but the code itself is much cleaner.
2021-11-08 06:07:04 +01:00
66c77c3f8d Update README 2021-11-07 23:21:32 +01:00
8 changed files with 330 additions and 221 deletions

View File

@@ -1,5 +1,5 @@
cmake_minimum_required (VERSION 3.0)
project (nncmpp VERSION 1.1.1 LANGUAGES C)
project (nncmpp VERSION 1.2.0 LANGUAGES C)
# Moar warnings
if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)
@@ -90,8 +90,23 @@ configure_file (${PROJECT_SOURCE_DIR}/config.h.in
${PROJECT_BINARY_DIR}/config.h)
include_directories (${PROJECT_SOURCE_DIR} ${PROJECT_BINARY_DIR})
# Assuming a Unix-compatible system with a standalone preprocessor
set (actions_list ${PROJECT_SOURCE_DIR}/nncmpp.actions)
set (actions ${PROJECT_BINARY_DIR}/nncmpp-actions.h)
add_custom_command (OUTPUT ${actions}
COMMAND cpp -I${PROJECT_BINARY_DIR} -P ${actions_list}
| grep . | tr [[\n]] ^ | sed -ne [[h; s/,[^^]*/,/g]] -e [[s/$/COUNT/]]
-e [[s/[^^]*/\tACTION_&/g]] -e [[s/.*/enum action {\n&\n};\n/p]]
-e [[g; s/,[^^]*//g; y/_/-/]] -e [[s/[^^]\{1,\}/\t"&",/g]]
-e [[s/.*/static const char *g_action_names[] = {\n&};\n/p]]
-e [[g; s/[^^]*, *//g;]] -e [[s/[^^]\{1,\}/\t"&",/g]]
-e [[s/.*/static const char *g_action_descriptions[] = {\n&};/p]]
| tr ^ [[\n]] > ${actions}
COMMAND test -s ${actions}
DEPENDS ${actions_list} ${PROJECT_BINARY_DIR}/config.h VERBATIM)
# Build the main executable and link it
add_executable (${PROJECT_NAME} ${PROJECT_NAME}.c)
add_executable (${PROJECT_NAME} ${PROJECT_NAME}.c ${actions})
target_link_libraries (${PROJECT_NAME} ${Unistring_LIBRARIES}
${Ncursesw_LIBRARIES} termo-static ${curl_LIBRARIES} ${extra_libraries})
add_threads (${PROJECT_NAME})

11
NEWS
View File

@@ -1,3 +1,14 @@
1.2.0 (2021-12-21)
* Added ability to control the volume of MPD's current PulseAudio sink
* Now fetching Internet stream information asynchronously
* Added basic incremental search, normally bound to C-s, in all tabs
* Fixed jumping to the beginning of the queue after deleting items
1.1.1 (2021-11-04)
* Terminal focus in/out events no longer ring the terminall bell

View File

@@ -11,12 +11,14 @@ names, and should be pronounced as "nincompoop".
Features
--------
Most things are there. Enough for me to use it exclusively. Notably, it can
control PulseAudio volume directly to cover the use case of remote control,
and it has a fast spectrum visualiser.
Most stuff is there. Enough for me to use the program exclusively. Among other
things, it can display and change PulseAudio volume directly to cover the use
case of remote control, it has a fast spectrum visualiser, and both
the appearance and key bindings can be customized.
Note that since I only use the filesystem browsing mode, that's also the only
thing I care to implement for the time being. The search feature is awkward.
thing I care to implement for the time being. Similarly, the search feature is
known to be clumsy.
image::nncmpp.png[align="center"]

View File

@@ -1,12 +1,11 @@
#ifndef CONFIG_H
#define CONFIG_H
#define PROGRAM_NAME "${CMAKE_PROJECT_NAME}"
#define PROGRAM_NAME "${PROJECT_NAME}"
#define PROGRAM_VERSION "${PROJECT_VERSION}"
#cmakedefine HAVE_RESIZETERM
#cmakedefine WITH_FFTW
#cmakedefine WITH_PULSE
#endif // ! CONFIG_H
#endif /* ! CONFIG_H */

Submodule liberty updated: 782a9a5977...7e8e085c97

72
nncmpp.actions Normal file
View File

@@ -0,0 +1,72 @@
#include "config.h"
NONE, Do nothing
QUIT, Quit
REDRAW, Redraw screen
TAB_HELP, Switch to help tab
TAB_LAST, Switch to last tab
TAB_PREVIOUS, Switch to previous tab
TAB_NEXT, Switch to next tab
MPD_TOGGLE, Toggle play/pause
MPD_STOP, Stop playback
MPD_PREVIOUS, Previous song
MPD_NEXT, Next song
MPD_BACKWARD, Seek backwards
MPD_FORWARD, Seek forwards
MPD_VOLUME_UP, Increase MPD volume
MPD_VOLUME_DOWN, Decrease MPD volume
MPD_SEARCH, Global search
MPD_ADD, Add selection to playlist
MPD_REPLACE, Replace playlist
MPD_REPEAT, Toggle repeat
MPD_RANDOM, Toggle random playback
MPD_SINGLE, Toggle single song playback
MPD_CONSUME, Toggle consume
MPD_UPDATE_DB, Update MPD database
MPD_COMMAND, Send raw command to MPD
#ifdef WITH_PULSE
PULSE_VOLUME_UP, Increase PulseAudio volume
PULSE_VOLUME_DOWN, Decrease PulseAudio volume
PULSE_MUTE, Toggle PulseAudio sink mute
#endif
CHOOSE, Choose item
DELETE, Delete item
UP, Go up a level
MULTISELECT, Toggle multiselect
INCREMENTAL_SEARCH, Incremental search
SCROLL_UP, Scroll up
SCROLL_DOWN, Scroll down
MOVE_UP, Move selection up
MOVE_DOWN, Move selection down
GOTO_TOP, Go to top
GOTO_BOTTOM, Go to bottom
GOTO_ITEM_PREVIOUS, Go to previous item
GOTO_ITEM_NEXT, Go to next item
GOTO_PAGE_PREVIOUS, Go to previous page
GOTO_PAGE_NEXT, Go to next page
GOTO_VIEW_TOP, Select top item
GOTO_VIEW_CENTER, Select center item
GOTO_VIEW_BOTTOM, Select bottom item
EDITOR_CONFIRM, Confirm input
EDITOR_B_CHAR, Go back a character
EDITOR_F_CHAR, Go forward a character
EDITOR_B_WORD, Go back a word
EDITOR_F_WORD, Go forward a word
EDITOR_HOME, Go to start of line
EDITOR_END, Go to end of line
EDITOR_B_DELETE, Delete last character
EDITOR_F_DELETE, Delete next character
EDITOR_B_KILL_WORD, Delete last word
EDITOR_B_KILL_LINE, Delete everything up to BOL
EDITOR_F_KILL_LINE, Delete everything up to EOL

View File

@@ -45,28 +45,27 @@ snippet:
....
settings = {
address = "~/.mpd/mpd.socket"
password = "<your password>"
root = "~/Music"
pulseaudio = on
address = "~/.mpd/mpd.socket"
password = "<your password>"
pulseaudio = on
}
colors = {
normal = ""
highlight = "bold"
elapsed = "reverse"
remains = "ul"
tab_bar = "reverse"
tab_active = "ul"
even = ""
odd = ""
selection = "reverse"
multiselect = "-1 6"
defocused = "ul"
scrollbar = ""
normal = ""
highlight = "bold"
elapsed = "reverse"
remains = "ul"
tab_bar = "reverse"
tab_active = "ul"
even = ""
odd = ""
selection = "reverse"
multiselect = "-1 6"
defocused = "ul"
scrollbar = ""
}
streams = {
"dnbradio.com" = "http://www.dnbradio.com/hi.m3u"
"BassDrive.com" = "http://bassdrive.com/v2/streams/BassDrive.pls"
"dnbradio.com" = "http://www.dnbradio.com/hi.m3u"
"BassDrive.com" = "http://bassdrive.com/v2/streams/BassDrive.pls"
}
....
@@ -87,17 +86,39 @@ need to set it up manually to match your MPD configuration, e.g.:
....
settings = {
...
spectrum_path = "~/.mpd/mpd.fifo" # "path"
spectrum_format = "44100:16:2" # "format" (samplerate:bits:channels)
spectrum_bars = 8 # beware of exponential complexity
...
...
spectrum_path = "~/.mpd/mpd.fifo" # "path"
spectrum_format = "44100:16:2" # "format" (samplerate:bits:channels)
spectrum_bars = 8 # beware of exponential complexity
...
}
....
The sample rate should be greater than 40 kHz, the number of bits 8 or 16,
and the number of channels doesn't matter, as they're simply averaged together.
PulseAudio
----------
If you find standard MPD volume control useless, you may instead configure
*nncmpp* to show and control the volume of any PulseAudio sink MPD is currently
connected to.
This feature may be enabled with the *settings.pulseaudio* configuration option,
as in the snippet above. To replace the default volume control bindings, use:
....
normal = {
"M-PageUp" = "pulse-volume-up"
"M-PageDown" = "pulse-volume-down"
}
....
The respective actions may also be invoked from the help tab directly.
For this to work, *nncmpp* needs to access the right PulseAudio daemon--in case
your setup is unusual, consult the list of environment variables in
*pulseaudio*(1). MPD-compatibles are currently unsupported.
Files
-----
*nncmpp* follows the XDG Base Directory Specification.
@@ -112,4 +133,4 @@ or submit pull requests.
See also
--------
*mpd*(1)
*mpd*(1), *pulseaudio*(1)

363
nncmpp.c
View File

@@ -154,6 +154,8 @@ xbasename (const char *path)
return last_slash ? last_slash + 1 : path;
}
static char *xstrdup0 (const char *s) { return s ? xstrdup (s) : NULL; }
static char *
latin1_to_utf8 (const char *latin1)
{
@@ -280,6 +282,10 @@ struct poller_curl
struct poller_timer timer; ///< cURL timer
CURLM *multi; ///< cURL multi interface handle
struct poller_curl_fd *fds; ///< List of all FDs
// TODO: also make sure to dispose of them at the end of the program
int registered; ///< Number of attached easy handles
};
static void
@@ -388,6 +394,7 @@ poller_curl_init (struct poller_curl *self, struct poller *poller,
|| (mres = curl_multi_setopt (self->multi, CURLMOPT_TIMERDATA, self)))
{
curl_multi_cleanup (self->multi);
self->multi = NULL;
return error_set (e, "%s: %s",
"cURL setup failed", curl_multi_strerror (mres));
}
@@ -448,6 +455,7 @@ poller_curl_add (struct poller_curl *self, CURL *easy, struct error **e)
// "CURLMOPT_TIMERFUNCTION [...] will be called from within this function"
if ((mres = curl_multi_add_handle (self->multi, easy)))
return error_set (e, "%s", curl_multi_strerror (mres));
self->registered++;
return true;
}
@@ -457,6 +465,7 @@ poller_curl_remove (struct poller_curl *self, CURL *easy, struct error **e)
CURLMcode mres;
if ((mres = curl_multi_remove_handle (self->multi, easy)))
return error_set (e, "%s", curl_multi_strerror (mres));
self->registered--;
return true;
}
@@ -1182,6 +1191,7 @@ static struct app_context
// Event loop:
struct poller poller; ///< Poller
struct poller_curl poller_curl; ///< cURL abstractor
bool quitting; ///< Quit signal for the event loop
bool polling; ///< The event loop is running
@@ -1243,6 +1253,7 @@ static struct app_context
#ifdef WITH_PULSE
struct pulse pulse; ///< PulseAudio control
#endif // WITH_PULSE
bool pulse_control_requested; ///< PulseAudio control desired by user
struct line_editor editor; ///< Line editor
struct poller_idle refresh_event; ///< Refresh the screen
@@ -1304,6 +1315,13 @@ on_poll_elapsed_time_changed (struct config_item *item)
g.elapsed_poll = item->value.boolean;
}
static void
on_pulseaudio_changed (struct config_item *item)
{
// This is only set once, on application startup
g.pulse_control_requested = item->value.boolean;
}
static struct config_schema g_config_settings[] =
{
{ .name = "address",
@@ -1338,8 +1356,9 @@ static struct config_schema g_config_settings[] =
#ifdef WITH_PULSE
{ .name = "pulseaudio",
.comment = "Visualizer feed data format",
.comment = "Look up MPD in PulseAudio for improved volume controls",
.type = CONFIG_ITEM_BOOLEAN,
.on_change = on_pulseaudio_changed,
.default_ = "off" },
#endif // WITH_PULSE
@@ -1376,14 +1395,6 @@ get_config_string (struct config_item *root, const char *key)
return item->value.string.str;
}
static bool
get_config_boolean (struct config_item *root, const char *key)
{
struct config_item *item = config_item_get (root, key, NULL);
hard_assert (item && item->type == CONFIG_ITEM_BOOLEAN);
return item->value.boolean;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
@@ -1497,6 +1508,7 @@ static void
app_init_context (void)
{
poller_init (&g.poller);
hard_assert (poller_curl_init (&g.poller_curl, &g.poller, NULL));
g.client = mpd_client_make (&g.poller);
g.config = config_make ();
g.streams = strv_make ();
@@ -1585,6 +1597,7 @@ app_free_context (void)
line_editor_free (&g.editor);
config_free (&g.config);
poller_curl_free (&g.poller_curl);
poller_free (&g.poller);
free (g.message);
@@ -1782,18 +1795,31 @@ app_draw_status (void)
// It gets a bit complicated due to the only right-aligned item on the row
struct str volume = str_make ();
int remaining = COLS - buf.total_width;
if (g.volume >= 0)
{
str_append (&volume, " ");
#ifdef WITH_PULSE
if (pulse_volume_status (&g.pulse, &volume))
str_append (&volume, " @ ");
#endif // WITH_PULSE
str_append_printf (&volume, "%3d%%", g.volume);
remaining -= volume.len;
}
if (g.pulse_control_requested)
{
struct str buf = str_make ();
if (pulse_volume_status (&g.pulse, &buf))
{
if (g.volume >= 0 && g.volume != 100)
str_append_printf (&buf, " (%d%%)", g.volume);
}
else
{
if (g.volume >= 0)
str_append_printf (&buf, "(%d%%)", g.volume);
}
if (buf.len)
str_append_printf (&volume, " %s", buf.str);
str_free (&buf);
}
else
#endif // WITH_PULSE
if (g.volume >= 0)
str_append_printf (&volume, " %3d%%", g.volume);
int remaining = COLS - buf.total_width - volume.len;
if (!stopped && g.song_elapsed >= 0 && g.song_duration >= 1
&& remaining > 0)
{
@@ -2063,6 +2089,8 @@ app_write_mpd_status (struct row_buffer *buf)
row_buffer_append (buf, msg, APP_ATTR (HIGHLIGHT));
free (msg);
}
else if (g.poller_curl.registered)
row_buffer_append (buf, "Downloading...", APP_ATTR (NORMAL));
else if (str_map_find (map, "updating_db"))
row_buffer_append (buf, "Updating database...", APP_ATTR (NORMAL));
else
@@ -2235,117 +2263,14 @@ app_goto_tab (int tab_index)
// --- Actions -----------------------------------------------------------------
#ifdef WITH_PULSE
#define WITH_PULSE_01 1
#else
#define WITH_PULSE_01 0
#endif
// TODO: use the C preprocessor and a tool to generate this from nncmpp.actions
#define ACTIONS(XX) \
XX( 1, NONE, Do nothing ) \
\
XX( 1, QUIT, Quit ) \
XX( 1, REDRAW, Redraw screen ) \
XX( 1, TAB_HELP, Switch to help tab ) \
XX( 1, TAB_LAST, Switch to last tab ) \
XX( 1, TAB_PREVIOUS, Switch to previous tab ) \
XX( 1, TAB_NEXT, Switch to next tab ) \
\
XX( 1, MPD_TOGGLE, Toggle play/pause ) \
XX( 1, MPD_STOP, Stop playback ) \
XX( 1, MPD_PREVIOUS, Previous song ) \
XX( 1, MPD_NEXT, Next song ) \
XX( 1, MPD_BACKWARD, Seek backwards ) \
XX( 1, MPD_FORWARD, Seek forwards ) \
XX( 1, MPD_VOLUME_UP, Increase volume ) \
XX( 1, MPD_VOLUME_DOWN, Decrease volume ) \
\
XX( 1, MPD_SEARCH, Global search ) \
XX( 1, MPD_ADD, Add selection to playlist ) \
XX( 1, MPD_REPLACE, Replace playlist ) \
XX( 1, MPD_REPEAT, Toggle repeat ) \
XX( 1, MPD_RANDOM, Toggle random playback ) \
XX( 1, MPD_SINGLE, Toggle single song playback ) \
XX( 1, MPD_CONSUME, Toggle consume ) \
XX( 1, MPD_UPDATE_DB, Update MPD database ) \
XX( 1, MPD_COMMAND, Send raw command to MPD ) \
\
XX( WITH_PULSE_01, PULSE_VOLUME_UP, Increase PulseAudio volume ) \
XX( WITH_PULSE_01, PULSE_VOLUME_DOWN, Decrease PulseAudio volume ) \
XX( WITH_PULSE_01, PULSE_MUTE, Toggle mute of MPD PulseAudio sink ) \
\
XX( 1, CHOOSE, Choose item ) \
XX( 1, DELETE, Delete item ) \
XX( 1, UP, Go up a level ) \
XX( 1, MULTISELECT, Toggle multiselect ) \
\
XX( 1, SCROLL_UP, Scroll up ) \
XX( 1, SCROLL_DOWN, Scroll down ) \
XX( 1, MOVE_UP, Move selection up ) \
XX( 1, MOVE_DOWN, Move selection down ) \
\
XX( 1, GOTO_TOP, Go to top ) \
XX( 1, GOTO_BOTTOM, Go to bottom ) \
XX( 1, GOTO_ITEM_PREVIOUS, Go to previous item ) \
XX( 1, GOTO_ITEM_NEXT, Go to next item ) \
XX( 1, GOTO_PAGE_PREVIOUS, Go to previous page ) \
XX( 1, GOTO_PAGE_NEXT, Go to next page ) \
\
XX( 1, GOTO_VIEW_TOP, Select top item ) \
XX( 1, GOTO_VIEW_CENTER, Select center item ) \
XX( 1, GOTO_VIEW_BOTTOM, Select bottom item ) \
\
XX( 1, EDITOR_CONFIRM, Confirm input ) \
\
XX( 1, EDITOR_B_CHAR, Go back a character ) \
XX( 1, EDITOR_F_CHAR, Go forward a character ) \
XX( 1, EDITOR_B_WORD, Go back a word ) \
XX( 1, EDITOR_F_WORD, Go forward a word ) \
XX( 1, EDITOR_HOME, Go to start of line ) \
XX( 1, EDITOR_END, Go to end of line ) \
\
XX( 1, EDITOR_B_DELETE, Delete last character ) \
XX( 1, EDITOR_F_DELETE, Delete next character ) \
XX( 1, EDITOR_B_KILL_WORD, Delete last word ) \
XX( 1, EDITOR_B_KILL_LINE, Delete everything up to BOL ) \
XX( 1, EDITOR_F_KILL_LINE, Delete everything up to EOL )
enum action
{
#define XX(usable, name, description) ACTION_ ## name,
ACTIONS (XX)
#undef XX
ACTION_COUNT
};
static struct action_info
{
const char *name; ///< Name for user bindings
const char *description; ///< Human-readable description
bool usable; ///< Usable?
}
g_actions[] =
{
#define XX(usable, name, description) { #name, #description, usable },
ACTIONS (XX)
#undef XX
};
/// Accept a more human format of action-name instead of ACTION_NAME
static int action_toupper (int c) { return c == '-' ? '_' : toupper_ascii (c); }
#include "nncmpp-actions.h"
static int
action_resolve (const char *name)
{
const unsigned char *s = (const unsigned char *) name;
for (int i = 0; i < ACTION_COUNT; i++)
{
const char *target = g_actions[i].name;
for (size_t k = 0; action_toupper (s[k]) == target[k]; k++)
if (!s[k] && !target[k])
return i;
}
if (!strcasecmp_ascii (g_action_names[i], name))
return i;
return -1;
}
@@ -2406,7 +2331,7 @@ app_setvol (int value)
}
static void
app_on_editor_end (bool confirmed)
app_on_mpd_command_editor_end (bool confirmed)
{
struct mpd_client *c = &g.client;
if (!confirmed)
@@ -2421,6 +2346,54 @@ app_on_editor_end (bool confirmed)
mpd_client_idle (c, 0);
}
static size_t
incremental_search_match (const ucs4_t *needle, size_t len,
const struct row_buffer *row)
{
// XXX: this is slow and simplistic, but unistring is awkward to use
size_t best = 0;
for (size_t start = 0; start < row->chars_len; start++)
{
size_t i = 0;
for (; i < len && start + i < row->chars_len; i++)
if (uc_tolower(needle[i]) != uc_tolower(row->chars[start + i].c))
break;
best = MAX (best, i);
}
return best;
}
static void
incremental_search_on_changed (void)
{
struct tab *tab = g.active_tab;
if (!tab->item_count)
return;
size_t best = 0, current = 0, index = MAX (tab->item_selected, 0), i = 0;
while (i++ < tab->item_count)
{
struct row_buffer buf = row_buffer_make ();
tab->on_item_draw (index, &buf, COLS);
current = incremental_search_match (g.editor.line, g.editor.len, &buf);
row_buffer_free (&buf);
if (best < current)
{
best = current;
tab->item_selected = index;
app_move_selection (0);
}
index = (index + 1) % tab->item_count;
}
}
static void
incremental_search_on_end (bool confirmed)
{
(void) confirmed;
// Required callback, nothing to do here.
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static bool
@@ -2463,7 +2436,7 @@ app_process_action (enum action action)
return true;
case ACTION_MPD_COMMAND:
line_editor_start (&g.editor, ':');
g.editor.on_end = app_on_editor_end;
g.editor.on_end = app_on_mpd_command_editor_end;
app_invalidate ();
return true;
default:
@@ -2480,6 +2453,12 @@ app_process_action (enum action action)
else
tab->item_mark = tab->item_selected;
return true;
case ACTION_INCREMENTAL_SEARCH:
line_editor_start (&g.editor, '/');
g.editor.on_changed = incremental_search_on_changed;
g.editor.on_end = incremental_search_on_end;
app_invalidate ();
return true;
case ACTION_TAB_LAST:
if (!g.last_tab)
@@ -2770,6 +2749,7 @@ g_normal_defaults[] =
{ "M-Up", ACTION_UP },
{ "Backspace", ACTION_UP },
{ "v", ACTION_MULTISELECT },
{ "C-s", ACTION_INCREMENTAL_SEARCH },
{ "/", ACTION_MPD_SEARCH },
{ "a", ACTION_MPD_ADD },
{ "r", ACTION_MPD_REPLACE },
@@ -3549,8 +3529,8 @@ struct stream_tab_task
{
struct poller_curl_task curl; ///< Superclass
struct str data; ///< Downloaded data
bool polling; ///< Still downloading
bool replace; ///< Should playlist be replaced?
struct curl_slist *alias_ok;
};
static bool
@@ -3634,24 +3614,39 @@ streams_tab_extract_links (struct str *data, const char *content_type,
return true;
}
static void
streams_tab_task_finalize (struct stream_tab_task *self)
{
curl_easy_cleanup (self->curl.easy);
curl_slist_free_all (self->alias_ok);
str_free (&self->data);
free (self);
}
static void
streams_tab_task_dispose (struct stream_tab_task *self)
{
hard_assert (poller_curl_remove (&g.poller_curl, self->curl.easy, NULL));
streams_tab_task_finalize (self);
}
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)
{
cstr_uncapitalize (self->curl.curl_error);
print_error ("%s", self->curl.curl_error);
return;
goto dispose;
}
struct mpd_client *c = &g.client;
if (c->state != MPD_CONNECTED)
return;
goto dispose;
CURL *easy = msg->easy_handle;
CURLcode res;
@@ -3664,13 +3659,13 @@ streams_tab_on_downloaded (CURLMsg *msg, struct poller_curl_task *task)
{
print_error ("%s: %s",
"cURL info retrieval failed", curl_easy_strerror (res));
return;
goto dispose;
}
// 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);
return;
goto dispose;
}
mpd_client_list_begin (c);
@@ -3689,6 +3684,9 @@ streams_tab_on_downloaded (CURLMsg *msg, struct poller_curl_task *task)
mpd_client_list_end (c);
mpd_client_add_task (c, mpd_on_simple_response, NULL);
mpd_client_idle (c, 0);
dispose:
streams_tab_task_dispose (self);
}
static size_t
@@ -3707,60 +3705,44 @@ write_callback (char *ptr, size_t size, size_t nmemb, void *user_data)
static bool
streams_tab_process (const char *uri, bool replace, struct error **e)
{
struct poller poller;
poller_init (&poller);
// TODO: streams_tab_task_dispose() on that running task
if (g.poller_curl.registered)
{
print_error ("waiting for the last stream to time out");
return false;
}
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));
struct stream_tab_task *task = xcalloc (1, sizeof *task);
hard_assert (poller_curl_spawn (&task->curl, NULL));
CURL *easy = task.curl.easy;
task.data = str_make ();
task.replace = replace;
bool result = false;
struct curl_slist *ok_headers = curl_slist_append (NULL, "ICY 200 OK");
CURL *easy = task->curl.easy;
task->data = str_make ();
task->replace = replace;
task->alias_ok = curl_slist_append (NULL, "ICY 200 OK");
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))
|| (res = curl_easy_setopt (easy, CURLOPT_TIMEOUT, 10L))
// 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_HTTP200ALIASES, ok_headers))
|| (res = curl_easy_setopt (easy, CURLOPT_HTTP200ALIASES, task->alias_ok))
|| (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_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;
streams_tab_task_finalize (task);
return false;
}
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:
curl_easy_cleanup (task.curl.easy);
curl_slist_free_all (ok_headers);
str_free (&task.data);
poller_curl_free (&pc);
poller_free (&poller);
return result;
task->curl.on_done = streams_tab_on_downloaded;
hard_assert (poller_curl_add (&g.poller_curl, task->curl.easy, NULL));
return true;
}
static bool
@@ -3943,9 +3925,6 @@ help_tab_group (struct binding *keys, size_t len, struct strv *out,
{
for (enum action i = 0; i < ACTION_COUNT; i++)
{
if (!g_actions[i].usable)
continue;
struct strv ass = strv_make ();
for (size_t k = 0; k < len; k++)
if (keys[k].action == i)
@@ -3954,7 +3933,7 @@ help_tab_group (struct binding *keys, size_t len, struct strv *out,
{
char *joined = strv_join (&ass, ", ");
strv_append_owned (out, xstrdup_printf
(" %-30s %s", g_actions[i].description, joined));
(" %-30s %s", g_action_descriptions[i], joined));
free (joined);
bound[i] = true;
@@ -3971,7 +3950,7 @@ help_tab_unbound (struct strv *out, bool bound[ACTION_COUNT])
if (!bound[i])
{
strv_append_owned (out,
xstrdup_printf (" %-30s", g_actions[i].description));
xstrdup_printf (" %-30s", g_action_descriptions[i]));
help_tab_assign_action (i);
}
}
@@ -4279,7 +4258,7 @@ static void
pulse_update (void)
{
struct mpd_client *c = &g.client;
if (!get_config_boolean (g.config.root, "settings.pulseaudio"))
if (!g.pulse_control_requested)
return;
// The read permission is sufficient for this command
@@ -4456,7 +4435,7 @@ mpd_find_pos_of_id (const char *desired_id)
return -1;
}
static char *
static const char *
mpd_id_of_pos (int pos)
{
compact_map_t map = item_list_get (&g.playlist, pos);
@@ -4466,29 +4445,39 @@ mpd_id_of_pos (int pos)
static void
mpd_process_info (const struct strv *data)
{
int *selected = &g_current_tab.item_selected;
int *marked = &g_current_tab.item_mark;
char *prev_sel_id = mpd_id_of_pos (*selected);
char *prev_mark_id = mpd_id_of_pos (*marked);
if (prev_sel_id) prev_sel_id = xstrdup (prev_sel_id);
if (prev_mark_id) prev_mark_id = xstrdup (prev_mark_id);
struct tab *tab = &g_current_tab;
char *prev_sel_id = xstrdup0 (mpd_id_of_pos (tab->item_selected));
char *prev_mark_id = xstrdup0 (mpd_id_of_pos (tab->item_mark));
char *fallback_id = NULL;
struct tab_range r = tab_selection_range (g.active_tab);
if (r.upto >= 0)
{
if (!(fallback_id = xstrdup0 (mpd_id_of_pos (r.upto + 1))))
fallback_id = xstrdup0 (mpd_id_of_pos (r.from - 1));
}
mpd_process_info_data (data);
const char *sel_id = mpd_id_of_pos (*selected);
const char *mark_id = mpd_id_of_pos (*marked);
const char *sel_id = mpd_id_of_pos (tab->item_selected);
const char *mark_id = mpd_id_of_pos (tab->item_mark);
if (prev_mark_id && (!mark_id || strcmp (prev_mark_id, mark_id)))
*marked = mpd_find_pos_of_id (prev_mark_id);
tab->item_mark = mpd_find_pos_of_id (prev_mark_id);
if (prev_sel_id && (!sel_id || strcmp (prev_sel_id, sel_id)))
{
if ((*selected = mpd_find_pos_of_id (prev_sel_id)) < 0)
*marked = -1;
if ((tab->item_selected = mpd_find_pos_of_id (prev_sel_id)) < 0)
{
tab->item_mark = -1;
if (fallback_id)
tab->item_selected = mpd_find_pos_of_id (fallback_id);
}
app_move_selection (0);
}
free (prev_sel_id);
free (prev_mark_id);
free (fallback_id);
}
static void