diff --git a/src/db/cartographer.c b/src/db/cartographer.c index 25133c2..477d4e1 100644 --- a/src/db/cartographer.c +++ b/src/db/cartographer.c @@ -65,7 +65,7 @@ void koto_cartographer_add_playlist(KotoCartographer *self, KotoPlaylist *playli g_object_get(playlist, "uuid", &playlist_uuid, NULL); if ((playlist_uuid != NULL) && (!koto_cartographer_has_playlist_by_uuid(self, playlist_uuid))) { // Don't have this album - g_hash_table_replace(self->artists, playlist_uuid, playlist); + g_hash_table_replace(self->playlists, playlist_uuid, playlist); } } @@ -74,7 +74,7 @@ void koto_cartographer_add_track(KotoCartographer *self, KotoIndexedTrack *track g_object_get(track, "uuid", &track_uuid, NULL); if ((track_uuid != NULL) && (!koto_cartographer_has_playlist_by_uuid(self, track_uuid))) { // Don't have this album - g_hash_table_replace(self->artists, track_uuid, track); + g_hash_table_replace(self->tracks, track_uuid, track); } } diff --git a/src/db/db.c b/src/db/db.c index e8c75a4..fe98c50 100644 --- a/src/db/db.c +++ b/src/db/db.c @@ -37,7 +37,9 @@ void close_db() { int create_db_tables() { char *tables_creation_queries = "CREATE TABLE IF NOT EXISTS artists(id string UNIQUE PRIMARY KEY, path string, type int, name string, art_path string);" "CREATE TABLE IF NOT EXISTS albums(id string UNIQUE PRIMARY KEY, path string, artist_id string, name string, art_path string, FOREIGN KEY(artist_id) REFERENCES artists(id) ON DELETE CASCADE);" - "CREATE TABLE IF NOT EXISTS tracks(id string UNIQUE PRIMARY KEY, path string, type int, artist_id string, album_id string, file_name string, name string, disc int, position int, FOREIGN KEY(artist_id) REFERENCES artists(id) ON DELETE CASCADE);"; + "CREATE TABLE IF NOT EXISTS tracks(id string UNIQUE PRIMARY KEY, path string, type int, artist_id string, album_id string, file_name string, name string, disc int, position int, FOREIGN KEY(artist_id) REFERENCES artists(id) ON DELETE CASCADE);" + "CREATE TABLE IF NOT EXISTS playlist_meta(id string UNIQUE PRIMARY KEY, name string, art_path string);" + "CREATE TABLE IF NOT EXISTS playlist_tracks(playlist_id string PRIMARY KEY, track_id string, position int, current int, FOREIGN KEY(playlist_id) REFERENCES playlist_meta(id), FOREIGN KEY(track_id) REFERENCES tracks(id) ON DELETE CASCADE);"; gchar *create_tables_errmsg = NULL; int rc = sqlite3_exec(koto_db, tables_creation_queries, 0,0, &create_tables_errmsg); diff --git a/src/indexer/album.c b/src/indexer/album.c index d1bd2ae..6d9fcde 100644 --- a/src/indexer/album.c +++ b/src/indexer/album.c @@ -19,23 +19,25 @@ #include #include #include +#include "../db/cartographer.h" #include "../playlist/current.h" #include "../playlist/playlist.h" #include "structs.h" #include "koto-utils.h" +extern KotoCartographer *koto_maps; extern KotoCurrentPlaylist *current_playlist; extern sqlite3 *koto_db; struct _KotoIndexedAlbum { GObject parent_instance; - KotoIndexedArtist *artist; gchar *uuid; gchar *path; gchar *name; gchar *art_path; - GHashTable *tracks; + gchar *artist_uuid; + GList *tracks; gboolean has_album_art; gboolean do_initial_index; @@ -45,12 +47,12 @@ G_DEFINE_TYPE(KotoIndexedAlbum, koto_indexed_album, G_TYPE_OBJECT); enum { PROP_0, - PROP_ARTIST, PROP_UUID, PROP_DO_INITIAL_INDEX, PROP_PATH, PROP_ALBUM_NAME, PROP_ART_PATH, + PROP_ARTIST_UUID, N_PROPERTIES }; @@ -65,14 +67,6 @@ static void koto_indexed_album_class_init(KotoIndexedAlbumClass *c) { gobject_class->set_property = koto_indexed_album_set_property; gobject_class->get_property = koto_indexed_album_get_property; - props[PROP_ARTIST] = g_param_spec_object( - "artist", - "Artist associated with Album", - "Artist associated with Album", - KOTO_TYPE_INDEXED_ARTIST, - G_PARAM_CONSTRUCT_ONLY|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_READWRITE - ); - props[PROP_UUID] = g_param_spec_string( "uuid", "UUID to Album in database", @@ -113,37 +107,36 @@ static void koto_indexed_album_class_init(KotoIndexedAlbumClass *c) { G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_READWRITE ); + props[PROP_ARTIST_UUID] = g_param_spec_string( + "artist-uuid", + "UUID of Artist associated with Album", + "UUID of Artist associated with Album", + NULL, + G_PARAM_CONSTRUCT_ONLY|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_READWRITE + ); + g_object_class_install_properties(gobject_class, N_PROPERTIES, props); } static void koto_indexed_album_init(KotoIndexedAlbum *self) { self->has_album_art = FALSE; + self->tracks = NULL; } -void koto_indexed_album_add_file(KotoIndexedAlbum *self, KotoIndexedTrack *track) { +void koto_indexed_album_add_track(KotoIndexedAlbum *self, KotoIndexedTrack *track) { if (track == NULL) { // Not a file return; } - if (self->tracks == NULL) { // No HashTable yet - self->tracks = g_hash_table_new(g_str_hash, g_str_equal); // Create a new HashTable + gchar *track_uuid; + g_object_get(track, "uuid", &track_uuid, NULL); + + if (g_list_index(self->tracks, track_uuid) == -1) { + self->tracks = g_list_insert_sorted_with_data(self->tracks, track_uuid, koto_indexed_album_sort_tracks, NULL); } - - gchar *track_name; - g_object_get(track, "parsed-name", &track_name, NULL); - - if (g_hash_table_contains(self->tracks, track_name)) { // If we have this file already - g_free(track_name); - return; - } - - g_hash_table_insert(self->tracks, track_name, track); // Add the file } void koto_indexed_album_commit(KotoIndexedAlbum *self) { - gchar *artist_uuid; - g_object_get(self->artist, "uuid", &artist_uuid, NULL); // Get the artist UUID - if (self->art_path == NULL) { // If art_path isn't defined when committing koto_indexed_album_set_album_art(self, ""); // Set to an empty string } @@ -154,7 +147,7 @@ void koto_indexed_album_commit(KotoIndexedAlbum *self) { "ON CONFLICT(id) DO UPDATE SET path=excluded.path, name=excluded.name, art_path=excluded.art_path;", self->uuid, self->path, - artist_uuid, + self->artist_uuid, self->name, self->art_path ); @@ -314,7 +307,7 @@ void koto_indexed_album_find_tracks(KotoIndexedAlbum *self, magic_t magic_cookie KotoIndexedTrack *track = koto_indexed_track_new(self, full_path, cd); if (track != NULL) { // Is a file - koto_indexed_album_add_file(self, track); // Add our file + koto_indexed_album_add_track(self, track); // Add our file } } @@ -326,9 +319,6 @@ static void koto_indexed_album_get_property(GObject *obj, guint prop_id, GValue KotoIndexedAlbum *self = KOTO_INDEXED_ALBUM(obj); switch (prop_id) { - case PROP_ARTIST: - g_value_set_object(val, self->artist); - break; case PROP_UUID: g_value_set_string(val, self->uuid); break; @@ -344,6 +334,38 @@ static void koto_indexed_album_get_property(GObject *obj, guint prop_id, GValue case PROP_ART_PATH: g_value_set_string(val, koto_indexed_album_get_album_art(self)); break; + case PROP_ARTIST_UUID: + g_value_set_string(val, self->artist_uuid); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +static void koto_indexed_album_set_property(GObject *obj, guint prop_id, const GValue *val, GParamSpec *spec){ + KotoIndexedAlbum *self = KOTO_INDEXED_ALBUM(obj); + + switch (prop_id) { + case PROP_UUID: + self->uuid = g_strdup(g_value_get_string(val)); + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_UUID]); + break; + case PROP_DO_INITIAL_INDEX: + self->do_initial_index = g_value_get_boolean(val); + break; + case PROP_PATH: // Path to the album + koto_indexed_album_update_path(self, g_value_get_string(val)); + break; + case PROP_ALBUM_NAME: // Name of album + koto_indexed_album_set_album_name(self, g_value_get_string(val)); + break; + case PROP_ART_PATH: // Path to art + koto_indexed_album_set_album_art(self, g_value_get_string(val)); + break; + case PROP_ARTIST_UUID: + koto_indexed_album_set_artist_uuid(self, g_value_get_string(val)); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); break; @@ -351,15 +373,11 @@ static void koto_indexed_album_get_property(GObject *obj, guint prop_id, GValue } gchar* koto_indexed_album_get_album_art(KotoIndexedAlbum *self) { - return g_strdup((self->has_album_art) ? self->art_path : ""); + return g_strdup((self->has_album_art && (self->art_path != NULL) && (strcmp(self->art_path, "") != 0)) ? self->art_path : ""); } -GList* koto_indexed_album_get_files(KotoIndexedAlbum *self) { - if (self->tracks == NULL) { // No HashTable yet - self->tracks = g_hash_table_new(g_str_hash, g_str_equal); // Create a new HashTable - } - - return g_hash_table_get_values(self->tracks); +GList* koto_indexed_album_get_tracks(KotoIndexedAlbum *self) { + return self->tracks; // Return } void koto_indexed_album_set_album_art(KotoIndexedAlbum *self, const gchar *album_art) { @@ -381,22 +399,9 @@ void koto_indexed_album_remove_file(KotoIndexedAlbum *self, KotoIndexedTrack *tr return; } - if (self->tracks == NULL) { // No HashTable yet - self->tracks = g_hash_table_new(g_str_hash, g_str_equal); // Create a new HashTable - } - - gchar *track_name; - g_object_get(track, "parsed-name", &track_name, NULL); - g_hash_table_remove(self->tracks, track_name); -} - -void koto_indexed_album_remove_file_by_name(KotoIndexedAlbum *self, const gchar *track_name) { - if (track_name == NULL) { - return; - } - - KotoIndexedTrack *track = g_hash_table_lookup(self->tracks, track_name); // Get the files - koto_indexed_album_remove_file(self, track); + gchar *track_uuid; + g_object_get(track, "parsed-name", &track_uuid, NULL); + self->tracks = g_list_remove(self->tracks, track_uuid); } void koto_indexed_album_set_album_name(KotoIndexedAlbum *self, const gchar *album_name) { @@ -411,49 +416,77 @@ void koto_indexed_album_set_album_name(KotoIndexedAlbum *self, const gchar *albu self->name = g_strdup(album_name); } +void koto_indexed_album_set_artist_uuid(KotoIndexedAlbum *self, const gchar *artist_uuid) { + if (artist_uuid == NULL) { + return; + } + + if (self->artist_uuid != NULL) { + g_free(self->artist_uuid); + } + + self->artist_uuid = g_strdup(artist_uuid); +} + void koto_indexed_album_set_as_current_playlist(KotoIndexedAlbum *self) { if (self->tracks == NULL) { // No files to add to the playlist return; } KotoPlaylist *new_album_playlist = koto_playlist_new(); // Create a new playlist + g_object_set(new_album_playlist, "ephemeral", TRUE, NULL); // Set as ephemeral / temporary - GList *tracks_list_uuids = g_hash_table_get_keys(self->tracks); // Get the UUIDs - for (guint i = 0; i < g_list_length(tracks_list_uuids); i++) { // For each of the tracks - koto_playlist_add_track_by_uuid(new_album_playlist, (gchar*) g_list_nth_data(tracks_list_uuids, i)); // Add the UUID + GList *t; + for (t = self->tracks; t != NULL; t = t->next) { // For each of the tracks + koto_playlist_add_track_by_uuid(new_album_playlist, (gchar*) t->data); // Add the UUID } - g_list_free(tracks_list_uuids); // Free the uuids - koto_current_playlist_set_playlist(current_playlist, new_album_playlist); // Set our new current playlist } -static void koto_indexed_album_set_property(GObject *obj, guint prop_id, const GValue *val, GParamSpec *spec){ - KotoIndexedAlbum *self = KOTO_INDEXED_ALBUM(obj); +gint koto_indexed_album_sort_tracks(gconstpointer track1_uuid, gconstpointer track2_uuid, gpointer user_data) { + (void) user_data; + KotoIndexedTrack *track1 = koto_cartographer_get_track_by_uuid(koto_maps, (gchar*) track1_uuid); + KotoIndexedTrack *track2 = koto_cartographer_get_track_by_uuid(koto_maps, (gchar*) track2_uuid); - switch (prop_id) { - case PROP_ARTIST: - self->artist = (KotoIndexedArtist*) g_value_get_object(val); - break; - case PROP_UUID: - self->uuid = g_strdup(g_value_get_string(val)); - g_object_notify_by_pspec(G_OBJECT(self), props[PROP_UUID]); - break; - case PROP_DO_INITIAL_INDEX: - self->do_initial_index = g_value_get_boolean(val); - break; - case PROP_PATH: // Path to the album - koto_indexed_album_update_path(self, g_value_get_string(val)); - break; - case PROP_ALBUM_NAME: // Name of album - koto_indexed_album_set_album_name(self, g_value_get_string(val)); - break; - case PROP_ART_PATH: // Path to art - koto_indexed_album_set_album_art(self, g_value_get_string(val)); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); - break; + if ((track1 == NULL) && (track2 == NULL)) { // Neither tracks actually exist + return 0; + } else if ((track1 != NULL) && (track2 == NULL)) { // Only track2 does not exist + return -1; + } else if ((track1 == NULL) && (track2 != NULL)) { // Only track1 does not exist + return 1; + } + + guint *track1_disc = (guint*) 1; + guint *track2_disc = (guint*) 2; + + g_object_get(track1, "cd", &track1_disc, NULL); + g_object_get(track2, "cd", &track2_disc, NULL); + + if (track1_disc < track2_disc) { // Track 2 is in a later CD / Disc + return -1; + } else if (track1_disc > track2_disc) { // Track1 is later + return 1; + } + + guint16 *track1_pos; + guint16 *track2_pos; + + g_object_get(track1, "position", &track1_pos, NULL); + g_object_get(track2, "position", &track2_pos, NULL); + + if (track1_pos == track2_pos) { // Identical positions (like reported as 0) + gchar *track1_name; + gchar *track2_name; + + g_object_get(track1, "parsed-name", &track1_name, NULL); + g_object_get(track2, "parsed-name", &track2_name, NULL); + + return g_utf8_collate(track1_name, track2_name); + } else if (track1_pos < track2_pos) { + return -1; + } else { + return 1; } } @@ -478,8 +511,11 @@ void koto_indexed_album_update_path(KotoIndexedAlbum *self, const gchar* new_pat } KotoIndexedAlbum* koto_indexed_album_new(KotoIndexedArtist *artist, const gchar *path) { + gchar *artist_uuid = NULL; + g_object_get(artist, "uuid", &artist_uuid, NULL); + KotoIndexedAlbum* album = g_object_new(KOTO_TYPE_INDEXED_ALBUM, - "artist", artist, + "artist-uuid", artist_uuid, "uuid", g_strdup(g_uuid_string_random()), "do-initial-index", TRUE, "path", path, @@ -493,8 +529,11 @@ KotoIndexedAlbum* koto_indexed_album_new(KotoIndexedArtist *artist, const gchar } KotoIndexedAlbum* koto_indexed_album_new_with_uuid(KotoIndexedArtist *artist, const gchar *uuid) { + gchar *artist_uuid = NULL; + g_object_get(artist, "uuid", &artist_uuid, NULL); + return g_object_new(KOTO_TYPE_INDEXED_ALBUM, - "artist", artist, + "artist-uuid", artist, "uuid", g_strdup(uuid), "do-initial-index", FALSE, NULL diff --git a/src/indexer/artist.c b/src/indexer/artist.c index a3b0133..0dfa990 100644 --- a/src/indexer/artist.c +++ b/src/indexer/artist.c @@ -29,7 +29,7 @@ struct _KotoIndexedArtist { gboolean has_artist_art; gchar *artist_name; - GHashTable *albums; + GList *albums; }; G_DEFINE_TYPE(KotoIndexedArtist, koto_indexed_artist, G_TYPE_OBJECT); @@ -108,7 +108,7 @@ void koto_indexed_artist_commit(KotoIndexedArtist *self) { static void koto_indexed_artist_init(KotoIndexedArtist *self) { self->has_artist_art = FALSE; - self->albums = NULL; // Set to null initially maybe + self->albums = NULL; // Create a new GList } static void koto_indexed_artist_get_property(GObject *obj, guint prop_id, GValue *val, GParamSpec *spec) { @@ -130,7 +130,7 @@ static void koto_indexed_artist_get_property(GObject *obj, guint prop_id, GValue } } -static void koto_indexed_artist_set_property(GObject *obj, guint prop_id, const GValue *val, GParamSpec *spec){ +static void koto_indexed_artist_set_property(GObject *obj, guint prop_id, const GValue *val, GParamSpec *spec) { KotoIndexedArtist *self = KOTO_INDEXED_ARTIST(obj); switch (prop_id) { @@ -150,45 +150,20 @@ static void koto_indexed_artist_set_property(GObject *obj, guint prop_id, const } } -void koto_indexed_artist_add_album(KotoIndexedArtist *self, KotoIndexedAlbum *album) { - if (album == NULL) { // No album really defined +void koto_indexed_artist_add_album(KotoIndexedArtist *self, gchar *album_uuid) { + if ((album_uuid == NULL) || (strcmp(album_uuid, "") == 0)) { // No album UUID really defined return; } - if (self->albums == NULL) { // No HashTable yet - self->albums = g_hash_table_new(g_str_hash, g_str_equal); // Create a new HashTable + gchar *uuid = g_strdup(album_uuid); // Duplicate our UUID + + if (g_list_index(self->albums, uuid) == -1) { + self->albums = g_list_append(self->albums, uuid); // Push to end of list } - - gchar *album_uuid; - g_object_get(album, "uuid", &album_uuid, NULL); - - if (album_uuid == NULL) { - g_free(album_uuid); - return; - } - - if (g_hash_table_contains(self->albums, album_uuid)) { // If we have this album already - g_free(album_uuid); - return; - } - - g_hash_table_insert(self->albums, album_uuid, album); // Add the album } GList* koto_indexed_artist_get_albums(KotoIndexedArtist *self) { - if (self->albums == NULL) { // No HashTable yet - self->albums = g_hash_table_new(g_str_hash, g_str_equal); // Create a new HashTable - } - - return g_hash_table_get_values(self->albums); -} - -KotoIndexedAlbum* koto_indexed_artist_get_album(KotoIndexedArtist *self, gchar *album_uuid) { - if (self->albums == NULL) { - return NULL; - } - - return g_hash_table_lookup(self->albums, album_uuid); + return g_list_copy(self->albums); } void koto_indexed_artist_remove_album(KotoIndexedArtist *self, KotoIndexedAlbum *album) { @@ -196,14 +171,9 @@ void koto_indexed_artist_remove_album(KotoIndexedArtist *self, KotoIndexedAlbum return; } - if (self->albums == NULL) { // No HashTable yet - self->albums = g_hash_table_new(g_str_hash, g_str_equal); // Create a new HashTable - } - gchar *album_uuid; g_object_get(album, "uuid", &album_uuid, NULL); - - g_hash_table_remove(self->albums, album_uuid); + self->albums = g_list_remove(self->albums, album_uuid); } void koto_indexed_artist_update_path(KotoIndexedArtist *self, const gchar *new_path) { diff --git a/src/indexer/file-indexer.c b/src/indexer/file-indexer.c index 5ead789..f88df44 100644 --- a/src/indexer/file-indexer.c +++ b/src/indexer/file-indexer.c @@ -20,10 +20,12 @@ #include #include #include +#include "../db/cartographer.h" #include "../db/db.h" -#include "structs.h" #include "../koto-utils.h" +#include "structs.h" +extern KotoCartographer *koto_maps; extern sqlite3 *koto_db; struct _KotoIndexedLibrary { @@ -76,14 +78,15 @@ void koto_indexed_library_add_artist(KotoIndexedLibrary *self, KotoIndexedArtist koto_indexed_library_get_artists(self); // Call to generate if needed gchar *artist_name; - g_object_get(artist, "name", &artist_name, NULL); + gchar *artist_uuid; + g_object_get(artist, "name", &artist_name, "uuid", &artist_uuid, NULL); if (g_hash_table_contains(self->music_artists, artist_name)) { // Already have the artist g_free(artist_name); return; } - g_hash_table_insert(self->music_artists, artist_name, artist); // Add the artist by its name (this needs to be done so we can get the artist when doing the depth of 2 indexing for the album) + g_hash_table_insert(self->music_artists, artist_name, artist_uuid); // Add the artist by its name (this needs to be done so we can get the artist when doing the depth of 2 indexing for the album) } KotoIndexedArtist* koto_indexed_library_get_artist(KotoIndexedLibrary *self, gchar *artist_name) { @@ -93,7 +96,14 @@ KotoIndexedArtist* koto_indexed_library_get_artist(KotoIndexedLibrary *self, gch koto_indexed_library_get_artists(self); // Call to generate if needed - return g_hash_table_lookup(self->music_artists, (KotoIndexedArtist*) artist_name); + gchar *artist_uuid = g_hash_table_lookup(self->music_artists, artist_name); // Get the UUID from our music artists + + if (artist_uuid != NULL) { + KotoIndexedArtist *artist = koto_cartographer_get_artist_by_uuid(koto_maps, artist_uuid); // Return any artist from cartographer + return artist; + } else { + return NULL; + } } GHashTable* koto_indexed_library_get_artists(KotoIndexedLibrary *self) { @@ -162,10 +172,9 @@ void koto_indexed_library_set_path(KotoIndexedLibrary *self, gchar *path) { } int process_artists(void *data, int num_columns, char **fields, char **column_names) { - (void) num_columns; (void) column_names; // Don't need these - - KotoIndexedLibrary *self = (KotoIndexedLibrary*) data; + (void) num_columns; (void) column_names; // Don't need any of the params + KotoIndexedLibrary *library = (KotoIndexedLibrary*) data; gchar *artist_uuid = g_strdup(koto_utils_unquote_string(fields[0])); // First column is UUID gchar *artist_path = g_strdup(koto_utils_unquote_string(fields[1])); // Second column is path gchar *artist_name = g_strdup(koto_utils_unquote_string(fields[3])); // Fourth column is artist name @@ -177,7 +186,8 @@ int process_artists(void *data, int num_columns, char **fields, char **column_na "name", artist_name, // Set name NULL); - koto_indexed_library_add_artist(self, artist); // Add the artist + koto_cartographer_add_artist(koto_maps, artist); // Add the artist to our global cartographer + koto_indexed_library_add_artist(library, artist); int albums_rc = sqlite3_exec(koto_db, g_strdup_printf("SELECT * FROM albums WHERE artist_id=\"%s\"", artist_uuid), process_albums, artist, NULL); // Process our albums if (albums_rc != SQLITE_OK) { // Failed to get our albums @@ -211,7 +221,8 @@ int process_albums(void *data, int num_columns, char **fields, char **column_nam "art-path", album_art, // Set art path if any NULL); - koto_indexed_artist_add_album(artist, album); // Add the album + koto_cartographer_add_album(koto_maps, album); // Add the album to our global cartographer + koto_indexed_artist_add_album(artist, album_uuid); // Add the album int tracks_rc = sqlite3_exec(koto_db, g_strdup_printf("SELECT * FROM tracks WHERE album_id=\"%s\"", album_uuid), process_tracks, album, NULL); // Process our tracks if (tracks_rc != SQLITE_OK) { // Failed to get our tracks @@ -241,8 +252,8 @@ int process_tracks(void *data, int num_columns, char **fields, char **column_nam gchar *album_uuid = g_strdup(koto_utils_unquote_string(fields[4])); gchar *file_name = g_strdup(koto_utils_unquote_string(fields[5])); gchar *name = g_strdup(koto_utils_unquote_string(fields[6])); - guint *disc_num = (guint*) g_ascii_strtoull(fields[6], NULL, 10); - guint *position = (guint*) g_ascii_strtoull(fields[7], NULL, 10); + guint *disc_num = (guint*) g_ascii_strtoull(fields[7], NULL, 10); + guint *position = (guint*) g_ascii_strtoull(fields[8], NULL, 10); KotoIndexedTrack *track = koto_indexed_track_new_with_uuid(track_uuid); // Create our file g_object_set(track, @@ -255,7 +266,8 @@ int process_tracks(void *data, int num_columns, char **fields, char **column_nam "position", position, NULL); - koto_indexed_album_add_file(album, track); // Add the file + koto_cartographer_add_track(koto_maps, track); // Add the track to cartographer + koto_indexed_album_add_track(album, track); // Add the track g_free(track_uuid); g_free(path); @@ -341,21 +353,25 @@ void index_folder(KotoIndexedLibrary *self, gchar *path, guint depth) { NULL ); + koto_cartographer_add_artist(koto_maps, artist); // Add the artist to cartographer koto_indexed_library_add_artist(self, artist); // Add the artist index_folder(self, full_path, depth); // Index this directory g_free(artist_name); } else if (depth == 2) { // If we are following FOLDER/ARTIST/ALBUM then this would be album gchar *artist_name = g_path_get_basename(path); // Get the last entry from our path which is probably the artist - KotoIndexedArtist *artist = koto_indexed_library_get_artist(self, artist_name); // Get the artist if (artist == NULL) { + g_message("Failed to get artist by name of: %s", artist_name); continue; } KotoIndexedAlbum *album = koto_indexed_album_new(artist, full_path); + koto_cartographer_add_album(koto_maps, album); // Add our album to the cartographer - koto_indexed_artist_add_album(artist, album); // Add the album + gchar *album_uuid = NULL; + g_object_get(album, "uuid", &album_uuid, NULL); + koto_indexed_artist_add_album(artist, album_uuid); // Add the album g_free(artist_name); } } @@ -367,9 +383,13 @@ void index_folder(KotoIndexedLibrary *self, gchar *path, guint depth) { } void output_artists(gpointer artist_key, gpointer artist_ptr, gpointer data) { - (void)artist_key; - (void)data; - KotoIndexedArtist *artist = (KotoIndexedArtist*) artist_ptr; + (void) artist_ptr; (void) data; + KotoIndexedArtist *artist = koto_cartographer_get_artist_by_uuid(koto_maps, (gchar*) artist_key); + + if (artist == NULL) { + return; + } + gchar *artist_name; g_object_get(artist, "name", &artist_name, NULL); g_debug("Artist: %s", artist_name); @@ -382,32 +402,45 @@ void output_artists(gpointer artist_key, gpointer artist_ptr, gpointer data) { GList *a; for (a = albums; a != NULL; a = a->next) { - KotoIndexedAlbum *album = (KotoIndexedAlbum*) a->data; + gchar *album_uuid = a->data; + KotoIndexedAlbum *album = koto_cartographer_get_album_by_uuid(koto_maps, album_uuid); + + if (album == NULL) { + continue; + } + gchar *artwork = koto_indexed_album_get_album_art(album); gchar *album_name; g_object_get(album, "name", &album_name, NULL); g_debug("Album Art: %s", artwork); g_debug("Album Name: %s", album_name); - GList *tracks = koto_indexed_album_get_files(album); // Get the files for the album - GList *t; - - for (t = tracks; t != NULL; t = t->next) { - KotoIndexedTrack *track = (KotoIndexedTrack*) t->data; - gchar *filepath; - gchar *parsed_name; - guint *pos; - - g_object_get(track, - "path", &filepath, - "parsed-name", &parsed_name, - "position", &pos, - NULL); - g_debug("File Path: %s", filepath); - g_debug("Parsed Name: %s", parsed_name); - g_debug("Position: %d", GPOINTER_TO_INT(pos)); - g_free(filepath); - g_free(parsed_name); - } + g_list_foreach(koto_indexed_album_get_tracks(album), output_track, NULL); } } + +void output_track(gpointer data, gpointer user_data) { + (void) user_data; + + g_message("Track UUID: %s", g_strdup(data)); + KotoIndexedTrack *track = koto_cartographer_get_track_by_uuid(koto_maps, (gchar*) data); + + if (track == NULL) { + return; + } + + gchar *filepath; + gchar *parsed_name; + guint *pos; + + g_object_get(track, + "path", &filepath, + "parsed-name", &parsed_name, + "position", &pos, + NULL); + g_debug("File Path: %s", filepath); + g_debug("Parsed Name: %s", parsed_name); + g_debug("Position: %d", GPOINTER_TO_INT(pos)); + g_free(filepath); + g_free(parsed_name); +} diff --git a/src/indexer/structs.h b/src/indexer/structs.h index 9cb1370..3224402 100644 --- a/src/indexer/structs.h +++ b/src/indexer/structs.h @@ -36,6 +36,7 @@ G_DECLARE_FINAL_TYPE (KotoIndexedAlbum, koto_indexed_album, KOTO, INDEXED_ALBUM, #define KOTO_TYPE_INDEXED_TRACK koto_indexed_track_get_type() G_DECLARE_FINAL_TYPE(KotoIndexedTrack, koto_indexed_track, KOTO, INDEXED_TRACK, GObject); +#define KOTO_IS_INDEXED_TRACK(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_INDEXED_TRACK)) /** * Library Functions @@ -54,6 +55,7 @@ int process_tracks(void *data, int num_columns, char **fields, char **column_nam void read_from_db(KotoIndexedLibrary *self); void start_indexing(KotoIndexedLibrary *self); void index_folder(KotoIndexedLibrary *self, gchar *path, guint depth); +void output_track(gpointer data, gpointer user_data); /** * Artist Functions @@ -62,11 +64,10 @@ void index_folder(KotoIndexedLibrary *self, gchar *path, guint depth); KotoIndexedArtist* koto_indexed_artist_new(const gchar *path); KotoIndexedArtist* koto_indexed_artist_new_with_uuid(const gchar *uuid); -void koto_indexed_artist_add_album(KotoIndexedArtist *self, KotoIndexedAlbum *album); +void koto_indexed_artist_add_album(KotoIndexedArtist *self, gchar *album_uuid); void koto_indexed_artist_commit(KotoIndexedArtist *self); guint koto_indexed_artist_find_album_with_name(gconstpointer *album_data, gconstpointer *album_name_data); GList* koto_indexed_artist_get_albums(KotoIndexedArtist *self); -KotoIndexedAlbum* koto_indexed_artist_get_album(KotoIndexedArtist *self, gchar *album_uuid); void koto_indexed_artist_remove_album(KotoIndexedArtist *self, KotoIndexedAlbum *album); void koto_indexed_artist_remove_album_by_name(KotoIndexedArtist *self, gchar *album_name); void koto_indexed_artist_set_artist_name(KotoIndexedArtist *self, const gchar *artist_name); @@ -80,18 +81,19 @@ void output_artists(gpointer artist_key, gpointer artist_ptr, gpointer data); KotoIndexedAlbum* koto_indexed_album_new(KotoIndexedArtist *artist, const gchar *path); KotoIndexedAlbum* koto_indexed_album_new_with_uuid(KotoIndexedArtist *artist, const gchar *uuid); -void koto_indexed_album_add_file(KotoIndexedAlbum *self, KotoIndexedTrack *track); +void koto_indexed_album_add_track(KotoIndexedAlbum *self, KotoIndexedTrack *track); void koto_indexed_album_commit(KotoIndexedAlbum *self); void koto_indexed_album_find_album_art(KotoIndexedAlbum *self); void koto_indexed_album_find_tracks(KotoIndexedAlbum *self, magic_t magic_cookie, const gchar *path); gchar* koto_indexed_album_get_album_art(KotoIndexedAlbum *self); -GList* koto_indexed_album_get_files(KotoIndexedAlbum *self); +GList* koto_indexed_album_get_tracks(KotoIndexedAlbum *self); void koto_indexed_album_remove_file(KotoIndexedAlbum *self, KotoIndexedTrack *track); -void koto_indexed_album_remove_file_by_name(KotoIndexedAlbum *self, const gchar *track_name); void koto_indexed_album_set_album_art(KotoIndexedAlbum *self, const gchar *album_art); void koto_indexed_album_set_album_name(KotoIndexedAlbum *self, const gchar *album_name); +void koto_indexed_album_set_artist_uuid(KotoIndexedAlbum *self, const gchar *artist_uuid); void koto_indexed_album_set_as_current_playlist(KotoIndexedAlbum *self); void koto_indexed_album_update_path(KotoIndexedAlbum *self, const gchar *path); +gint koto_indexed_album_sort_tracks(gconstpointer track1_uuid, gconstpointer track2_uuid, gpointer user_data); /** * File / Track Functions @@ -102,6 +104,7 @@ KotoIndexedTrack* koto_indexed_track_new_with_uuid(const gchar *uuid); void koto_indexed_track_commit(KotoIndexedTrack *self); void koto_indexed_track_parse_name(KotoIndexedTrack *self); +void koto_indexed_track_save_to_playlist(KotoIndexedTrack *self, gchar *playlist_uuid, guint position, gint current); void koto_indexed_track_set_file_name(KotoIndexedTrack *self, gchar *new_file_name); void koto_indexed_track_set_cd(KotoIndexedTrack *self, guint cd); void koto_indexed_track_set_parsed_name(KotoIndexedTrack *self, gchar *new_parsed_name); diff --git a/src/indexer/track.c b/src/indexer/track.c index fc26068..476de59 100644 --- a/src/indexer/track.c +++ b/src/indexer/track.c @@ -18,9 +18,11 @@ #include #include #include +#include "../db/cartographer.h" #include "structs.h" #include "koto-utils.h" +extern KotoCartographer *koto_maps; extern sqlite3 *koto_db; struct _KotoIndexedTrack { @@ -32,10 +34,9 @@ struct _KotoIndexedTrack { gchar *file_name; gchar *parsed_name; - gchar *artist; - gchar *album; guint *cd; guint *position; + guint *playback_position; gboolean acquired_metadata_from_id3; gboolean do_initial_index; @@ -49,13 +50,12 @@ enum { PROP_ALBUM_UUID, PROP_UUID, PROP_DO_INITIAL_INDEX, - PROP_ARTIST, - PROP_ALBUM, PROP_PATH, PROP_FILE_NAME, PROP_PARSED_NAME, PROP_CD, PROP_POSITION, + PROP_PLAYBACK_POSITION, N_PROPERTIES }; @@ -110,22 +110,6 @@ static void koto_indexed_track_class_init(KotoIndexedTrackClass *c) { G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_READWRITE ); - props[PROP_ARTIST] = g_param_spec_string( - "artist", - "Name of Artist", - "Name of Artist", - NULL, - G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_READWRITE - ); - - props[PROP_ALBUM] = g_param_spec_string( - "album", - "Name of Album", - "Name of Album", - NULL, - G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_READWRITE - ); - props[PROP_FILE_NAME] = g_param_spec_string( "file-name", "Name of File", @@ -162,6 +146,16 @@ static void koto_indexed_track_class_init(KotoIndexedTrackClass *c) { G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_READWRITE ); + props[PROP_PLAYBACK_POSITION] = g_param_spec_uint( + "playback-position", + "Current playback position", + "Current playback position", + 0, + G_MAXUINT16, + 0, + G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_READWRITE + ); + g_object_class_install_properties(gobject_class, N_PROPERTIES, props); } @@ -182,12 +176,6 @@ static void koto_indexed_track_get_property(GObject *obj, guint prop_id, GValue case PROP_UUID: g_value_set_string(val, self->uuid); break; - case PROP_ARTIST: - g_value_set_string(val, self->artist); - break; - case PROP_ALBUM: - g_value_set_string(val, self->album); - break; case PROP_PATH: g_value_set_string(val, self->path); break; @@ -203,6 +191,9 @@ static void koto_indexed_track_get_property(GObject *obj, guint prop_id, GValue case PROP_POSITION: g_value_set_uint(val, GPOINTER_TO_UINT(self->position)); break; + case PROP_PLAYBACK_POSITION: + g_value_set_uint(val, GPOINTER_TO_UINT(self->playback_position)); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); break; @@ -228,12 +219,6 @@ static void koto_indexed_track_set_property(GObject *obj, guint prop_id, const G case PROP_DO_INITIAL_INDEX: self->do_initial_index = g_value_get_boolean(val); break; - case PROP_ARTIST: - self->artist = g_strdup(g_value_get_string(val)); - break; - case PROP_ALBUM: - self->album = g_strdup(g_value_get_string(val)); - break; case PROP_PATH: koto_indexed_track_update_path(self, g_value_get_string(val)); // Update the path break; @@ -249,6 +234,9 @@ static void koto_indexed_track_set_property(GObject *obj, guint prop_id, const G case PROP_POSITION: koto_indexed_track_set_position(self, g_value_get_uint(val)); break; + case PROP_PLAYBACK_POSITION: + self->playback_position = GUINT_TO_POINTER(g_value_get_uint(val)); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); break; @@ -293,14 +281,22 @@ void koto_indexed_track_commit(KotoIndexedTrack *self) { void koto_indexed_track_parse_name(KotoIndexedTrack *self) { gchar *copied_file_name = g_strdelimit(g_strdup(self->file_name), "_", ' '); // Replace _ with whitespace for starters - if (self->artist != NULL && strcmp(self->artist, "") != 0) { // If we have artist set - gchar **split = g_strsplit(copied_file_name, self->artist, -1); // Split whenever we encounter the artist - copied_file_name = g_strjoinv("", split); // Remove the artist - g_strfreev(split); + KotoIndexedArtist *artist = NULL; + artist = koto_cartographer_get_artist_by_uuid(koto_maps, self->artist_uuid); - split = g_strsplit(copied_file_name, g_utf8_strdown(self->artist, -1), -1); // Lowercase album name and split by that - copied_file_name = g_strjoinv("", split); // Remove the artist - g_strfreev(split); + if (artist != NULL) { // If we have artist + gchar *artist_name = NULL; + g_object_get(artist, "name", &artist_name, NULL); + + if (artist_name != NULL && (strcmp(artist_name, "") != 0)) { + gchar **split = g_strsplit(copied_file_name, artist_name, -1); // Split whenever we encounter the artist + copied_file_name = g_strjoinv("", split); // Remove the artist + g_strfreev(split); + + split = g_strsplit(copied_file_name, g_utf8_strdown(artist_name, -1), -1); // Lowercase album name and split by that + copied_file_name = g_strjoinv("", split); // Remove the artist + g_strfreev(split); + } } gchar *file_without_ext = koto_utils_get_filename_without_extension(copied_file_name); @@ -339,6 +335,28 @@ void koto_indexed_track_parse_name(KotoIndexedTrack *self) { g_free(file_without_ext); } +void koto_indexed_track_save_to_playlist(KotoIndexedTrack *self, gchar *playlist_uuid, guint position, gint current) { + gchar *commit_op = g_strdup_printf( + "INSERT INTO playlist_tracks(playlist_id, track_id, position, current)" + "VALUES('%s', '%s', quote(\"%d\"), quote(\"%d\")" + "ON CONFLICT(playlist_id, track_id) DO UPDATE SET position=excluded.position, current=excluded.current;", + playlist_uuid, + self->uuid, + position, + current + ); + + gchar *commit_op_errmsg = NULL; + int rc = sqlite3_exec(koto_db, commit_op, 0, 0, &commit_op_errmsg); + + if (rc != SQLITE_OK) { + g_warning("Failed to save track to playlist: %s", commit_op_errmsg); + } + + g_free(commit_op); + g_free(commit_op_errmsg); +} + void koto_indexed_track_set_file_name(KotoIndexedTrack *self, gchar *new_file_name) { if (new_file_name == NULL) { return; @@ -402,9 +420,8 @@ void koto_indexed_track_update_metadata(KotoIndexedTrack *self) { self->acquired_metadata_from_id3 = TRUE; TagLib_Tag *tag = taglib_file_tag(t_file); // Get our tag koto_indexed_track_set_parsed_name(self, taglib_tag_title(tag)); // Set the title of the file - self->artist = g_strdup(taglib_tag_artist((tag))); // Set the artist - self->album = g_strdup(taglib_tag_album(tag)); // Set the album koto_indexed_track_set_position(self, (uint) taglib_tag_track(tag)); // Get the track, convert to uint and cast as a pointer + koto_indexed_track_set_file_name(self, g_path_get_basename(self->path)); // Update our file name } else { koto_indexed_track_set_file_name(self, g_path_get_basename(self->path)); // Update our file name } @@ -432,29 +449,14 @@ void koto_indexed_track_update_path(KotoIndexedTrack *self, const gchar *new_pat } KotoIndexedTrack* koto_indexed_track_new(KotoIndexedAlbum *album, const gchar *path, guint *cd) { - KotoIndexedArtist *artist; - gchar *artist_uuid; - gchar *artist_name; gchar *album_uuid; - gchar *album_name; - g_object_get(album, "artist", &artist, NULL); // Get the artist from our Album - g_object_get(artist, - "name", &artist_name, - "uuid", &artist_uuid, // Get the artist UUID from the artist - NULL); - - g_object_get(album, - "name", &album_name, - "uuid", &album_uuid, // Get the album UUID from the album - NULL); + g_object_get(album, "artist-uuid", &artist_uuid, "uuid", &album_uuid, NULL); // Get the artist and album uuids from our Album KotoIndexedTrack *track = g_object_new(KOTO_TYPE_INDEXED_TRACK, "artist-uuid", artist_uuid, "album-uuid", album_uuid, - "artist", artist_name, - "album", album_name, "do-initial-index", TRUE, "uuid", g_uuid_string_random(), "path", path, diff --git a/src/koto-button.c b/src/koto-button.c index 7af5dc7..b45e475 100644 --- a/src/koto-button.c +++ b/src/koto-button.c @@ -338,6 +338,10 @@ void koto_button_set_text(KotoButton *self, gchar *text) { } void koto_button_show_image(KotoButton *self, gboolean use_alt) { + if (!KOTO_IS_BUTTON(self)) { + return; + } + if (use_alt && ((self->alt_icon_name == NULL) || (strcmp(self->alt_icon_name, "") == 0))) { // Don't have an alt icon set return; } else if (!use_alt && ((self->icon_name == NULL) || (strcmp(self->icon_name, "") == 0))) { // Don't have icon set diff --git a/src/koto-button.h b/src/koto-button.h index d839709..f99015c 100644 --- a/src/koto-button.h +++ b/src/koto-button.h @@ -36,8 +36,8 @@ typedef enum { #define NUM_BUILTIN_SIZES 7 #define KOTO_TYPE_BUTTON (koto_button_get_type()) - G_DECLARE_FINAL_TYPE (KotoButton, koto_button, KOTO, BUTTON, GtkBox) +#define KOTO_IS_BUTTON(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_BUTTON)) guint koto_get_pixbuf_size(KotoButtonPixbufSize size); diff --git a/src/koto-expander.c b/src/koto-expander.c index 1201137..0a95ae0 100644 --- a/src/koto-expander.c +++ b/src/koto-expander.c @@ -58,8 +58,7 @@ static void koto_expander_get_property(GObject *obj, guint prop_id, GValue *val, static void koto_expander_set_property(GObject *obj, guint prop_id, const GValue *val, GParamSpec *spec); static void koto_expander_class_init(KotoExpanderClass *c) { - GObjectClass *gobject_class; - gobject_class = G_OBJECT_CLASS(c); + GObjectClass *gobject_class = G_OBJECT_CLASS(c); gobject_class->set_property = koto_expander_set_property; gobject_class->get_property = koto_expander_get_property; diff --git a/src/koto-playerbar.c b/src/koto-playerbar.c index d2524dd..72460e6 100644 --- a/src/koto-playerbar.c +++ b/src/koto-playerbar.c @@ -15,14 +15,19 @@ * limitations under the License. */ +#include #include +#include "playback/engine.h" #include "koto-button.h" #include "koto-config.h" #include "koto-playerbar.h" +extern KotoPlaybackEngine *playback_engine; + struct _KotoPlayerBar { GObject parent_instance; GtkWidget *main; + GtkWidget *controls; /* Sections */ GtkWidget *playback_section; @@ -39,6 +44,8 @@ struct _KotoPlayerBar { KotoButton *eq_button; GtkWidget *volume_button; + GtkWidget *progress_bar; + /* Selected Playback Section */ GtkWidget *playback_details_section; @@ -65,23 +72,38 @@ static void koto_playerbar_class_init(KotoPlayerBarClass *c) { static void koto_playerbar_constructed(GObject *obj) { KotoPlayerBar *self = KOTO_PLAYERBAR(obj); - self->main = gtk_center_box_new(); - gtk_center_box_set_baseline_position(GTK_CENTER_BOX(self->main), GTK_BASELINE_POSITION_CENTER); + self->main = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + self->progress_bar = gtk_scale_new_with_range(GTK_ORIENTATION_HORIZONTAL, 0, 120, 1); // Default to 120 as random max + gtk_scale_set_draw_value(GTK_SCALE(self->progress_bar), FALSE); + gtk_scale_set_digits(GTK_SCALE(self->progress_bar), 0); + gtk_range_set_increments(GTK_RANGE(self->progress_bar), 1, 1); + + self->controls = gtk_center_box_new(); + gtk_center_box_set_baseline_position(GTK_CENTER_BOX(self->controls), GTK_BASELINE_POSITION_CENTER); gtk_widget_add_css_class(self->main, "player-bar"); self->playback_section = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); self->primary_controls_section = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); self->secondary_controls_section = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); - gtk_center_box_set_start_widget(GTK_CENTER_BOX(self->main), GTK_WIDGET(self->primary_controls_section)); - gtk_center_box_set_center_widget(GTK_CENTER_BOX(self->main), GTK_WIDGET(self->playback_section)); - gtk_center_box_set_end_widget(GTK_CENTER_BOX(self->main), GTK_WIDGET(self->secondary_controls_section)); + gtk_center_box_set_start_widget(GTK_CENTER_BOX(self->controls), GTK_WIDGET(self->primary_controls_section)); + gtk_center_box_set_center_widget(GTK_CENTER_BOX(self->controls), GTK_WIDGET(self->playback_section)); + gtk_center_box_set_end_widget(GTK_CENTER_BOX(self->controls), GTK_WIDGET(self->secondary_controls_section)); koto_playerbar_create_playback_details(self); koto_playerbar_create_primary_controls(self); koto_playerbar_create_secondary_controls(self); + gtk_box_prepend(GTK_BOX(self->main), self->progress_bar); + gtk_box_append(GTK_BOX(self->main), self->controls); + gtk_widget_set_hexpand(GTK_WIDGET(self->main), TRUE); + + // Set up the bindings + + g_signal_connect(playback_engine, "duration-changed", G_CALLBACK(koto_playerbar_handle_duration_change), self); + g_signal_connect(playback_engine, "play-state-changed", G_CALLBACK(koto_playerbar_handle_engine_state_change), self); + g_signal_connect(playback_engine, "progress-changed", G_CALLBACK(koto_playerbar_handle_progress_change), self); } static void koto_playerbar_init(KotoPlayerBar *self) { @@ -139,6 +161,19 @@ void koto_playerbar_create_primary_controls(KotoPlayerBar* bar) { if (bar->forward_button != NULL) { gtk_box_append(GTK_BOX(bar->primary_controls_section), GTK_WIDGET(bar->forward_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)); + + 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) { @@ -170,6 +205,83 @@ void koto_playerbar_create_secondary_controls(KotoPlayerBar* bar) { } } +void koto_playerbar_go_backwards(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data) { + (void) gesture; (void) n_press; (void) x; (void) y; (void) data; + + koto_playback_engine_backwards(playback_engine); +} + +void koto_playerbar_go_forwards(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data) { + (void) gesture; (void) n_press; (void) x; (void) y; (void) data; + + koto_playback_engine_forwards(playback_engine); +} + +void koto_playerbar_handle_duration_change(KotoPlaybackEngine *engine, gpointer user_data) { + if (!KOTO_IS_PLAYBACK_ENGINE(engine)) { + return; + } + + KotoPlayerBar *bar = user_data; + koto_playerbar_set_progressbar_duration(bar, koto_playback_engine_get_duration(engine)); +} + +void koto_playerbar_handle_engine_state_change(KotoPlaybackEngine *engine, gpointer user_data) { + if (!KOTO_IS_PLAYBACK_ENGINE(engine)) { + return; + } + + KotoPlayerBar *bar = user_data; + + if (!KOTO_IS_PLAYERBAR(bar)) { + return; + } + + GstState new_state = koto_playback_engine_get_state(playback_engine); + + if (new_state == GST_STATE_PLAYING) { // Now playing + koto_button_show_image(bar->play_pause_button, TRUE); // Set to TRUE to show pause as the next action + } else if (new_state == GST_STATE_PAUSED) { // Now paused + koto_button_show_image(bar->play_pause_button, FALSE); // Set to FALSE to show play as the next action + } +} + +void koto_playerbar_handle_progress_change(KotoPlaybackEngine *engine, gpointer user_data) { + if (!KOTO_IS_PLAYBACK_ENGINE(engine)) { + return; + } + + KotoPlayerBar *bar = user_data; + koto_playerbar_set_progressbar_value(bar, koto_playback_engine_get_progress(engine)); +} + +void koto_playerbar_reset_progressbar(KotoPlayerBar* bar) { + gtk_range_set_range(GTK_RANGE(bar->progress_bar), 0, 0); // Reset range + gtk_range_set_value(GTK_RANGE(bar->progress_bar), 0); // Set value to 0 +} + +void koto_playerbar_set_progressbar_duration(KotoPlayerBar* bar, gint64 duration) { + if (duration < 0) { + return; + } + + gtk_range_set_range(GTK_RANGE(bar->progress_bar), 0, duration); +} + +void koto_playerbar_set_progressbar_value(KotoPlayerBar* bar, gint64 progress) { + if (progress < 0) { + return; + } + + gtk_range_set_value(GTK_RANGE(bar->progress_bar), progress); +} + +void koto_playerbar_toggle_play_pause(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data) { + (void) gesture; (void) n_press; (void) x; (void) y; (void) data; + + koto_playback_engine_toggle(playback_engine); +} + GtkWidget* koto_playerbar_get_main(KotoPlayerBar* bar) { return bar->main; } diff --git a/src/koto-playerbar.h b/src/koto-playerbar.h index 140bd24..5ad9300 100644 --- a/src/koto-playerbar.h +++ b/src/koto-playerbar.h @@ -16,18 +16,29 @@ */ #pragma once +#include #include +#include "playback/engine.h" G_BEGIN_DECLS #define KOTO_TYPE_PLAYERBAR (koto_playerbar_get_type()) - G_DECLARE_FINAL_TYPE (KotoPlayerBar, koto_playerbar, KOTO, PLAYERBAR, GObject) +#define KOTO_IS_PLAYERBAR(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_PLAYERBAR)) KotoPlayerBar* koto_playerbar_new (void); GtkWidget* koto_playerbar_get_main(KotoPlayerBar* bar); void koto_playerbar_create_playback_details(KotoPlayerBar* bar); void koto_playerbar_create_primary_controls(KotoPlayerBar* bar); void koto_playerbar_create_secondary_controls(KotoPlayerBar* bar); +void koto_playerbar_go_backwards(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data); +void koto_playerbar_go_forwards(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data); +void koto_playerbar_handle_duration_change(KotoPlaybackEngine *engine, gpointer user_data); +void koto_playerbar_handle_engine_state_change(KotoPlaybackEngine *engine, gpointer user_data); +void koto_playerbar_handle_progress_change(KotoPlaybackEngine *engine, gpointer user_data); +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); G_END_DECLS diff --git a/src/koto-window.c b/src/koto-window.c index 38914ad..e94e73c 100644 --- a/src/koto-window.c +++ b/src/koto-window.c @@ -18,6 +18,7 @@ #include #include "indexer/structs.h" #include "pages/music/music-local.h" +#include "playback/engine.h" #include "playlist/current.h" #include "koto-config.h" #include "koto-nav.h" @@ -25,6 +26,7 @@ #include "koto-window.h" extern KotoCurrentPlaylist *current_playlist; +extern KotoPlaybackEngine *playback_engine; struct _KotoWindow { GtkApplicationWindow parent_instance; @@ -51,6 +53,7 @@ static void koto_window_class_init (KotoWindowClass *klass) { static void koto_window_init (KotoWindow *self) { current_playlist = koto_current_playlist_new(); + playback_engine = koto_playback_engine_new(); GtkCssProvider* provider = gtk_css_provider_new(); gtk_css_provider_load_from_resource(provider, "/com/github/joshstrobl/koto/style.css"); diff --git a/src/main.c b/src/main.c index 9d431dd..76a067e 100644 --- a/src/main.c +++ b/src/main.c @@ -16,6 +16,7 @@ */ #include +#include #include "db/cartographer.h" #include "db/db.h" @@ -53,6 +54,8 @@ int main (int argc, char *argv[]) { textdomain (GETTEXT_PACKAGE); gtk_init(); + gst_init(&argc, &argv); + koto_maps = koto_cartographer_new(); // Create our new cartographer and their collection of maps open_db(); // Open our database diff --git a/src/meson.build b/src/meson.build index 1726e50..84d10f2 100644 --- a/src/meson.build +++ b/src/meson.build @@ -11,6 +11,7 @@ koto_sources = [ 'pages/music/artist-view.c', 'pages/music/disc-view.c', 'pages/music/music-local.c', + 'playback/engine.c', 'playlist/current.c', 'playlist/playlist.c', 'main.c', @@ -26,6 +27,8 @@ koto_sources = [ koto_deps = [ dependency('glib-2.0', version: '>= 2.66'), dependency('gio-2.0', version: '>= 2.66'), + dependency('gstreamer-1.0', version: '>= 1.18'), + dependency('gstreamer-player-1.0', version: '>= 1.18'), dependency('gtk4', version: '>= 4.0'), dependency('libmagic', version: '>=5.39'), dependency('sqlite3', version: '>=3.34'), diff --git a/src/pages/music/album-view.c b/src/pages/music/album-view.c index 603dec9..57f1118 100644 --- a/src/pages/music/album-view.c +++ b/src/pages/music/album-view.c @@ -17,13 +17,16 @@ #include #include -#include "../../koto-button.h" +#include "../../db/cartographer.h" #include "../../indexer/structs.h" +#include "../../koto-button.h" #include "album-view.h" #include "disc-view.h" #include "koto-config.h" #include "koto-utils.h" +extern KotoCartographer *koto_maps; + struct _KotoAlbumView { GObject parent_instance; KotoIndexedAlbum *album; @@ -176,10 +179,15 @@ void koto_album_view_set_album(KotoAlbumView *self, KotoIndexedAlbum *album) { gtk_box_prepend(GTK_BOX(self->album_tracks_box), self->album_label); // Prepend our new label to the album + tracks box GHashTable *discs = g_hash_table_new(g_str_hash, g_str_equal); - GList *tracks = koto_indexed_album_get_files(album); // Get the tracks for this album + GList *tracks = koto_indexed_album_get_tracks(album); // Get the tracks for this album for (guint i = 0; i < g_list_length(tracks); i++) { - KotoIndexedTrack *track = (KotoIndexedTrack*) g_list_nth_data(tracks, i); // Get the + KotoIndexedTrack *track = koto_cartographer_get_track_by_uuid(koto_maps, (gchar*) g_list_nth_data(tracks, i)); // Get the track by its UUID + + if (track == NULL) { // Track doesn't exist + continue; + } + guint *disc_number; g_object_get(track, "cd", &disc_number, NULL); gchar *disc_num_as_str = g_strdup_printf("%u", GPOINTER_TO_UINT(disc_number)); diff --git a/src/pages/music/artist-view.c b/src/pages/music/artist-view.c index 8ad4501..8cdcf48 100644 --- a/src/pages/music/artist-view.c +++ b/src/pages/music/artist-view.c @@ -17,12 +17,15 @@ #include #include +#include "../../db/cartographer.h" #include "../../indexer/structs.h" #include "album-view.h" #include "artist-view.h" #include "koto-config.h" #include "koto-utils.h" +extern KotoCartographer *koto_maps; + struct _KotoArtistView { GObject parent_instance; KotoIndexedArtist *artist; @@ -160,8 +163,11 @@ void koto_artist_view_add_artist(KotoArtistView *self, KotoIndexedArtist *artist GList *a; for (a = albums; a != NULL; a = a->next) { - KotoIndexedAlbum *album = (KotoIndexedAlbum*) a->data; - koto_artist_view_add_album(self, album); // Add the album + KotoIndexedAlbum *album = koto_cartographer_get_album_by_uuid(koto_maps, (gchar*) a->data); + + if (album != NULL) { + koto_artist_view_add_album(self, album); // Add the album + } } } diff --git a/src/pages/music/disc-view.c b/src/pages/music/disc-view.c index 99fcbda..9071262 100644 --- a/src/pages/music/disc-view.c +++ b/src/pages/music/disc-view.c @@ -16,10 +16,13 @@ */ #include +#include "../../db/cartographer.h" #include "../../indexer/structs.h" #include "../../koto-track-item.h" #include "disc-view.h" +extern KotoCartographer *koto_maps; + struct _KotoDiscView { GtkBox parent_instance; KotoIndexedAlbum *album; @@ -138,25 +141,26 @@ void koto_disc_view_set_album(KotoDiscView *self, KotoIndexedAlbum *album) { self->list = gtk_list_box_new(); // Create our list of our tracks gtk_list_box_set_selection_mode(GTK_LIST_BOX(self->list), GTK_SELECTION_MULTIPLE); - gtk_list_box_set_sort_func(GTK_LIST_BOX(self->list), koto_album_view_sort_tracks, NULL, NULL); // Ensure we can sort our tracks gtk_widget_add_css_class(self->list, "track-list"); gtk_widget_set_size_request(self->list, 600, -1); gtk_box_append(GTK_BOX(self), self->list); - GList *t; - for (t = koto_indexed_album_get_files(self->album); t != NULL; t = t->next) { // For each file / track - KotoIndexedTrack *track = (KotoIndexedTrack*) t->data; + g_list_foreach(koto_indexed_album_get_tracks(self->album), koto_disc_view_list_tracks, self); +} - guint *disc_number; - g_object_get(track, "cd", &disc_number, NULL); // get the disc number +void koto_disc_view_list_tracks(gpointer data, gpointer selfptr) { + KotoDiscView *self = (KotoDiscView*) selfptr; + KotoIndexedTrack *track = koto_cartographer_get_track_by_uuid(koto_maps, (gchar*) data); // Get the track by its UUID - if (GPOINTER_TO_UINT(self->disc_number) != GPOINTER_TO_UINT(disc_number)) { // Track does not belong to this CD - continue; - } + guint *disc_number; + g_object_get(track, "cd", &disc_number, NULL); // get the disc number - KotoTrackItem *track_item = koto_track_item_new(track); // Create our new track item - gtk_list_box_prepend(GTK_LIST_BOX(self->list), GTK_WIDGET(track_item)); // Add to our tracks list box + if (GPOINTER_TO_UINT(self->disc_number) != GPOINTER_TO_UINT(disc_number)) { // Track does not belong to this CD + return; } + + KotoTrackItem *track_item = koto_track_item_new(track); // Create our new track item + gtk_list_box_append(GTK_LIST_BOX(self->list), GTK_WIDGET(track_item)); // Add to our tracks list box } void koto_disc_view_set_disc_number(KotoDiscView *self, guint disc_number) { @@ -176,38 +180,6 @@ void koto_disc_view_set_disc_label_visible(KotoDiscView *self, gboolean visible) (visible) ? gtk_widget_show(self->header) : gtk_widget_hide(self->header); } -int koto_album_view_sort_tracks(GtkListBoxRow *track1, GtkListBoxRow *track2, gpointer user_data) { - (void) user_data; - KotoTrackItem *track1_item = KOTO_TRACK_ITEM(gtk_list_box_row_get_child(track1)); - KotoTrackItem *track2_item = KOTO_TRACK_ITEM(gtk_list_box_row_get_child(track2)); - - KotoIndexedTrack *track1_file; - KotoIndexedTrack *track2_file; - - g_object_get(track1_item, "track", &track1_file, NULL); - g_object_get(track2_item, "track", &track2_file, NULL); - - guint16 *track1_pos; - guint16 *track2_pos; - - g_object_get(track1_file, "position", &track1_pos, NULL); - g_object_get(track2_file, "position", &track2_pos, NULL); - - if (track1_pos == track2_pos) { // Identical positions (like reported as 0) - gchar *track1_name; - gchar *track2_name; - - g_object_get(track1_file, "parsed-name", &track1_name, NULL); - g_object_get(track2_file, "parsed-name", &track2_name, NULL); - - return g_utf8_collate(track1_name, track2_name); - } else if (track1_pos < track2_pos) { - return -1; - } else { - return 1; - } -} - KotoDiscView* koto_disc_view_new(KotoIndexedAlbum *album, guint *disc_number) { return g_object_new(KOTO_TYPE_DISC_VIEW, "disc", disc_number, diff --git a/src/pages/music/disc-view.h b/src/pages/music/disc-view.h index c392aa2..b2233f6 100644 --- a/src/pages/music/disc-view.h +++ b/src/pages/music/disc-view.h @@ -28,9 +28,9 @@ G_BEGIN_DECLS G_DECLARE_FINAL_TYPE(KotoDiscView, koto_disc_view, KOTO, DISC_VIEW, GtkBox) KotoDiscView* koto_disc_view_new(KotoIndexedAlbum *album, guint *disc); +void koto_disc_view_list_tracks(gpointer data, gpointer selfptr); void koto_disc_view_set_album(KotoDiscView *self, KotoIndexedAlbum *album); void koto_disc_view_set_disc_label_visible(KotoDiscView *self, gboolean visible); void koto_disc_view_set_disc_number(KotoDiscView *self, guint disc_number); -int koto_album_view_sort_tracks(GtkListBoxRow *track1, GtkListBoxRow *track2, gpointer user_data); G_END_DECLS diff --git a/src/pages/music/music-local.c b/src/pages/music/music-local.c index ce5a693..4cd4756 100644 --- a/src/pages/music/music-local.c +++ b/src/pages/music/music-local.c @@ -17,11 +17,14 @@ #include #include +#include "../../db/cartographer.h" #include "../../indexer/structs.h" #include "koto-button.h" #include "koto-config.h" #include "music-local.h" +extern KotoCartographer *koto_maps; + enum { PROP_0, PROP_LIB, @@ -175,8 +178,11 @@ void koto_page_music_local_set_library(KotoPageMusicLocal *self, KotoIndexedLibr g_hash_table_iter_init(&artist_list_iter, artists); while (g_hash_table_iter_next(&artist_list_iter, &artist_key, &artist_data)) { // For each of the music artists - KotoIndexedArtist *artist = (KotoIndexedArtist*) artist_data; // Cast our data as a KotoIndexedArtist - koto_page_music_local_add_artist(self, artist); + KotoIndexedArtist *artist = koto_cartographer_get_artist_by_uuid(koto_maps, (gchar*) artist_data); // Cast our data as a KotoIndexedArtist + + if (artist != NULL) { + koto_page_music_local_add_artist(self, artist); + } } } diff --git a/src/playback/engine.c b/src/playback/engine.c new file mode 100644 index 0000000..97967f5 --- /dev/null +++ b/src/playback/engine.c @@ -0,0 +1,314 @@ +/* engine.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 +#include "../db/cartographer.h" +#include "../playlist/current.h" +#include "../indexer/structs.h" +#include "engine.h" + +enum { + SIGNAL_DURATION_CHANGE, + SIGNAL_PLAY_STATE_CHANGE, + SIGNAL_PROGRESS_CHANGE, + N_SIGNALS +}; + +static glong NS = 1000000000; +static guint playback_engine_signals[N_SIGNALS] = { 0 }; + +extern KotoCartographer *koto_maps; +extern KotoCurrentPlaylist *current_playlist; + +KotoPlaybackEngine *playback_engine; + +struct _KotoPlaybackEngine { + GObject parent_class; + GstElement *player; + GstElement *playbin; + GstElement *suppress_video; + + GstBus *monitor; + + gboolean is_paused; + gboolean is_playing; + gboolean is_muted; + gboolean is_repeat_enabled; + gboolean is_shuffle_enabled; + gboolean requested_playing; + guint playback_position; + gdouble volume; +}; + +struct _KotoPlaybackEngineClass { + GObjectClass parent_class; + + void (* duration_changed) (KotoPlaybackEngine *engine, gint64 duration); + void (* play_state_changed) (KotoPlaybackEngine *engine); + void (* progress_changed) (KotoPlaybackEngine *engine, gint64 progress); +}; + +G_DEFINE_TYPE(KotoPlaybackEngine, koto_playback_engine, G_TYPE_OBJECT); + +static void koto_playback_engine_class_init(KotoPlaybackEngineClass *c) { + c->play_state_changed = NULL; + + GObjectClass *gobject_class; + gobject_class = G_OBJECT_CLASS(c); + + playback_engine_signals[SIGNAL_DURATION_CHANGE] = g_signal_new( + "duration-changed", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoPlaybackEngineClass, duration_changed), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0 + ); + + playback_engine_signals[SIGNAL_PLAY_STATE_CHANGE] = g_signal_new( + "play-state-changed", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoPlaybackEngineClass, play_state_changed), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0 + ); + + playback_engine_signals[SIGNAL_PROGRESS_CHANGE] = g_signal_new( + "progress-changed", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoPlaybackEngineClass, progress_changed), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0 + ); +} + +static void koto_playback_engine_init(KotoPlaybackEngine *self) { + self->player = gst_pipeline_new("player"); + self->playbin = gst_element_factory_make("playbin", NULL); + self->suppress_video = gst_element_factory_make("fakesink", "suppress-video"); + + g_object_set(self->playbin, "video-sink", self->suppress_video, NULL); + g_object_set(self->playbin, "volume", 0.5, NULL); + + gst_bin_add(GST_BIN(self->player), self->playbin); + self->monitor = gst_bus_new(); // Get the bus for the playbin + + if (GST_IS_BUS(self->monitor)) { + gst_bus_add_watch(self->monitor, koto_playback_engine_monitor_changed, self); + gst_element_set_bus(self->playbin, self->monitor); // Set our bus to monitor changes + } + + self->is_muted = FALSE; + self->is_playing = FALSE; + self->is_paused = FALSE; + self->is_repeat_enabled = FALSE; + self->is_shuffle_enabled = FALSE; + self->requested_playing = FALSE; + + if (KOTO_IS_CURRENT_PLAYLIST(current_playlist)) { + g_signal_connect(current_playlist, "notify::current-playlist", G_CALLBACK(koto_playback_engine_current_playlist_changed), NULL); + } +} + +void koto_playback_engine_backwards(KotoPlaybackEngine *self) { + KotoPlaylist *playlist = koto_current_playlist_get_playlist(current_playlist); // Get the current playlist + + if (!KOTO_IS_PLAYLIST(playlist)) { // If we do not have a playlist currently + return; + } + + koto_playback_engine_set_track_by_uuid(self, koto_playlist_go_to_previous(playlist)); +} + +void koto_playback_engine_current_playlist_changed() { + if (!KOTO_IS_PLAYBACK_ENGINE(playback_engine)) { + return; + } + + KotoPlaylist *playlist = koto_current_playlist_get_playlist(current_playlist); // Get the current playlist + + if (!KOTO_IS_PLAYLIST(playlist)) { // If we do not have a playlist currently + return; + } + + koto_playback_engine_set_track_by_uuid(playback_engine, koto_playlist_get_current_uuid(playlist)); // Get the current UUID +} + +void koto_playback_engine_forwards(KotoPlaybackEngine *self) { + KotoPlaylist *playlist = koto_current_playlist_get_playlist(current_playlist); // Get the current playlist + + if (!KOTO_IS_PLAYLIST(playlist)) { // If we do not have a playlist currently + return; + } + + koto_playback_engine_set_track_by_uuid(self, koto_playlist_go_to_next(playlist)); +} + +gint64 koto_playback_engine_get_duration(KotoPlaybackEngine *self) { + gint64 duration = 0; + if (gst_element_query_duration(self->playbin, GST_FORMAT_TIME, &duration)) { + duration = duration / NS; // Divide by NS to get seconds + } + + return duration; +} + +GstState koto_playback_engine_get_state(KotoPlaybackEngine *self) { + GstState current_state; + GstStateChangeReturn ret = gst_element_get_state(self->playbin, ¤t_state, NULL, GST_SECOND); // Get the current state, allowing up to a second to get it + + if (ret != GST_STATE_CHANGE_SUCCESS) { // Got the data we need + return GST_STATE_NULL; + } + + return current_state; +} + +gint64 koto_playback_engine_get_progress(KotoPlaybackEngine *self) { + gint64 progress = 0; + if (gst_element_query_position(self->playbin, GST_FORMAT_TIME, &progress)) { + progress = progress / NS; // Divide by NS to get seconds + } + + return progress; +} + +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: { // Finished loading + if (self->requested_playing) { + self->is_playing = TRUE; + self->is_paused = FALSE; + g_timeout_add(50, koto_playback_engine_update_progress, self); + gst_element_set_state(self->playbin, GST_STATE_PLAYING); // Make double sure the state is playing + g_signal_emit(self, playback_engine_signals[SIGNAL_DURATION_CHANGE], 0); // Emit our duration signal + g_signal_emit(self, playback_engine_signals[SIGNAL_PLAY_STATE_CHANGE], 0); // Emit our playing signal + } + break; + } + case GST_MESSAGE_STATE_CHANGED: { // State changed + GstState old_state; + GstState requested_state; + GstState new_state; + gst_message_parse_state_changed(msg, &old_state, &requested_state, &new_state); + + if ((old_state == GST_STATE_PLAYING) && (requested_state == GST_STATE_PAUSED)) { + self->is_playing = FALSE; + self->is_paused = TRUE; + + g_signal_emit(self, playback_engine_signals[SIGNAL_PLAY_STATE_CHANGE], 0); // Emit our paused signal + } else if (requested_state == GST_STATE_PLAYING && !self->is_playing) { + self->is_playing = TRUE; + self->is_paused = FALSE; + g_signal_emit(self, playback_engine_signals[SIGNAL_PLAY_STATE_CHANGE], 0); // Emit our paused signal + } else if (((old_state == GST_STATE_PLAYING) || (old_state == GST_STATE_PAUSED)) && (new_state == GST_STATE_NULL)) { // If we're freeing resources + gst_bus_post(self->monitor, gst_message_new_reset_time(GST_OBJECT(self->playbin), 0)); + } + + break; + } + case GST_MESSAGE_EOS: { // Reached end of stream + koto_playback_engine_forwards(self); // Go to the next track + break; + } + default: + break; + } + return TRUE; +} + +void koto_playback_engine_play(KotoPlaybackEngine *self) { + self->requested_playing = TRUE; + gst_element_set_state(self->playbin, GST_STATE_PLAYING); // Set our state to play +} + +void koto_playback_engine_pause(KotoPlaybackEngine *self) { + self->requested_playing = FALSE; + gst_element_set_state(self->playbin, GST_STATE_PAUSED); // Set our state to paused +} + +void koto_playback_engine_set_track_by_uuid(KotoPlaybackEngine *self, gchar *track_uuid) { + if (track_uuid == NULL) { + return; + } + + KotoIndexedTrack *track = koto_cartographer_get_track_by_uuid(koto_maps, track_uuid); // Get the track from cartographer + + if (!KOTO_IS_INDEXED_TRACK(track)) { // Not a track + return; + } + + gchar *track_file_path = NULL; + g_object_get(track, "path", &track_file_path, NULL); // Get the path to the track + + koto_playback_engine_stop(self); // Stop current track + + gchar *gst_filename = gst_filename_to_uri(track_file_path, NULL); // Get the gst supported file naem + + g_object_set(self->playbin, "uri", gst_filename, NULL); + g_free(gst_filename); // Free the filename + + koto_playback_engine_play(self); // Play the new track +} + +void koto_playback_engine_stop(KotoPlaybackEngine *self) { + gst_element_set_state(self->playbin, GST_STATE_NULL); + GstPad *pad = gst_element_get_static_pad(self->playbin, "audio-sink"); // Get the static pad of the audio element + + if (!GST_IS_PAD(pad)) { + return; + } + + gst_pad_set_offset(pad, 0); // Change offset +} + +void koto_playback_engine_toggle(KotoPlaybackEngine *self) { + if (koto_playback_engine_get_state(self) == GST_STATE_PLAYING) { // Currently playing + koto_playback_engine_pause(self); // Pause + } else { + koto_playback_engine_play(self); // Play + } +} + +gboolean koto_playback_engine_update_progress(gpointer user_data) { + KotoPlaybackEngine *self = user_data; + if (self->is_playing) { // Is playing + g_signal_emit(self, playback_engine_signals[SIGNAL_PROGRESS_CHANGE], 0); // Emit our progress change signal + } + + return self->is_playing; +} + +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 new file mode 100644 index 0000000..9871cda --- /dev/null +++ b/src/playback/engine.h @@ -0,0 +1,63 @@ +/* engine.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 +#include +#include "../playlist/current.h" + +G_BEGIN_DECLS + +/** + * Type Definition +**/ + +#define KOTO_TYPE_PLAYBACK_ENGINE (koto_playback_engine_get_type()) +#define KOTO_PLAYBACK_ENGINE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), KOTO_TYPE_PLAYBACK_ENGINE, KotoPlaybackEngine)) +#define KOTO_IS_PLAYBACK_ENGINE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_PLAYBACK_ENGINE)) + +typedef struct _KotoPlaybackEngine KotoPlaybackEngine; +typedef struct _KotoPlaybackEngineClass KotoPlaybackEngineClass; + +GLIB_AVAILABLE_IN_ALL +GType koto_playback_engine_get_type(void) G_GNUC_CONST; + +/** + * Playback Engine Functions +**/ + +KotoPlaybackEngine* koto_playback_engine_new(); +void koto_playback_engine_backwards(KotoPlaybackEngine *self); +void koto_playback_engine_current_playlist_changed(); +void koto_playback_engine_forwards(KotoPlaybackEngine *self); +gint64 koto_playback_engine_get_duration(KotoPlaybackEngine *self); +GstState koto_playback_engine_get_state(KotoPlaybackEngine *self); +gint64 koto_playback_engine_get_progress(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, guint 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_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_update_duration(KotoPlaybackEngine *self); +gboolean koto_playback_engine_update_progress(gpointer user_data); diff --git a/src/playlist/current.c b/src/playlist/current.c index bbf640b..b0309c2 100644 --- a/src/playlist/current.c +++ b/src/playlist/current.c @@ -18,6 +18,14 @@ #include #include "current.h" +enum { + PROP_0, + PROP_CURRENT_PLAYLIST, + N_PROPERTIES +}; + +static GParamSpec *props[N_PROPERTIES] = { NULL, }; + KotoCurrentPlaylist *current_playlist = NULL; struct _KotoCurrentPlaylist { @@ -27,14 +35,56 @@ struct _KotoCurrentPlaylist { G_DEFINE_TYPE(KotoCurrentPlaylist, koto_current_playlist, G_TYPE_OBJECT); +static void koto_current_playlist_get_property(GObject *obj, guint prop_id, GValue *val, GParamSpec *spec); +static void koto_current_playlist_set_property(GObject *obj, guint prop_id, const GValue *val, GParamSpec *spec); + static void koto_current_playlist_class_init(KotoCurrentPlaylistClass *c) { - (void) c; + GObjectClass *gobject_class; + gobject_class = G_OBJECT_CLASS(c); + gobject_class->set_property = koto_current_playlist_set_property; + gobject_class->get_property = koto_current_playlist_get_property; + + props[PROP_CURRENT_PLAYLIST] = g_param_spec_object( + "current-playlist", + "Current Playlist", + "Current Playlist", + KOTO_TYPE_PLAYLIST, + G_PARAM_EXPLICIT_NOTIFY|G_PARAM_READWRITE + ); + + g_object_class_install_properties(gobject_class, N_PROPERTIES, props); } static void koto_current_playlist_init(KotoCurrentPlaylist *self) { self->current_playlist = NULL; } +void koto_current_playlist_get_property(GObject *obj, guint prop_id, GValue *val, GParamSpec *spec) { + KotoCurrentPlaylist *self = KOTO_CURRENT_PLAYLIST(obj); + + switch (prop_id) { + case PROP_CURRENT_PLAYLIST: + g_value_set_object(val, self->current_playlist); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +void koto_current_playlist_set_property(GObject *obj, guint prop_id, const GValue *val, GParamSpec *spec) { + KotoCurrentPlaylist *self = KOTO_CURRENT_PLAYLIST(obj); + + switch (prop_id) { + case PROP_CURRENT_PLAYLIST: + koto_current_playlist_set_playlist(self, (KotoPlaylist*) g_value_get_object(val)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + KotoPlaylist* koto_current_playlist_get_playlist(KotoCurrentPlaylist *self) { return self->current_playlist; } @@ -44,15 +94,26 @@ void koto_current_playlist_set_playlist(KotoCurrentPlaylist *self, KotoPlaylist return; } - if (KOTO_IS_PLAYLIST(self->current_playlist)) { - // TODO: Save current playlist state if needed + if (!KOTO_IS_PLAYLIST(playlist)) { // Not a playlist + return; + } + + if (self->current_playlist != NULL && KOTO_IS_PLAYLIST(self->current_playlist)) { + gboolean *is_temp = FALSE; + g_object_get(self->current_playlist, "ephemeral", &is_temp, NULL); // Get the current ephemeral value + + if (is_temp) { // Is a temporary playlist + koto_playlist_unmap(self->current_playlist); // Unmap the playlist if needed + } else { // Not a temporary playlist + koto_playlist_commit(self->current_playlist); // Save the current playlist + } + g_object_unref(self->current_playlist); // Unreference } self->current_playlist = playlist; g_object_ref(playlist); // Increment the reference - g_object_notify(G_OBJECT(self), "current-playlist-changed"); - koto_playlist_debug(self->current_playlist); + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_CURRENT_PLAYLIST]); } KotoCurrentPlaylist* koto_current_playlist_new() { diff --git a/src/playlist/playlist.c b/src/playlist/playlist.c index 9d1c31d..9f100b6 100644 --- a/src/playlist/playlist.c +++ b/src/playlist/playlist.c @@ -18,8 +18,10 @@ #include #include #include +#include "../db/cartographer.h" #include "playlist.h" +extern KotoCartographer *koto_maps; extern sqlite3 *koto_db; struct _KotoPlaylist { @@ -28,8 +30,11 @@ struct _KotoPlaylist { gchar *name; gchar *art_path; guint current_position; + gboolean ephemeral; + gboolean is_shuffle_enabled; GQueue *tracks; + GQueue *played_tracks; }; G_DEFINE_TYPE(KotoPlaylist, koto_playlist, G_TYPE_OBJECT); @@ -39,6 +44,7 @@ enum { PROP_UUID, PROP_NAME, PROP_ART_PATH, + PROP_EPHEMERAL, N_PROPERTIES, }; @@ -76,6 +82,14 @@ static void koto_playlist_class_init(KotoPlaylistClass *c) { G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_READWRITE ); + props[PROP_EPHEMERAL] = g_param_spec_boolean( + "ephemeral", + "Is the playlist ephemeral (temporary)", + "Is the playlist ephemeral (temporary)", + FALSE, + G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_READWRITE + ); + g_object_class_install_properties(gobject_class, N_PROPERTIES, props); } @@ -92,6 +106,9 @@ static void koto_playlist_get_property(GObject *obj, guint prop_id, GValue *val, case PROP_ART_PATH: g_value_set_string(val, self->art_path); break; + case PROP_EPHEMERAL: + g_value_set_boolean(val, self->ephemeral); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); break; @@ -111,6 +128,9 @@ static void koto_playlist_set_property(GObject *obj, guint prop_id, const GValue case PROP_ART_PATH: koto_playlist_set_artwork(self, g_value_get_string(val)); break; + case PROP_EPHEMERAL: + self->ephemeral = g_value_get_boolean(val); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); break; @@ -119,6 +139,7 @@ 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->played_tracks = g_queue_new(); // Set as an empty GQueue self->tracks = g_queue_new(); // Set as an empty GQueue } @@ -135,14 +156,45 @@ void koto_playlist_add_track_by_uuid(KotoPlaylist *self, const gchar *uuid) { // TODO: Add to table } -void koto_playlist_debug(KotoPlaylist *self) { - g_queue_foreach(self->tracks, koto_playlist_debug_foreach, NULL); +void koto_playlist_commit(KotoPlaylist *self) { + if (self->ephemeral) { // Temporary playlist + return; + } + + gchar *commit_op = g_strdup_printf( + "INSERT INTO playlist_meta(id, name, art_path)" + "VALUES('%s', quote(\"%s\"), quote(\"%s\")" + "ON CONFLICT(id) DO UPDATE SET name=excluded.name, art_path=excluded.art_path;", + self->uuid, + self->name, + self->art_path + ); + + gchar *commit_op_errmsg = NULL; + int rc = sqlite3_exec(koto_db, commit_op, 0, 0, &commit_op_errmsg); + + if (rc != SQLITE_OK) { + g_warning("Failed to save playlist: %s", commit_op_errmsg); + } else { // Successfully saved our playlist + g_queue_foreach(self->tracks, koto_playlist_commit_tracks, self); // Iterate over each track to save it + } + + g_free(commit_op); + g_free(commit_op_errmsg); } -void koto_playlist_debug_foreach(gpointer data, gpointer user_data) { - (void) user_data; - gchar *uuid = data; - g_message("UUID in Playlist: %s", uuid); +void koto_playlist_commit_tracks(gpointer data, gpointer user_data) { + KotoIndexedTrack *track = koto_cartographer_get_track_by_uuid(koto_maps, data); // Get the track + + if (track == NULL) { // Not a track + KotoPlaylist *self = user_data; + gchar *playlist_uuid = self->uuid; // Get the playlist UUID + + gchar *current_track = g_queue_peek_nth(self->tracks, self->current_position); // Get the UUID of the current track + koto_indexed_track_save_to_playlist(track, playlist_uuid, g_queue_index(self->tracks, data), (data == current_track) ? 1 : 0); // Call to save the playlist to the track + g_free(playlist_uuid); + g_free(current_track); + } } gchar* koto_playlist_get_artwork(KotoPlaylist *self) { @@ -165,6 +217,38 @@ gchar* koto_playlist_get_name(KotoPlaylist *self) { return (self->name == NULL) ? NULL : g_strdup(self->name); } +gchar* koto_playlist_get_random_track(KotoPlaylist *self) { + gchar *track_uuid = NULL; + guint tracks_len = g_queue_get_length(self->tracks); + + if (tracks_len == g_queue_get_length(self->played_tracks)) { // Played all tracks + track_uuid = g_list_nth_data(self->tracks->head, 0); // Get the first + g_queue_clear(self->played_tracks); // Clear our played tracks + } else { // Have not played all tracks + GRand* rando_calrissian = g_rand_new(); // Create a new RNG + guint attempt = 0; + + 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 + track_uuid = selected_track; + break; + } else { // Failed to get the track + if (attempt > tracks_len / 2) { + break; // Give up + } + } + } + + g_rand_free(rando_calrissian); // Free rando + } + + return track_uuid; +} + GQueue* koto_playlist_get_tracks(KotoPlaylist *self) { return self->tracks; } @@ -174,6 +258,10 @@ 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 *current_uuid = koto_playlist_get_current_uuid(self); // Get the current UUID if (current_uuid == self->tracks->tail->data) { // Current UUID matches the last item in the playlist @@ -185,6 +273,10 @@ gchar* koto_playlist_go_to_next(KotoPlaylist *self) { } gchar* koto_playlist_go_to_previous(KotoPlaylist *self) { + if (self->is_shuffle_enabled) { // Shuffling enabled + return koto_playlist_get_random_track(self); // Get a random track + } + gchar *current_uuid = koto_playlist_get_current_uuid(self); // Get the current UUID if (current_uuid == self->tracks->head->data) { // Current UUID matches the first item in the playlist @@ -280,6 +372,10 @@ void koto_playlist_set_uuid(KotoPlaylist *self, const gchar *uuid) { return; } +void koto_playlist_unmap(KotoPlaylist *self) { + koto_cartographer_remove_playlist_by_uuid(koto_maps, self->uuid); // Remove from our cartographer +} + KotoPlaylist* koto_playlist_new() { return g_object_new(KOTO_TYPE_PLAYLIST, "uuid", g_uuid_string_random(), diff --git a/src/playlist/playlist.h b/src/playlist/playlist.h index 3a32318..0b28095 100644 --- a/src/playlist/playlist.h +++ b/src/playlist/playlist.h @@ -37,8 +37,8 @@ KotoPlaylist* koto_playlist_new(); KotoPlaylist* koto_playlist_new_with_uuid(const 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_debug(KotoPlaylist *self); -void koto_playlist_debug_foreach(gpointer data, gpointer user_data); +void koto_playlist_commit(KotoPlaylist *self); +void koto_playlist_commit_tracks(gpointer data, gpointer user_data); gchar* koto_playlist_get_artwork(KotoPlaylist *self); guint koto_playlist_get_current_position(KotoPlaylist *self); gchar* koto_playlist_get_current_uuid(KotoPlaylist *self); @@ -55,5 +55,6 @@ void koto_playlist_save_state(KotoPlaylist *self); void koto_playlist_set_name(KotoPlaylist *self, const gchar *name); void koto_playlist_set_position(KotoPlaylist *self, guint pos); void koto_playlist_set_uuid(KotoPlaylist *self, const gchar *uuid); +void koto_playlist_unmap(KotoPlaylist *self); G_END_DECLS