From 07c3c00f1e658fea8ef59e1b5d8f2746de53a62d Mon Sep 17 00:00:00 2001 From: Joshua Strobl Date: Tue, 6 Apr 2021 10:41:15 +0300 Subject: [PATCH] Implement mimetype support reporting for MPRIS, start implementation of bulk of getters. Implemented the following getters for MPRIS: - CanQuit - CanRaise - HasTrackList - Identity - DesktopEntry - SupportedUriSchemas - SupportedMimeTypes - Metadata - CanPlay / CanPause / CanSeek - CanControl - PlaybackStatus Implemented a koto_push_track_info_to_builder function that enables us to easily push KotoIndexedTrack as well as associated album and artist info to a GVariantBuilder for use in a GVariant for various getters. Implemented a koto_update_mpris_info_for_track function that emits a signal for PropertiesChanged + "Metadata" when our track info changes. --- com.github.joshstrobl.koto.json | 9 +- src/koto-playerbar.c | 153 ++++++++++---- src/koto-playerbar.h | 6 +- src/koto-utils.c | 12 ++ src/koto-utils.h | 1 + src/main.c | 38 +++- src/meson.build | 2 + src/playback/engine.c | 48 +++-- src/playback/engine.h | 6 +- src/playback/mimes.c | 66 ++++++ src/playback/mimes.h | 28 +++ src/playback/mpris.c | 357 ++++++++++++++++++++++++++++++++ src/playback/mpris.h | 27 +++ src/playlist/playlist.c | 43 +++- src/playlist/playlist.h | 2 + theme/_button.scss | 4 + theme/_vars.scss | 1 + 17 files changed, 734 insertions(+), 69 deletions(-) create mode 100644 src/playback/mimes.c create mode 100644 src/playback/mimes.h create mode 100644 src/playback/mpris.c create mode 100644 src/playback/mpris.h diff --git a/com.github.joshstrobl.koto.json b/com.github.joshstrobl.koto.json index 587014a..2d66e1e 100644 --- a/com.github.joshstrobl.koto.json +++ b/com.github.joshstrobl.koto.json @@ -1,7 +1,7 @@ { "app-id" : "com.github.joshstrobl.koto", "runtime" : "org.gnome.Platform", - "runtime-version" : "3.38", + "runtime-version" : "40", "sdk" : "org.gnome.Sdk", "command" : "com.github.joshstrobl.koto", "finish-args" : [ @@ -29,9 +29,12 @@ "sources" : [ { "type" : "git", - "url" : "file:///home/joshua/Code/Personal/Koto" + "url" : "https://github.com/JoshStrobl/koto.git" } ] } - ] + ], + "build-options" : { + "env" : { } + } } diff --git a/src/koto-playerbar.c b/src/koto-playerbar.c index 50b5267..adbacc3 100644 --- a/src/koto-playerbar.c +++ b/src/koto-playerbar.c @@ -18,11 +18,14 @@ #include #include #include "db/cartographer.h" +#include "playlist/current.h" +#include "playlist/playlist.h" #include "playback/engine.h" #include "koto-button.h" #include "koto-config.h" #include "koto-playerbar.h" +extern KotoCurrentPlaylist *current_playlist; extern KotoCartographer* koto_maps; extern KotoPlaybackEngine *playback_engine; @@ -85,9 +88,18 @@ static void koto_playerbar_constructed(GObject *obj) { gtk_range_set_increments(GTK_RANGE(self->progress_bar), 1, 1); gtk_range_set_round_digits(GTK_RANGE(self->progress_bar), 1); + GtkEventController *scroll_controller = gtk_event_controller_scroll_new(GTK_EVENT_CONTROLLER_SCROLL_HORIZONTAL); + g_signal_connect(scroll_controller, "scroll-begin", G_CALLBACK(koto_playerbar_handle_progressbar_scroll_begin), self); + gtk_widget_add_controller(GTK_WIDGET(self->progress_bar), scroll_controller); + GtkGesture *press_controller = gtk_gesture_click_new(); // Create a new GtkGestureLongPress + gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(press_controller), 1); // Set to left click + + g_signal_connect(GTK_GESTURE(press_controller), "begin", G_CALLBACK(koto_playerbar_handle_progressbar_gesture_begin), self); + g_signal_connect(GTK_GESTURE(press_controller), "end", G_CALLBACK(koto_playerbar_handle_progressbar_gesture_end), self); g_signal_connect(press_controller, "pressed", G_CALLBACK(koto_playerbar_handle_progressbar_pressed), self); - g_signal_connect(press_controller, "unpaired-release", G_CALLBACK(koto_playerbar_handle_progressbar_released), self); + //g_signal_connect(press_controller, "unpaired-release", G_CALLBACK(koto_playerbar_handle_progressbar_unpaired_release), self); + gtk_widget_add_controller(GTK_WIDGET(self->progress_bar), GTK_EVENT_CONTROLLER(press_controller)); g_signal_connect(GTK_RANGE(self->progress_bar), "value-changed", G_CALLBACK(koto_playerbar_handle_progressbar_value_changed), self); @@ -167,30 +179,29 @@ void koto_playerbar_create_primary_controls(KotoPlayerBar* bar) { bar->play_pause_button = koto_button_new_with_icon("", "media-playback-start-symbolic", "media-playback-pause-symbolic", KOTO_BUTTON_PIXBUF_SIZE_LARGE); // TODO: Have this take in a state and switch to a different icon if necessary bar->forward_button = koto_button_new_with_icon("", "media-skip-forward-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); - if (bar->back_button != NULL) { + if (KOTO_IS_BUTTON(bar->back_button)) { gtk_box_append(GTK_BOX(bar->primary_controls_section), GTK_WIDGET(bar->back_button)); + + GtkGesture *back_controller = gtk_gesture_click_new(); // Create a new GtkGestureClick + g_signal_connect(back_controller, "pressed", G_CALLBACK(koto_playerbar_go_backwards), NULL); + gtk_widget_add_controller(GTK_WIDGET(bar->back_button), GTK_EVENT_CONTROLLER(back_controller)); } - if (bar->play_pause_button != NULL) { + if (KOTO_IS_BUTTON(bar->play_pause_button)) { gtk_box_append(GTK_BOX(bar->primary_controls_section), GTK_WIDGET(bar->play_pause_button)); + + GtkGesture *controller = gtk_gesture_click_new(); // Create a new GtkGestureClick + g_signal_connect(controller, "pressed", G_CALLBACK(koto_playerbar_toggle_play_pause), NULL); + gtk_widget_add_controller(GTK_WIDGET(bar->play_pause_button), GTK_EVENT_CONTROLLER(controller)); } - if (bar->forward_button != NULL) { + if (KOTO_IS_BUTTON(bar->forward_button)) { gtk_box_append(GTK_BOX(bar->primary_controls_section), GTK_WIDGET(bar->forward_button)); + + GtkGesture *forwards_controller = gtk_gesture_click_new(); // Create a new GtkGestureClick + g_signal_connect(forwards_controller, "pressed", G_CALLBACK(koto_playerbar_go_forwards), NULL); + gtk_widget_add_controller(GTK_WIDGET(bar->forward_button), GTK_EVENT_CONTROLLER(forwards_controller)); } - - - GtkGesture *back_controller = gtk_gesture_click_new(); // Create a new GtkGestureClick - g_signal_connect(back_controller, "pressed", G_CALLBACK(koto_playerbar_go_backwards), NULL); - gtk_widget_add_controller(GTK_WIDGET(bar->back_button), GTK_EVENT_CONTROLLER(back_controller)); - - GtkGesture *controller = gtk_gesture_click_new(); // Create a new GtkGestureClick - g_signal_connect(controller, "pressed", G_CALLBACK(koto_playerbar_toggle_play_pause), NULL); - gtk_widget_add_controller(GTK_WIDGET(bar->play_pause_button), GTK_EVENT_CONTROLLER(controller)); - - GtkGesture *forwards_controller = gtk_gesture_click_new(); // Create a new GtkGestureClick - g_signal_connect(forwards_controller, "pressed", G_CALLBACK(koto_playerbar_go_forwards), NULL); - gtk_widget_add_controller(GTK_WIDGET(bar->forward_button), GTK_EVENT_CONTROLLER(forwards_controller)); } void koto_playerbar_create_secondary_controls(KotoPlayerBar* bar) { @@ -198,30 +209,42 @@ void koto_playerbar_create_secondary_controls(KotoPlayerBar* bar) { bar->shuffle_button = koto_button_new_with_icon("", "media-playlist-shuffle-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); bar->playlist_button = koto_button_new_with_icon("", "playlist-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); bar->eq_button = koto_button_new_with_icon("", "multimedia-equalizer-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); + bar->volume_button = gtk_volume_button_new(); // Have this take in a state and switch to a different icon if necessary - g_object_set(bar->volume_button, "use-symbolic", TRUE, NULL); - gtk_scale_button_set_value(GTK_SCALE_BUTTON(bar->volume_button), 0.5); - g_signal_connect(GTK_SCALE_BUTTON(bar->volume_button), "value-changed", G_CALLBACK(koto_playerbar_handle_volume_button_change), bar); - - if (bar->repeat_button != NULL) { + if (KOTO_IS_BUTTON(bar->repeat_button)) { gtk_box_append(GTK_BOX(bar->secondary_controls_section), GTK_WIDGET(bar->repeat_button)); + + GtkGesture *controller = gtk_gesture_click_new(); // Create a new GtkGestureClick + g_signal_connect(controller, "pressed", G_CALLBACK(koto_playerbar_toggle_track_repeat), bar); + gtk_widget_add_controller(GTK_WIDGET(bar->repeat_button), GTK_EVENT_CONTROLLER(controller)); } - if (bar->shuffle_button != NULL) { + if (KOTO_IS_BUTTON(bar->shuffle_button)) { gtk_box_append(GTK_BOX(bar->secondary_controls_section), GTK_WIDGET(bar->shuffle_button)); + + GtkGesture *controller = gtk_gesture_click_new(); // Create a new GtkGestureClick + g_signal_connect(controller, "pressed", G_CALLBACK(koto_playerbar_toggle_playlist_shuffle), bar); + gtk_widget_add_controller(GTK_WIDGET(bar->shuffle_button), GTK_EVENT_CONTROLLER(controller)); } - if (bar->playlist_button != NULL) { + if (KOTO_IS_BUTTON(bar->playlist_button)) { gtk_box_append(GTK_BOX(bar->secondary_controls_section), GTK_WIDGET(bar->playlist_button)); } - if (bar->eq_button != NULL) { + if (KOTO_IS_BUTTON(bar->eq_button)) { gtk_box_append(GTK_BOX(bar->secondary_controls_section), GTK_WIDGET(bar->eq_button)); } - if (bar->volume_button != NULL) { + if (GTK_IS_VOLUME_BUTTON(bar->volume_button)) { + GtkAdjustment *granular_volume_change = gtk_adjustment_new(0.5, 0, 1.0, 0.02, 0.02, 0.02); + g_object_set(bar->volume_button, "use-symbolic", TRUE, NULL); + gtk_range_set_round_digits(GTK_RANGE(bar->volume_button), FALSE); + gtk_scale_button_set_adjustment(GTK_SCALE_BUTTON(bar->volume_button), granular_volume_change); // Set our adjustment gtk_box_append(GTK_BOX(bar->secondary_controls_section), bar->volume_button); + + g_signal_connect(GTK_SCALE_BUTTON(bar->volume_button), "value-changed", G_CALLBACK(koto_playerbar_handle_volume_button_change), bar); + } } @@ -265,6 +288,35 @@ void koto_playerbar_handle_is_paused(KotoPlaybackEngine *engine, gpointer user_d koto_button_show_image(bar->play_pause_button, FALSE); // Set to FALSE to show play as the next action } +void koto_playerbar_handle_progressbar_scroll_begin(GtkEventControllerScroll *controller, gpointer data){ + (void) controller; + g_message("scroll-begin"); +} + +void koto_playerbar_handle_progressbar_gesture_begin(GtkGesture *gesture, GdkEventSequence *seq, gpointer data) { + (void) gesture; (void) seq; + KotoPlayerBar *bar = data; + + if (!KOTO_IS_PLAYERBAR(bar)) { + return; + } + + g_message("Begin"); + bar->is_progressbar_seeking = TRUE; +} + +void koto_playerbar_handle_progressbar_gesture_end(GtkGesture *gesture, GdkEventSequence *seq, gpointer data) { + (void) gesture; (void) seq; + KotoPlayerBar *bar = data; + + g_message("Ended"); + + if (!KOTO_IS_PLAYERBAR(bar)) { + return; + } + bar->is_progressbar_seeking = FALSE; +} + void koto_playerbar_handle_progressbar_pressed(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data) { (void) gesture; (void) n_press; (void) x; (void) y; KotoPlayerBar *bar = data; @@ -273,21 +325,10 @@ void koto_playerbar_handle_progressbar_pressed(GtkGestureClick *gesture, int n_p return; } + g_message("Pressed"); bar->is_progressbar_seeking = TRUE; } -void koto_playerbar_handle_progressbar_released(GtkGestureClick *gesture, double x, double y, guint button, GdkEventSequence *seq, gpointer data) { - (void) gesture; (void) x; (void) y; (void) button; (void) seq; - KotoPlayerBar *bar = data; - - if (!KOTO_IS_PLAYERBAR(bar)) { - return; - } - - bar->is_progressbar_seeking = FALSE; -} - - void koto_playerbar_handle_progressbar_value_changed(GtkRange *progress_bar, gpointer data) { KotoPlayerBar *bar = data; @@ -301,6 +342,7 @@ void koto_playerbar_handle_progressbar_value_changed(GtkRange *progress_bar, gpo int desired_position = (int) gtk_range_get_value(progress_bar); + g_message("value changed"); koto_playback_engine_set_position(playback_engine, desired_position); // Update our position } @@ -366,6 +408,41 @@ void koto_playerbar_toggle_play_pause(GtkGestureClick *gesture, int n_press, dou koto_playback_engine_toggle(playback_engine); } +void koto_playerbar_toggle_playlist_shuffle(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data) { + (void) gesture; (void) n_press; (void) x; (void) y; + KotoPlayerBar *bar = data; + + if (!KOTO_IS_PLAYERBAR(bar)) { + return; + } + + KotoPlaylist *playlist = koto_current_playlist_get_playlist(current_playlist); + + if (!KOTO_IS_PLAYLIST(playlist)) { // Don't have a playlist currently + gtk_widget_remove_css_class(GTK_WIDGET(bar->shuffle_button), "active"); // Remove active state + return; + } + + gboolean currently_shuffling = FALSE; + g_object_get(playlist, "is-shuffle-enabled", ¤tly_shuffling, NULL); // Get the current is-shuffle-enabled + + (currently_shuffling) ? gtk_widget_remove_css_class(GTK_WIDGET(bar->shuffle_button), "active") : gtk_widget_add_css_class(GTK_WIDGET(bar->shuffle_button), "active"); + g_object_set(playlist, "is-shuffle-enabled", !currently_shuffling, NULL); // Provide inverse value +} + +void koto_playerbar_toggle_track_repeat(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data) { + (void) gesture; (void) n_press; (void) x; (void) y; + KotoPlayerBar *bar = data; + + if (koto_playback_engine_get_track_repeat(playback_engine)) { // Toggled on at the moment + gtk_widget_remove_css_class(GTK_WIDGET(bar->repeat_button), "active"); // Remove active CSS class + } else { + gtk_widget_add_css_class(GTK_WIDGET(bar->repeat_button), "active"); // Add active CSS class + } + + koto_playback_engine_toggle_track_repeat(playback_engine); // Toggle the state +} + void koto_playerbar_update_track_info(KotoPlaybackEngine *engine, gpointer user_data) { if (!KOTO_IS_PLAYBACK_ENGINE(engine)) { return; diff --git a/src/koto-playerbar.h b/src/koto-playerbar.h index 23838ae..70756dd 100644 --- a/src/koto-playerbar.h +++ b/src/koto-playerbar.h @@ -35,8 +35,10 @@ void koto_playerbar_go_backwards(GtkGestureClick *gesture, int n_press, double x void koto_playerbar_go_forwards(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data); void koto_playerbar_handle_is_playing(KotoPlaybackEngine *engine, gpointer user_data); void koto_playerbar_handle_is_paused(KotoPlaybackEngine *engine, gpointer user_data); +void koto_playerbar_handle_progressbar_scroll_begin(GtkEventControllerScroll *controller, gpointer data); +void koto_playerbar_handle_progressbar_gesture_begin(GtkGesture *gesture, GdkEventSequence *seq, gpointer data); +void koto_playerbar_handle_progressbar_gesture_end(GtkGesture *gesture, GdkEventSequence *seq, gpointer data); void koto_playerbar_handle_progressbar_pressed(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data); -void koto_playerbar_handle_progressbar_released(GtkGestureClick *gesture, double x, double y, guint button, GdkEventSequence *seq, gpointer data); void koto_playerbar_handle_progressbar_value_changed(GtkRange *progress_bar, gpointer data); void koto_playerbar_handle_tick_duration(KotoPlaybackEngine *engine, gpointer user_data); void koto_playerbar_handle_tick_track(KotoPlaybackEngine *engine, gpointer user_data); @@ -45,6 +47,8 @@ void koto_playerbar_reset_progressbar(KotoPlayerBar* bar); void koto_playerbar_set_progressbar_duration(KotoPlayerBar* bar, gint64 duration); void koto_playerbar_set_progressbar_value(KotoPlayerBar* bar, gint64 progress); void koto_playerbar_toggle_play_pause(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data); +void koto_playerbar_toggle_playlist_shuffle(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data); +void koto_playerbar_toggle_track_repeat(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data); void koto_playerbar_update_track_info(KotoPlaybackEngine *engine, gpointer user_data); G_END_DECLS diff --git a/src/koto-utils.c b/src/koto-utils.c index 0e325cb..130452b 100644 --- a/src/koto-utils.c +++ b/src/koto-utils.c @@ -68,6 +68,18 @@ gchar* koto_utils_get_filename_without_extension(gchar *filename) { return stripped_file_name; } +gchar *koto_utils_replace_string_all(gchar *str, gchar *find, gchar *repl) { + gchar *cleaned_string = ""; + gchar **split = g_strsplit(str, find, -1); // Split on find + + for (guint i = 0; i < g_strv_length(split); i++) { // For each split + cleaned_string = g_strjoin(repl, cleaned_string, split[i], NULL); // Join the strings with our replace string + } + + g_strfreev(split); + return cleaned_string; +} + gchar* koto_utils_unquote_string(gchar *s) { gchar *new_s = NULL; diff --git a/src/koto-utils.h b/src/koto-utils.h index 9cf7a79..58d16b7 100644 --- a/src/koto-utils.h +++ b/src/koto-utils.h @@ -23,6 +23,7 @@ G_BEGIN_DECLS GtkWidget* koto_utils_create_image_from_filepath(gchar *filepath, gchar *fallback_icon, guint width, guint height); gchar* koto_utils_get_filename_without_extension(gchar *filename); +gchar *koto_utils_replace_string_all(gchar *str, gchar *find, gchar *repl); gchar* koto_utils_unquote_string(gchar *s); G_END_DECLS diff --git a/src/main.c b/src/main.c index 76a067e..0eed029 100644 --- a/src/main.c +++ b/src/main.c @@ -19,33 +19,43 @@ #include #include "db/cartographer.h" #include "db/db.h" +#include "playback/mimes.h" +#include "playback/mpris.h" #include "koto-config.h" #include "koto-window.h" +extern guint mpris_bus_id; +extern GDBusNodeInfo *introspection_data; + extern KotoCartographer *koto_maps; extern sqlite3 *koto_db; +extern GHashTable *supported_mimes_hash; +extern GList *supported_mimes; + +GtkApplication *app = NULL; +GtkWindow *main_window; + static void on_activate (GtkApplication *app) { g_assert(GTK_IS_APPLICATION (app)); - GtkWindow *window; - - window = gtk_application_get_active_window (app); - if (window == NULL) { - window = g_object_new(KOTO_TYPE_WINDOW, "application", app, "default-width", 1200, "default-height", 675, NULL); + main_window = gtk_application_get_active_window (app); + if (main_window == NULL) { + main_window = g_object_new(KOTO_TYPE_WINDOW, "application", app, "default-width", 1200, "default-height", 675, NULL); } - gtk_window_present(window); + gtk_window_present(main_window); } static void on_shutdown(GtkApplication *app) { (void) app; close_db(); // Close the database + g_bus_unown_name(mpris_bus_id); + g_dbus_node_info_unref(introspection_data); } int main (int argc, char *argv[]) { - g_autoptr(GtkApplication) app = NULL; int ret; /* Set up gettext translations */ @@ -56,8 +66,22 @@ int main (int argc, char *argv[]) { gtk_init(); gst_init(&argc, &argv); + supported_mimes_hash = g_hash_table_new(g_str_hash, g_str_equal); + supported_mimes = NULL; // Ensure our mimes GList is initialized + koto_playback_engine_get_supported_mimetypes(supported_mimes); + + g_message("Length: %d", g_list_length(supported_mimes)); + koto_maps = koto_cartographer_new(); // Create our new cartographer and their collection of maps open_db(); // Open our database + setup_mpris_interfaces(); // Set up our MPRIS interfaces + + GList *md; + md = NULL; + for (md = supported_mimes; md != NULL; md = md->next) { + g_message("Mimetype: %s", (gchar*) md->data); + } + g_list_free(md); app = gtk_application_new ("com.github.joshstrobl.koto", G_APPLICATION_FLAGS_NONE); g_signal_connect (app, "activate", G_CALLBACK (on_activate), NULL); diff --git a/src/meson.build b/src/meson.build index 84d10f2..957b92b 100644 --- a/src/meson.build +++ b/src/meson.build @@ -12,6 +12,8 @@ koto_sources = [ 'pages/music/disc-view.c', 'pages/music/music-local.c', 'playback/engine.c', + 'playback/mimes.c', + 'playback/mpris.c', 'playlist/current.c', 'playlist/playlist.c', 'main.c', diff --git a/src/playback/engine.c b/src/playback/engine.c index 9edee31..ce05cd3 100644 --- a/src/playback/engine.c +++ b/src/playback/engine.c @@ -22,6 +22,7 @@ #include "../playlist/current.h" #include "../indexer/structs.h" #include "engine.h" +#include "mpris.h" enum { SIGNAL_IS_PLAYING, @@ -52,7 +53,6 @@ struct _KotoPlaybackEngine { gboolean is_muted; gboolean is_repeat_enabled; - gboolean is_shuffle_enabled; gboolean is_playing; @@ -163,6 +163,7 @@ static void koto_playback_engine_init(KotoPlaybackEngine *self) { self->suppress_video = gst_element_factory_make("fakesink", "suppress-video"); g_object_set(self->playbin, "video-sink", self->suppress_video, NULL); + self->volume = 0.5; koto_playback_engine_set_volume(self, 0.5); gst_bin_add(GST_BIN(self->player), self->playbin); @@ -175,7 +176,6 @@ static void koto_playback_engine_init(KotoPlaybackEngine *self) { self->is_muted = FALSE; self->is_repeat_enabled = FALSE; - self->is_shuffle_enabled = FALSE; self->tick_duration_timer_running = FALSE; self->tick_track_timer_running = FALSE; @@ -205,7 +205,7 @@ void koto_playback_engine_current_playlist_changed() { return; } - koto_playback_engine_set_track_by_uuid(playback_engine, koto_playlist_get_current_uuid(playlist)); // Get the current UUID + koto_playback_engine_set_track_by_uuid(playback_engine, koto_playlist_go_to_next(playlist)); // Go to "next" which is the first track } void koto_playback_engine_forwards(KotoPlaybackEngine *self) { @@ -215,7 +215,11 @@ void koto_playback_engine_forwards(KotoPlaybackEngine *self) { return; } - koto_playback_engine_set_track_by_uuid(self, koto_playlist_go_to_next(playlist)); + if (self->is_repeat_enabled) { // Is repeat enabled + koto_playback_engine_set_position(self, 0); // Set position back to 0 to repeat the track + } else { // Repeat not enabled + koto_playback_engine_set_track_by_uuid(self, koto_playlist_go_to_next(playlist)); + } } KotoIndexedTrack* koto_playback_engine_get_current_track(KotoPlaybackEngine *self) { @@ -231,13 +235,6 @@ gint64 koto_playback_engine_get_duration(KotoPlaybackEngine *self) { return duration; } -GstState koto_playback_engine_get_state(KotoPlaybackEngine *self) { - GstState current_state; - gst_element_get_state(self->player, ¤t_state, NULL, GST_SECOND); // Get the current state, allowing up to a second to get it - - return current_state; -} - gint64 koto_playback_engine_get_progress(KotoPlaybackEngine *self) { gint64 progress = 0; if (gst_element_query_position(self->player, GST_FORMAT_TIME, &progress)) { @@ -247,13 +244,26 @@ gint64 koto_playback_engine_get_progress(KotoPlaybackEngine *self) { return progress; } +GstState koto_playback_engine_get_state(KotoPlaybackEngine *self) { + GstState current_state; + gst_element_get_state(self->player, ¤t_state, NULL, GST_SECOND); // Get the current state, allowing up to a second to get it + + return current_state; +} + +gboolean koto_playback_engine_get_track_repeat(KotoPlaybackEngine *self) { + return self->is_repeat_enabled; +} + gboolean koto_playback_engine_monitor_changed(GstBus *bus, GstMessage *msg, gpointer user_data) { (void) bus; KotoPlaybackEngine *self = user_data; switch (GST_MESSAGE_TYPE(msg)) { + case GST_MESSAGE_ASYNC_DONE: case GST_MESSAGE_DURATION_CHANGED: { // Duration changed koto_playback_engine_tick_duration(self); + koto_playback_engine_tick_track(self); break; } case GST_MESSAGE_STATE_CHANGED: { // State changed @@ -263,6 +273,7 @@ gboolean koto_playback_engine_monitor_changed(GstBus *bus, GstMessage *msg, gpoi gst_message_parse_state_changed(msg, &old_state, &new_state, &pending_state); if (new_state == GST_STATE_PLAYING) { // Now playing + koto_playback_engine_tick_duration(self); g_signal_emit(self, playback_engine_signals[SIGNAL_IS_PLAYING], 0); // Emit our is playing state signal } else if (new_state == GST_STATE_PAUSED) { // Now paused g_signal_emit(self, playback_engine_signals[SIGNAL_IS_PAUSED], 0); // Emit our is paused state signal @@ -308,6 +319,10 @@ void koto_playback_engine_set_position(KotoPlaybackEngine *self, int position) { gst_element_seek_simple(self->playbin, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH, position * NS); } +void koto_playback_engine_set_track_repeat(KotoPlaybackEngine *self, gboolean enable) { + self->is_repeat_enabled = enable; +} + void koto_playback_engine_set_track_by_uuid(KotoPlaybackEngine *self, gchar *track_uuid) { if (track_uuid == NULL) { return; @@ -335,17 +350,20 @@ void koto_playback_engine_set_track_by_uuid(KotoPlaybackEngine *self, gchar *tra // TODO: Add prior position state setting here, like picking up at a specific part of an audiobook or podcast koto_playback_engine_set_position(self, 0); + koto_playback_engine_set_volume(self, self->volume); // Re-enforce our volume on the updated playbin g_signal_emit(self, playback_engine_signals[SIGNAL_TRACK_CHANGE], 0); // Emit our track change signal + koto_update_mpris_info_for_track(self->current_track); } void koto_playback_engine_set_volume(KotoPlaybackEngine *self, gdouble volume) { - g_object_set(self->playbin, "volume", volume, NULL); + self->volume = volume; + g_object_set(self->playbin, "volume", self->volume, NULL); } void koto_playback_engine_stop(KotoPlaybackEngine *self) { gst_element_set_state(self->player, GST_STATE_NULL); - GstPad *pad = gst_element_get_static_pad(self->playbin, "audio-sink"); // Get the static pad of the audio element + GstPad *pad = gst_element_get_static_pad(self->player, "sink"); // Get the static pad of the audio element if (!GST_IS_PAD(pad)) { return; @@ -386,6 +404,10 @@ gboolean koto_playback_engine_tick_track(gpointer user_data) { return self->is_playing; } +void koto_playback_engine_toggle_track_repeat(KotoPlaybackEngine *self) { + self->is_repeat_enabled = !self->is_repeat_enabled; +} + KotoPlaybackEngine* koto_playback_engine_new() { return g_object_new(KOTO_TYPE_PLAYBACK_ENGINE, NULL); } diff --git a/src/playback/engine.h b/src/playback/engine.h index eb4991c..0c503c1 100644 --- a/src/playback/engine.h +++ b/src/playback/engine.h @@ -49,17 +49,19 @@ KotoIndexedTrack* koto_playback_engine_get_current_track(KotoPlaybackEngine *sel gint64 koto_playback_engine_get_duration(KotoPlaybackEngine *self); GstState koto_playback_engine_get_state(KotoPlaybackEngine *self); gint64 koto_playback_engine_get_progress(KotoPlaybackEngine *self); +gboolean koto_playback_engine_get_track_repeat(KotoPlaybackEngine *self); void koto_playback_engine_mute(KotoPlaybackEngine *self); gboolean koto_playback_engine_monitor_changed(GstBus *bus, GstMessage *msg, gpointer user_data); void koto_playback_engine_pause(KotoPlaybackEngine *self); void koto_playback_engine_play(KotoPlaybackEngine *self); void koto_playback_engine_toggle(KotoPlaybackEngine *self); void koto_playback_engine_set_position(KotoPlaybackEngine *self, int position); -void koto_playback_engine_set_repeat(KotoPlaybackEngine *self, gboolean enable); -void koto_playback_engine_set_shuffle(KotoPlaybackEngine *self, gboolean enable); +void koto_playback_engine_set_track_repeat(KotoPlaybackEngine *self, gboolean enable); void koto_playback_engine_set_track_by_uuid(KotoPlaybackEngine *self, gchar *track_uuid); void koto_playback_engine_set_volume(KotoPlaybackEngine *self, gdouble volume); void koto_playback_engine_stop(KotoPlaybackEngine *self); +void koto_playback_engine_toggle_track_repeat(KotoPlaybackEngine *self); void koto_playback_engine_update_duration(KotoPlaybackEngine *self); + gboolean koto_playback_engine_tick_duration(gpointer user_data); gboolean koto_playback_engine_tick_track(gpointer user_data); diff --git a/src/playback/mimes.c b/src/playback/mimes.c new file mode 100644 index 0000000..2cf8b32 --- /dev/null +++ b/src/playback/mimes.c @@ -0,0 +1,66 @@ +/* mimes.c + * + * Copyright 2021 Joshua Strobl + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#include +#include +#include "../koto-utils.h" + +GHashTable *supported_mimes_hash = NULL; +GList *supported_mimes = NULL; + +gboolean koto_playback_engine_gst_caps_iter(GstCapsFeatures *features, GstStructure *structure, gpointer user_data) { + (void) features; (void) user_data; + gchar *caps_name = (gchar*) gst_structure_get_name(structure); // Get the name, typically a mimetype + + if (g_str_has_prefix(caps_name, "unknown")) { // Is unknown + return TRUE; + } + + if (g_hash_table_contains(supported_mimes_hash, caps_name)) { // Found in list already + return TRUE; + } + + g_hash_table_insert(supported_mimes_hash, g_strdup(caps_name), TRUE); + supported_mimes = g_list_prepend(supported_mimes, g_strdup(caps_name)); + supported_mimes = g_list_prepend(supported_mimes, g_strdup(koto_utils_replace_string_all(caps_name, "x-", ""))); + + return TRUE; +} + +void koto_playback_engine_gst_pad_iter(gpointer list_data, gpointer user_data) { + (void) user_data; + GstStaticPadTemplate *templ = list_data; + if (templ->direction == GST_PAD_SINK) { // Is a sink pad + GstCaps *capabilities = gst_static_pad_template_get_caps(templ); // Get the capabilities + gst_caps_foreach(capabilities, koto_playback_engine_gst_caps_iter, NULL); // Iterate over and add to mimes + gst_caps_unref(capabilities); + } +} + +void koto_playback_engine_get_supported_mimetypes() { + // Credit for code goes to https://github.com/mopidy/mopidy/issues/812#issuecomment-75868363 + // These are GstElementFactory + GList *elements = gst_element_factory_list_get_elements(GST_ELEMENT_FACTORY_TYPE_DEPAYLOADER | GST_ELEMENT_FACTORY_TYPE_DEMUXER | GST_ELEMENT_FACTORY_TYPE_PARSER | GST_ELEMENT_FACTORY_TYPE_DECODER | GST_ELEMENT_FACTORY_TYPE_MEDIA_AUDIO, GST_RANK_NONE); + + GList *ele; + for (ele = elements; ele != NULL; ele = ele->next) { // For each of the elements + // GList of GstStaticPadTemplate + GList *static_pads = (GList*) gst_element_factory_get_static_pad_templates(ele->data); // Get the pads + g_list_foreach(static_pads, koto_playback_engine_gst_pad_iter, NULL); + } +} diff --git a/src/playback/mimes.h b/src/playback/mimes.h new file mode 100644 index 0000000..cef291b --- /dev/null +++ b/src/playback/mimes.h @@ -0,0 +1,28 @@ +/* mimes.h + * + * Copyright 2021 Joshua Strobl + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include + +G_BEGIN_DECLS + +gboolean koto_bplayback_engine_gst_caps_iter(GstCapsFeatures *features, GstStructure *structure, gpointer user_data); +void koto_playback_engine_gst_pad_iter(gpointer list_data, gpointer user_data); +void koto_playback_engine_get_supported_mimetypes(GList *mimes); + +G_END_DECLS diff --git a/src/playback/mpris.c b/src/playback/mpris.c new file mode 100644 index 0000000..e0e5905 --- /dev/null +++ b/src/playback/mpris.c @@ -0,0 +1,357 @@ +/* mpris.c + * + * Copyright 2021 Joshua Strobl + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Huge thanks to the GLib folks for providing an example server test + +#include +#include +#include +#include "../db/cartographer.h" +#include "../playlist/current.h" +#include "../playlist/playlist.h" +#include "mimes.h" +#include "engine.h" + +extern KotoCartographer *koto_maps; +extern KotoCurrentPlaylist *current_playlist; +extern GtkApplication *app; +extern GtkWindow *main_window; +extern KotoPlaybackEngine *playback_engine; +extern GList *supported_mimes; + +GDBusConnection *dbus_conn = NULL; +guint mpris_bus_id = 0; +GDBusNodeInfo *introspection_data = NULL; + +static const gchar introspection_xml[] = +"" +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +""; + +void handle_main_mpris_method_call( + GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *method_name, + GVariant *parameters, + GDBusMethodInvocation *invocation, + gpointer user_data +) { + if (g_strcmp0(interface_name, "org.mpris.MediaPlayer2") == 0) { // Root mediaplayer2 interface + if (g_strcmp0(method_name, "Raise") == 0) { // Raise the window + gtk_window_unminimize(main_window); // Ensure we unminimize the window + gtk_window_present(main_window); // Present our main window + return; + } + + if (g_strcmp0(method_name, "Quit") == 0) { // Quit the application + gtk_application_remove_window(app, main_window); // Remove the window, thereby closing the app + return; + } + } else if (g_strcmp0(interface_name, "org.mpris.MediaPlayer2.koto") == 0) { // Root mediaplayer2 interface + if (g_strcmp0(method_name, "Next") == 0) { // Next track + koto_playback_engine_forwards(playback_engine); + return; + } + + if (g_strcmp0(method_name, "Previous") == 0) { // Previous track + koto_playback_engine_backwards(playback_engine); + return; + } + } +} + +GVariant* handle_get_property( + GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *property_name, + GError **error, + gpointer user_data +) { + GVariant *ret; + ret = NULL; + + g_message("asking for %s", property_name); + + if (g_strcmp0(property_name, "CanQuit") == 0) { // If property is CanQuit + ret = g_variant_new_boolean(TRUE); // Allow quitting. You can escape Hotel California for now. + } + + if (g_strcmp0(property_name, "CanRaise") == 0) { // If property is CanRaise + ret = g_variant_new_boolean(TRUE); // Allow raising. + } + + if (g_strcmp0(property_name, "HasTrackList") == 0) { // If property is HasTrackList + KotoPlaylist *playlist = koto_current_playlist_get_playlist(current_playlist); + if (KOTO_IS_PLAYLIST(playlist)) { + ret = g_variant_new_boolean(koto_playlist_get_length(playlist) > 0); + } else { // Don't have a playlist + ret = g_variant_new_boolean(FALSE); + } + } + + if (g_strcmp0(property_name, "Identity") == 0) { // Want identity + ret = g_variant_new_string("Koto"); // Not Jason Bourne but close enough + } + + if (g_strcmp0(property_name, "DesktopEntry") == 0) { // Desktop Entry + ret = g_variant_new_string("com.github.joshstrobl.koto"); + } + + if (g_strcmp0(property_name, "SupportedUriSchemas") == 0) { // Supported URI Schemas + GVariantBuilder *builder = g_variant_builder_new(G_VARIANT_TYPE("as")); // Array of strings + g_variant_builder_add(builder, "s", "file"); + ret = g_variant_new("as", builder); + g_variant_builder_unref(builder); // Unref builder since we no longer need it + } + + if (g_strcmp0(property_name, "SupportedMimeTypes") == 0) { // Supported mimetypes + GVariantBuilder *builder = g_variant_builder_new(G_VARIANT_TYPE("as")); // Array of strings + GList *mimes; + mimes = NULL; + + for (mimes = supported_mimes; mimes != NULL; mimes = mimes->next) { // For each mimetype + g_variant_builder_add(builder, "s", mimes->data); // Add the mime as a string + } + + ret = g_variant_new("as", builder); + g_variant_builder_unref(builder); + g_list_free(mimes); + } + + if (g_strcmp0(property_name, "Metadata") == 0) { // Metadata + GVariantBuilder *builder = g_variant_builder_new(G_VARIANT_TYPE_VARDICT); + + KotoIndexedTrack *current_track = koto_playback_engine_get_current_track(playback_engine); + + if (KOTO_IS_INDEXED_TRACK(current_track)) { // Currently playing a track + koto_push_track_info_to_builder(builder, current_track); + } + + ret = g_variant_builder_end(builder); + } + + if ( + (g_strcmp0(property_name, "CanPlay") == 0) || + (g_strcmp0(property_name, "CanPause") == 0) || + (g_strcmp0(property_name, "CanSeek") == 0) + ) { + KotoIndexedTrack *current_track = koto_playback_engine_get_current_track(playback_engine); + ret = g_variant_new_boolean(KOTO_IS_INDEXED_TRACK(current_track)); + } + + if (g_strcmp0(property_name, "CanGoNext") == 0) { // Can Go Next + // TODO: Add some actual logic here + ret = g_variant_new_boolean(TRUE); + } + + if (g_strcmp0(property_name, "CanGoPrevious") == 0) { // Can Go Previous + // TODO: Add some actual logic here + ret = g_variant_new_boolean(TRUE); + } + + if (g_strcmp0(property_name, "CanControl") == 0){ // Can Control + ret = g_variant_new_boolean(TRUE); + } + + if (g_strcmp0(property_name, "PlaybackStatus") == 0) { // Get our playback status + GstState current_state = koto_playback_engine_get_state(playback_engine); // Get the current state + + if (current_state == GST_STATE_PLAYING) { + ret = g_variant_new_string("Playing"); + } else if (current_state == GST_STATE_PAUSED) { + ret = g_variant_new_string("Paused"); + } else { + ret = g_variant_new_string("Stopped"); + } + } + + return ret; +} + +void koto_push_track_info_to_builder(GVariantBuilder *builder, KotoIndexedTrack *track) { + if (!KOTO_IS_INDEXED_TRACK(track)) { + return; + } + + gchar *album_art_path = NULL; + gchar *album_name = NULL; + gchar *album_uuid = NULL; + gchar *artist_uuid = NULL; + gchar *artist_name = NULL; + gchar *track_name = NULL; + gchar *track_path = NULL; + gchar *track_uuid = NULL; + guint track_position = 0; + guint track_disc = 0; + + g_object_get(track, + "album-uuid", &album_uuid, + "artist-uuid", &artist_uuid, + "cd", &track_disc, + "path", &track_path, + "parsed-name", &track_name, + "position", &track_position, + "uuid", &track_uuid, + NULL); + + KotoIndexedArtist *artist = koto_cartographer_get_artist_by_uuid(koto_maps, artist_uuid); + KotoIndexedAlbum *album = koto_cartographer_get_album_by_uuid(koto_maps, album_uuid); + + g_object_get(album, + "art-path", &album_art_path, + "name", &album_name, + NULL); + + g_object_get(artist, + "name", &artist_name, + NULL); + + g_variant_builder_add(builder, "{sv}", "mpris:trackid", g_variant_new_string(track_uuid)); + + g_message("Art path: %s", album_art_path); + if (g_strcmp0(album_art_path, "") != 0) { // Not empty + album_art_path = g_strconcat("file://", album_art_path); // Prepend with file:// + g_variant_builder_add(builder, "{sv}", "mpris:artUrl", g_variant_new_string(album_art_path)); + } + + g_variant_builder_add(builder, "{sv}", "xesam:album", g_variant_new_string(album_name)); + + if (g_strcmp0(artist_name, "") != 0) { // Got artist name + GVariant *artist_name_variant; + GVariantBuilder *artist_list_builder = g_variant_builder_new(G_VARIANT_TYPE("as")); + g_variant_builder_add(artist_list_builder, "s", artist_name); + artist_name_variant = g_variant_new("as", artist_list_builder); + g_variant_builder_unref(artist_list_builder); + + g_variant_builder_add(builder, "{sv}", "xesam:artist", artist_name_variant); + } + + g_variant_builder_add(builder, "{sv}", "xesam:discNumber", g_variant_new_uint64(track_disc)); + g_variant_builder_add(builder, "{sv}", "xesam:title", g_variant_new_string(track_name)); + g_variant_builder_add(builder, "{sv}", "xesam:url", g_variant_new_string(track_path)); + g_variant_builder_add(builder, "{sv}", "xesam:trackNumber", g_variant_new_uint64(track_position)); +} + +void koto_update_mpris_info_for_track(KotoIndexedTrack *track) { + if (!KOTO_IS_INDEXED_TRACK(track)) { + return; + } + + GVariantBuilder *builder = g_variant_builder_new(G_VARIANT_TYPE_ARRAY); + GVariantBuilder *metadata_builder = g_variant_builder_new(G_VARIANT_TYPE_VARDICT); + GError *error = NULL; + + koto_push_track_info_to_builder(metadata_builder, track); + GVariant *metadata_ret = g_variant_builder_end(metadata_builder); + g_variant_builder_add(builder, "{sv}", "Metadata", metadata_ret); + + g_dbus_connection_emit_signal(dbus_conn, + NULL, + "/org/mpris/MediaPlayer2", + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + g_variant_new("(sa{sv}as)", "org.mpris.MediaPlayer2.Player", builder, NULL), + &error + ); +} + +static const GDBusInterfaceVTable main_mpris_interface_vtable = { + handle_main_mpris_method_call, + handle_get_property, +}; + +void on_main_mpris_bus_acquired(GDBusConnection *connection, const gchar *name, gpointer user_data) { + dbus_conn = connection; + g_dbus_connection_register_object(dbus_conn, + "/org/mpris/MediaPlayer2", + introspection_data->interfaces[0], + &main_mpris_interface_vtable, + NULL, + NULL, + NULL + ); + + g_dbus_connection_register_object(dbus_conn, + "/org/mpris/MediaPlayer2", + introspection_data->interfaces[1], + &main_mpris_interface_vtable, + NULL, + NULL, + NULL + ); +} + +void setup_mpris_interfaces() { + introspection_data = g_dbus_node_info_new_for_xml(introspection_xml, NULL); + g_assert(introspection_data != NULL); + + mpris_bus_id = g_bus_own_name(G_BUS_TYPE_SESSION, + "org.mpris.MediaPlayer2.koto", + G_BUS_NAME_OWNER_FLAGS_NONE, + on_main_mpris_bus_acquired, + NULL, + NULL, + NULL, + NULL + ); + + g_assert(mpris_bus_id > 0); +} diff --git a/src/playback/mpris.h b/src/playback/mpris.h new file mode 100644 index 0000000..97734c9 --- /dev/null +++ b/src/playback/mpris.h @@ -0,0 +1,27 @@ +/* mpris.h + * + * Copyright 2021 Joshua Strobl + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "../indexer/structs.h" + +void koto_push_track_info_to_builder(GVariantBuilder *builder, KotoIndexedTrack *track); +void koto_update_mpris_info_for_track(KotoIndexedTrack *track); +void handle_main_mpris_method_call(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data); +GVariant* handle_get_property(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GError **error, gpointer user_data); +void on_main_mpris_bus_acquired(GDBusConnection *connection, const gchar *name, gpointer user_data); +void setup_mpris_interfaces(); diff --git a/src/playlist/playlist.c b/src/playlist/playlist.c index 9f100b6..3d85be0 100644 --- a/src/playlist/playlist.c +++ b/src/playlist/playlist.c @@ -29,7 +29,7 @@ struct _KotoPlaylist { gchar *uuid; gchar *name; gchar *art_path; - guint current_position; + gint current_position; gboolean ephemeral; gboolean is_shuffle_enabled; @@ -45,6 +45,7 @@ enum { PROP_NAME, PROP_ART_PATH, PROP_EPHEMERAL, + PROP_IS_SHUFFLE_ENABLED, N_PROPERTIES, }; @@ -90,6 +91,14 @@ static void koto_playlist_class_init(KotoPlaylistClass *c) { G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_READWRITE ); + props[PROP_IS_SHUFFLE_ENABLED] = g_param_spec_boolean( + "is-shuffle-enabled", + "Is shuffling enabled", + "Is shuffling enabled", + FALSE, + G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_READWRITE + ); + g_object_class_install_properties(gobject_class, N_PROPERTIES, props); } @@ -109,6 +118,9 @@ static void koto_playlist_get_property(GObject *obj, guint prop_id, GValue *val, case PROP_EPHEMERAL: g_value_set_boolean(val, self->ephemeral); break; + case PROP_IS_SHUFFLE_ENABLED: + g_value_set_boolean(val, self->is_shuffle_enabled); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); break; @@ -131,6 +143,9 @@ static void koto_playlist_set_property(GObject *obj, guint prop_id, const GValue case PROP_EPHEMERAL: self->ephemeral = g_value_get_boolean(val); break; + case PROP_IS_SHUFFLE_ENABLED: + self->is_shuffle_enabled = g_value_get_boolean(val); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); break; @@ -138,11 +153,20 @@ static void koto_playlist_set_property(GObject *obj, guint prop_id, const GValue } static void koto_playlist_init(KotoPlaylist *self) { - self->current_position = 0; // Default to 0 + self->current_position = -1; // Default to -1 so first time incrementing puts it at 0 + self->is_shuffle_enabled = FALSE; self->played_tracks = g_queue_new(); // Set as an empty GQueue self->tracks = g_queue_new(); // Set as an empty GQueue } +void koto_playlist_add_to_played_tracks(KotoPlaylist *self, gchar *uuid) { + if (g_queue_index(self->played_tracks, uuid) != -1) { // Already added + return; + } + + g_queue_push_tail(self->played_tracks, uuid); // Add to end +} + void koto_playlist_add_track(KotoPlaylist *self, KotoIndexedTrack *track) { gchar *uuid = NULL; g_object_get(track, "uuid", &uuid, NULL); // Get the UUID @@ -228,12 +252,13 @@ gchar* koto_playlist_get_random_track(KotoPlaylist *self) { GRand* rando_calrissian = g_rand_new(); // Create a new RNG guint attempt = 0; - while (track_uuid != NULL) { // Haven't selected a track yet + while (track_uuid == NULL) { // Haven't selected a track yet attempt++; gint32 *selected_item = g_rand_int_range(rando_calrissian, 0, (gint32) tracks_len); gchar *selected_track = g_queue_peek_nth(self->tracks, (guint) selected_item); // Get the UUID of the selected item if (g_queue_index(self->played_tracks, selected_track) == -1) { // Haven't played the track + self->current_position = (int) selected_item; track_uuid = selected_track; break; } else { // Failed to get the track @@ -259,7 +284,9 @@ gchar* koto_playlist_get_uuid(KotoPlaylist *self) { gchar* koto_playlist_go_to_next(KotoPlaylist *self) { if (self->is_shuffle_enabled) { // Shuffling enabled - return koto_playlist_get_random_track(self); // Get a random track + gchar *random_track_uuid = koto_playlist_get_random_track(self); // Get a random track + koto_playlist_add_to_played_tracks(self, random_track_uuid); + return random_track_uuid; } gchar *current_uuid = koto_playlist_get_current_uuid(self); // Get the current UUID @@ -269,7 +296,9 @@ gchar* koto_playlist_go_to_next(KotoPlaylist *self) { } self->current_position++; // Increment our position - return koto_playlist_get_current_uuid(self); // Return the new UUID + current_uuid = koto_playlist_get_current_uuid(self); // Return the new UUID + koto_playlist_add_to_played_tracks(self, current_uuid); + return current_uuid; } gchar* koto_playlist_go_to_previous(KotoPlaylist *self) { @@ -287,6 +316,10 @@ gchar* koto_playlist_go_to_previous(KotoPlaylist *self) { return koto_playlist_get_current_uuid(self); // Return the new UUID } +void koto_playlist_remove_from_played_tracks(KotoPlaylist *self, gchar *uuid) { + g_queue_remove(self->played_tracks, uuid); +} + void koto_playlist_remove_track(KotoPlaylist *self, KotoIndexedTrack *track) { gchar *track_uuid = NULL; g_object_get(track, "uuid", &track_uuid, NULL); diff --git a/src/playlist/playlist.h b/src/playlist/playlist.h index 0b28095..ca8cfd7 100644 --- a/src/playlist/playlist.h +++ b/src/playlist/playlist.h @@ -35,6 +35,7 @@ G_DECLARE_FINAL_TYPE(KotoPlaylist, koto_playlist, KOTO, PLAYLIST, GObject); KotoPlaylist* koto_playlist_new(); KotoPlaylist* koto_playlist_new_with_uuid(const gchar *uuid); +void koto_playlist_add_to_played_tracks(KotoPlaylist *self, gchar *uuid); void koto_playlist_add_track(KotoPlaylist *self, KotoIndexedTrack *track); void koto_playlist_add_track_by_uuid(KotoPlaylist *self, const gchar *uuid); void koto_playlist_commit(KotoPlaylist *self); @@ -48,6 +49,7 @@ GQueue* koto_playlist_get_tracks(KotoPlaylist *self); gchar* koto_playlist_get_uuid(KotoPlaylist *self); gchar* koto_playlist_go_to_next(KotoPlaylist *self); gchar* koto_playlist_go_to_previous(KotoPlaylist *self); +void koto_playlist_remove_from_played_tracks(KotoPlaylist *self, gchar *uuid); void koto_playlist_remove_track(KotoPlaylist *self, KotoIndexedTrack *track); void koto_playlist_remove_track_by_uuid(KotoPlaylist *self, gchar *uuid); void koto_playlist_set_artwork(KotoPlaylist *self, const gchar *path); diff --git a/theme/_button.scss b/theme/_button.scss index b46ad88..85e078d 100644 --- a/theme/_button.scss +++ b/theme/_button.scss @@ -2,4 +2,8 @@ & > image { margin-right: 10px; } + + &.active > image { + color: $green; + } } diff --git a/theme/_vars.scss b/theme/_vars.scss index 37e72f4..4996f31 100644 --- a/theme/_vars.scss +++ b/theme/_vars.scss @@ -1,6 +1,7 @@ $grey: #2e2e2e; $midnight: #1d1d1d; $darkgrey: #666666; +$green: #60E078; $itempadding: 40px; $halvedpadding: $itempadding / 2;