├── .gitignore ├── groover.gresources.xml ├── groover.desktop ├── .ycm_extra_conf.py ├── menus.xml ├── README.md ├── Makefile └── groover.c /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | groover 3 | *.py[co] 4 | .*.sw[op] 5 | -------------------------------------------------------------------------------- /groover.gresources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | menus.xml 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /groover.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Hidden=false 3 | Name=Groover 4 | Exec=groover 5 | TryExec=groover 6 | Icon=gnome-music 7 | Comment=Control the Groove Basin music player daemon 8 | Type=Application 9 | NoDisplay=false 10 | StartupNotify=true 11 | StartupWMClass=Groover 12 | Categories=GNOME;GTK;Multimedia; 13 | Terminal=false 14 | -------------------------------------------------------------------------------- /.ycm_extra_conf.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # vim:fenc=utf-8 4 | # 5 | # Copyright © 2014 Adrian Perez 6 | # 7 | # Distributed under terms of the MIT license. 8 | 9 | from subprocess import check_output 10 | from shlex import split as sh_split 11 | 12 | def FlagsForFile(path, **kwarg): 13 | flags = sh_split(check_output(["make", "print-flags"])) 14 | flags.append("-Qunused-arguments") 15 | return { 'flags': flags, 'do_cache': True } 16 | 17 | -------------------------------------------------------------------------------- /menus.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | _Play / Pause 7 | app.toggle-play 8 | 9 |
10 |
11 | 12 | _About 13 | app.about 14 | 15 | 16 | _Quit 17 | app.quit 18 | 19 |
20 |
21 |
22 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Groover 2 | 3 | Groover is a simple remote for the [Groove Basin](http://groovebasin.com) 4 | music player daemon. It uses GTK+ and WebKitGTK+ to provide a window where 5 | Groove Basin's standard web interface is loaded — honestly I do not see the 6 | whole point of re-creating the UI from scratch, so the idea is to keep 7 | Groover simple, and focus on desktop integration instead. 8 | 9 | 10 | ## Features 11 | 12 | - Shows the Groove Basin UI as a standalone application (and its own icon, 13 | and so on), without the need to launch a complete web browser to access 14 | it. This is not very different from the “web application mode” of Web; 15 | but does not require it to be installed. 16 | 17 | 18 | ## (Planned) Features 19 | 20 | - Simple “Now Playing” screen mode which uses only GTK+ widgets, without 21 | requiring WebKitGTK+ to load Groove Basin's web UI. 22 | - Support acting as a command-line remote for Groove Basin. 23 | - MPRIS2 provider, so other applications can control Groove Basin via a 24 | compatible client. 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | PREFIX ?= /usr/local 3 | CFLAGS += -Wall -std=gnu99 4 | CPPFLAGS += -DG_DISABLE_DEPRECATED \ 5 | -DPREFIX=\"$(PREFIX)\" 6 | 7 | # Usage: 8 | # $(eval $(call PKG, name, module1 [module2 [... moduleN]])) 9 | # 10 | # Creates variables: 11 | # PKG_name_MODULES 12 | # PKG_name_CFLAGS 13 | # PKG_name_LDLIBS 14 | define PKG 15 | PKG_$1_MODULES := $2 16 | PKG_$1_CFLAGS := $$(shell pkg-config $$(PKG_$1_MODULES) --cflags) 17 | PKG_$1_LDLIBS := $$(shell pkg-config $$(PKG_$1_MODULES) --libs) 18 | endef 19 | 20 | $(eval $(call PKG,WEBKIT,webkit2gtk-3.0 gtk+-3.0)) 21 | $(eval $(call PKG,WEBKIT_EXTENSION,webkit2gtk-web-extension-3.0)) 22 | 23 | all: groover groover.desktop 24 | 25 | groover: CFLAGS += $(PKG_WEBKIT_CFLAGS) 26 | groover: LDFLAGS += $(PKG_WEBKIT_LDLIBS) 27 | groover: groover.o groover.gresources.o 28 | 29 | #%: %.o 30 | # $(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@ 31 | 32 | groover.gresources.o: menus.xml 33 | 34 | %.gresources.c: %.gresources.xml 35 | glib-compile-resources --generate-source --target=$@ $< 36 | 37 | %.gresources.h: %.gresources.xml 38 | glib-compile-resources --generate-header --target=$@ $< 39 | 40 | clean: 41 | $(RM) groover groover.o 42 | 43 | install: all 44 | install -m 755 -d $(DESTDIR)$(PREFIX)/bin 45 | install -m 755 -t $(DESTDIR)$(PREFIX)/bin groover 46 | install -m 755 -d $(DESTDIR)$(PREFIX)/share/applications 47 | install -m 644 -t $(DESTDIR)$(PREFIX)/share/applications groover.desktop 48 | 49 | ifeq ($(origin TAG),command line) 50 | VERSION := $(TAG) 51 | else 52 | VERSION := $(shell git tag 2> /dev/null | tail -1) 53 | endif 54 | 55 | dist: 56 | ifeq ($(strip $(VERSION)),) 57 | @echo "ERROR: Either Git is not installed, or no tags were found" 58 | else 59 | git archive --prefix=groover-$(VERSION)/ $(VERSION) | xz -c > groover-$(VERSION).tar.xz 60 | endif 61 | 62 | print-flags: 63 | @echo "$(PKG_WEBKIT_CFLAGS) $(PKG_WEBKIT_EXTENSION) $(CPPFLAGS)" 64 | 65 | .PHONY: clean install dist print-flags 66 | 67 | -------------------------------------------------------------------------------- /groover.c: -------------------------------------------------------------------------------- 1 | /* 2 | * groover.c 3 | * Copyright (C) 2014 Adrian Perez 4 | * 5 | * Distributed under terms of the MIT license. 6 | */ 7 | 8 | #include 9 | #include 10 | 11 | #ifndef WEB_EXTENSIONS_DIRECTORY 12 | # ifndef PREFIX 13 | # error Either WEB_EXTENSIONS_DIRECTORY or PREFIX must be defined 14 | # endif /* !PREFIX */ 15 | # define WEB_EXTENSIONS_DIRECTORY (PREFIX "/lib/groover") 16 | #endif /* !WEB_EXTENSIONS_DIRECTORY */ 17 | 18 | #define GROOVER_GRESOURCE(name) ("/org/perezdecastro/groover/" name) 19 | 20 | 21 | static void 22 | add_transport_buttons (GtkContainer *container) 23 | { 24 | const struct { 25 | const gchar *icon; 26 | const gchar *action; 27 | } button_map[] = { 28 | { "media-skip-backward-symbolic", "app.previous-song" }, 29 | { "media-playback-start-symbolic", "app.toggle-play" }, 30 | { "media-skip-forward-symbolic", "app.next-song" }, 31 | }; 32 | 33 | for (guint i = 0; i < G_N_ELEMENTS (button_map); i++) { 34 | GtkWidget *button = gtk_button_new_from_icon_name (button_map[i].icon, 35 | GTK_ICON_SIZE_BUTTON); 36 | gtk_button_set_focus_on_click (GTK_BUTTON (button), FALSE); 37 | gtk_style_context_add_class (gtk_widget_get_style_context (button), "image-button"); 38 | gtk_style_context_add_class (gtk_widget_get_style_context (button), "linked"); 39 | gtk_actionable_set_action_name (GTK_ACTIONABLE (button), 40 | button_map[i].action); 41 | gtk_container_add (container, button); 42 | } 43 | 44 | gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (container)), 45 | "raised"); 46 | gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (container)), 47 | "linked"); 48 | } 49 | 50 | 51 | static void 52 | application_window_setup_header_bar (GtkWindow *window) 53 | { 54 | GtkWidget *header = gtk_header_bar_new (); 55 | gtk_header_bar_set_show_close_button (GTK_HEADER_BAR (header), TRUE); 56 | gtk_header_bar_set_has_subtitle (GTK_HEADER_BAR (header), TRUE); 57 | gtk_header_bar_set_title (GTK_HEADER_BAR (header), "Groover"); 58 | 59 | gtk_window_set_titlebar (window, header); 60 | 61 | GtkWidget *hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); 62 | add_transport_buttons (GTK_CONTAINER (hbox)); 63 | gtk_header_bar_pack_start (GTK_HEADER_BAR (header), hbox); 64 | } 65 | 66 | 67 | static GtkWidget * 68 | application_window_new (GtkApplication *application) 69 | { 70 | GtkWidget *window = gtk_application_window_new (application); 71 | gtk_window_set_title (GTK_WINDOW (window), "Groover"); 72 | gtk_window_set_has_resize_grip (GTK_WINDOW (window), TRUE); 73 | // TODO: Save and restore window sizes 74 | gtk_widget_set_size_request (window, 900, 680); 75 | application_window_setup_header_bar (GTK_WINDOW (window)); 76 | 77 | GtkWidget *web_view = webkit_web_view_new (); 78 | // TODO: Save the server URL in a setting 79 | webkit_web_view_load_uri (WEBKIT_WEB_VIEW (web_view), 80 | "http://127.0.0.1:16242"); 81 | 82 | gtk_container_add (GTK_CONTAINER (window), web_view); 83 | return window; 84 | } 85 | 86 | 87 | static WebKitWebView * 88 | application_find_web_view (GtkApplication *application) 89 | { 90 | GtkWindow *window = gtk_application_get_active_window (application); 91 | return WEBKIT_WEB_VIEW (gtk_bin_get_child (GTK_BIN (window))); 92 | } 93 | 94 | 95 | static void 96 | run_javascript_finished_discard_result (GObject *object, 97 | GAsyncResult *result, 98 | gpointer userdata) 99 | { 100 | WebKitJavascriptResult *js_result = 101 | webkit_web_view_run_javascript_finish (WEBKIT_WEB_VIEW (object), 102 | result, 103 | NULL); 104 | webkit_javascript_result_unref (js_result); 105 | } 106 | 107 | 108 | static void 109 | prev_song_action_activated (GSimpleAction *action, 110 | GVariant *parameter, 111 | gpointer userdata) 112 | { 113 | WebKitWebView *web_view = 114 | application_find_web_view (GTK_APPLICATION (userdata)); 115 | webkit_web_view_run_javascript (web_view, 116 | "_debug_player.prev()", 117 | NULL, 118 | run_javascript_finished_discard_result, 119 | NULL); 120 | } 121 | 122 | 123 | static void 124 | next_song_action_activated (GSimpleAction *action, 125 | GVariant *parameter, 126 | gpointer userdata) 127 | { 128 | WebKitWebView *web_view = 129 | application_find_web_view (GTK_APPLICATION (userdata)); 130 | webkit_web_view_run_javascript (web_view, 131 | "_debug_player.next()", 132 | NULL, 133 | run_javascript_finished_discard_result, 134 | NULL); 135 | } 136 | 137 | 138 | 139 | static void 140 | toggle_play_action_activated (GSimpleAction *action, 141 | GVariant *parameter, 142 | gpointer userdata) 143 | { 144 | WebKitWebView *web_view = 145 | application_find_web_view (GTK_APPLICATION (userdata)); 146 | webkit_web_view_run_javascript (web_view, 147 | "_debug_player.isPlaying" 148 | " ? _debug_player.pause()" 149 | " : _debug_player.play()", 150 | NULL, 151 | run_javascript_finished_discard_result, 152 | NULL); 153 | } 154 | 155 | 156 | static void 157 | about_action_activated (GSimpleAction *action, 158 | GVariant *parameter, 159 | gpointer userdata) 160 | { 161 | static const gchar *authors[] = { 162 | "Adrián Pérez de Castro", 163 | NULL, 164 | }; 165 | 166 | gtk_show_about_dialog (NULL, 167 | "authors", authors, 168 | "logo-icon-name", "gnome-music", 169 | "license-type", GTK_LICENSE_MIT_X11, 170 | "comments", "A simple Groove Basin remote using its web-based UI", 171 | "website", "https://github.com/aperezdc/groover", 172 | NULL); 173 | } 174 | 175 | 176 | static void 177 | quit_action_activated (GSimpleAction *action, 178 | GVariant *parameter, 179 | gpointer userdata) 180 | { 181 | g_application_quit (G_APPLICATION (userdata)); 182 | } 183 | 184 | 185 | static const GActionEntry app_actions[] = { 186 | { "next-song", next_song_action_activated, NULL, NULL, NULL }, 187 | { "previous-song", prev_song_action_activated, NULL, NULL, NULL }, 188 | { "toggle-play", toggle_play_action_activated, NULL, NULL, NULL }, 189 | { "about", about_action_activated, NULL, NULL, NULL }, 190 | { "quit", quit_action_activated, NULL, NULL, NULL }, 191 | }; 192 | 193 | 194 | static void 195 | application_started (GtkApplication *application) 196 | { 197 | WebKitWebContext *context = webkit_web_context_get_default (); 198 | webkit_web_context_set_process_model (context, 199 | WEBKIT_PROCESS_MODEL_SHARED_SECONDARY_PROCESS); 200 | webkit_web_context_set_web_extensions_directory ( 201 | context, WEB_EXTENSIONS_DIRECTORY); 202 | 203 | gtk_window_set_default_icon_name ("gnome-music"); 204 | g_object_set (gtk_settings_get_default (), 205 | "gtk-application-prefer-dark-theme", 206 | TRUE, NULL); 207 | 208 | g_action_map_add_action_entries (G_ACTION_MAP (application), app_actions, 209 | G_N_ELEMENTS (app_actions), application); 210 | 211 | GtkBuilder *builder = gtk_builder_new_from_resource (GROOVER_GRESOURCE ("menus.xml")); 212 | gtk_application_set_app_menu (application, 213 | G_MENU_MODEL (gtk_builder_get_object (builder, "app-menu"))); 214 | g_object_unref (builder); 215 | 216 | const struct { 217 | const gchar *action; 218 | const gchar *accel; 219 | GVariant *param; 220 | } accel_map[] = { 221 | { "app.toggle-play", "m", NULL }, 222 | { "app.previous-song", "comma", NULL }, 223 | { "app.next-song", "period", NULL }, 224 | }; 225 | 226 | for (guint i = 0; i < G_N_ELEMENTS (accel_map); i++) { 227 | gtk_application_add_accelerator (application, 228 | accel_map[i].accel, 229 | accel_map[i].action, 230 | accel_map[i].param); 231 | } 232 | } 233 | 234 | 235 | static void 236 | application_activated (GtkApplication *application) 237 | { 238 | static GtkWidget *main_window = NULL; 239 | 240 | if (!main_window) 241 | main_window = application_window_new (application); 242 | gtk_widget_show_all (main_window); 243 | gtk_window_present (GTK_WINDOW (main_window)); 244 | } 245 | 246 | 247 | static const gchar * 248 | get_application_id (void) 249 | { 250 | static const gchar *app_id = NULL; 251 | if (!app_id) { 252 | if (!(app_id = g_getenv ("GROOVER_APPLICATION_ID"))) 253 | app_id = "org.perezdecastro.groover"; 254 | } 255 | return app_id; 256 | } 257 | 258 | 259 | int 260 | main (int argc, char **argv) 261 | { 262 | GtkApplication *application = 263 | gtk_application_new (get_application_id (), 264 | G_APPLICATION_FLAGS_NONE); 265 | 266 | g_signal_connect (G_OBJECT (application), "startup", 267 | G_CALLBACK (application_started), NULL); 268 | g_signal_connect (G_OBJECT (application), "activate", 269 | G_CALLBACK (application_activated), NULL); 270 | 271 | gint status = g_application_run (G_APPLICATION (application), argc, argv); 272 | g_object_unref (application); 273 | return status; 274 | } 275 | 276 | --------------------------------------------------------------------------------