[rhythmbox] rhythmbox-client: add interactive mode



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]