[rhythmbox] rhythmbox-client: add interactive mode
- From: Jonathan Matthew <jmatthew src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [rhythmbox] rhythmbox-client: add interactive mode
- Date: Tue, 7 Oct 2014 12:25:19 +0000 (UTC)
commit 00812189d6e9ee82859794ffa451cc7f0fb4a46c
Author: Jonathan Matthew <jonathan d14n org>
Date: Thu Sep 25 09:20:07 2014 +1000
rhythmbox-client: add interactive mode
This mode provides basic playback control and prints details of track changes.
data/rhythmbox-client.1 | 5 +-
remote/dbus/rb-client.c | 464 +++++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 448 insertions(+), 21 deletions(-)
---
diff --git a/data/rhythmbox-client.1 b/data/rhythmbox-client.1
index 866ea11..ab69f9e 100644
--- a/data/rhythmbox-client.1
+++ b/data/rhythmbox-client.1
@@ -24,7 +24,8 @@ rhythmbox-client \- controls a running instance of rhythmbox
is a tool for controlling an already running instance of
.B rhythmbox.
It's useful for remote control scripting, adding specific files to the library
-from the shell, or printing details of what's playing. Any files specified after
+from the shell, or printing details of what's playing. It also has an interactive
+mode, useful for controlling playback remotely via ssh. Any files specified after
the option arguments will be added to the library. If the
.B \-\-enqueue
option is given, the files will also be added to the play queue.
@@ -42,6 +43,8 @@ Do not start a new instance of rhythmbox
.B \-\-quit
Quit rhythmbox
.TP
+.B \-i,\-\-interactive
+Start interactive mode
.B \-\-no-present
Don't present an existing rhythmbox window
.TP
diff --git a/remote/dbus/rb-client.c b/remote/dbus/rb-client.c
index 69287af..70e682b 100644
--- a/remote/dbus/rb-client.c
+++ b/remote/dbus/rb-client.c
@@ -30,10 +30,14 @@
#include <locale.h>
#include <stdlib.h>
+#include <termios.h>
+#include <math.h>
+
#include <glib.h>
#include <glib/gi18n.h>
#include <glib-object.h>
#include <gio/gio.h>
+#include <gio/gunixinputstream.h>
#include "rb-debug.h"
@@ -78,6 +82,8 @@ static gboolean print_volume = FALSE;
static gboolean unmute = FALSE; */
static gdouble set_rating = -1.0;
+static gboolean interactive = FALSE;
+
static gchar **other_stuff = NULL;
static GMainLoop *mainloop = NULL;
@@ -122,12 +128,52 @@ static GOptionEntry args[] = {
/* { "mute", 0, 0, G_OPTION_ARG_NONE, &mute, N_("Mute playback"), NULL },
{ "unmute", 0, 0, G_OPTION_ARG_NONE, &unmute, N_("Unmute playback"), NULL }, */
{ "set-rating", 0, 0, G_OPTION_ARG_DOUBLE, &set_rating, N_("Set the rating of the current song"),
NULL },
+ { "interactive", 'i', 0, G_OPTION_ARG_NONE, &interactive, N_("Start interactive mode"), NULL },
{ G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &other_stuff, NULL, NULL },
{ NULL }
};
+typedef struct {
+ GApplication *app;
+ GDBusProxy *mpris_player;
+ GDBusProxy *mpris_playlists;
+
+ char *playing_track;
+ gdouble volume;
+ gint64 position;
+} InteractData;
+
+static void interact_quit (InteractData *data, int ch);
+static void interact_next (InteractData *data, int ch);
+static void interact_prev (InteractData *data, int ch);
+static void interact_playpause (InteractData *data, int ch);
+static void interact_print_state (InteractData *data, int ch);
+static void interact_volume (InteractData *data, int ch);
+static void interact_help (InteractData *data, int ch);
+
+struct {
+ int ch;
+ void (*handler)(InteractData *, int);
+ const char *help;
+} interact_keys[] = {
+ { 'n', interact_next, N_("n - Next track") },
+ { 'p', interact_prev, N_("p - Previous track") },
+ { ' ', interact_playpause, N_("space - Play/pause") },
+ { 's', interact_print_state, N_("s - Show playing track details") },
+ { 'v', interact_volume, N_("v - Decrease volume") },
+ { 'V', interact_volume, N_("V - Increase volume") },
+ { '?', interact_help, NULL },
+ { 'h', interact_help, N_("h/? - Help") },
+ { 'q', interact_quit, N_("q - Quit") },
+ { 3, interact_quit, NULL }, /* ctrl-c */
+ { 4, interact_quit, NULL }, /* ctrl-d */
+ /* seeking? */
+ /* quit rhythmbox? */
+};
+
+
static gboolean
annoy (GError **error)
{
@@ -142,7 +188,7 @@ annoy (GError **error)
static char *
-rb_make_duration_string (gint64 duration)
+rb_make_duration_string (gint64 duration, gboolean show_zero)
{
char *str;
int hours, minutes, seconds;
@@ -152,7 +198,7 @@ rb_make_duration_string (gint64 duration)
minutes = (duration - (hours * 60 * 60)) / 60;
seconds = duration % 60;
- if (hours == 0 && minutes == 0 && seconds == 0)
+ if (hours == 0 && minutes == 0 && seconds == 0 && show_zero)
str = g_strdup (_("Unknown"));
else if (hours == 0)
str = g_strdup_printf (_("%d:%02d"), minutes, seconds);
@@ -190,7 +236,7 @@ rb_make_duration_string (gint64 duration)
* %st -- stream title
*/
static char *
-parse_pattern (const char *pattern, GHashTable *properties, gint64 elapsed)
+parse_pattern (const char *pattern, GHashTable *properties, gint64 elapsed, gboolean bold)
{
/* p is the pattern iterator, i is a general purpose iterator */
const char *p;
@@ -369,11 +415,11 @@ parse_pattern (const char *pattern, GHashTable *properties, gint64 elapsed)
/* Track duration */
value = g_hash_table_lookup (properties, "mpris:length");
if (value)
- string = rb_make_duration_string (g_variant_get_int64 (value));
+ string = rb_make_duration_string (g_variant_get_int64 (value), TRUE);
break;
case 'e':
/* Track elapsed time */
- string = rb_make_duration_string (elapsed);
+ string = rb_make_duration_string (elapsed, FALSE);
break;
case 'b':
/* Track bitrate */
@@ -411,8 +457,13 @@ parse_pattern (const char *pattern, GHashTable *properties, gint64 elapsed)
string = g_strdup_printf ("%%%c", *p);
}
- if (string)
+ if (string) {
+ if (bold)
+ g_string_append (s, "\033[1m");
g_string_append (s, string);
+ if (bold)
+ g_string_append (s, "\033[0m");
+ }
g_free (string);
++p;
@@ -423,6 +474,22 @@ parse_pattern (const char *pattern, GHashTable *properties, gint64 elapsed)
return temp;
}
+static GHashTable *
+metadata_to_properties (GVariant *metadata)
+{
+ GHashTable *properties;
+ GVariant *value;
+ GVariantIter iter;
+ char *key;
+
+ properties = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify)
g_variant_unref);
+ g_variant_iter_init (&iter, metadata);
+ while (g_variant_iter_loop (&iter, "{sv}", &key, &value)) {
+ g_hash_table_insert (properties, g_strdup (key), g_variant_ref (value));
+ }
+
+ return properties;
+}
static GHashTable *
get_playing_song_info (GDBusProxy *mpris)
@@ -430,9 +497,6 @@ get_playing_song_info (GDBusProxy *mpris)
GHashTable *properties;
GVariant *prop;
GVariant *metadata;
- GVariantIter iter;
- GVariant *value;
- char *key;
GError *error = NULL;
prop = g_dbus_proxy_call_sync (mpris,
@@ -447,13 +511,7 @@ get_playing_song_info (GDBusProxy *mpris)
}
g_variant_get (prop, "(v)", &metadata);
-
- properties = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify)
g_variant_unref);
- g_variant_iter_init (&iter, metadata);
- while (g_variant_iter_loop (&iter, "{sv}", &key, &value)) {
- g_hash_table_insert (properties, g_strdup (key), g_variant_ref (value));
- }
-
+ properties = metadata_to_properties (metadata);
g_variant_unref (prop);
return properties;
}
@@ -478,7 +536,7 @@ print_playing_song (GDBusProxy *mpris, const char *format)
g_variant_unref (v);
}
- string = parse_pattern (format, properties, elapsed);
+ string = parse_pattern (format, properties, elapsed, FALSE);
g_print ("%s\n", string);
g_hash_table_destroy (properties);
g_free (string);
@@ -489,6 +547,8 @@ print_playing_song_default (GDBusProxy *mpris)
{
GHashTable *properties;
char *string;
+ GVariant *v;
+ gint64 elapsed = 0;
properties = get_playing_song_info (mpris);
if (properties == NULL) {
@@ -496,10 +556,16 @@ print_playing_song_default (GDBusProxy *mpris)
return;
}
+ v = g_dbus_proxy_get_cached_property (mpris, "Position");
+ if (v != NULL) {
+ elapsed = g_variant_get_int64 (v);
+ g_variant_unref (v);
+ }
+
if (g_hash_table_lookup (properties, "rhythmbox:streamTitle") != NULL) {
- string = parse_pattern ("%st (%tt)", properties, 0);
+ string = parse_pattern ("%st (%tt)", properties, elapsed, FALSE);
} else {
- string = parse_pattern ("%ta - %tt", properties, 0);
+ string = parse_pattern ("%ta - %tt", properties, elapsed, FALSE);
}
g_print ("%s\n", string);
@@ -550,6 +616,344 @@ rate_song (GDBusProxy *mpris, gdouble song_rating)
g_hash_table_destroy (properties);
}
+static void
+interact_quit (InteractData *data, int ch)
+{
+ g_main_loop_quit (mainloop);
+}
+
+static void
+interact_next (InteractData *data, int ch)
+{
+ GError *error = NULL;
+ g_dbus_proxy_call_sync (data->mpris_player, "Next", NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);
+ annoy (&error);
+}
+
+static void
+interact_prev (InteractData *data, int ch)
+{
+ GError *error = NULL;
+ g_dbus_proxy_call_sync (data->mpris_player, "Previous", NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL,
&error);
+ annoy (&error);
+}
+
+static void
+interact_playpause (InteractData *data, int ch)
+{
+ GError *error = NULL;
+ g_dbus_proxy_call_sync (data->mpris_player, "PlayPause", NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL,
&error);
+ annoy (&error);
+}
+
+static void
+interact_help (InteractData *data, int ch)
+{
+ int i;
+
+ for (i = 0; i < G_N_ELEMENTS (interact_keys); i++) {
+ if (interact_keys[i].help != NULL) {
+ g_print ("%s\r\n", _(interact_keys[i].help));
+ }
+ }
+ g_print ("\r\n");
+}
+
+static void
+interact_volume (InteractData *data, int ch)
+{
+ gdouble set_volume;
+ GError *error = NULL;
+
+ if (data->volume < 0.0) {
+ GVariant *prop;
+ GVariant *val;
+ prop = g_dbus_proxy_call_sync (data->mpris_player,
+ "org.freedesktop.DBus.Properties.Get",
+ g_variant_new ("(ss)", "org.mpris.MediaPlayer2.Player",
"Volume"),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ NULL,
+ &error);
+ if (annoy (&error)) {
+ return;
+ }
+
+ g_variant_get (prop, "(v)", &val);
+ data->volume = g_variant_get_double (val);
+ g_variant_unref (prop);
+ }
+
+ set_volume = data->volume + (ch == 'V' ? 0.1 : -0.1);
+ g_dbus_proxy_call_sync (data->mpris_player,
+ "org.freedesktop.DBus.Properties.Set",
+ g_variant_new ("(ssv)", "org.mpris.MediaPlayer2.Player", "Volume",
g_variant_new_double (set_volume)),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ NULL,
+ &error);
+ annoy (&error);
+}
+
+static gboolean
+has_useful_value (GHashTable *properties, const char *property, gboolean multi)
+{
+ GVariant *var;
+ const char *v;
+
+ var = g_hash_table_lookup (properties, property);
+ if (var == NULL)
+ return FALSE;
+
+ if (multi) {
+ const char **strv = g_variant_get_strv (var, NULL);
+ v = strv[0];
+ } else {
+ v = g_variant_get_string (var, NULL);
+ }
+
+ return (v != NULL && (g_str_equal (v, _("Unknown")) == FALSE));
+}
+
+static char *
+now_playing_string (GHashTable *properties)
+{
+ gboolean has_artist = has_useful_value (properties, "xesam:artist", TRUE);
+ gboolean has_album = has_useful_value (properties, "xesam:album", FALSE);
+
+ if (g_hash_table_lookup (properties, "rhythmbox:streamTitle") != NULL) {
+ return parse_pattern ("%st (%tt)", properties, 0, TRUE);
+ } else if (has_artist && has_album) {
+ /* Translators: title by artist from album */
+ return parse_pattern (_("%tt by %ta from %at"), properties, 0, TRUE);
+ } else if (has_artist) {
+ /* Translators: title by artist */
+ return parse_pattern (_("%tt by %ta"), properties, 0, TRUE);
+ } else if (has_album) {
+ /* Translators: title from album */
+ return parse_pattern (_("%tt from %ta"), properties, 0, TRUE);
+ } else {
+ return parse_pattern ("%tt", properties, 0, TRUE);
+ }
+}
+
+static char *
+playing_time_string (GHashTable *properties, gint64 elapsed)
+{
+ if (g_hash_table_lookup (properties, "mpris:length") != NULL) {
+ /* Translators: %te is replaced with elapsed time, %td is replaced with track duration */
+ return parse_pattern (_("[%te of %td]"), properties, elapsed, FALSE);
+ } else {
+ return parse_pattern ("[%te]", properties, elapsed, FALSE);
+ }
+}
+
+static void
+interact_print_state (InteractData *data, int ch)
+{
+ GHashTable *properties;
+ GVariant *prop;
+ GVariant *val;
+ char *string;
+ char *timestr;
+ const char *s;
+ GError *error = NULL;
+ gboolean playing;
+ gint64 elapsed;
+
+ prop = g_dbus_proxy_call_sync (data->mpris_player,
+ "org.freedesktop.DBus.Properties.Get",
+ g_variant_new ("(ss)", "org.mpris.MediaPlayer2.Player",
"PlaybackStatus"),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ NULL,
+ &error);
+ if (annoy (&error)) {
+ return;
+ }
+
+ g_variant_get (prop, "(v)", &val);
+ s = g_variant_get_string (val, NULL);
+ if (g_str_equal(s, "Stopped")) {
+ g_print ("%s\r\n", _("Not playing"));
+ return;
+ }
+ playing = g_str_equal (s, "Playing");
+ g_variant_unref (prop);
+
+ prop = g_dbus_proxy_call_sync (data->mpris_player,
+ "org.freedesktop.DBus.Properties.Get",
+ g_variant_new ("(ss)", "org.mpris.MediaPlayer2.Player", "Position"),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ NULL,
+ &error);
+ if (annoy (&error)) {
+ return;
+ }
+ g_variant_get (prop, "(v)", &val);
+ elapsed = g_variant_get_int64 (val);
+
+ properties = get_playing_song_info (data->mpris_player);
+ string = now_playing_string (properties);
+ timestr = playing_time_string (properties, elapsed);
+
+ g_print ("%s: %s %s\r\n", playing ? _("Playing") : _("Paused"), string, timestr);
+ g_hash_table_destroy (properties);
+ g_free (string);
+ g_free (timestr);
+}
+
+
+static gboolean
+interact_input (GObject *stream, gpointer user_data)
+{
+ GInputStream *in = G_INPUT_STREAM (stream);
+ InteractData *data = user_data;
+ GError *error = NULL;
+ char b[2];
+ int k;
+
+ switch (g_pollable_input_stream_read_nonblocking (G_POLLABLE_INPUT_STREAM (in), b, 1, NULL, &error)) {
+ case -1:
+ annoy(&error);
+ g_main_loop_quit (mainloop);
+ break;
+ case 0:
+ break;
+ default:
+ for (k = 0; k < G_N_ELEMENTS (interact_keys); k++) {
+ if (interact_keys[k].ch == b[0]) {
+ interact_keys[k].handler(data, b[0]);
+ break;
+ }
+ }
+ break;
+ }
+ return TRUE;
+}
+
+static void
+interact_mpris_player_signal (GDBusProxy *proxy, char *sender, char *signal_name, GVariant *parameters,
InteractData *data)
+{
+ if (g_str_equal (signal_name, "Seeked")) {
+ gint64 pos;
+ char *str;
+ g_variant_get (parameters, "(x)", &pos);
+ if (abs(pos - data->position) >= G_USEC_PER_SEC) {
+ str = rb_make_duration_string (pos, FALSE);
+ g_print (_("Seeked to %s"), str);
+ g_print ("\r\n");
+ g_free (str);
+ }
+ data->position = pos;
+ }
+}
+
+static void
+interact_mpris_player_properties (GDBusProxy *proxy, GVariant *changed, GStrv invalidated, InteractData
*data)
+{
+ GVariant *metadata;
+ char *status = NULL;
+ gdouble volume;
+
+ if (g_variant_lookup (changed, "PlaybackStatus", "s", &status)) {
+ if (g_str_equal (status, "Stopped")) {
+ g_print (_("Not playing"));
+ g_print ("\r\n");
+ return;
+ }
+ }
+
+ metadata = g_variant_lookup_value (changed, "Metadata", NULL);
+ if (metadata) {
+ GHashTable *properties;
+ char *string;
+
+ properties = metadata_to_properties (metadata);
+ string = now_playing_string (properties);
+
+ if (data->playing_track == NULL || g_str_equal (string, data->playing_track) == FALSE) {
+ char *timestr;
+ timestr = playing_time_string (properties, 0);
+ g_print (_("Now playing: %s %s"), string, timestr);
+ g_print ("\r\n");
+ g_free (timestr);
+ g_free (data->playing_track);
+ data->playing_track = string;
+ data->position = 0;
+ } else {
+ g_free (string);
+ }
+ } else if (status != NULL) {
+ /* include elapsed/duration here? */
+ if (g_str_equal (status, "Playing")) {
+ g_print (_("Playing"));
+ } else if (g_str_equal (status, "Paused")) {
+ g_print (_("Paused"));
+ } else {
+ g_print (_("Unknown playback state: %s"), status);
+ }
+ g_print ("\r\n");
+ g_free (status);
+ }
+
+ if (g_variant_lookup (changed, "Volume", "d", &volume)) {
+ if (data->volume < -0.1) {
+ data->volume = volume;
+ } else if (fabs(volume - data->volume) > 0.001) {
+ g_print (_("Volume is now %.02f"), volume);
+ g_print ("\r\n");
+ data->volume = volume;
+ }
+ }
+}
+
+static gboolean
+interact_print_state_idle (InteractData *data)
+{
+ interact_print_state (data, 's');
+ return FALSE;
+}
+
+static void
+interact (InteractData *data)
+{
+ GInputStream *in;
+ GSource *insrc;
+ struct termios orig_tt, tt;
+
+ if (mainloop == NULL)
+ mainloop = g_main_loop_new (NULL, FALSE);
+
+ tcgetattr(0, &orig_tt);
+ tt = orig_tt;
+ cfmakeraw(&tt);
+ tt.c_lflag &= ~ECHO;
+ tcsetattr(0, TCSAFLUSH, &tt);
+
+ g_signal_connect (data->mpris_player, "g-signal", G_CALLBACK (interact_mpris_player_signal), data);
+ g_signal_connect (data->mpris_player, "g-properties-changed", G_CALLBACK
(interact_mpris_player_properties), data);
+
+ in = g_unix_input_stream_new (0, FALSE);
+ insrc = g_pollable_input_stream_create_source (G_POLLABLE_INPUT_STREAM (in), NULL);
+ g_source_set_callback (insrc, (GSourceFunc) interact_input, data, NULL);
+
+ /* should print this before dbus setup, really */
+ g_print (_("Press 'h' for help."));
+ g_print ("\r\n\r\n");
+ g_source_attach (insrc, g_main_loop_get_context (mainloop));
+
+ g_idle_add ((GSourceFunc) interact_print_state_idle, data);
+
+ g_main_loop_run (mainloop);
+
+ g_signal_handlers_disconnect_by_func (data->mpris_player, G_CALLBACK (interact_mpris_player_signal),
data);
+ g_signal_handlers_disconnect_by_func (data->mpris_player, G_CALLBACK
(interact_mpris_player_properties), data);
+
+ tcsetattr(0, TCSAFLUSH, &orig_tt);
+}
+
static void
check_loaded_state (GVariant *state)
@@ -701,7 +1105,8 @@ main (int argc, char **argv)
play || do_pause || play_pause ||
stop || volume_up || volume_down ||
repeat || no_repeat || shuffle ||
- no_shuffle || (set_volume > -0.01)) {
+ no_shuffle || (set_volume > -0.01) ||
+ interactive) {
exit (1);
}
}
@@ -721,6 +1126,25 @@ main (int argc, char **argv)
}
}
+ /* interactive mode takes precedence over anything else */
+ if (interactive) {
+ InteractData data;
+ rb_debug ("entering interactive mode");
+ if (!isatty(1)) {
+ g_warning ("interactive mode only works on ttys");
+ exit (1);
+ }
+ data.app = app;
+ data.mpris_player = mpris;
+ data.mpris_playlists = NULL;
+ data.playing_track = NULL;
+ data.volume = -1.0;
+ /* more things? */
+
+ interact (&data);
+ exit (0);
+ }
+
/* activate or quit */
if (quit) {
rb_debug ("quitting existing instance");
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]