diff --git a/.vscode/settings.json b/.vscode/settings.json index 553253d..2ea2f84 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,18 @@ "__node_handle": "c", "gtk.h": "c", "gtktreeview.h": "c", - "cartographer.h": "c" + "cartographer.h": "c", + "structs.h": "c", + "gst.h": "c", + "player.h": "c", + "config.h": "c", + "toml.h": "c", + "chrono": "c", + "sqlite3.h": "c", + "unistd.h": "c", + "ui.h": "c", + "koto-utils.h": "c", + "random": "c", + "add-remove-track-popover.h": "c" } } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 161c8df..1359393 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -51,7 +51,7 @@ "args": [ "compile", "-C", - "builddir" + "builddir", ], "problemMatcher": [], "group": { diff --git a/src/components/koto-cover-art-button.c b/src/components/koto-cover-art-button.c index 6db185d..3d872b9 100644 --- a/src/components/koto-cover-art-button.c +++ b/src/components/koto-cover-art-button.c @@ -27,7 +27,7 @@ struct _KotoCoverArtButton { GtkWidget * art; GtkWidget * main; GtkWidget * revealer; - KotoButton * play_pause_button; + KotoButton * play_button; guint height; guint width; @@ -107,8 +107,8 @@ static void koto_cover_art_button_init(KotoCoverArtButton * self) { GtkWidget * controls = gtk_center_box_new(); // Create a center box for the controls - self->play_pause_button = koto_button_new_with_icon("", "media-playback-start-symbolic", "media-playback-pause-symbolic", KOTO_BUTTON_PIXBUF_SIZE_NORMAL); - gtk_center_box_set_center_widget(GTK_CENTER_BOX(controls), GTK_WIDGET(self->play_pause_button)); + self->play_button = koto_button_new_with_icon("", "media-playback-start-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); + gtk_center_box_set_center_widget(GTK_CENTER_BOX(controls), GTK_WIDGET(self->play_button)); gtk_revealer_set_child(GTK_REVEALER(self->revealer), controls); koto_cover_art_button_hide_overlay_controls(NULL, self); // Hide by default @@ -175,7 +175,7 @@ KotoButton * koto_cover_art_button_get_button(KotoCoverArtButton * self) { return NULL; } - return self->play_pause_button; + return self->play_button; } GtkWidget * koto_cover_art_button_get_main(KotoCoverArtButton * self) { diff --git a/src/config/config.c b/src/config/config.c index cf4a05b..b034c0f 100644 --- a/src/config/config.c +++ b/src/config/config.c @@ -20,6 +20,7 @@ #include #include +#include "../db/cartographer.h" #include "../playback/engine.h" #include "../koto-paths.h" #include "../koto-utils.h" @@ -28,6 +29,7 @@ extern int errno; extern const gchar * koto_config_template; +extern KotoCartographer * koto_maps; extern KotoPlaybackEngine * playback_engine; enum { @@ -46,6 +48,7 @@ static GParamSpec * config_props[N_PROPS] = { struct _KotoConfig { GObject parent_instance; + toml_table_t * toml_ref; GFile * config_file; GFileMonitor * config_file_monitor; @@ -53,6 +56,13 @@ struct _KotoConfig { gchar * path; gboolean finalized; + /* Library Attributes */ + + // These are useful for when we need to determine separately if we need to index initial builtin folders that did not exist previously (during load) + gboolean has_type_audiobook; + gboolean has_type_music; + gboolean has_type_podcast; + /* Playback Settings */ gboolean playback_continue_on_playlist; @@ -143,6 +153,9 @@ static void koto_config_class_init(KotoConfigClass * c) { static void koto_config_init(KotoConfig * self) { self->finalized = FALSE; + self->has_type_audiobook = FALSE; + self->has_type_music = FALSE; + self->has_type_podcast = FALSE; } static void koto_config_constructed(GObject * obj) { @@ -214,6 +227,25 @@ static void koto_config_set_property( } } +toml_table_t * koto_config_get_library( + KotoConfig * self, + gchar * library_uuid +) { + toml_array_t * libraries = toml_array_in(self->toml_ref, "library"); // Get the array of tables + for (int i = 0; i < toml_array_nelem(libraries); i++) { // For each library + toml_table_t * library = toml_table_at(libraries, i); // Get this library + toml_datum_t uuid_datum = toml_string_in(library, "uuid"); // Get the datum for the uuid + + gchar * lib_uuid = (uuid_datum.ok) ? (gchar*) uuid_datum.u.s : NULL; + + if (koto_utils_is_string_valid(lib_uuid) && (g_strcmp0(library_uuid, lib_uuid) == 0)) { // Is a valid string and the libraries match + return library; + } + } + + return NULL; +} + /** * Load our TOML file from the specified path into our KotoConfig **/ @@ -288,25 +320,29 @@ void koto_config_load( return; } + self->toml_ref = conf; + /** Supplemental Libraries (Excludes Built-in) */ - toml_table_t * libraries_section = toml_table_in(conf, "libraries"); + toml_array_t * libraries = toml_array_in(conf, "library"); // Get all of our libraries + if (libraries) { // If we have libraries already + for (int i = 0; i < toml_array_nelem(libraries); i++) { // Iterate over each library + toml_table_t * lib = toml_table_at(libraries, i); // Get the library datum + KotoLibrary * koto_lib = koto_library_new_from_toml_table(lib); // Get the library based on the TOML data for this specific type - if (libraries_section) { // Have supplemental libraries - toml_array_t * library_uuids = toml_array_in(libraries_section, "uuids"); + if (!KOTO_IS_LIBRARY(koto_lib)) { // Something wrong with it, not a library + continue; + } - if (library_uuids && (toml_array_nelem(library_uuids) != 0)) { // Have UUIDs - for (int i = 0; i < toml_array_nelem(library_uuids); i++) { // Iterate over each UUID - toml_datum_t uuid = toml_string_at(library_uuids, i); // Get the UUID + koto_cartographer_add_library(koto_maps, koto_lib); // Add library to Cartographer + KotoLibraryType lib_type = koto_library_get_lib_type(koto_lib); // Get the type - if (!uuid.ok) { // Not a UUID string - continue; // Skip this entry in the array - } - - g_message("UUID: %s", uuid.u.s); - // TODO: Implement Koto library creation - free(uuid.u.s); - toml_free(conf); + if (lib_type == KOTO_LIBRARY_TYPE_AUDIOBOOK) { // Is an audiobook lib + self->has_type_audiobook = TRUE; + } else if (lib_type == KOTO_LIBRARY_TYPE_MUSIC) { // Is a music lib + self->has_type_music = TRUE; + } else if (lib_type == KOTO_LIBRARY_TYPE_PODCAST) { // Is a podcast lib + self->has_type_podcast = TRUE; } } } @@ -371,6 +407,51 @@ monitor: } } +void koto_config_load_libs(KotoConfig * self) { + gchar * home_dir = g_strdup(g_get_home_dir()); // Get the home directory + + if (!self->has_type_audiobook) { // If we do not have a KotoLibrary for Audiobooks + gchar * audiobooks_path = g_build_path(G_DIR_SEPARATOR_S, home_dir, "Audiobooks", NULL); + koto_utils_mkdir(audiobooks_path); // Create the directory just in case + KotoLibrary * lib = koto_library_new(KOTO_LIBRARY_TYPE_AUDIOBOOK, NULL, audiobooks_path); // Audiobooks relative to home directory + + if (KOTO_IS_LIBRARY(lib)) { // Created built-in audiobooks lib successfully + koto_cartographer_add_library(koto_maps, lib); + koto_config_save(config); + koto_library_index(lib); // Index this library + } + + g_free(audiobooks_path); + } + + if (!self->has_type_music) { // If we do not have a KotoLibrary for Music + KotoLibrary * lib = koto_library_new(KOTO_LIBRARY_TYPE_MUSIC, NULL, g_get_user_special_dir(G_USER_DIRECTORY_MUSIC)); // Create a library using the user's MUSIC directory defined + + if (KOTO_IS_LIBRARY(lib)) { // Created built-in music lib successfully + koto_cartographer_add_library(koto_maps, lib); + koto_config_save(config); + koto_library_index(lib); // Index this library + } + } + + if (!self->has_type_podcast) { // If we do not have a KotoLibrary for Podcasts + gchar * podcasts_path = g_build_path(G_DIR_SEPARATOR_S, home_dir, "Podcasts", NULL); + koto_utils_mkdir(podcasts_path); // Create the directory just in case + KotoLibrary * lib = koto_library_new(KOTO_LIBRARY_TYPE_PODCAST, NULL, podcasts_path); // Podcasts relative to home dir + + if (KOTO_IS_LIBRARY(lib)) { // Created built-in podcasts lib successfully + koto_cartographer_add_library(koto_maps, lib); + koto_config_save(config); + koto_library_index(lib); // Index this library + } + + g_free(podcasts_path); + } + + g_free(home_dir); + g_thread_exit(0); +} + void koto_config_monitor_handle_changed( GFileMonitor * monitor, GFile * file, @@ -404,6 +485,18 @@ void koto_config_refresh(KotoConfig * self) { void koto_config_save(KotoConfig * self) { GStrvBuilder * root_builder = g_strv_builder_new(); // Create a new strv builder + /* Iterate over our libraries */ + + GList * libs = koto_cartographer_get_libraries(koto_maps); // Get our libraries + GList * current_libs; + + for (current_libs = libs; current_libs != NULL; current_libs = current_libs->next) { // Iterate over our libraries + KotoLibrary * lib = current_libs->data; + gchar * lib_config = koto_library_to_config_string(lib); // Get the config string + g_strv_builder_add(root_builder, lib_config); // Add the config to our string builder + g_free(lib_config); + } + GParamSpec ** props_list = g_object_class_list_properties(G_OBJECT_GET_CLASS(self), NULL); // Get the propreties associated with our settings GHashTable * sections_to_prop_keys = g_hash_table_new(g_str_hash, g_str_equal); // Create our section to hold our various sections based on props diff --git a/src/config/config.h b/src/config/config.h index c0b6877..2c68b18 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -35,6 +35,8 @@ void koto_config_load( gchar * path ); +void koto_config_load_libs(KotoConfig * self); + void koto_config_monitor_handle_changed( GFileMonitor * monitor, GFile * file, diff --git a/src/db/cartographer.c b/src/db/cartographer.c index 2e0cf8c..87421dc 100644 --- a/src/db/cartographer.c +++ b/src/db/cartographer.c @@ -46,6 +46,7 @@ struct _KotoCartographer { GHashTable * libraries; GHashTable * playlists; GHashTable * tracks; + GHashTable * tracks_by_uniqueish_key; }; struct _KotoCartographerClass { @@ -150,7 +151,8 @@ static void koto_cartographer_class_init(KotoCartographerClass * c) { NULL, NULL, G_TYPE_NONE, - 1, + 2, + G_TYPE_CHAR, G_TYPE_CHAR ); @@ -240,6 +242,7 @@ static void koto_cartographer_init(KotoCartographer * self) { self->libraries = g_hash_table_new(g_str_hash, g_str_equal); self->playlists = g_hash_table_new(g_str_hash, g_str_equal); self->tracks = g_hash_table_new(g_str_hash, g_str_equal); + self->tracks_by_uniqueish_key = g_hash_table_new(g_str_hash, g_str_equal); } void koto_cartographer_add_album( @@ -256,7 +259,7 @@ void koto_cartographer_add_album( gchar * album_uuid = koto_album_get_uuid(album); // Get the album UUID - if (!koto_utils_is_string_valid(album_uuid)|| koto_cartographer_has_album_by_uuid(self, album_uuid)) { // Have the album or invalid UUID + if (!koto_utils_is_string_valid(album_uuid) || koto_cartographer_has_album_by_uuid(self, album_uuid)) { // Have the album or invalid UUID return; } @@ -288,8 +291,7 @@ void koto_cartographer_add_artist( return; } - g_hash_table_replace(self->artists_name_to_uuid, koto_artist_get_name(artist), koto_artist_get_uuid(artist)); // Add the UUID as a value with the key being the name of the artist - + g_hash_table_replace(self->artists_name_to_uuid, koto_artist_get_name(artist), artist_uuid); // Add the UUID as a value with the key being the name of the artist g_hash_table_replace(self->artists, artist_uuid, artist); g_signal_emit( @@ -312,15 +314,15 @@ void koto_cartographer_add_library( return; } - gchar * library_uuid = NULL; - g_object_get(library, "uuid", &library_uuid, NULL); + gchar * library_uuid = koto_library_get_uuid(library); - if (!koto_utils_is_string_valid(library_uuid)|| koto_cartographer_has_library_by_uuid(self, library_uuid)) { // Have the library or invalid UUID + if (!koto_utils_is_string_valid(library_uuid) || koto_cartographer_has_library_by_uuid(self, library_uuid)) { // Have the library or invalid UUID return; } g_hash_table_replace(self->libraries, library_uuid, library); // Add the library - g_signal_emit( // Emit our library added signal + g_signal_emit( + // Emit our library added signal self, cartographer_signals[SIGNAL_LIBRARY_ADDED], 0, @@ -445,7 +447,7 @@ KotoArtist * koto_cartographer_get_artist_by_uuid( } KotoLibrary * koto_cartographer_get_library_by_uuid( - KotoCartographer *self, + KotoCartographer * self, gchar * library_uuid ) { if (!KOTO_IS_CARTOGRAPHER(self)) { @@ -459,8 +461,40 @@ KotoLibrary * koto_cartographer_get_library_by_uuid( return g_hash_table_lookup(self->libraries, library_uuid); } +KotoLibrary * koto_cartographer_get_library_containing_path( + KotoCartographer * self, + gchar * relative_path +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return NULL; + } + + if (!koto_utils_is_string_valid(relative_path)) { // Not a valid string + return NULL; + } + + GList * libs = koto_cartographer_get_libraries(self); // Get all the libraries, sorted based on priority + GList * current; + + for (current = libs; current != NULL; current = current->next) { // For each library + KotoLibrary * lib = (KotoLibrary*) current->data; + gchar * lib_path = koto_library_get_path(lib); // Get the path for the library + + GFile * track_file = g_file_new_build_filename(lib_path, relative_path, NULL); // Build a path from storage to file + + if (g_file_query_exists(track_file, NULL)) { // If this library contains this file + g_object_unref(track_file); + return lib; + } + + g_object_unref(track_file); + } + + return NULL; +} + GList * koto_cartographer_get_libraries_for_storage_uuid( - KotoCartographer *self, + KotoCartographer * self, gchar * storage_uuid ) { if (!KOTO_IS_CARTOGRAPHER(self)) { @@ -477,6 +511,12 @@ GList * koto_cartographer_get_libraries_for_storage_uuid( return libraries; } +GList * koto_cartographer_get_libraries(KotoCartographer * self) { + GList * libraries = g_hash_table_get_values(self->libraries); + // TODO: Implement priority mechanism + return libraries; +} + GHashTable * koto_cartographer_get_playlists(KotoCartographer * self) { if (!KOTO_IS_CARTOGRAPHER(self)) { return NULL; @@ -507,6 +547,21 @@ KotoTrack * koto_cartographer_get_track_by_uuid( return g_hash_table_lookup(self->tracks, track_uuid); } +KotoTrack * koto_cartographer_get_track_by_uniqueish_key( + KotoCartographer * self, + gchar * key +) { + if (!KOTO_IS_CARTOGRAPHER(self)) { + return NULL; + } + + if (!koto_utils_is_string_valid(key)) { + return NULL; + } + + return g_hash_table_lookup(self->tracks_by_uniqueish_key, key); +} + gboolean koto_cartographer_has_album( KotoCartographer * self, KotoAlbum * album @@ -572,7 +627,7 @@ gboolean koto_cartographer_has_artist_by_uuid( gboolean koto_cartographer_has_library( KotoCartographer * self, - KotoLibrary *library + KotoLibrary * library ) { if (!KOTO_IS_CARTOGRAPHER(self)) { return FALSE; @@ -714,8 +769,19 @@ void koto_cartographer_remove_artist( return; } - koto_cartographer_remove_artist_by_uuid(self, koto_artist_get_uuid(artist)); - g_hash_table_remove(self->artists_name_to_uuid, koto_artist_get_name(artist)); // Add the UUID as a value with the key being the name of the artist + gchar * artist_uuid = koto_artist_get_uuid(artist); + gchar * artist_name = koto_artist_get_name(artist); + + g_hash_table_remove(self->artists_name_to_uuid, artist_name); // Add the UUID as a value with the key being the name of the artist + g_hash_table_remove(self->artists, artist_uuid); + + g_signal_emit( + self, + cartographer_signals[SIGNAL_ARTIST_REMOVED], + 0, + artist_uuid, + artist_name + ); } void koto_cartographer_remove_artist_by_uuid( @@ -734,13 +800,23 @@ void koto_cartographer_remove_artist_by_uuid( return; } + KotoArtist * artist = koto_cartographer_get_artist_by_uuid(self, artist_uuid); + + if (!KOTO_IS_ARTIST(artist)) { + return; + } + + gchar * artist_name = koto_artist_get_name(artist); + + g_hash_table_remove(self->artists_name_to_uuid, artist_name); // Add the UUID as a value with the key being the name of the artist g_hash_table_remove(self->artists, artist_uuid); g_signal_emit( self, cartographer_signals[SIGNAL_ARTIST_REMOVED], 0, - artist_uuid + artist_uuid, + artist_name ); } @@ -792,7 +868,7 @@ void koto_cartographer_remove_track( return; } - if (!KOTO_IS_TRACK(track)) { // Not a track + if (!KOTO_IS_TRACK(track)) { // Not a track return; } @@ -815,14 +891,14 @@ void koto_cartographer_remove_track_by_uuid( return; } - g_hash_table_remove(self->tracks, track_uuid); + g_hash_table_remove(self->tracks, track_uuid); - g_signal_emit( - self, - cartographer_signals[SIGNAL_TRACK_REMOVED], - 0, - track_uuid - ); + g_signal_emit( + self, + cartographer_signals[SIGNAL_TRACK_REMOVED], + 0, + track_uuid + ); } KotoCartographer * koto_cartographer_new() { diff --git a/src/db/cartographer.h b/src/db/cartographer.h index dbaa7c5..8b752bc 100644 --- a/src/db/cartographer.h +++ b/src/db/cartographer.h @@ -90,12 +90,19 @@ KotoArtist * koto_cartographer_get_artist_by_uuid( ); KotoLibrary * koto_cartographer_get_library_by_uuid( - KotoCartographer *self, + KotoCartographer * self, gchar * library_uuid ); +KotoLibrary * koto_cartographer_get_library_containing_path( + KotoCartographer * self, + gchar * path +); + +GList * koto_cartographer_get_libraries(KotoCartographer * self); + GList * koto_cartographer_get_libraries_for_storage_uuid( - KotoCartographer *self, + KotoCartographer * self, gchar * storage_uuid ); @@ -111,6 +118,11 @@ KotoTrack * koto_cartographer_get_track_by_uuid( gchar * track_uuid ); +KotoTrack * koto_cartographer_get_track_by_uniqueish_key( + KotoCartographer * self, + gchar * key +); + gboolean koto_cartographer_has_album( KotoCartographer * self, KotoAlbum * album @@ -133,7 +145,7 @@ gboolean koto_cartographer_has_artist_by_uuid( gboolean koto_cartographer_has_library( KotoCartographer * self, - KotoLibrary *library + KotoLibrary * library ); gboolean koto_cartographer_has_library_by_uuid( diff --git a/src/db/db.c b/src/db/db.c index 1bb7181..509feef 100644 --- a/src/db/db.c +++ b/src/db/db.c @@ -38,32 +38,23 @@ 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 playlist_meta(id string UNIQUE PRIMARY KEY, name string, art_path string, preferred_model int);" - "CREATE TABLE IF NOT EXISTS playlist_tracks(position INTEGER PRIMARY KEY AUTOINCREMENT, playlist_id string, track_id string, current int, FOREIGN KEY(playlist_id) REFERENCES playlist_meta(id), FOREIGN KEY(track_id) REFERENCES tracks(id) ON DELETE CASCADE);"; + gchar * tables_creation_queries = "CREATE TABLE IF NOT EXISTS artists(id string UNIQUE PRIMARY KEY, name string, art_path string);" + "CREATE TABLE IF NOT EXISTS albums(id string UNIQUE PRIMARY KEY, 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, artist_id string, album_id string, name string, disc int, position int, FOREIGN KEY(artist_id) REFERENCES artists(id) ON DELETE CASCADE);" + "CREATE TABLE IF NOT EXISTS libraries_albums(id string, album_id string, path string, PRIMARY KEY (id, album_id) FOREIGN KEY(album_id) REFERENCES albums(id) ON DELETE CASCADE);" + "CREATE TABLE IF NOT EXISTS libraries_artists(id string, artist_id string, path string, PRIMARY KEY(id, artist_id) FOREIGN KEY(artist_id) REFERENCES artists(id) ON DELETE CASCADE);" + "CREATE TABLE IF NOT EXISTS libraries_tracks(id string, track_id string, path string, PRIMARY KEY(id, track_id) FOREIGN KEY(track_id) REFERENCES tracks(id) ON DELETE CASCADE);" + "CREATE TABLE IF NOT EXISTS playlist_meta(id string UNIQUE PRIMARY KEY, name string, art_path string, preferred_model int);" + "CREATE TABLE IF NOT EXISTS playlist_tracks(position INTEGER PRIMARY KEY AUTOINCREMENT, playlist_id string, track_id string, 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); - - if (rc != SQLITE_OK) { - g_critical("Failed to create required tables: %s", create_tables_errmsg); - } - - return (rc == SQLITE_OK) ? KOTO_DB_SUCCESS : KOTO_DB_FAIL; + return (new_transaction(tables_creation_queries, "Failed to create required tables", TRUE) == SQLITE_OK) ? KOTO_DB_SUCCESS : KOTO_DB_FAIL; } int enable_foreign_keys() { - gchar * enable_foreign_keys_err = NULL; - int rc = sqlite3_exec(koto_db, "PRAGMA foreign_keys = ON;", 0, 0, &enable_foreign_keys_err); + gchar * commit_op = g_strdup("PRAGMA foreign_keys = ON;"); + const gchar * transaction_err_msg = "Failed to enable foreign key support. Ensure your sqlite3 is compiled with neither SQLITE_OMIT_FOREIGN_KEY or SQLITE_OMIT_TRIGGER defined"; - if (rc != SQLITE_OK) { - g_critical("Failed to enable foreign key support. Ensure your sqlite3 is compiled with neither SQLITE_OMIT_FOREIGN_KEY or SQLITE_OMIT_TRIGGER defined: %s", enable_foreign_keys_err); - } - - g_free(enable_foreign_keys_err); - return (rc == SQLITE_OK) ? KOTO_DB_SUCCESS : KOTO_DB_FAIL; + return (new_transaction(commit_op, transaction_err_msg, FALSE) == SQLITE_OK) ? KOTO_DB_SUCCESS : KOTO_DB_FAIL; } int have_existing_db() { @@ -72,6 +63,25 @@ int have_existing_db() { return ((success == 0) && S_ISREG(db_stat.st_mode)) ? 0 : 1; } +int new_transaction( + gchar * operation, + const gchar * transaction_err_msg, + gboolean fatal +) { + gchar * commit_op_errmsg = NULL; + int rc = sqlite3_exec(koto_db, operation, 0, 0, &commit_op_errmsg); + + if (rc != SQLITE_OK) { + (fatal) ? g_critical("%s: %s", transaction_err_msg, commit_op_errmsg) : g_warning("%s: %s", transaction_err_msg, commit_op_errmsg); + } + + if (commit_op_errmsg == NULL) { + g_free(commit_op_errmsg); + } + + return rc; +} + int open_db() { int ret = KOTO_DB_SUCCESS; // Default to last return being SUCCESS diff --git a/src/db/db.h b/src/db/db.h index 2748aa0..9ff7b74 100644 --- a/src/db/db.h +++ b/src/db/db.h @@ -21,7 +21,6 @@ extern int KOTO_DB_SUCCESS; extern int KOTO_DB_NEW; extern int KOTO_DB_FAIL; -extern gboolean created_new_db; void close_db(); @@ -33,4 +32,10 @@ int enable_foreign_keys(); int have_existing_db(); +int new_transaction( + gchar * operation, + const gchar * transaction_err_msg, + gboolean fatal +); + int open_db(); diff --git a/src/db/loaders.c b/src/db/loaders.c index 3b44bfe..41355a0 100644 --- a/src/db/loaders.c +++ b/src/db/loaders.c @@ -35,19 +35,23 @@ int process_artists( (void) column_names; // Don't need any of the params 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 + gchar * artist_name = g_strdup(koto_utils_unquote_string(fields[1])); // Second column is artist name KotoArtist * artist = koto_artist_new_with_uuid(artist_uuid); // Create our artist with the UUID g_object_set( artist, - "path", - artist_path, // Set path "name", artist_name, // Set name NULL); + int artist_paths = sqlite3_exec(koto_db, g_strdup_printf("SELECT * FROM libraries_artists WHERE artist_id=\"%s\"", artist_uuid), process_artist_paths, artist, NULL); // Process all the paths for this given artist + + if (artist_paths != SQLITE_OK) { // Failed to get our artists_paths + g_critical("Failed to read our paths for this artist: %s", sqlite3_errmsg(koto_db)); + return 1; + } + koto_cartographer_add_artist(koto_maps, artist); // Add the artist to our global cartographer 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 @@ -57,13 +61,44 @@ int process_artists( return 1; } + int tracks_rc = sqlite3_exec(koto_db, g_strdup_printf("SELECT * FROM tracks WHERE artist_id=\"%s\"", artist_uuid), process_tracks, NULL, NULL); // Process our tracks by artist uuid + + if (tracks_rc != SQLITE_OK) { // Failed to get our tracks + g_critical("Failed to read our tracks: %s", sqlite3_errmsg(koto_db)); + return 1; + } + g_free(artist_uuid); - g_free(artist_path); g_free(artist_name); return 0; } +int process_artist_paths( + void * data, + int num_columns, + char ** fields, + char ** column_names +) { + (void) num_columns; + (void) column_names; // Don't need these + + KotoArtist * artist = (KotoArtist*) data; + + gchar * library_uuid = g_strdup(koto_utils_unquote_string(fields[0])); + gchar * relative_path = g_strdup(koto_utils_unquote_string(fields[2])); + + KotoLibrary * lib = koto_cartographer_get_library_by_uuid(koto_maps, library_uuid); // Get the library for this artist + + if (!KOTO_IS_LIBRARY(lib)) { // Failed to get the library for this UUID + return 0; + } + + koto_artist_set_path(artist, lib, relative_path, FALSE); // Add the relative path from the db for this artist and lib to the Artist, do not commit + + return 0; +} + int process_albums( void * data, int num_columns, @@ -76,35 +111,25 @@ int process_albums( KotoArtist * artist = (KotoArtist*) data; gchar * album_uuid = g_strdup(koto_utils_unquote_string(fields[0])); - gchar * path = g_strdup(koto_utils_unquote_string(fields[1])); - gchar * artist_uuid = g_strdup(koto_utils_unquote_string(fields[2])); - gchar * album_name = g_strdup(koto_utils_unquote_string(fields[3])); - gchar * album_art = (fields[4] != NULL) ? g_strdup(koto_utils_unquote_string(fields[4])) : NULL; + gchar * artist_uuid = g_strdup(koto_utils_unquote_string(fields[1])); + gchar * album_name = g_strdup(koto_utils_unquote_string(fields[2])); + gchar * album_art = (fields[3] != NULL) ? g_strdup(koto_utils_unquote_string(fields[3])) : NULL; KotoAlbum * album = koto_album_new_with_uuid(artist, album_uuid); // Create our album g_object_set( album, - "path", - path, // Set the path "name", album_name, // Set name "art-path", album_art, // Set art path if any - NULL); + NULL + ); koto_cartographer_add_album(koto_maps, album); // Add the album to our global cartographer - koto_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 - g_critical("Failed to read our tracks: %s", sqlite3_errmsg(koto_db)); - return 1; - } + koto_artist_add_album(artist, album); // Add the album g_free(album_uuid); - g_free(path); g_free(artist_uuid); g_free(album_name); @@ -188,35 +213,81 @@ int process_tracks( char ** fields, char ** column_names ) { + (void) data; (void) num_columns; (void) column_names; // Don't need these - KotoAlbum * album = (KotoAlbum*) data; gchar * track_uuid = g_strdup(koto_utils_unquote_string(fields[0])); - gchar * path = g_strdup(koto_utils_unquote_string(fields[1])); - gchar * artist_uuid = g_strdup(koto_utils_unquote_string(fields[3])); - 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[7], NULL, 10); - guint * position = (guint*) g_ascii_strtoull(fields[8], NULL, 10); + gchar * artist_uuid = g_strdup(koto_utils_unquote_string(fields[1])); + gchar * album_uuid = g_strdup(koto_utils_unquote_string(fields[2])); + gchar * name = g_strdup(koto_utils_unquote_string(fields[3])); + guint * disc_num = (guint*) g_ascii_strtoull(fields[4], NULL, 10); + guint * position = (guint*) g_ascii_strtoull(fields[5], NULL, 10); KotoTrack * track = koto_track_new_with_uuid(track_uuid); // Create our file - g_object_set(track, "artist-uuid", artist_uuid, "album-uuid", album_uuid, "path", path, "file-name", file_name, "parsed-name", name, "cd", disc_num, "position", position, NULL); + g_object_set( + track, + "artist-uuid", + artist_uuid, + "album-uuid", + album_uuid, + "parsed-name", + name, + "cd", + disc_num, + "position", + position, + NULL + ); - koto_album_add_track(album, track); // Add the track + int track_paths = sqlite3_exec(koto_db, g_strdup_printf("SELECT id, path FROM libraries_tracks WHERE track_id=\"%s\"", track_uuid), process_track_paths, track, NULL); // Process all pathes associated with the track + + if (track_paths != SQLITE_OK) { // Failed to read the paths + g_warning("Failed to read paths associated with track %s: %s", track_uuid, sqlite3_errmsg(koto_db)); + return 1; + } + + KotoArtist * artist = koto_cartographer_get_artist_by_uuid(koto_maps, artist_uuid); // Get the artist + koto_artist_add_track(artist, track); // Add the track for the artist + + if (koto_utils_is_string_valid(album_uuid)) { // If we have an album UUID + KotoAlbum * album = koto_cartographer_get_album_by_uuid(koto_maps, album_uuid); // Attempt to get album + + if (KOTO_IS_ALBUM(album)) { // This is an album + koto_album_add_track(album, track); // Add the track + } + } g_free(track_uuid); - g_free(path); g_free(artist_uuid); g_free(album_uuid); - g_free(file_name); g_free(name); return 0; } +int process_track_paths( + void * data, + int num_columns, + char ** fields, + char ** column_names +) { + KotoTrack * track = (KotoTrack*) data; + (void) num_columns; + (void) column_names; // Don't need these + + KotoLibrary * library = koto_cartographer_get_library_by_uuid(koto_maps, koto_utils_unquote_string(fields[0])); + + if (!KOTO_IS_LIBRARY(library)) { // Not a library + return 1; + } + + koto_track_set_path(track, library, koto_utils_unquote_string(fields[1])); + return 0; +} + + void read_from_db() { int artists_rc = sqlite3_exec(koto_db, "SELECT * FROM artists", process_artists, NULL, NULL); // Process our artists diff --git a/src/db/loaders.h b/src/db/loaders.h index 7f6b04e..07c90d7 100644 --- a/src/db/loaders.h +++ b/src/db/loaders.h @@ -22,6 +22,13 @@ int process_artists( char ** column_names ); +int process_artist_paths( + void * data, + int num_columns, + char ** fields, + char ** column_names +); + int process_albums( void * data, int num_columns, @@ -50,4 +57,11 @@ int process_tracks( char ** column_names ); +int process_track_paths( + void * data, + int num_columns, + char ** fields, + char ** column_names +); + void read_from_db(); \ No newline at end of file diff --git a/src/indexer/album.c b/src/indexer/album.c index 5f2f1b2..884b73d 100644 --- a/src/indexer/album.c +++ b/src/indexer/album.c @@ -20,36 +20,22 @@ #include #include #include "../db/cartographer.h" +#include "../db/db.h" #include "../playlist/current.h" #include "../playlist/playlist.h" +#include "../koto-utils.h" #include "structs.h" -#include "koto-utils.h" +#include "track-helpers.h" extern KotoCartographer * koto_maps; extern KotoCurrentPlaylist * current_playlist; +extern magic_t magic_cookie; extern sqlite3 * koto_db; -struct _KotoAlbum { - GObject parent_instance; - gchar * uuid; - gchar * path; - - gchar * name; - gchar * art_path; - gchar * artist_uuid; - GList * tracks; - - gboolean has_album_art; - gboolean do_initial_index; -}; - -G_DEFINE_TYPE(KotoAlbum, koto_album, G_TYPE_OBJECT); - enum { PROP_0, PROP_UUID, PROP_DO_INITIAL_INDEX, - PROP_PATH, PROP_ALBUM_NAME, PROP_ART_PATH, PROP_ARTIST_UUID, @@ -60,6 +46,46 @@ static GParamSpec * props[N_PROPERTIES] = { NULL, }; +enum { + SIGNAL_TRACK_ADDED, + SIGNAL_TRACK_REMOVED, + N_SIGNALS +}; + +static guint album_signals[N_SIGNALS] = { + 0 +}; + +struct _KotoAlbum { + GObject parent_instance; + gchar * uuid; + + gchar * name; + gchar * art_path; + gchar * artist_uuid; + + GList * tracks; + GHashTable * paths; + + gboolean has_album_art; + gboolean do_initial_index; +}; + +struct _KotoAlbumClass { + GObjectClass parent_class; + + void (* track_added) ( + KotoAlbum * album, + KotoTrack * track + ); + void (* track_removed) ( + KotoAlbum * album, + KotoTrack * track + ); +}; + +G_DEFINE_TYPE(KotoAlbum, koto_album, G_TYPE_OBJECT); + static void koto_album_get_property( GObject * obj, guint prop_id, @@ -97,14 +123,6 @@ static void koto_album_class_init(KotoAlbumClass * c) { G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE ); - props[PROP_PATH] = g_param_spec_string( - "path", - "Path", - "Path to Album", - NULL, - G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE - ); - props[PROP_ALBUM_NAME] = g_param_spec_string( "name", "Name", @@ -130,28 +148,64 @@ static void koto_album_class_init(KotoAlbumClass * c) { ); g_object_class_install_properties(gobject_class, N_PROPERTIES, props); + + album_signals[SIGNAL_TRACK_ADDED] = g_signal_new( + "track-added", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoAlbumClass, track_added), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + KOTO_TYPE_TRACK + ); + + album_signals[SIGNAL_TRACK_REMOVED] = g_signal_new( + "track-removed", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoAlbumClass, track_removed), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + KOTO_TYPE_TRACK + ); } static void koto_album_init(KotoAlbum * self) { self->has_album_art = FALSE; self->tracks = NULL; + self->paths = g_hash_table_new(g_str_hash, g_str_equal); } void koto_album_add_track( KotoAlbum * self, KotoTrack * track ) { - if (track == NULL) { // Not a file + if (!KOTO_IS_ALBUM(self)) { // Not an album return; } - gchar * track_uuid; + if (!KOTO_IS_TRACK(track)) { // Not a track + return; + } - g_object_get(track, "uuid", &track_uuid, NULL); + gchar * track_uuid = koto_track_get_uuid(track); - if (g_list_index(self->tracks, track_uuid) == -1) { - koto_cartographer_add_track(koto_maps, track); // Add the track to cartographer - self->tracks = g_list_insert_sorted_with_data(self->tracks, track_uuid, koto_album_sort_tracks, NULL); + if (g_list_index(self->tracks, track_uuid) == -1) { // Haven't already added the track + koto_cartographer_add_track(koto_maps, track); // Add the track to cartographer if necessary + self->tracks = g_list_insert_sorted_with_data(self->tracks, track_uuid, koto_track_helpers_sort_tracks_by_uuid, NULL); + + g_signal_emit( + self, + album_signals[SIGNAL_TRACK_ADDED], + 0, + track + ); } } @@ -161,40 +215,44 @@ void koto_album_commit(KotoAlbum * self) { } gchar * commit_op = g_strdup_printf( - "INSERT INTO albums(id, path, artist_id, name, art_path)" - "VALUES('%s', quote(\"%s\"), '%s', quote(\"%s\"), quote(\"%s\"))" - "ON CONFLICT(id) DO UPDATE SET path=excluded.path, name=excluded.name, art_path=excluded.art_path;", + "INSERT INTO albums(id, artist_id, name, art_path)" + "VALUES('%s', '%s', quote(\"%s\"), quote(\"%s\"))" + "ON CONFLICT(id) DO UPDATE SET artist_id=excluded.artist_id, name=excluded.name, art_path=excluded.art_path;", self->uuid, - self->path, self->artist_uuid, self->name, self->art_path ); - gchar * commit_op_errmsg = NULL; - int rc = sqlite3_exec(koto_db, commit_op, 0, 0, &commit_op_errmsg); + new_transaction(commit_op, "Failed to write our album to the database", FALSE); - if (rc != SQLITE_OK) { - g_warning("Failed to write our album to the database: %s", commit_op_errmsg); + GHashTableIter paths_iter; + g_hash_table_iter_init(&paths_iter, self->paths); // Create an iterator for our paths + gpointer lib_uuid_ptr, album_rel_path_ptr; + while (g_hash_table_iter_next(&paths_iter, &lib_uuid_ptr, &album_rel_path_ptr)) { + gchar * lib_uuid = lib_uuid_ptr; + gchar * album_rel_path = album_rel_path_ptr; + + gchar * commit_op = g_strdup_printf( + "INSERT INTO libraries_albums(id, album_id, path)" + "VALUES ('%s', '%s', quote(\"%s\"))" + "ON CONFLICT(id, album_id) DO UPDATE SET path=excluded.path;", + lib_uuid, + self->uuid, + album_rel_path + ); + + new_transaction(commit_op, "Failed to add this path for the album", FALSE); } - - g_free(commit_op); - g_free(commit_op_errmsg); } void koto_album_find_album_art(KotoAlbum * self) { - magic_t magic_cookie = magic_open(MAGIC_MIME); - - if (magic_cookie == NULL) { + if (self->has_album_art) { // If we already have album art return; } - if (magic_load(magic_cookie, NULL) != 0) { - magic_close(magic_cookie); - return; - } - - DIR * dir = opendir(self->path); // Attempt to open our directory + gchar * optimal_album_path = koto_album_get_path(self); + DIR * dir = opendir(optimal_album_path); // Attempt to open our directory if (dir == NULL) { return; @@ -211,131 +269,37 @@ void koto_album_find_album_art(KotoAlbum * self) { continue; // Skip } - gchar * full_path = g_strdup_printf("%s%s%s", self->path, G_DIR_SEPARATOR_S, entry->d_name); + gchar * full_path = g_strdup_printf("%s%s%s", optimal_album_path, G_DIR_SEPARATOR_S, entry->d_name); const char * mime_type = magic_file(magic_cookie, full_path); - if (mime_type == NULL) { // Failed to get the mimetype + if ( + (mime_type == NULL) || // Failed to get the mimetype + ((mime_type != NULL) && !g_str_has_prefix(mime_type, "image/")) // Got the mimetype but it is not an image + ) { g_free(full_path); continue; // Skip } - if (g_str_has_prefix(mime_type, "image/") && !self->has_album_art) { // Is an image file and doesn't have album art yet - gchar * album_art_no_ext = g_strdup(koto_utils_get_filename_without_extension(entry->d_name)); // Get the name of the file without the extension - gchar * lower_art = g_strdup(g_utf8_strdown(album_art_no_ext, -1)); // Lowercase + gchar * album_art_no_ext = g_strdup(koto_utils_get_filename_without_extension(entry->d_name)); // Get the name of the file without the extension - if ( - (g_strrstr(lower_art, "Small") == NULL) && // Not Small - (g_strrstr(lower_art, "back") == NULL) // Not back - ) { - koto_album_set_album_art(self, full_path); - g_free(album_art_no_ext); - g_free(lower_art); - break; - } + gchar * lower_art = g_strdup(g_utf8_strdown(album_art_no_ext, -1)); // Lowercase + g_free(album_art_no_ext); - g_free(album_art_no_ext); - g_free(lower_art); + gboolean should_set = (g_strrstr(lower_art, "Small") == NULL) && (g_strrstr(lower_art, "back") == NULL); // Not back or small + + g_free(lower_art); + + if (should_set) { + koto_album_set_album_art(self, full_path); + g_free(full_path); + break; } g_free(full_path); } closedir(dir); - magic_close(magic_cookie); -} - -void koto_album_find_tracks( - KotoAlbum * self, - magic_t magic_cookie, - const gchar * path -) { - if (magic_cookie == NULL) { // No cookie provided - magic_cookie = magic_open(MAGIC_MIME); - } - - if (magic_cookie == NULL) { - return; - } - - if (path == NULL) { - path = self->path; - } - - if (magic_load(magic_cookie, NULL) != 0) { - magic_close(magic_cookie); - return; - } - - DIR * dir = opendir(path); // Attempt to open our directory - - if (dir == NULL) { - return; - } - - struct dirent * entry; - - while ((entry = readdir(dir))) { - if (g_str_has_prefix(entry->d_name, ".")) { // Reference to parent dir, self, or a hidden item - continue; // Skip - } - - gchar * full_path = g_strdup_printf("%s%s%s", path, G_DIR_SEPARATOR_S, entry->d_name); - - if (entry->d_type == DT_DIR) { // If this is a directory - koto_album_find_tracks(self, magic_cookie, full_path); // Recursively find tracks - g_free(full_path); - continue; - } - - if (entry->d_type != DT_REG) { // Not a regular file - continue; // SKIP - } - - const char * mime_type = magic_file(magic_cookie, full_path); - - if (mime_type == NULL) { // Failed to get the mimetype - g_free(full_path); - continue; // Skip - } - - if (g_str_has_prefix(mime_type, "audio/") || g_str_has_prefix(mime_type, "video/ogg")) { // Is an audio file or ogg because it is special - gchar * appended_slash_to_path = g_strdup_printf("%s%s", g_strdup(self->path), G_DIR_SEPARATOR_S); - gchar ** possible_cd_split = g_strsplit(full_path, appended_slash_to_path, -1); // Split based on the album path - guint * cd = (guint*) 1; - - gchar * track_with_cd_sep = g_strdup(possible_cd_split[1]); // Duplicate - gchar ** split_on_cd = g_strsplit(track_with_cd_sep, G_DIR_SEPARATOR_S, -1); // Split based on separator (e.g. / ) - - if (g_strv_length(split_on_cd) > 1) { - gchar * cdd = g_strdup(split_on_cd[0]); - gchar ** cd_sep = g_strsplit(g_utf8_strdown(cdd, -1), "cd", -1); - - if (g_strv_length(cd_sep) > 1) { - gchar * pos_str = g_strdup(cd_sep[1]); - cd = (guint*) g_ascii_strtoull(pos_str, NULL, 10); // Attempt to convert - g_free(pos_str); - } - - g_strfreev(cd_sep); - g_free(cdd); - } - - g_strfreev(split_on_cd); - g_free(track_with_cd_sep); - - g_strfreev(possible_cd_split); - g_free(appended_slash_to_path); - - KotoTrack * track = koto_track_new(self, full_path, cd); - - if (track != NULL) { // Is a file - koto_album_add_track(self, track); // Add our file - } - } - - g_free(full_path); - } } static void koto_album_get_property( @@ -353,14 +317,11 @@ static void koto_album_get_property( case PROP_DO_INITIAL_INDEX: g_value_set_boolean(val, self->do_initial_index); break; - case PROP_PATH: - g_value_set_string(val, self->path); - break; case PROP_ALBUM_NAME: g_value_set_string(val, self->name); break; case PROP_ART_PATH: - g_value_set_string(val, koto_album_get_album_art(self)); + g_value_set_string(val, koto_album_get_art(self)); break; case PROP_ARTIST_UUID: g_value_set_string(val, self->artist_uuid); @@ -387,9 +348,6 @@ static void koto_album_set_property( case PROP_DO_INITIAL_INDEX: self->do_initial_index = g_value_get_boolean(val); break; - case PROP_PATH: // Path to the album - koto_album_update_path(self, (gchar*) g_value_get_string(val)); - break; case PROP_ALBUM_NAME: // Name of album koto_album_set_album_name(self, g_value_get_string(val)); break; @@ -405,7 +363,7 @@ static void koto_album_set_property( } } -gchar * koto_album_get_album_art(KotoAlbum * self) { +gchar * koto_album_get_art(KotoAlbum * self) { if (!KOTO_IS_ALBUM(self)) { // Not an album return g_strdup(""); } @@ -413,7 +371,7 @@ gchar * koto_album_get_album_art(KotoAlbum * self) { return g_strdup((self->has_album_art && koto_utils_is_string_valid(self->art_path)) ? self->art_path : ""); } -gchar * koto_album_get_album_name(KotoAlbum * self) { +gchar * koto_album_get_name(KotoAlbum * self) { if (!KOTO_IS_ALBUM(self)) { // Not an album return NULL; } @@ -437,6 +395,28 @@ gchar * koto_album_get_album_uuid(KotoAlbum * self) { return g_strdup(self->uuid); // Return a duplicate of the UUID } +gchar * koto_album_get_path(KotoAlbum * self) { + if (!KOTO_IS_ALBUM(self) || (KOTO_IS_ALBUM(self) && (g_list_length(g_hash_table_get_keys(self->paths)) == 0))) { // If this is not an album or is but we have no paths associated with it + return NULL; + } + + GList * libs = koto_cartographer_get_libraries(koto_maps); // Get all of our libraries + GList * cur_lib_list; + + for (cur_lib_list = libs; cur_lib_list != NULL; cur_lib_list = libs->next) { // Iterate over our libraries + KotoLibrary * cur_library = libs->data; // Get this as a KotoLibrary + gchar * library_relative_path = g_hash_table_lookup(self->paths, koto_library_get_uuid(cur_library)); // Get any relative path in our paths based on the current UUID + + if (!koto_utils_is_string_valid(library_relative_path)) { // Not a valid path + continue; + } + + return g_strdup(g_build_path(G_DIR_SEPARATOR_S, koto_library_get_path(cur_library), library_relative_path, NULL)); // Build our full library path using library's path and our file relative path + } + + return NULL; +} + GList * koto_album_get_tracks(KotoAlbum * self) { if (!KOTO_IS_ALBUM(self)) { // Not an album return NULL; @@ -445,7 +425,7 @@ GList * koto_album_get_tracks(KotoAlbum * self) { return self->tracks; // Return the tracks } -gchar * koto_album_get_uuid(KotoAlbum *self) { +gchar * koto_album_get_uuid(KotoAlbum * self) { if (!KOTO_IS_ALBUM(self)) { // Not an album return NULL; } @@ -474,7 +454,32 @@ void koto_album_set_album_art( self->has_album_art = TRUE; } -void koto_album_remove_file( +void koto_album_set_path( + KotoAlbum * self, + KotoLibrary * lib, + const gchar * fixed_path +) { + if (!KOTO_IS_ALBUM(self)) { // Not an album + return; + } + + gchar * path = g_strdup(fixed_path); // Duplicate our fixed_path + gchar * relative_path = koto_library_get_relative_path_to_file(lib, path); // Get the relative path to the file for the given library + + gchar * library_uuid = koto_library_get_uuid(lib); // Get the library for this path + g_hash_table_replace(self->paths, library_uuid, relative_path); // Replace any existing value or add this one + + koto_album_set_album_name(self, g_path_get_basename(relative_path)); // Update our album name based on the base name + + if (!self->do_initial_index) { // Not doing our initial index + return; + } + + koto_album_find_album_art(self); // Update our path for the album art + self->do_initial_index = FALSE; +} + +void koto_album_remove_track( KotoAlbum * self, KotoTrack * track ) { @@ -482,14 +487,17 @@ void koto_album_remove_file( return; } - if (track == NULL) { // Not a file + if (!KOTO_IS_TRACK(track)) { // Not a track return; } - gchar * track_uuid; - - g_object_get(track, "parsed-name", &track_uuid, NULL); - self->tracks = g_list_remove(self->tracks, track_uuid); + self->tracks = g_list_remove(self->tracks, koto_track_get_uuid(track)); + g_signal_emit( + self, + album_signals[SIGNAL_TRACK_REMOVED], + 0, + track + ); } void koto_album_set_album_name( @@ -565,90 +573,11 @@ void koto_album_set_as_current_playlist(KotoAlbum * self) { koto_current_playlist_set_playlist(current_playlist, new_album_playlist); // Set our new current playlist } -gint koto_album_sort_tracks( - gconstpointer track1_uuid, - gconstpointer track2_uuid, - gpointer user_data -) { - (void) user_data; - KotoTrack * track1 = koto_cartographer_get_track_by_uuid(koto_maps, (gchar*) track1_uuid); - KotoTrack * track2 = koto_cartographer_get_track_by_uuid(koto_maps, (gchar*) track2_uuid); - - 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; +KotoAlbum * koto_album_new(gchar * artist_uuid) { + if (!koto_utils_is_string_valid(artist_uuid)) { // Invalid artist UUID provided + return NULL; } - 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; - } -} - -void koto_album_update_path( - KotoAlbum * self, - gchar * new_path -) { - if (!KOTO_IS_ALBUM(self)) { // Not an album - return; - } - - if (!koto_utils_is_string_valid(new_path)) { - return; - } - - if (koto_utils_is_string_valid(self->path)) { // Path is currently set - g_free(self->path); - } - - self->path = g_strdup(new_path); - koto_album_set_album_name(self, g_path_get_basename(self->path)); // Update our album name based on the base name - - if (!self->do_initial_index) { // Not doing our initial index - return; - } - - koto_album_find_album_art(self); // Update our path for the album art -} - -KotoAlbum * koto_album_new( - KotoArtist * artist, - const gchar * path -) { - gchar * artist_uuid = NULL; - - g_object_get(artist, "uuid", &artist_uuid, NULL); - KotoAlbum * album = g_object_new( KOTO_TYPE_ALBUM, "artist-uuid", @@ -657,14 +586,9 @@ KotoAlbum * koto_album_new( g_strdup(g_uuid_string_random()), "do-initial-index", TRUE, - "path", - path, NULL ); - koto_album_commit(album); - koto_album_find_tracks(album, NULL, NULL); // Scan for tracks now that we committed to the database (hopefully) - return album; } diff --git a/src/indexer/artist.c b/src/indexer/artist.c index 6756e36..d07cc5b 100644 --- a/src/indexer/artist.c +++ b/src/indexer/artist.c @@ -17,28 +17,17 @@ #include #include -#include "structs.h" #include "../db/db.h" +#include "../db/cartographer.h" #include "../koto-utils.h" +#include "structs.h" +#include "track-helpers.h" -extern sqlite3 * koto_db; - -struct _KotoArtist { - GObject parent_instance; - gchar * uuid; - gchar * path; - - gboolean has_artist_art; - gchar * artist_name; - GList * albums; -}; - -G_DEFINE_TYPE(KotoArtist, koto_artist, G_TYPE_OBJECT); +extern KotoCartographer * koto_maps; enum { PROP_0, PROP_UUID, - PROP_PATH, PROP_ARTIST_NAME, N_PROPERTIES }; @@ -47,6 +36,53 @@ static GParamSpec * props[N_PROPERTIES] = { NULL, }; +enum { + SIGNAL_ALBUM_ADDED, + SIGNAL_ALBUM_REMOVED, + SIGNAL_TRACK_ADDED, + SIGNAL_TRACK_REMOVED, + N_SIGNALS +}; + +static guint artist_signals[N_SIGNALS] = { + 0 +}; + +struct _KotoArtist { + GObject parent_instance; + gchar * uuid; + + gboolean has_artist_art; + gchar * artist_name; + GList * albums; + GList * tracks; + GHashTable * paths; + KotoLibraryType type; +}; + +struct _KotoArtistClass { + GObjectClass parent_class; + + void (* album_added) ( + KotoArtist * artist, + KotoAlbum * album + ); + void (* album_removed) ( + KotoArtist * artist, + KotoAlbum * album + ); + void (* track_added) ( + KotoArtist * artist, + KotoTrack * track + ); + void (* track_removed) ( + KotoArtist * artist, + KotoTrack * track + ); +}; + +G_DEFINE_TYPE(KotoArtist, koto_artist, G_TYPE_OBJECT); + static void koto_artist_get_property( GObject * obj, guint prop_id, @@ -68,6 +104,58 @@ static void koto_artist_class_init(KotoArtistClass * c) { gobject_class->set_property = koto_artist_set_property; gobject_class->get_property = koto_artist_get_property; + artist_signals[SIGNAL_ALBUM_ADDED] = g_signal_new( + "album-added", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoArtistClass, album_added), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + KOTO_TYPE_ALBUM + ); + + artist_signals[SIGNAL_ALBUM_REMOVED] = g_signal_new( + "album-removed", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoArtistClass, album_removed), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + KOTO_TYPE_ALBUM + ); + + artist_signals[SIGNAL_TRACK_ADDED] = g_signal_new( + "track-added", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoArtistClass, track_added), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + KOTO_TYPE_TRACK + ); + + artist_signals[SIGNAL_TRACK_REMOVED] = g_signal_new( + "track-removed", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoArtistClass, track_removed), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + KOTO_TYPE_TRACK + ); + props[PROP_UUID] = g_param_spec_string( "uuid", "UUID to Artist in database", @@ -76,14 +164,6 @@ static void koto_artist_class_init(KotoArtistClass * c) { G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE ); - props[PROP_PATH] = g_param_spec_string( - "path", - "Path", - "Path to Artist", - NULL, - G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE - ); - props[PROP_ARTIST_NAME] = g_param_spec_string( "name", "Name", @@ -102,28 +182,41 @@ void koto_artist_commit(KotoArtist * self) { // TODO: Support multiple types instead of just local music artist gchar * commit_op = g_strdup_printf( - "INSERT INTO artists(id,path,type,name,art_path)" - "VALUES ('%s', quote(\"%s\"), 0, quote(\"%s\"), NULL)" - "ON CONFLICT(id) DO UPDATE SET path=excluded.path, type=excluded.type, name=excluded.name, art_path=excluded.art_path;", + "INSERT INTO artists(id , name, art_path)" + "VALUES ('%s', quote(\"%s\"), NULL)" + "ON CONFLICT(id) DO UPDATE SET name=excluded.name, art_path=excluded.art_path;", self->uuid, - self->path, self->artist_name ); - gchar * commit_opt_errmsg = NULL; - int rc = sqlite3_exec(koto_db, commit_op, 0, 0, &commit_opt_errmsg); + new_transaction(commit_op, "Failed to write our artist to the database", FALSE); - if (rc != SQLITE_OK) { - g_warning("Failed to write our artist to the database: %s", commit_opt_errmsg); + GHashTableIter paths_iter; + g_hash_table_iter_init(&paths_iter, self->paths); // Create an iterator for our paths + gpointer lib_uuid_ptr, artist_rel_path_ptr; + while (g_hash_table_iter_next(&paths_iter, &lib_uuid_ptr, &artist_rel_path_ptr)) { + gchar * lib_uuid = lib_uuid_ptr; + gchar * artist_rel_path = artist_rel_path_ptr; + + gchar * commit_op = g_strdup_printf( + "INSERT INTO libraries_artists(id, artist_id, path)" + "VALUES ('%s', '%s', quote(\"%s\"))" + "ON CONFLICT(id, artist_id) DO UPDATE SET path=excluded.path;", + lib_uuid, + self->uuid, + artist_rel_path + ); + + new_transaction(commit_op, "Failed to add this path for the artist", FALSE); } - - g_free(commit_op); - g_free(commit_opt_errmsg); } static void koto_artist_init(KotoArtist * self) { - self->has_artist_art = FALSE; self->albums = NULL; // Create a new GList + self->has_artist_art = FALSE; + self->paths = g_hash_table_new(g_str_hash, g_str_equal); + self->tracks = NULL; + self->type = KOTO_LIBRARY_TYPE_UNKNOWN; } static void koto_artist_get_property( @@ -138,9 +231,6 @@ static void koto_artist_get_property( case PROP_UUID: g_value_set_string(val, self->uuid); break; - case PROP_PATH: - g_value_set_string(val, self->path); - break; case PROP_ARTIST_NAME: g_value_set_string(val, self->artist_name); break; @@ -163,9 +253,6 @@ static void koto_artist_set_property( self->uuid = g_strdup(g_value_get_string(val)); g_object_notify_by_pspec(G_OBJECT(self), props[PROP_UUID]); break; - case PROP_PATH: - koto_artist_update_path(self, (gchar*) g_value_get_string(val)); - break; case PROP_ARTIST_NAME: koto_artist_set_artist_name(self, (gchar*) g_value_get_string(val)); break; @@ -177,21 +264,59 @@ static void koto_artist_set_property( void koto_artist_add_album( KotoArtist * self, - gchar * album_uuid + KotoAlbum * album ) { if (!KOTO_IS_ARTIST(self)) { // Not an artist return; } - if (!koto_utils_is_string_valid(album_uuid)) { // No album UUID really defined + if (!KOTO_IS_ALBUM(album)) { // Album provided is not an album return; } - gchar * uuid = g_strdup(album_uuid); // Duplicate our UUID + gchar * album_uuid = koto_album_get_uuid(album); - if (g_list_index(self->albums, uuid) == -1) { - self->albums = g_list_append(self->albums, uuid); // Push to end of list + if (g_list_index(self->albums, album_uuid) != -1) { // If we have already added the album + return; } + + self->albums = g_list_append(self->albums, album_uuid); // Push to our album's UUID to the end of the list + + g_signal_emit( + self, + artist_signals[SIGNAL_ALBUM_ADDED], + 0, + album + ); +} + +void koto_artist_add_track( + KotoArtist * self, + KotoTrack * track +) { + if (!KOTO_IS_ARTIST(self)) { // Not an artist + return; + } + + if (!KOTO_IS_TRACK(track)) { // Not a track + return; + } + + gchar * track_uuid = koto_track_get_uuid(track); + + if (g_list_index(self->tracks, track_uuid) != -1) { // If we have already added the track + return; + } + + koto_cartographer_add_track(koto_maps, track); // Add the track to cartographer if necessary + self->tracks = g_list_insert_sorted_with_data(self->tracks, track_uuid, koto_track_helpers_sort_tracks_by_uuid, NULL); + + g_signal_emit( + self, + artist_signals[SIGNAL_TRACK_ADDED], + 0, + track + ); } GList * koto_artist_get_albums(KotoArtist * self) { @@ -202,6 +327,33 @@ GList * koto_artist_get_albums(KotoArtist * self) { return g_list_copy(self->albums); } +KotoAlbum * koto_artist_get_album_by_name( + KotoArtist * self, + gchar * album_name +) { + if (!KOTO_IS_ARTIST(self)) { // Not an artist + return NULL; + } + + KotoAlbum * album = NULL; + + GList * cur_list_iter; + for (cur_list_iter = self->albums; cur_list_iter != NULL; cur_list_iter = cur_list_iter->next) { // Iterate through our albums by their UUIDs + KotoAlbum * album_for_uuid = koto_cartographer_get_album_by_uuid(koto_maps, cur_list_iter->data); // Get the album + + if (!KOTO_IS_ALBUM(album_for_uuid)) { // Not an album + continue; + } + + if (g_strcmp0(koto_album_get_name(album_for_uuid), album_name) == 0) { // These album names match + album = album_for_uuid; + break; + } + } + + return album; +} + gchar * koto_artist_get_name(KotoArtist * self) { if (!KOTO_IS_ARTIST(self)) { // Not an artist return g_strdup(""); @@ -210,8 +362,16 @@ gchar * koto_artist_get_name(KotoArtist * self) { return g_strdup(koto_utils_is_string_valid(self->artist_name) ? self->artist_name : ""); // Return artist name if set } +GList * koto_artist_get_tracks(KotoArtist * self) { + return KOTO_IS_ARTIST(self) ? self->tracks : NULL; +} + +KotoLibraryType koto_artist_get_lib_type(KotoArtist * self) { + return KOTO_IS_ARTIST(self) ? self->type : KOTO_LIBRARY_TYPE_UNKNOWN; +} + gchar * koto_artist_get_uuid(KotoArtist * self) { - return self->uuid; + return KOTO_IS_ARTIST(self) ? self->uuid : NULL; } void koto_artist_remove_album( @@ -226,30 +386,36 @@ void koto_artist_remove_album( return; } - gchar * album_uuid; + self->albums = g_list_remove(self->albums, koto_album_get_uuid(album)); - g_object_get(album, "uuid", &album_uuid, NULL); - self->albums = g_list_remove(self->albums, album_uuid); + g_signal_emit( + self, + artist_signals[SIGNAL_ALBUM_REMOVED], + 0, + album + ); } -void koto_artist_update_path( +void koto_artist_remove_track( KotoArtist * self, - gchar * new_path + KotoTrack * track ) { if (!KOTO_IS_ARTIST(self)) { // Not an artist return; } - if (!koto_utils_is_string_valid(new_path)) { // No path really + if (!KOTO_IS_TRACK(track)) { // Not a track return; } - if (koto_utils_is_string_valid(self->path)) { // Already have a path set - g_free(self->path); // Free - } + self->tracks = g_list_remove(self->tracks, koto_track_get_uuid(track)); - self->path = g_strdup(new_path); - g_object_notify_by_pspec(G_OBJECT(self), props[PROP_PATH]); + g_signal_emit( + self, + artist_signals[SIGNAL_TRACK_ADDED], + 0, + track + ); } void koto_artist_set_artist_name( @@ -272,19 +438,39 @@ void koto_artist_set_artist_name( g_object_notify_by_pspec(G_OBJECT(self), props[PROP_ARTIST_NAME]); } -KotoArtist * koto_artist_new(gchar * path) { +void koto_artist_set_path( + KotoArtist * self, + KotoLibrary * lib, + const gchar * fixed_path, + gboolean should_commit +) { + if (!KOTO_IS_ARTIST(self)) { // Not an artist + return; + } + + gchar * path = g_strdup(fixed_path); // Duplicate our fixed_path + gchar * relative_path = koto_library_get_relative_path_to_file(lib, path); // Get the relative path to the file for the given library + + gchar * library_uuid = koto_library_get_uuid(lib); // Get the library for this path + g_hash_table_replace(self->paths, library_uuid, relative_path); // Replace any existing value or add this one + + if (should_commit) { // Should commit to the DB + koto_artist_commit(self); // Save the artist + } + + self->type = koto_library_get_lib_type(lib); // Define our artist type as the type from the library +} + +KotoArtist * koto_artist_new(gchar * artist_name) { KotoArtist * artist = g_object_new( KOTO_TYPE_ARTIST, "uuid", g_uuid_string_random(), - "path", - path, "name", - g_path_get_basename(path), + artist_name, NULL ); - koto_artist_commit(artist); // Commit the artist immediately to the database return artist; } diff --git a/src/indexer/file-indexer.c b/src/indexer/file-indexer.c index d5c69a2..6e66011 100644 --- a/src/indexer/file-indexer.c +++ b/src/indexer/file-indexer.c @@ -19,173 +19,13 @@ #include #include #include -#include #include "../db/cartographer.h" -#include "../db/db.h" -#include "../db/loaders.h" -#include "../playlist/playlist.h" +#include "../koto-utils.h" #include "structs.h" +#include "track-helpers.h" extern KotoCartographer * koto_maps; -extern sqlite3 * koto_db; - -struct _KotoLibrary { - GObject parent_instance; - - gchar * path; // Compat - - gchar * directory; - gchar * name; - KotoLibraryType type; - gchar * uuid; - - gboolean override_builtin; - - magic_t magic_cookie; -}; - -G_DEFINE_TYPE(KotoLibrary, koto_library, G_TYPE_OBJECT); - -enum { - PROP_0, - PROP_PATH, - N_PROPERTIES -}; - -static GParamSpec * props[N_PROPERTIES] = { - NULL, -}; - -static void koto_library_get_property( - GObject * obj, - guint prop_id, - GValue * val, - GParamSpec * spec -); - -static void koto_library_set_property( - GObject * obj, - guint prop_id, - const GValue * val, - GParamSpec * spec -); - -static void koto_library_class_init(KotoLibraryClass * c) { - GObjectClass * gobject_class; - - gobject_class = G_OBJECT_CLASS(c); - gobject_class->set_property = koto_library_set_property; - gobject_class->get_property = koto_library_get_property; - - props[PROP_PATH] = g_param_spec_string( - "path", - "Path", - "Path to Music", - NULL, - G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE - ); - - g_object_class_install_properties(gobject_class, N_PROPERTIES, props); - taglib_id3v2_set_default_text_encoding(TagLib_ID3v2_UTF8); // Ensure our id3v2 text encoding is UTF-8 -} - -static void koto_library_init(KotoLibrary * self) { - (void) self; -} - -static void koto_library_get_property( - GObject * obj, - guint prop_id, - GValue * val, - GParamSpec * spec -) { - KotoLibrary * self = KOTO_LIBRARY(obj); - - switch (prop_id) { - case PROP_PATH: - g_value_set_string(val, self->path); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); - break; - } -} - -static void koto_library_set_property( - GObject * obj, - guint prop_id, - const GValue * val, - GParamSpec * spec -) { - KotoLibrary * self = KOTO_LIBRARY(obj); - - switch (prop_id) { - case PROP_PATH: - koto_library_set_path(self, g_strdup(g_value_get_string(val))); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); - break; - } -} - -void koto_library_set_path( - KotoLibrary * self, - gchar * path -) { - if (path == NULL) { - return; - } - - if (self->path != NULL) { - g_free(self->path); - } - - self->path = path; - - if (created_new_db) { // Created new database - start_indexing(self); // Start index operation - } else { // Have existing database - read_from_db(NULL); // Read from the database - } -} - -void start_indexing(KotoLibrary * self) { - struct stat library_stat; - int success = stat(self->path, &library_stat); - - if (success != 0) { // Failed to read the library path - return; - } - - if (!S_ISDIR(library_stat.st_mode)) { // Is not a directory - g_warning("%s is not a directory", self->path); - return; - } - - self->magic_cookie = magic_open(MAGIC_MIME); - - if (self->magic_cookie == NULL) { - return; - } - - if (magic_load(self->magic_cookie, NULL) != 0) { - magic_close(self->magic_cookie); - return; - } - - index_folder(self, self->path, 0); - magic_close(self->magic_cookie); -} - -KotoLibrary * koto_library_new(const gchar * path) { - return g_object_new( - KOTO_TYPE_LIBRARY, - "path", - path, - NULL - ); -} +extern magic_t magic_cookie; void index_folder( KotoLibrary * self, @@ -210,20 +50,14 @@ void index_folder( gchar * full_path = g_strdup_printf("%s%s%s", path, G_DIR_SEPARATOR_S, entry->d_name); if (entry->d_type == DT_DIR) { // Directory - if (depth == 1) { // If we are following FOLDER/ARTIST/ALBUM then this would be artist - KotoArtist * artist = koto_artist_new(full_path); // Attempt to get the artist - gchar * artist_name; + if (depth == 1) { // If we are following (ARTIST,AUTHOR,PODCAST)/ALBUM then this would be artist + KotoArtist * artist = koto_artist_new(entry->d_name); // Attempt to get the artist - g_object_get( - artist, - "name", - &artist_name, - NULL - ); - - koto_cartographer_add_artist(koto_maps, artist); // Add the artist to cartographer - index_folder(self, full_path, depth); // Index this directory - g_free(artist_name); + if (KOTO_IS_ARTIST(artist)) { + koto_artist_set_path(artist, self, full_path, TRUE); // Add the path for this library on this Artist and commit immediately + koto_cartographer_add_artist(koto_maps, artist); // Add the artist to cartographer + index_folder(self, full_path, depth); // Index this directory + } } 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 KotoArtist * artist = koto_cartographer_get_artist_by_name(koto_maps, artist_name); @@ -232,18 +66,155 @@ void index_folder( continue; } - KotoAlbum * album = koto_album_new(artist, full_path); - koto_cartographer_add_album(koto_maps, album); // Add our album to the cartographer + gchar * artist_uuid = koto_artist_get_uuid(artist); // Get the artist's UUID - gchar * album_uuid = NULL; - g_object_get(album, "uuid", &album_uuid, NULL); - koto_artist_add_album(artist, album_uuid); // Add the album + KotoAlbum * album = koto_album_new(artist_uuid); + + koto_album_set_path(album, self, full_path); + koto_album_commit(album); // Save to database immediately + + koto_cartographer_add_album(koto_maps, album); // Add our album to the cartographer + koto_artist_add_album(artist, album); // Add the album + + index_folder(self, full_path, depth); // Index inside the album g_free(artist_name); + } else if (depth == 3) { // Possibly CD within album + gchar ** split = g_strsplit(full_path, G_DIR_SEPARATOR_S, -1); + guint split_len = g_strv_length(split); + + if (split_len < 4) { + g_strfreev(split); + continue; + } + + gchar * album_name = g_strdup(split[split_len - 2]); + gchar * artist_name = g_strdup(split[split_len - 3]); + g_strfreev(split); + + if (!koto_utils_is_string_valid(album_name)) { + g_free(album_name); + continue; + } + + if (!koto_utils_is_string_valid(artist_name)) { + g_free(album_name); + g_free(artist_name); + continue; + } + + KotoArtist * artist = koto_cartographer_get_artist_by_name(koto_maps, artist_name); + g_free(artist_name); + + if (!KOTO_IS_ARTIST(artist)) { + continue; + } + + KotoAlbum * album = koto_artist_get_album_by_name(artist, album_name); // Get the album + g_free(album_name); + + if (!KOTO_IS_ALBUM(album)) { + continue; + } + + index_folder(self, full_path, depth); // Index inside the album } + } else if ((entry->d_type == DT_REG)) { // Is a file in artist folder or lower in FS hierarchy + index_file(self, full_path); // Index this audio file or weird ogg thing } g_free(full_path); } closedir(dir); // Close the directory +} + +void index_file( + KotoLibrary * lib, + const gchar * path +) { + const char * mime_type = magic_file(magic_cookie, path); + + if (mime_type == NULL) { // Failed to get the mimetype + return; + } + + if (!g_str_has_prefix(mime_type, "audio/") && !g_str_has_prefix(mime_type, "video/ogg")) { // Is not an audio file or ogg + return; + } + + gchar * relative_path_to_file = koto_library_get_relative_path_to_file(lib, g_strdup(path)); // Strip out library path so we have a relative path to the file + gchar ** split_on_relative_slashes = g_strsplit(relative_path_to_file, G_DIR_SEPARATOR_S, -1); // Split based on separator (e.g. / ) + guint slash_sep_count = g_strv_length(split_on_relative_slashes); + + gchar * artist_author_podcast_name = g_strdup(split_on_relative_slashes[0]); // No matter what, artist should be first + gchar * album_or_audiobook_name = NULL; + gchar * file_name = koto_track_helpers_get_name_for_file(path, artist_author_podcast_name); // Get the name of the file + guint cd = (guint) 1; + + if (slash_sep_count >= 3) { // If this it is at least "artist" + "album" + "file" (or with CD) + album_or_audiobook_name = g_strdup(split_on_relative_slashes[1]); // Duplicate the second item as the album or audiobook name + } + + // #region CD parsing logic + + if ((slash_sep_count == 4)) { // If is at least "artist" + "album" + "cd" + "file" + gchar * cd_str = g_strdup(g_strstrip(koto_utils_replace_string_all(g_utf8_strdown(split_on_relative_slashes[2], -1), "cd", ""))); // Replace a lowercased version of our CD ("cd") and trim any whitespace + + cd = (guint) g_ascii_strtoull(cd_str, NULL, 10); // Attempt to convert + + if (cd == 0) { // Had an error during conversion + cd = 1; // Set back to 1 + } + } + + // #endregion + + g_strfreev(split_on_relative_slashes); + + gchar * sorta_uniqueish_key = NULL; + + if (koto_utils_is_string_valid(album_or_audiobook_name)) { // Have audiobook or album name + sorta_uniqueish_key = g_strdup_printf("%s-%s-%s", artist_author_podcast_name, album_or_audiobook_name, file_name); + } else { // No audiobook or album name + sorta_uniqueish_key = g_strdup_printf("%s-%s", artist_author_podcast_name, file_name); + } + + KotoTrack * track = koto_cartographer_get_track_by_uniqueish_key(koto_maps, sorta_uniqueish_key); // Attempt to get any existing KotoTrack + + if (KOTO_IS_TRACK(track)) { // Got a track already + koto_track_set_path(track, lib, relative_path_to_file); // Add this path, which will determine the associated library within that function + } else { // Don't already have a track for this file + KotoArtist * artist = koto_cartographer_get_artist_by_name(koto_maps, artist_author_podcast_name); // Get the possible artist + + if (!KOTO_IS_ARTIST(artist)) { // Have an artist for this already + return; + } + + KotoAlbum * album = NULL; + + if (koto_utils_is_string_valid(album_or_audiobook_name)) { // Have an album or audiobook name + KotoAlbum * possible_album = koto_artist_get_album_by_name(artist, album_or_audiobook_name); + album = KOTO_IS_ALBUM(possible_album) ? possible_album : NULL; + } + + if (!KOTO_IS_ALBUM(album)) { + return; + } + + gchar * album_uuid = KOTO_IS_ALBUM(album) ? koto_album_get_uuid(album) : NULL; + + track = koto_track_new(koto_artist_get_uuid(artist), album_uuid, file_name, cd); + koto_track_set_path(track, lib, relative_path_to_file); // Immediately add the path to this file, for this Library + koto_artist_add_track(artist, track); // Add the track to the artist in the event this is a podcast (no album) or the track is directly in the artist directory + + if (KOTO_IS_ALBUM(album)) { // Have an album + koto_album_add_track(album, track); // Add this track since we haven't yet + } + + koto_cartographer_add_track(koto_maps, track); // Add to our cartographer tracks hashtable + } + + if (KOTO_IS_TRACK(track)) { // Is a track + koto_track_commit(track); // Save the track immediately + } } \ No newline at end of file diff --git a/src/indexer/library.c b/src/indexer/library.c new file mode 100644 index 0000000..410a1d2 --- /dev/null +++ b/src/indexer/library.c @@ -0,0 +1,511 @@ +/* library.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 "../config/config.h" +#include "../koto-utils.h" +#include "structs.h" + +extern KotoConfig * config; +extern GVolumeMonitor * volume_monitor; + +enum { + PROP_0, + PROP_TYPE, + PROP_UUID, + PROP_STORAGE_UUID, + PROP_CONSTRUCTION_PATH, + PROP_NAME, + N_PROPERTIES +}; + +static GParamSpec * props[N_PROPERTIES] = { + NULL, +}; + +enum { + SIGNAL_NOW_AVAILABLE, + SIGNAL_NOW_UNAVAILABLE, + N_SIGNALS +}; + +static guint library_signals[N_SIGNALS] = { + 0 +}; + +struct _KotoLibrary { + GObject parent_instance; + gchar * uuid; + + KotoLibraryType type; + gchar * directory; + gchar * storage_uuid; + + GMount * mount; + gulong mount_unmounted_handler; + gchar * mount_path; + + gboolean should_index; + + gchar * path; + gchar * relative_path; + gchar * name; +}; + +struct _KotoLibraryClass { + GObjectClass parent_class; + + void (* now_available) (KotoLibrary * library); + + void (* now_unavailable) (KotoLibrary * library); +}; + +G_DEFINE_TYPE(KotoLibrary, koto_library, G_TYPE_OBJECT); + +static void koto_library_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +); + +static void koto_library_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +); + +static void koto_library_class_init(KotoLibraryClass * c) { + GObjectClass * gobject_class; + + gobject_class = G_OBJECT_CLASS(c); + gobject_class->set_property = koto_library_set_property; + gobject_class->get_property = koto_library_get_property; + + library_signals[SIGNAL_NOW_AVAILABLE] = g_signal_new( + "now-available", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoLibraryClass, now_available), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0 + ); + + library_signals[SIGNAL_NOW_UNAVAILABLE] = g_signal_new( + "now-unavailable", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoLibraryClass, now_unavailable), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0 + ); + + props[PROP_UUID] = g_param_spec_string( + "uuid", + "UUID of Library", + "UUID of Library", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_TYPE] = g_param_spec_string( + "type", + "Type of Library", + "Type of Library", + koto_library_type_to_string(KOTO_LIBRARY_TYPE_MUSIC), + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_STORAGE_UUID] = g_param_spec_string( + "storage-uuid", + "Storage UUID to associated Mount of Library", + "Storage UUID to associated Mount of Library", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_CONSTRUCTION_PATH] = g_param_spec_string( + "construction-path", + "Construction Path", + "Path to this library during construction", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_WRITABLE + ); + + props[PROP_NAME] = g_param_spec_string( + "name", + "Name of the Library", + "Name of the Library", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_WRITABLE + ); + + g_object_class_install_properties(gobject_class, N_PROPERTIES, props); +} + +static void koto_library_init(KotoLibrary * self) { + (void) self; +} + +static void koto_library_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +) { + KotoLibrary * self = KOTO_LIBRARY(obj); + + switch (prop_id) { + case PROP_NAME: + g_value_set_string(val, g_strdup(self->name)); + break; + case PROP_UUID: + g_value_set_string(val, self->uuid); + break; + case PROP_STORAGE_UUID: + g_value_set_string(val, self->storage_uuid); + break; + case PROP_TYPE: + g_value_set_string(val, koto_library_type_to_string(self->type)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +static void koto_library_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +) { + KotoLibrary * self = KOTO_LIBRARY(obj); + + switch (prop_id) { + case PROP_UUID: + self->uuid = g_strdup(g_value_get_string(val)); + break; + case PROP_TYPE: + self->type = koto_library_type_from_string(g_strdup(g_value_get_string(val))); + break; + case PROP_STORAGE_UUID: + koto_library_set_storage_uuid(self, g_strdup(g_value_get_string(val))); + break; + case PROP_CONSTRUCTION_PATH: + koto_library_set_path(self, g_strdup(g_value_get_string(val))); + break; + case PROP_NAME: + koto_library_set_name(self, g_strdup(g_value_get_string(val))); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +gchar * koto_library_get_path(KotoLibrary * self) { + if (!KOTO_IS_LIBRARY(self)) { + return NULL; + } + + if (G_IS_MOUNT(self->mount)) { + + } + + return self->path; +} + +gchar * koto_library_get_relative_path_to_file( + KotoLibrary * self, + gchar * full_path +) { + if (!KOTO_IS_LIBRARY(self)) { + return NULL; + } + + gchar * appended_slash_to_library_path = g_str_has_suffix(self->path, G_DIR_SEPARATOR_S) ? g_strdup(self->path) : g_strdup_printf("%s%s", g_strdup(self->path), G_DIR_SEPARATOR_S); + gchar * cleaned_path = koto_utils_replace_string_all(full_path, appended_slash_to_library_path, ""); // Replace the full path + + g_free(appended_slash_to_library_path); + return cleaned_path; +} + +gchar * koto_library_get_storage_uuid(KotoLibrary * self) { + if (!KOTO_IS_LIBRARY(self)) { + return NULL; + } + + return self->storage_uuid; +} + +KotoLibraryType koto_library_get_lib_type(KotoLibrary * self) { + if (!KOTO_IS_LIBRARY(self)) { + return KOTO_LIBRARY_TYPE_UNKNOWN; + } + + return self->type; +} + +gchar * koto_library_get_uuid(KotoLibrary * self) { + if (!KOTO_IS_LIBRARY(self)) { + return NULL; + } + + return self->uuid; +} + +void koto_library_index(KotoLibrary * self) { + if (!KOTO_IS_LIBRARY(self) || !self->should_index) { // Not a library or should not index + return; + } + + index_folder(self, self->path, 0); // Start index operation at the top +} + +gboolean koto_library_is_available(KotoLibrary * self) { + if (!KOTO_IS_LIBRARY(self)) { + return FALSE; + } + + return FALSE; +} + +void koto_library_set_name( + KotoLibrary * self, + gchar * library_name +) { + if (!KOTO_IS_LIBRARY(self)) { + return; + } + + if (!koto_utils_is_string_valid(library_name)) { // Not a string + return; + } + + if (koto_utils_is_string_valid(self->name)) { // Name already set + g_free(self->name); // Free the existing value + } + + self->name = g_strdup(library_name); + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_NAME]); +} + +void koto_library_set_path( + KotoLibrary * self, + gchar * path +) { + if (!KOTO_IS_LIBRARY(self)) { + return; + } + + if (!koto_utils_is_string_valid(path)) { // Not a valid string + return; + } + + if (koto_utils_is_string_valid(self->path)) { + g_free(self->path); + } + + self->relative_path = g_path_is_absolute(path) ? koto_utils_replace_string_all(path, self->mount_path, "") : path; // Ensure path is relative to our mount, even if the mount is really our own system partition + self->path = g_build_path(G_DIR_SEPARATOR_S, self->mount_path, self->relative_path, NULL); // Ensure our path is to whatever the current path of the mount + relative path is +} + +void koto_library_set_storage_uuid( + KotoLibrary * self, + gchar * storage_uuid +) { + if (!KOTO_IS_LIBRARY(self)) { + return; + } + + if (G_IS_MOUNT(self->mount)) { // Already have a mount + g_signal_handler_disconnect(self->mount, self->mount_unmounted_handler); // Stop listening to the unmounted signal for this existing mount + g_object_unref(self->mount); // Dereference the mount + g_free(self->mount_path); + } + + if (!koto_utils_is_string_valid(storage_uuid)) { // Not a valid string, which actually is allowed for built-ins + self->mount = NULL; + self->mount_path = g_strdup_printf("%s%s", g_get_home_dir(), G_DIR_SEPARATOR_S); // Set mount path to user's home directory + self->storage_uuid = NULL; + return; + } + + GMount * mount = g_volume_monitor_get_mount_for_uuid(volume_monitor, storage_uuid); // Attempt to get the mount by this UUID + + if (!G_IS_MOUNT(mount)) { + g_warning("Failed to get mount for UUID: %s", storage_uuid); + self->mount = NULL; + return; + } + + if (g_mount_is_shadowed(mount)) { // Is shadowed and should not use + g_message("This mount is considered \"shadowed\" and will not be used."); + return; + } + + GFile * mount_file = g_mount_get_default_location(mount); // Get the file for the entry location of the mount + + self->mount = mount; + self->mount_path = g_strdup(g_file_get_path(mount_file)); // Set the mount path to the path defined for the Mount File + self->storage_uuid = g_strdup(storage_uuid); +} + +gchar * koto_library_to_config_string(KotoLibrary * self) { + GStrvBuilder * lib_builder = g_strv_builder_new(); // Create new strv builder + g_strv_builder_add(lib_builder, g_strdup("[[library]]")); // Add our library array header + + g_strv_builder_add(lib_builder, g_strdup_printf("\tdirectory=\"%s\"", self->relative_path)); // Add the directory + + if (koto_utils_is_string_valid(self->name)) { // Have a library name + g_strv_builder_add(lib_builder, g_strdup_printf("\tname=\"%s\"", self->name)); // Add the name + } + + if (koto_utils_is_string_valid(self->storage_uuid)) { // Have a storage UUID (not applicable to built-ins) + g_strv_builder_add(lib_builder, g_strdup_printf("\tstorage_uuid=\"%s\"", self->storage_uuid)); // Add the storage UUID + } + + g_strv_builder_add(lib_builder, g_strdup_printf("\ttype=\"%s\"", koto_library_type_to_string(self->type))); // Add the type + g_strv_builder_add(lib_builder, g_strdup_printf("\tuuid=\"%s\"", self->uuid)); + + GStrv lines = g_strv_builder_end(lib_builder); // Get all the lines as a GStrv which is a gchar ** + gchar * content = g_strjoinv("\n", lines); // Separate all lines with newline + g_strfreev(lines); // Free our lines + + g_strv_builder_unref(lib_builder); // Unref our builder + return g_strdup(content); +} + +KotoLibrary * koto_library_new( + KotoLibraryType type, + const gchar * storage_uuid, + const gchar * path +) { + KotoLibrary * lib = g_object_new( + KOTO_TYPE_LIBRARY, + "type", + koto_library_type_to_string(type), + "uuid", + g_uuid_string_random(), // Create a new Library with a new UUID + "storage-uuid", + storage_uuid, + "construction-path", + path, + NULL + ); + + lib->should_index = TRUE; + return lib; +} + +KotoLibrary * koto_library_new_from_toml_table(toml_table_t * lib_datum) { + toml_datum_t uuid_datum = toml_string_in(lib_datum, "uuid"); // Get the library UUID + + if (!uuid_datum.ok) { // No UUID defined + g_warning("No UUID set for this library. Ignoring"); + return NULL; + } + + gchar * uuid = g_strdup(uuid_datum.u.s); // Duplicate our UUID + + toml_datum_t type_datum = toml_string_in(lib_datum, "type"); + + if (!type_datum.ok) { // No type defined + g_warning("Unknown type for library with UUID of %s", uuid); + return NULL; + } + + gchar * lib_type_as_str = g_strdup(type_datum.u.s); + KotoLibraryType lib_type = koto_library_type_from_string(lib_type_as_str); // Get the library's type + + if (lib_type == KOTO_LIBRARY_TYPE_UNKNOWN) { // Known type + return NULL; + } + + toml_datum_t dir_datum = toml_string_in(lib_datum, "directory"); + + if (!dir_datum.ok) { + g_critical("Failed to get directory path for library with UUID of %s", uuid); + return NULL; + } + + gchar * path = g_strdup(dir_datum.u.s); // Duplicate the path string + + toml_datum_t storage_uuid_datum = toml_string_in(lib_datum, "storage_uuid"); // Get the datum for the storage UUID + gchar * storage_uuid = g_strdup((storage_uuid_datum.ok) ? storage_uuid_datum.u.s : ""); + + toml_datum_t name_datum = toml_string_in(lib_datum, "name"); // Get the datum for the name + gchar * name = g_strdup((name_datum.ok) ? name_datum.u.s : ""); + + KotoLibrary * lib = g_object_new( + KOTO_TYPE_LIBRARY, + "type", + lib_type_as_str, + "uuid", + uuid, + "storage-uuid", + storage_uuid, + "construction-path", + path, + "name", + name, + NULL + ); + + lib->should_index = FALSE; + return lib; +} + +KotoLibraryType koto_library_type_from_string(gchar * t) { + if ( + (g_strcmp0(t, "audiobooks") == 0) || + (g_strcmp0(t, "audiobook") == 0) + ) { + return KOTO_LIBRARY_TYPE_AUDIOBOOK; + } else if (g_strcmp0(t, "music") == 0) { + return KOTO_LIBRARY_TYPE_MUSIC; + } else if ( + (g_strcmp0(t, "podcasts") == 0) || + (g_strcmp0(t, "podcast") == 0) + ) { + return KOTO_LIBRARY_TYPE_PODCAST; + } + + + g_warning("Invalid type provided for koto_library_type_from_string: %s", t); + return KOTO_LIBRARY_TYPE_UNKNOWN; +} + +gchar * koto_library_type_to_string(KotoLibraryType t) { + switch (t) { + case KOTO_LIBRARY_TYPE_AUDIOBOOK: + return g_strdup("audiobook"); + case KOTO_LIBRARY_TYPE_MUSIC: + return g_strdup("music"); + case KOTO_LIBRARY_TYPE_PODCAST: + return g_strdup("podcast"); + default: + return g_strdup("UNKNOWN"); + } +} \ No newline at end of file diff --git a/src/indexer/structs.h b/src/indexer/structs.h index 1753df2..0086c37 100644 --- a/src/indexer/structs.h +++ b/src/indexer/structs.h @@ -18,11 +18,13 @@ #pragma once #include #include +#include typedef enum { KOTO_LIBRARY_TYPE_AUDIOBOOK = 1, KOTO_LIBRARY_TYPE_MUSIC = 2, - KOTO_LIBRARY_TYPE_PODCAST = 3 + KOTO_LIBRARY_TYPE_PODCAST = 3, + KOTO_LIBRARY_TYPE_UNKNOWN = 4 } KotoLibraryType; G_BEGIN_DECLS @@ -32,15 +34,33 @@ G_BEGIN_DECLS **/ #define KOTO_TYPE_LIBRARY koto_library_get_type() -G_DECLARE_FINAL_TYPE(KotoLibrary, koto_library, KOTO, LIBRARY, GObject); +#define KOTO_LIBRARY(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), KOTO_TYPE_LIBRARY, KotoLibrary)) +typedef struct _KotoLibrary KotoLibrary; +typedef struct _KotoLibraryClass KotoLibraryClass; + +GLIB_AVAILABLE_IN_ALL +GType koto_library_get_type(void) G_GNUC_CONST; + #define KOTO_IS_LIBRARY(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_LIBRARY)) #define KOTO_TYPE_ARTIST koto_artist_get_type() -G_DECLARE_FINAL_TYPE(KotoArtist, koto_artist, KOTO, ARTIST, GObject); +#define KOTO_ARTIST(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), KOTO_TYPE_ARTIST, KotoArtist)) +typedef struct _KotoArtist KotoArtist; +typedef struct _KotoArtistClass KotoArtistClass; + +GLIB_AVAILABLE_IN_ALL +GType koto_artist_get_type(void) G_GNUC_CONST; + #define KOTO_IS_ARTIST(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_ARTIST)) #define KOTO_TYPE_ALBUM koto_album_get_type() -G_DECLARE_FINAL_TYPE(KotoAlbum, koto_album, KOTO, ALBUM, GObject); +#define KOTO_ALBUM(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), KOTO_TYPE_ALBUM, KotoAlbum)) +typedef struct _KotoAlbum KotoAlbum; +typedef struct _KotoAlbumClass KotoAlbumClass; + +GLIB_AVAILABLE_IN_ALL +GType koto_album_get_type(void) G_GNUC_CONST; + #define KOTO_IS_ALBUM(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_ALBUM)) #define KOTO_TYPE_TRACK koto_track_get_type() @@ -51,14 +71,53 @@ G_DECLARE_FINAL_TYPE(KotoTrack, koto_track, KOTO, TRACK, GObject); * Library Functions **/ -KotoLibrary * koto_library_new(const gchar * path); +KotoLibrary * koto_library_new( + KotoLibraryType type, + const gchar * storage_uuid, + const gchar * path +); + +KotoLibrary * koto_library_new_from_toml_table(toml_table_t * lib_datum); + +gchar * koto_library_get_path(KotoLibrary * self); + +gchar * koto_library_get_relative_path_to_file( + KotoLibrary * self, + gchar * full_path +); + +gchar * koto_library_get_storage_uuid(KotoLibrary * self); + +KotoLibraryType koto_library_get_lib_type(KotoLibrary * self); + +gchar * koto_library_get_uuid(KotoLibrary * self); + +void koto_library_index(KotoLibrary * self); + +gboolean koto_library_is_available(KotoLibrary * self); + +gchar * koto_library_get_storage_uuid(KotoLibrary * self); + +void koto_library_set_name( + KotoLibrary * self, + gchar * library_name +); void koto_library_set_path( KotoLibrary * self, gchar * path ); -void start_indexing(KotoLibrary * self); +void koto_library_set_storage_uuid( + KotoLibrary * self, + gchar * uuid +); + +gchar * koto_library_to_config_string(KotoLibrary * self); + +KotoLibraryType koto_library_type_from_string(gchar * t); + +gchar * koto_library_type_to_string(KotoLibraryType t); void index_folder( KotoLibrary * self, @@ -66,40 +125,54 @@ void index_folder( guint depth ); +void index_file( + KotoLibrary * lib, + const gchar * path +); + /** * Artist Functions **/ -KotoArtist * koto_artist_new(gchar * path); +KotoArtist * koto_artist_new(gchar * artist_name); KotoArtist * koto_artist_new_with_uuid(const gchar * uuid); void koto_artist_add_album( KotoArtist * self, - gchar * album_uuid + KotoAlbum * album +); + +void koto_artist_add_track( + KotoArtist * self, + KotoTrack * track ); void koto_artist_commit(KotoArtist * self); -guint koto_artist_find_album_with_name( - gconstpointer * album_data, - gconstpointer * album_name_data -); - GList * koto_artist_get_albums(KotoArtist * self); +KotoAlbum * koto_artist_get_album_by_name( + KotoArtist * self, + gchar * album_name +); + gchar * koto_artist_get_name(KotoArtist * self); +GList * koto_artist_get_tracks(KotoArtist * self); + gchar * koto_artist_get_uuid(KotoArtist * self); +KotoLibraryType koto_artist_get_lib_type(KotoArtist * self); + void koto_artist_remove_album( KotoArtist * self, KotoAlbum * album ); -void koto_artist_remove_album_by_name( +void koto_artist_remove_track( KotoArtist * self, - gchar * album_name + KotoTrack * track ); void koto_artist_set_artist_name( @@ -107,19 +180,18 @@ void koto_artist_set_artist_name( gchar * artist_name ); -void koto_artist_update_path( +void koto_artist_set_path( KotoArtist * self, - gchar * new_path + KotoLibrary * lib, + const gchar * fixed_path, + gboolean should_commit ); /** * Album Functions **/ -KotoAlbum * koto_album_new( - KotoArtist * artist, - const gchar * path -); +KotoAlbum * koto_album_new(gchar * artist_uuid); KotoAlbum * koto_album_new_with_uuid( KotoArtist * artist, @@ -135,23 +207,19 @@ void koto_album_commit(KotoAlbum * self); void koto_album_find_album_art(KotoAlbum * self); -void koto_album_find_tracks( - KotoAlbum * self, - magic_t magic_cookie, - const gchar * path -); +gchar * koto_album_get_art(KotoAlbum * self); -gchar * koto_album_get_album_art(KotoAlbum * self); - -gchar * koto_album_get_album_name(KotoAlbum * self); +gchar * koto_album_get_name(KotoAlbum * self); gchar * koto_album_get_album_uuid(KotoAlbum * self); +gchar * koto_album_get_path(KotoAlbum * self); + GList * koto_album_get_tracks(KotoAlbum * self); -gchar * koto_album_get_uuid(KotoAlbum *self); +gchar * koto_album_get_uuid(KotoAlbum * self); -void koto_album_remove_file( +void koto_album_remove_track( KotoAlbum * self, KotoTrack * track ); @@ -173,15 +241,11 @@ void koto_album_set_artist_uuid( void koto_album_set_as_current_playlist(KotoAlbum * self); -void koto_album_update_path( - KotoAlbum * self, - gchar * path -); -gint koto_album_sort_tracks( - gconstpointer track1_uuid, - gconstpointer track2_uuid, - gpointer user_data +void koto_album_set_path( + KotoAlbum * self, + KotoLibrary * lib, + const gchar * fixed_path ); /** @@ -189,20 +253,29 @@ gint koto_album_sort_tracks( **/ KotoTrack * koto_track_new( - KotoAlbum * album, - const gchar * path, - guint * cd + const gchar * artist_uuid, + const gchar * album_uuid, + const gchar * parsed_name, + guint cd ); KotoTrack * koto_track_new_with_uuid(const gchar * uuid); void koto_track_commit(KotoTrack * self); +guint koto_track_get_disc_number(KotoTrack * self); + GVariant * koto_track_get_metadata_vardict(KotoTrack * self); -gchar * koto_track_get_uuid(KotoTrack * self); +gchar * koto_track_get_path(KotoTrack * self); -void koto_track_parse_name(KotoTrack * self); +gchar * koto_track_get_name(KotoTrack * self); + +guint koto_track_get_position(KotoTrack * self); + +gchar * koto_track_get_uniqueish_key(KotoTrack * self); + +gchar * koto_track_get_uuid(KotoTrack * self); void koto_track_remove_from_playlist( KotoTrack * self, @@ -230,6 +303,12 @@ void koto_track_set_parsed_name( gchar * new_parsed_name ); +void koto_track_set_path( + KotoTrack * self, + KotoLibrary * lib, + gchar * fixed_path +); + void koto_track_set_position( KotoTrack * self, guint pos @@ -237,9 +316,4 @@ void koto_track_set_position( void koto_track_update_metadata(KotoTrack * self); -void koto_track_update_path( - KotoTrack * self, - const gchar * new_path -); - G_END_DECLS diff --git a/src/indexer/track-helpers.c b/src/indexer/track-helpers.c new file mode 100644 index 0000000..0a3c4fb --- /dev/null +++ b/src/indexer/track-helpers.c @@ -0,0 +1,149 @@ +/* track-helpers.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 "../db/cartographer.h" +#include "../koto-track-item.h" +#include "../koto-utils.h" +#include "structs.h" + +extern KotoCartographer * koto_maps; + +gchar * koto_track_helpers_get_name_for_file( + const gchar * path, + gchar * optional_artist_name +) { + gchar * file_name = NULL; + TagLib_File * t_file = taglib_file_new(path); // Get a taglib file for this file + + if ((t_file != NULL) && taglib_file_is_valid(t_file)) { // If we got the taglib file and it is valid + TagLib_Tag * tag = taglib_file_tag(t_file); // Get our tag + file_name = g_strdup(taglib_tag_title(tag)); // Get the tag title and duplicate it + } + + taglib_tag_free_strings(); // Free strings + taglib_file_free(t_file); // Free the file + + if (koto_utils_is_string_valid(file_name)) { // File name not set yet + return file_name; + } + + gchar * file_name_without_path = koto_utils_get_filename_without_extension((gchar*) path); // Get the "default" file name without the extension or path (strips all but basename) + + if (koto_utils_is_string_valid(optional_artist_name)) { // Was provided an optional artist name + gchar * removed_artist_name = koto_utils_replace_string_all(file_name_without_path, optional_artist_name, ""); // Remove the artist + g_free(file_name_without_path); + file_name_without_path = removed_artist_name; + } + + gchar ** split = g_regex_split_simple("^([\\d]+)", file_name_without_path, G_REGEX_JAVASCRIPT_COMPAT, 0); // Split based on any possible position + if (g_strv_length(split) > 1) { // Has positional info at the beginning of the file name + g_free(file_name_without_path); // Free the prior name + file_name_without_path = g_strdup(split[2]); // Set to our second item which is the rest of the song name without the prefixed numbers + } + + g_strfreev(split); + split = g_strsplit(file_name_without_path, " - ", -1); // Split whenever we encounter " - " + file_name_without_path = g_strjoinv("", split); // Remove entirely + g_strfreev(split); + + split = g_strsplit(file_name_without_path, "-", -1); // Split whenever we encounter - + file_name_without_path = g_strjoinv("", split); // Remove entirely + g_strfreev(split); + + file_name = g_strdup(file_name_without_path); // Set file_name to the duplicate of the file name without the path, now all cleaned up + g_free(file_name_without_path); // Free old reference + return file_name; +} + +guint64 koto_track_helpers_get_position_based_on_file_name(const gchar * file_name) { + guint64 position = 0; // Default to position 0 + gchar ** split = g_regex_split_simple("^([\\d]+)", file_name, G_REGEX_JAVASCRIPT_COMPAT, 0); + + if (g_strv_length(split) > 1) { // Has positional info at the beginning of the file + gchar * num = split[1]; + + if ((strcmp(num, "0") != 0) && (strcmp(num, "00") != 0)) { // Is not zero + guint64 potential_pos = g_ascii_strtoull(num, NULL, 10); // Attempt to convert + + if (potential_pos != 0) { // Got a legitimate position + position = potential_pos; // Update position + } + } + } + + return position; +} + +gint koto_track_helpers_sort_tracks( + gconstpointer track1, + gconstpointer track2, + gpointer user_data +) { + (void) user_data; + KotoTrack * track1_real = (KotoTrack*) track1; + KotoTrack * track2_real = (KotoTrack*) track2; + + if (!KOTO_IS_TRACK(track1_real) && !KOTO_IS_TRACK(track2_real)) { // Neither tracks actually exist + return 0; + } else if (KOTO_IS_TRACK(track1_real) && !KOTO_IS_TRACK(track2_real)) { // Only track2 does not exist + return -1; + } else if (!KOTO_IS_TRACK(track1_real) && KOTO_IS_TRACK(track2_real)) { // Only track1 does not exist + return 1; + } + + guint track1_disc = koto_track_get_disc_number(track1_real); + guint track2_disc = koto_track_get_disc_number(track2_real); + + 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; + } + + guint track1_pos = koto_track_get_position(track1_real); + guint track2_pos = koto_track_get_position(track2_real); + + if (track1_pos == track2_pos) { // Identical positions (like reported as 0) + return g_utf8_collate(koto_track_get_name(track1_real), koto_track_get_name(track2_real)); + } else if (track1_pos < track2_pos) { + return -1; + } else { + return 1; + } +} + +gint koto_track_helpers_sort_track_items( + gconstpointer track1_item, + gconstpointer track2_item, + gpointer user_data +) { + KotoTrack * track1 = koto_track_item_get_track((KotoTrackItem*) track1_item); + KotoTrack * track2 = koto_track_item_get_track((KotoTrackItem*) track2_item); + return koto_track_helpers_sort_tracks(track1, track2, user_data); +} + +gint koto_track_helpers_sort_tracks_by_uuid( + gconstpointer track1_uuid, + gconstpointer track2_uuid, + gpointer user_data +) { + KotoTrack * track1 = koto_cartographer_get_track_by_uuid(koto_maps, (gchar*) track1_uuid); + KotoTrack * track2 = koto_cartographer_get_track_by_uuid(koto_maps, (gchar*) track2_uuid); + return koto_track_helpers_sort_tracks(track1, track2, user_data); +} \ No newline at end of file diff --git a/src/indexer/track-helpers.h b/src/indexer/track-helpers.h new file mode 100644 index 0000000..09bd0fa --- /dev/null +++ b/src/indexer/track-helpers.h @@ -0,0 +1,43 @@ +/* track-helpers.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 + +gchar * koto_track_helpers_get_name_for_file( + const gchar * path, + gchar * optional_artist_name +); + +guint64 koto_track_helpers_get_position_based_on_file_name(const gchar * file_name); + +gint koto_track_helpers_sort_track_items( + gconstpointer track1_item, + gconstpointer track2_item, + gpointer user_data +); + +gint koto_track_helpers_sort_tracks( + gconstpointer track1, + gconstpointer track2, + gpointer user_data +); + +gint koto_track_helpers_sort_tracks_by_uuid( + gconstpointer track1_uuid, + gconstpointer track2_uuid, + gpointer user_data +); \ No newline at end of file diff --git a/src/indexer/track.c b/src/indexer/track.c index e7519fa..668e0b2 100644 --- a/src/indexer/track.c +++ b/src/indexer/track.c @@ -1,4 +1,4 @@ -/* file.c +/* track.c * * Copyright 2021 Joshua Strobl * @@ -18,8 +18,10 @@ #include #include #include +#include "../db/db.h" #include "../db/cartographer.h" #include "structs.h" +#include "track-helpers.h" #include "koto-utils.h" extern KotoCartographer * koto_maps; @@ -30,15 +32,14 @@ struct _KotoTrack { gchar * artist_uuid; gchar * album_uuid; gchar * uuid; - gchar * path; - gchar * file_name; + GHashTable * paths; + gchar * parsed_name; - guint * cd; - guint * position; + guint cd; + guint position; guint * playback_position; - gboolean acquired_metadata_from_id3; gboolean do_initial_index; }; @@ -50,8 +51,6 @@ enum { PROP_ALBUM_UUID, PROP_UUID, PROP_DO_INITIAL_INDEX, - PROP_PATH, - PROP_FILE_NAME, PROP_PARSED_NAME, PROP_CD, PROP_POSITION, @@ -116,22 +115,6 @@ static void koto_track_class_init(KotoTrackClass * c) { G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE ); - props[PROP_PATH] = g_param_spec_string( - "path", - "Path", - "Path to File", - NULL, - G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE - ); - - props[PROP_FILE_NAME] = g_param_spec_string( - "file-name", - "Name of File", - "Name of File", - NULL, - G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE - ); - props[PROP_PARSED_NAME] = g_param_spec_string( "parsed-name", "Parsed Name of File", @@ -174,7 +157,7 @@ static void koto_track_class_init(KotoTrackClass * c) { } static void koto_track_init(KotoTrack * self) { - self->acquired_metadata_from_id3 = FALSE; + self->paths = g_hash_table_new(g_str_hash, g_str_equal); // Create our hash table of paths } static void koto_track_get_property( @@ -195,20 +178,14 @@ static void koto_track_get_property( case PROP_UUID: g_value_set_string(val, self->uuid); break; - case PROP_PATH: - g_value_set_string(val, self->path); - break; - case PROP_FILE_NAME: - g_value_set_string(val, self->file_name); - break; case PROP_PARSED_NAME: g_value_set_string(val, self->parsed_name); break; case PROP_CD: - g_value_set_uint(val, GPOINTER_TO_UINT(self->cd)); + g_value_set_uint(val, self->cd); break; case PROP_POSITION: - g_value_set_uint(val, GPOINTER_TO_UINT(self->position)); + g_value_set_uint(val, self->position); break; case PROP_PLAYBACK_POSITION: g_value_set_uint(val, GPOINTER_TO_UINT(self->playback_position)); @@ -243,12 +220,6 @@ static void koto_track_set_property( case PROP_DO_INITIAL_INDEX: self->do_initial_index = g_value_get_boolean(val); break; - case PROP_PATH: - koto_track_update_path(self, g_value_get_string(val)); // Update the path - break; - case PROP_FILE_NAME: - koto_track_set_file_name(self, g_strdup(g_value_get_string(val))); // Update the file name - break; case PROP_PARSED_NAME: koto_track_set_parsed_name(self, g_strdup(g_value_get_string(val))); break; @@ -268,37 +239,56 @@ static void koto_track_set_property( } void koto_track_commit(KotoTrack * self) { - if ((self->artist_uuid == NULL) || (strcmp(self->artist_uuid, "") == 0)) { // No valid required artist UUID + if (!KOTO_IS_TRACK(self)) { return; } - if (self->album_uuid == NULL) { - g_object_set(self, "album-uuid", "", NULL); // Set to an empty string + if (!koto_utils_is_string_valid(self->artist_uuid)) { // No valid required artist UUID + return; } + if (!koto_utils_is_string_valid(self->album_uuid)) { // If we do not have a valid album UUID + self->album_uuid = g_strdup(""); + } + + gchar * commit_msg = "INSERT INTO tracks(id, artist_id, album_id, name, disc, position)" \ + "VALUES('%s', '%s', '%s', quote(\"%s\"), %d, %d)" \ + "ON CONFLICT(id) DO UPDATE SET album_id=excluded.album_id, artist_id=excluded.artist_id, name=excluded.name, disc=excluded.disc, position=excluded.position;"; + gchar * commit_op = g_strdup_printf( - "INSERT INTO tracks(id, path, type, artist_id, album_id, file_name, name, disc, position)" - "VALUES('%s', quote(\"%s\"), 0, '%s', '%s', quote(\"%s\"), quote(\"%s\"), %d, %d)" - "ON CONFLICT(id) DO UPDATE SET path=excluded.path, type=excluded.type, album_id=excluded.album_id, file_name=excluded.file_name, name=excluded.file_name, disc=excluded.disc, position=excluded.position;", + commit_msg, self->uuid, - self->path, self->artist_uuid, self->album_uuid, - self->file_name, self->parsed_name, - GPOINTER_TO_INT((int*) self->cd), - GPOINTER_TO_INT((int*) self->position) + (int) self->cd, + (int) self->position ); - gchar * commit_op_errmsg = NULL; - int rc = sqlite3_exec(koto_db, commit_op, 0, 0, &commit_op_errmsg); + new_transaction(commit_op, "Failed to write our file to the database", FALSE); - if (rc != SQLITE_OK) { - g_warning("Failed to write our file to the database: %s", commit_op_errmsg); + GHashTableIter paths_iter; + g_hash_table_iter_init(&paths_iter, self->paths); // Create an iterator for our paths + gpointer lib_uuid_ptr, track_rel_path_ptr; + while (g_hash_table_iter_next(&paths_iter, &lib_uuid_ptr, &track_rel_path_ptr)) { + gchar * lib_uuid = lib_uuid_ptr; + gchar * track_rel_path = track_rel_path_ptr; + + gchar * commit_op = g_strdup_printf( + "INSERT INTO libraries_tracks(id, track_id, path)" + "VALUES ('%s', '%s', quote(\"%s\"))" + "ON CONFLICT(id, track_id) DO UPDATE SET path=excluded.path;", + lib_uuid, + self->uuid, + track_rel_path + ); + + new_transaction(commit_op, "Failed to add this path for the track", FALSE); } +} - g_free(commit_op); - g_free(commit_op_errmsg); +guint koto_track_get_disc_number(KotoTrack * self) { + return KOTO_IS_TRACK(self) ? self->cd : 1; } GVariant * koto_track_get_metadata_vardict(KotoTrack * self) { @@ -307,27 +297,28 @@ GVariant * koto_track_get_metadata_vardict(KotoTrack * self) { } GVariantBuilder * builder = g_variant_builder_new(G_VARIANT_TYPE_VARDICT); - - gchar * album_art_path = NULL; - gchar * album_name = NULL; - gchar * artist_name = NULL; - KotoArtist * artist = koto_cartographer_get_artist_by_uuid(koto_maps, self->artist_uuid); - KotoAlbum * album = koto_cartographer_get_album_by_uuid(koto_maps, self->album_uuid); + gchar * artist_name = koto_artist_get_name(artist); - g_object_get(album, "art-path", &album_art_path, "name", &album_name, NULL); + if (koto_utils_is_string_valid(self->album_uuid)) { // Have an album associated + KotoAlbum * album = koto_cartographer_get_album_by_uuid(koto_maps, self->album_uuid); - g_object_get(artist, "name", &artist_name, NULL); + if (KOTO_IS_ALBUM(album)) { + gchar * album_art_path = koto_album_get_art(album); + gchar * album_name = koto_album_get_name(album); + + if (koto_utils_is_string_valid(album_art_path)) { // Valid album art path + album_art_path = g_strconcat("file://", album_art_path, NULL); // 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)); + } + } else { + } // TODO: Implement artist artwork fetching here g_variant_builder_add(builder, "{sv}", "mpris:trackid", g_variant_new_string(self->uuid)); - if (koto_utils_is_string_valid(album_art_path)) { // Valid album art path - album_art_path = g_strconcat("file://", album_art_path, NULL); // 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 (koto_utils_is_string_valid(artist_name)) { // Valid artist name GVariant * artist_name_variant; GVariantBuilder * artist_list_builder = g_variant_builder_new(G_VARIANT_TYPE("as")); @@ -339,16 +330,74 @@ GVariant * koto_track_get_metadata_vardict(KotoTrack * self) { g_variant_builder_add(builder, "{sv}", "playbackengine:artist", g_variant_new_string(artist_name)); // Add a sort of "meta" string val for our playback engine so we don't need to mess about with the array } - g_variant_builder_add(builder, "{sv}", "xesam:discNumber", g_variant_new_uint64(GPOINTER_TO_UINT(self->cd))); + g_variant_builder_add(builder, "{sv}", "xesam:discNumber", g_variant_new_uint64(self->cd)); g_variant_builder_add(builder, "{sv}", "xesam:title", g_variant_new_string(self->parsed_name)); - g_variant_builder_add(builder, "{sv}", "xesam:url", g_variant_new_string(self->path)); - g_variant_builder_add(builder, "{sv}", "xesam:trackNumber", g_variant_new_uint64(GPOINTER_TO_UINT(self->position))); + g_variant_builder_add(builder, "{sv}", "xesam:url", g_variant_new_string(koto_track_get_path(self))); + g_variant_builder_add(builder, "{sv}", "xesam:trackNumber", g_variant_new_uint64(self->position)); GVariant * metadata_ret = g_variant_builder_end(builder); return metadata_ret; } +gchar * koto_track_get_name(KotoTrack * self) { + if (!KOTO_IS_TRACK(self)) { // Not a track + return NULL; + } + + return g_strdup(self->parsed_name); +} + +gchar * koto_track_get_path(KotoTrack * self) { + if (!KOTO_IS_TRACK(self) || (KOTO_IS_TRACK(self) && (g_list_length(g_hash_table_get_keys(self->paths)) == 0))) { // If this is not a track or is but we have no paths associated with it + return NULL; + } + + GList * libs = koto_cartographer_get_libraries(koto_maps); // Get all of our libraries + GList * cur_lib_list; + + for (cur_lib_list = libs; cur_lib_list != NULL; cur_lib_list = libs->next) { // Iterate over our libraries + KotoLibrary * cur_library = libs->data; // Get this as a KotoLibrary + gchar * library_relative_path = g_hash_table_lookup(self->paths, koto_library_get_uuid(cur_library)); // Get any relative path in our paths based on the current UUID + + if (!koto_utils_is_string_valid(library_relative_path)) { // Not a valid path + continue; + } + + return g_strdup(g_build_path(G_DIR_SEPARATOR_S, koto_library_get_path(cur_library), library_relative_path, NULL)); // Build our full library path using library's path and our file relative path + } + + return NULL; +} + +guint koto_track_get_position(KotoTrack * self) { + return KOTO_IS_TRACK(self) ? self->position : 0; +} + +gchar * koto_track_get_uniqueish_key(KotoTrack * self) { + KotoArtist * artist = koto_cartographer_get_artist_by_uuid(koto_maps, self->artist_uuid); // Get the artist associated with this track + + if (!KOTO_IS_ARTIST(artist)) { // Don't have an artist + return g_strdup(self->parsed_name); // Just return the name of the file, which is very likely not unique + } + + gchar * artist_name = koto_artist_get_name(artist); // Get the artist name + + if (koto_utils_is_string_valid(self->album_uuid)) { // If we have an album associated with this track (not necessarily guaranteed) + KotoAlbum * possible_album = koto_cartographer_get_album_by_uuid(koto_maps, self->album_uuid); + + if (KOTO_IS_ALBUM(possible_album)) { // Album exists + gchar * album_name = koto_album_get_name(possible_album); // Get the name of the album + + if (koto_utils_is_string_valid(album_name)) { + return g_strdup_printf("%s-%s-%s", artist_name, album_name, self->parsed_name); // Create a key of (ARTIST/WRITER)-(ALBUM/AUDIOBOOK)-(CHAPTER/TRACK) + } + } + } + + return g_strdup_printf("%s-%s", artist_name, self->parsed_name); // Create a key of just (ARTIST/WRITER)-(CHAPTER/TRACK) +} + gchar * koto_track_get_uuid(KotoTrack * self) { if (!KOTO_IS_TRACK(self)) { return NULL; @@ -357,65 +406,6 @@ gchar * koto_track_get_uuid(KotoTrack * self) { return self->uuid; // Do not return a duplicate since otherwise comparison refs fail due to pointer positions being different } -void koto_track_parse_name(KotoTrack * self) { - gchar * copied_file_name = g_strdelimit(g_strdup(self->file_name), "_", ' '); // Replace _ with whitespace for starters - - KotoArtist * artist = NULL; - - artist = koto_cartographer_get_artist_by_uuid(koto_maps, self->artist_uuid); - - 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); - - g_free(copied_file_name); - - gchar ** split = g_regex_split_simple("^([\\d]+)", file_without_ext, G_REGEX_JAVASCRIPT_COMPAT, 0); - - if (g_strv_length(split) > 1) { // Has positional info at the beginning of the file - gchar * num = split[1]; - - g_free(file_without_ext); // Free the prior name - file_without_ext = g_strdup(split[2]); // Set to our second item which is the rest of the song name without the prefixed numbers - - if ((strcmp(num, "0") == 0) || (strcmp(num, "00") == 0)) { // Is exactly zero - koto_track_set_position(self, 0); // Set position to 0 - } else { // Either starts with 0 (like 09) or doesn't start with it at all - guint64 potential_pos = g_ascii_strtoull(num, NULL, 10); // Attempt to convert - - if (potential_pos != 0) { // Got a legitimate position - koto_track_set_position(self, potential_pos); - } - } - } - - g_strfreev(split); - - split = g_strsplit(file_without_ext, " - ", -1); // Split whenever we encounter " - " - file_without_ext = g_strjoinv("", split); // Remove entirely - g_strfreev(split); - - split = g_strsplit(file_without_ext, "-", -1); // Split whenever we encounter - - file_without_ext = g_strjoinv("", split); // Remove entirely - g_strfreev(split); - - koto_track_set_parsed_name(self, file_without_ext); - g_free(file_without_ext); -} - void koto_track_remove_from_playlist( KotoTrack * self, gchar * playlist_uuid @@ -430,15 +420,7 @@ void koto_track_remove_from_playlist( playlist_uuid ); - 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 remove track from playlist: %s", commit_op_errmsg); - } - - g_free(commit_op); - g_free(commit_op_errmsg); + new_transaction(commit_op, "Failed to remove track from playlist", FALSE); } void koto_track_save_to_playlist( @@ -458,39 +440,7 @@ void koto_track_save_to_playlist( 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_track_set_file_name( - KotoTrack * self, - gchar * new_file_name -) { - if (new_file_name == NULL) { - return; - } - - if ((self->file_name != NULL) && (strcmp(self->file_name, new_file_name) == 0)) { // Not null and the same - return; // Don't do anything - } - - if (self->file_name != NULL) { // If it is defined - g_free(self->file_name); - } - - self->file_name = g_strdup(new_file_name); - g_object_notify_by_pspec(G_OBJECT(self), props[PROP_FILE_NAME]); - - if (!self->acquired_metadata_from_id3 && self->do_initial_index) { // Haven't acquired our information from ID3 - koto_track_parse_name(self); // Update our parsed name - } + new_transaction(commit_op, "Failed to save track to playlist", FALSE); } void koto_track_set_cd( @@ -501,7 +451,7 @@ void koto_track_set_cd( return; } - self->cd = GUINT_TO_POINTER(cd); + self->cd = cd; g_object_notify_by_pspec(G_OBJECT(self), props[PROP_CD]); } @@ -509,15 +459,17 @@ void koto_track_set_parsed_name( KotoTrack * self, gchar * new_parsed_name ) { - if (new_parsed_name == NULL) { + if (!koto_utils_is_string_valid(new_parsed_name)) { return; } - if ((self->parsed_name != NULL) && (strcmp(self->parsed_name, new_parsed_name) == 0)) { // Not null and the same + gboolean have_existing_name = koto_utils_is_string_valid(self->parsed_name); + + if (have_existing_name && (strcmp(self->parsed_name, new_parsed_name) == 0)) { // Have existing name that matches one provided return; // Don't do anything } - if (self->parsed_name != NULL) { + if (have_existing_name) { g_free(self->parsed_name); } @@ -525,6 +477,30 @@ void koto_track_set_parsed_name( g_object_notify_by_pspec(G_OBJECT(self), props[PROP_PARSED_NAME]); } +void koto_track_set_path( + KotoTrack * self, + KotoLibrary * lib, + gchar * fixed_path +) { + if (!KOTO_IS_TRACK(self)) { + return; + } + + if (!koto_utils_is_string_valid(fixed_path)) { // Not a valid path + return; + } + + gchar * path = g_strdup(fixed_path); // Duplicate our fixed_path + gchar * relative_path = koto_library_get_relative_path_to_file(lib, path); // Get the relative path to the file for the given library + + gchar * library_uuid = koto_library_get_uuid(lib); // Get the library for this path + g_hash_table_replace(self->paths, library_uuid, relative_path); // Replace any existing value or add this one + + if (self->do_initial_index) { + koto_track_update_metadata(self); // Attempt to get ID3 info + } +} + void koto_track_set_position( KotoTrack * self, guint pos @@ -533,58 +509,37 @@ void koto_track_set_position( return; } - self->position = GUINT_TO_POINTER(pos); + self->position = pos; g_object_notify_by_pspec(G_OBJECT(self), props[PROP_POSITION]); } void koto_track_update_metadata(KotoTrack * self) { - TagLib_File * t_file = taglib_file_new(self->path); // Get a taglib file for this file + if (!KOTO_IS_TRACK(self)) { // Not a track + return; + } + + gchar * optimal_track_path = koto_track_get_path(self); // Check all the libraries associated with this track, based on priority, return a built path using lib path + relative file path + TagLib_File * t_file = taglib_file_new(optimal_track_path); // Get a taglib file for this file + g_free(optimal_track_path); if ((t_file != NULL) && taglib_file_is_valid(t_file)) { // If we got the taglib file and it is valid - self->acquired_metadata_from_id3 = TRUE; TagLib_Tag * tag = taglib_file_tag(t_file); // Get our tag - koto_track_set_parsed_name(self, taglib_tag_title(tag)); // Set the title of the file koto_track_set_position(self, (uint) taglib_tag_track(tag)); // Get the track, convert to uint and cast as a pointer - koto_track_set_file_name(self, g_path_get_basename(self->path)); // Update our file name - } else { - koto_track_set_file_name(self, g_path_get_basename(self->path)); // Update our file name + } else { // Failed to get tag info + guint64 position = koto_track_helpers_get_position_based_on_file_name(g_path_get_basename(optimal_track_path)); // Get the likely position + koto_track_set_position(self, position); // Set our position } taglib_tag_free_strings(); // Free strings taglib_file_free(t_file); // Free the file } -void koto_track_update_path( - KotoTrack * self, - const gchar * new_path -) { - if (new_path == NULL) { - return; - } - - if (self->path != NULL) { // Already have a path - g_free(self->path); // Free it - } - - self->path = g_strdup(new_path); // Duplicate the path and set it - - if (self->do_initial_index) { - koto_track_update_metadata(self); // Attempt to get ID3 info - } - - g_object_notify_by_pspec(G_OBJECT(self), props[PROP_PATH]); -} - KotoTrack * koto_track_new( - KotoAlbum * album, - const gchar * path, - guint * cd + const gchar * artist_uuid, + const gchar * album_uuid, + const gchar * parsed_name, + guint cd ) { - gchar * artist_uuid; - gchar * album_uuid; - - g_object_get(album, "artist-uuid", &artist_uuid, "uuid", &album_uuid, NULL); // Get the artist and album uuids from our Album - KotoTrack * track = g_object_new( KOTO_TYPE_TRACK, "artist-uuid", @@ -595,14 +550,13 @@ KotoTrack * koto_track_new( TRUE, "uuid", g_uuid_string_random(), - "path", - path, "cd", cd, + "parsed-name", + parsed_name, NULL ); - koto_track_commit(track); // Immediately commit to the database return track; } diff --git a/src/koto-paths.c b/src/koto-paths.c index 3b5fef0..07eed76 100644 --- a/src/koto-paths.c +++ b/src/koto-paths.c @@ -20,6 +20,7 @@ #include #include #include "koto-paths.h" +#include "koto-utils.h" gchar * koto_rev_dns; gchar * koto_path_cache; @@ -30,21 +31,16 @@ gchar * koto_path_to_db; void koto_paths_setup() { koto_rev_dns = "com.github.joshstrobl.koto"; - const gchar * user_cache_dir = g_get_user_data_dir(); - const gchar * user_config_dir = g_get_user_config_dir(); + gchar * user_cache_dir = g_strdup(g_get_user_data_dir()); + gchar * user_config_dir = g_strdup(g_get_user_config_dir()); koto_path_cache = g_build_path(G_DIR_SEPARATOR_S, user_cache_dir, koto_rev_dns, NULL); koto_path_config = g_build_path(G_DIR_SEPARATOR_S, user_config_dir, koto_rev_dns, NULL); koto_path_to_conf = g_build_filename(koto_path_config, "config.toml", NULL); koto_path_to_db = g_build_filename( koto_path_cache, "db", NULL); - mkdir(user_cache_dir, 0755); - mkdir(user_config_dir, 0755); - mkdir(koto_path_cache, 0755); - mkdir(koto_path_config, 0755); - - chown(user_cache_dir, getuid(), getgid()); - chown(user_config_dir, getuid(), getgid()); - chown(koto_path_cache, getuid(), getgid()); - chown(koto_path_config, getuid(), getgid()); + koto_utils_mkdir(user_cache_dir); + koto_utils_mkdir(user_config_dir); + koto_utils_mkdir(koto_path_cache); + koto_utils_mkdir(koto_path_config); } \ No newline at end of file diff --git a/src/koto-track-item.c b/src/koto-track-item.c index 41f4c24..ca42dfe 100644 --- a/src/koto-track-item.c +++ b/src/koto-track-item.c @@ -126,6 +126,10 @@ static void koto_track_item_init(KotoTrackItem * self) { } KotoTrack * koto_track_item_get_track(KotoTrackItem * self) { + if (!KOTO_IS_TRACK_ITEM(self)) { + return NULL; + } + return self->track; } diff --git a/src/koto-utils.c b/src/koto-utils.c index bde0b0b..b03a371 100644 --- a/src/koto-utils.c +++ b/src/koto-utils.c @@ -17,6 +17,9 @@ #include #include +#include +#include +#include extern GtkWindow * main_window; @@ -68,8 +71,8 @@ gchar * koto_utils_gboolean_to_string(gboolean b) { } gchar * koto_utils_get_filename_without_extension(gchar * filename) { - gchar * trimmed_file_name = g_strdup(filename); - gchar ** split = g_strsplit(filename, ".", -1); // Split every time we see . + gchar * trimmed_file_name = g_strdup(g_path_get_basename(filename)); // Ensure the filename provided is the base name of any possible path and duplicate it + gchar ** split = g_strsplit(trimmed_file_name, ".", -1); // Split every time we see . g_free(trimmed_file_name); guint len_of_extension_split = g_strv_length(split); @@ -103,6 +106,11 @@ gboolean koto_utils_is_string_valid(gchar * str) { return ((str != NULL) && (g_strcmp0(str, "") != 0)); } +void koto_utils_mkdir(gchar * path) { + mkdir(path, 0755); + chown(path, getuid(), getgid()); +} + void koto_utils_push_queue_element_to_store( gpointer data, gpointer user_data diff --git a/src/koto-utils.h b/src/koto-utils.h index d7f4630..e1317fb 100644 --- a/src/koto-utils.h +++ b/src/koto-utils.h @@ -36,6 +36,8 @@ gchar * koto_utils_get_filename_without_extension(gchar * filename); gboolean koto_utils_is_string_valid(gchar * str); +void koto_utils_mkdir(gchar * path); + void koto_utils_push_queue_element_to_store( gpointer data, gpointer user_data diff --git a/src/koto-window.c b/src/koto-window.c index 5b0d02d..2513440 100644 --- a/src/koto-window.c +++ b/src/koto-window.c @@ -23,7 +23,6 @@ #include "pages/playlist/list.h" #include "playback/engine.h" #include "playlist/add-remove-track-popover.h" -#include "playlist/current.h" #include "playlist/create-modify-dialog.h" #include "config/config.h" #include "koto-dialog-container.h" @@ -37,16 +36,13 @@ extern KotoAddRemoveTrackPopover * koto_add_remove_track_popup; extern KotoCartographer * koto_maps; extern KotoCreateModifyPlaylistDialog * playlist_create_modify_dialog; extern KotoConfig * config; -extern KotoCurrentPlaylist * current_playlist; extern KotoPageMusicLocal * music_local_page; -extern KotoPlaybackEngine * playback_engine; extern gchar * koto_rev_dns; struct _KotoWindow { GtkApplicationWindow parent_instance; KotoLibrary * library; - KotoCurrentPlaylist * current_playlist; KotoDialogContainer * dialogs; @@ -72,9 +68,6 @@ 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(); - koto_window_manage_style(config, 0, self); // Immediately apply the theme g_signal_connect(config, "notify::ui-theme-desired", G_CALLBACK(koto_window_manage_style), self); // Handle changes to desired theme g_signal_connect(config, "notify::ui-theme-override", G_CALLBACK(koto_window_manage_style), self); // Handle changes to theme overriding @@ -145,7 +138,12 @@ static void koto_window_init (KotoWindow * self) { gtk_widget_queue_draw(self->content_layout); - g_thread_new("load-library", (void*) load_library, self); + music_local_page = koto_page_music_local_new(); + + // TODO: Remove and do some fancy state loading + koto_window_add_page(self, "music.local", GTK_WIDGET(music_local_page)); + koto_window_go_to_page(self, "music.local"); + gtk_widget_show(self->pages); // Do not remove this. Will cause sporadic hiding of the local page content otherwise. } void koto_window_add_page( @@ -153,6 +151,18 @@ void koto_window_add_page( gchar * page_name, GtkWidget * page ) { + if (!KOTO_IS_WINDOW(self)) { + return; + } + + if (!GTK_IS_STACK(self->pages)) { + return; + } + + if (!GTK_IS_WIDGET(page)) { + return; + } + gtk_stack_add_named(GTK_STACK(self->pages), page, page_name); } @@ -285,23 +295,6 @@ void create_new_headerbar(KotoWindow * self) { gtk_window_set_titlebar(GTK_WINDOW(self), self->header_bar); } -void load_library(KotoWindow * self) { - KotoLibrary * lib = koto_library_new(g_get_user_special_dir(G_USER_DIRECTORY_MUSIC)); - - if (lib != NULL) { - self->library = lib; - music_local_page = koto_page_music_local_new(); - - // TODO: Remove and do some fancy state loading - koto_window_add_page(self, "music.local", GTK_WIDGET(music_local_page)); - koto_window_go_to_page(self, "music.local"); - gtk_widget_show(self->pages); // Do not remove this. Will cause sporadic hiding of the local page content otherwise. - koto_page_music_local_build_ui(music_local_page); - } - - g_thread_exit(0); -} - void set_optimal_default_window_size(KotoWindow * self) { GdkDisplay * default_display = gdk_display_get_default(); diff --git a/src/main.c b/src/main.c index be06bad..4d27f8d 100644 --- a/src/main.c +++ b/src/main.c @@ -16,9 +16,13 @@ */ #include #include +#include +#include #include "config/config.h" #include "db/cartographer.h" #include "db/db.h" +#include "db/loaders.h" +#include "playback/engine.h" #include "playback/media-keys.h" #include "playback/mimes.h" #include "playback/mpris.h" @@ -33,6 +37,7 @@ extern KotoConfig * config; extern guint mpris_bus_id; extern GDBusNodeInfo * introspection_data; +extern KotoPlaybackEngine * playback_engine; extern KotoCartographer * koto_maps; extern sqlite3 * koto_db; @@ -41,9 +46,10 @@ extern GList * supported_mimes; extern gchar * koto_path_to_conf; extern gchar * koto_rev_dns; - +GVolumeMonitor * volume_monitor = NULL; GtkApplication * app = NULL; GtkWindow * main_window; +magic_t magic_cookie; static void on_activate (GtkApplication * app) { g_assert(GTK_IS_APPLICATION(app)); @@ -53,6 +59,7 @@ static void on_activate (GtkApplication * app) { main_window = g_object_new(KOTO_TYPE_WINDOW, "application", app, "default-width", 1200, "default-height", 675, NULL); setup_mpris_interfaces(); // Set up our MPRIS interfaces setup_mediakeys_interface(); // Set up our media key support + read_from_db(); // Read the database, allowing us to propagate the UI with various data such as artists and playlist navigation elements } gtk_window_present(main_window); @@ -83,9 +90,26 @@ int main ( koto_maps = koto_cartographer_new(); // Create our new cartographer and their collection of maps + volume_monitor = g_volume_monitor_get(); // Get a VolumeMonitor + config = koto_config_new(); // Set our config koto_config_load(config, koto_path_to_conf); + playback_engine = koto_playback_engine_new(); // Initialize the engine now that the config is available, since it listens on various config signals + + taglib_id3v2_set_default_text_encoding(TagLib_ID3v2_UTF8); // Ensure our id3v2 text encoding is UTF-8 + magic_cookie = magic_open(MAGIC_MIME); + + if (magic_cookie == NULL) { // Failed to open + g_critical("Failed to allocate a cookie pointer from libmagic."); + } + + if (magic_load(magic_cookie, NULL) != 0) { // Failed to load data + magic_close(magic_cookie); + g_critical("Failed to load the system magic database."); + } + open_db(); // Open our database + g_thread_new("indexing-any-necessary-libs", (void*) koto_config_load_libs, config); // Load our libraries, now that our database is set up. Note that read_from_db is called in koto-window.c app = gtk_application_new(koto_rev_dns, 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 5a030de..774e3d4 100644 --- a/src/meson.build +++ b/src/meson.build @@ -14,6 +14,8 @@ koto_sources = [ 'indexer/album.c', 'indexer/artist.c', 'indexer/file-indexer.c', + 'indexer/library.c', + 'indexer/track-helpers.c', 'indexer/track.c', 'pages/music/album-view.c', 'pages/music/artist-view.c', diff --git a/src/pages/music/album-view.c b/src/pages/music/album-view.c index 84243b0..d733dc2 100644 --- a/src/pages/music/album-view.c +++ b/src/pages/music/album-view.c @@ -38,7 +38,7 @@ struct _KotoAlbumView { KotoCoverArtButton * album_cover; GtkWidget * album_label; - GHashTable * cd_to_track_listbox; + GHashTable * cd_to_disc_views; }; G_DEFINE_TYPE(KotoAlbumView, koto_album_view, G_TYPE_OBJECT); @@ -85,7 +85,7 @@ static void koto_album_view_class_init(KotoAlbumViewClass * c) { } static void koto_album_view_init(KotoAlbumView * self) { - self->cd_to_track_listbox = g_hash_table_new(g_str_hash, g_str_equal); + self->cd_to_disc_views = g_hash_table_new(g_str_hash, g_str_equal); self->main = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); gtk_widget_add_css_class(self->main, "album-view"); gtk_widget_set_can_focus(self->main, FALSE); @@ -153,66 +153,87 @@ static void koto_album_view_set_property( } } -void koto_album_view_add_track_to_listbox( - KotoAlbum * self, +void koto_album_view_add_track( + KotoAlbumView * self, KotoTrack * track ) { - (void) self; - (void) track; + if (!KOTO_IS_ALBUM_VIEW(self)) { // Not an album view + return; + } + + if (!KOTO_IS_ALBUM(self->album)) { // Somehow don't have an album set + return; + } + + if (!KOTO_IS_TRACK(track)) { // Track doesn't exist + return; + } + + guint disc_number = koto_track_get_disc_number(track); // Get the disc number + gchar * disc_num_as_str = g_strdup_printf("%u", disc_number); + + KotoDiscView * disc_view; + + if (g_hash_table_contains(self->cd_to_disc_views, disc_num_as_str)) { // Already have this added this disc and its disc view + disc_view = g_hash_table_lookup(self->cd_to_disc_views, disc_num_as_str); // Get the disc view + } else { + disc_view = koto_disc_view_new(self->album, disc_number); // Build a new disc view + gtk_list_box_append(GTK_LIST_BOX(self->discs), GTK_WIDGET(disc_view)); // Add the Disc View to the List Box + g_hash_table_replace(self->cd_to_disc_views, disc_num_as_str, disc_view); // Add the new Disc View to our hash table + } + + if (!KOTO_IS_DISC_VIEW(disc_view)) { // If this is not a Disc View + return; + } + + koto_disc_view_add_track(disc_view, track); // Add the track to the disc view + koto_album_view_update_disc_labels(self); // Update our disc labels +} + +void koto_album_view_handle_track_added( + KotoAlbum * album, + KotoTrack * track, + gpointer user_data +) { + if (!KOTO_IS_ALBUM(album)) { // If not an album + return; + } + + if (!KOTO_IS_TRACK(track)) { // If not a track + return; + } + + KotoAlbumView * self = KOTO_ALBUM_VIEW(user_data); // Define as an album view + + if (!KOTO_IS_ALBUM_VIEW(self)) { + return; + } + + koto_album_view_add_track(self, track); // Add the track } void koto_album_view_set_album( KotoAlbumView * self, KotoAlbum * album ) { - if (album == NULL) { + if (!KOTO_IS_ALBUM_VIEW(self)) { + return; + } + + if (!KOTO_IS_ALBUM(album)) { return; } self->album = album; - gchar * album_art = koto_album_get_album_art(self->album); // Get the art for the album + gchar * album_art = koto_album_get_art(self->album); // Get the art for the album koto_cover_art_button_set_art_path(self->album_cover, album_art); - gchar * album_name; - g_object_get(album, "name", &album_name, NULL); // Get the album name - - self->album_label = gtk_label_new(album_name); + self->album_label = gtk_label_new(koto_album_get_name(album)); gtk_widget_set_halign(self->album_label, GTK_ALIGN_START); 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_album_get_tracks(album); // Get the tracks for this album - - for (guint i = 0; i < g_list_length(tracks); i++) { - KotoTrack * track = koto_cartographer_get_track_by_uuid(koto_maps, (gchar*) g_list_nth_data(tracks, i)); // Get the track by its UUID - - if (!KOTO_IS_TRACK(track)) { // 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)); - - if (g_hash_table_contains(discs, disc_num_as_str)) { // Already have this added - continue; // Skip - } - - g_hash_table_insert(discs, disc_num_as_str, "0"); // Mark this disc number in the hash table - KotoDiscView * disc_view = koto_disc_view_new(album, disc_number); - gtk_list_box_append(GTK_LIST_BOX(self->discs), GTK_WIDGET(disc_view)); // Add the Disc View to the List Box - } - - if (g_list_length(g_hash_table_get_keys(discs)) == 1) { // Only have one album - GtkListBoxRow * row = gtk_list_box_get_row_at_index(GTK_LIST_BOX(self->discs), 0); // Get the first item - - if (GTK_IS_LIST_BOX_ROW(row)) { // Is a row - koto_disc_view_set_disc_label_visible((KotoDiscView*) gtk_list_box_row_get_child(row), FALSE); // Get - } - } - - g_hash_table_destroy(discs); + g_signal_connect(self->album, "track-added", G_CALLBACK(koto_album_view_handle_track_added), self); // Handle track added on our album } int koto_album_view_sort_discs( @@ -255,6 +276,23 @@ void koto_album_view_toggle_album_playback( koto_album_set_as_current_playlist(self->album); // Set as the current playlist } +void koto_album_view_update_disc_labels(KotoAlbumView * self) { + gboolean show_disc_labels = g_hash_table_size(self->cd_to_disc_views) > 1; + gpointer disc_num_ptr; + gpointer disc_view_ptr; + + GHashTableIter disc_view_iter; + g_hash_table_iter_init(&disc_view_iter, self->cd_to_disc_views); + + while (g_hash_table_iter_next(&disc_view_iter, &disc_num_ptr, &disc_view_ptr)) { + (void) disc_num_ptr; + KotoDiscView * view = (KotoDiscView*) disc_view_ptr; + if (KOTO_IS_DISC_VIEW(view)) { + koto_disc_view_set_disc_label_visible(view, show_disc_labels); + } + } +} + KotoAlbumView * koto_album_view_new(KotoAlbum * album) { return g_object_new(KOTO_TYPE_ALBUM_VIEW, "album", album, NULL); } diff --git a/src/pages/music/album-view.h b/src/pages/music/album-view.h index bf1fe68..cc55195 100644 --- a/src/pages/music/album-view.h +++ b/src/pages/music/album-view.h @@ -30,16 +30,28 @@ G_DECLARE_FINAL_TYPE(KotoAlbumView, koto_album_view, KOTO, ALBUM_VIEW, GObject) KotoAlbumView* koto_album_view_new(KotoAlbum * album); GtkWidget * koto_album_view_get_main(KotoAlbumView * self); -void koto_album_view_add_track_to_listbox( - KotoAlbum * self, +void koto_album_view_add_track( + KotoAlbumView * self, KotoTrack * track ); +void koto_album_view_handle_track_added( + KotoAlbum * album, + KotoTrack * track, + gpointer user_data +); + void koto_album_view_set_album( KotoAlbumView * self, KotoAlbum * album ); +int koto_album_view_sort_discs( + GtkListBoxRow * track1, + GtkListBoxRow * track2, + gpointer user_data +); + void koto_album_view_toggle_album_playback( GtkGestureClick * gesture, int n_press, @@ -48,10 +60,6 @@ void koto_album_view_toggle_album_playback( gpointer data ); -int koto_album_view_sort_discs( - GtkListBoxRow * track1, - GtkListBoxRow * track2, - gpointer user_data -); +void koto_album_view_update_disc_labels(KotoAlbumView * self); G_END_DECLS diff --git a/src/pages/music/artist-view.c b/src/pages/music/artist-view.c index 5b434e9..5269b6e 100644 --- a/src/pages/music/artist-view.c +++ b/src/pages/music/artist-view.c @@ -31,12 +31,9 @@ struct _KotoArtistView { KotoArtist * artist; GtkWidget * scrolled_window; GtkWidget * content; - GtkWidget * favorites_list; GtkWidget * album_list; GHashTable * albums_to_component; - - gboolean constructed; }; G_DEFINE_TYPE(KotoArtistView, koto_artist_view, G_TYPE_OBJECT); @@ -113,7 +110,7 @@ static void koto_artist_view_set_property( switch (prop_id) { case PROP_ARTIST: - koto_artist_view_add_artist(self, (KotoArtist*) g_value_get_object(val)); + koto_artist_view_set_artist(self, (KotoArtist*) g_value_get_object(val)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); @@ -123,13 +120,12 @@ static void koto_artist_view_set_property( static void koto_artist_view_init(KotoArtistView * self) { self->artist = NULL; - self->constructed = FALSE; + self->albums_to_component = g_hash_table_new(g_str_hash, g_str_equal); } static void koto_artist_view_constructed(GObject * obj) { KotoArtistView * self = KOTO_ARTIST_VIEW(obj); - self->albums_to_component = g_hash_table_new(g_str_hash, g_str_equal); self->scrolled_window = gtk_scrolled_window_new(); // Create our scrolled window self->content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); // Create our content as a GtkBox @@ -139,77 +135,119 @@ static void koto_artist_view_constructed(GObject * obj) { gtk_widget_add_css_class(GTK_WIDGET(self->scrolled_window), "artist-view"); gtk_widget_add_css_class(GTK_WIDGET(self->content), "artist-view-content"); - self->favorites_list = gtk_flow_box_new(); // Create our favorites list - gtk_flow_box_set_activate_on_single_click(GTK_FLOW_BOX(self->favorites_list), TRUE); - gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(self->favorites_list), GTK_SELECTION_NONE); - gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(self->favorites_list), 6); - gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(self->favorites_list), 6); - gtk_widget_add_css_class(GTK_WIDGET(self->favorites_list), "album-strip"); - gtk_widget_set_halign(self->favorites_list, GTK_ALIGN_START); - self->album_list = gtk_flow_box_new(); // Create our list of our albums - //gtk_flow_box_set_activate_on_single_click(GTK_FLOW_BOX(self->album_list), FALSE); gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(self->album_list), GTK_SELECTION_NONE); gtk_widget_add_css_class(self->album_list, "album-list"); - gtk_box_prepend(GTK_BOX(self->content), self->favorites_list); // Add the strip gtk_box_append(GTK_BOX(self->content), self->album_list); // Add the list - gtk_widget_set_hexpand(GTK_WIDGET(self->favorites_list), TRUE); gtk_widget_set_hexpand(GTK_WIDGET(self->album_list), TRUE); G_OBJECT_CLASS(koto_artist_view_parent_class)->constructed(obj); - self->constructed = TRUE; } void koto_artist_view_add_album( KotoArtistView * self, KotoAlbum * album ) { - gchar * album_art = koto_album_get_album_art(album); // Get the art for the album + if (!KOTO_IS_ARTIST_VIEW(self)) { + return; + } - GtkWidget * art_image = koto_utils_create_image_from_filepath(album_art, "audio-x-generic-symbolic", 220, 220); + if (!KOTO_IS_ALBUM(album)) { + return; + } - gtk_widget_set_halign(art_image, GTK_ALIGN_START); // Align to start - gtk_flow_box_insert(GTK_FLOW_BOX(self->favorites_list), art_image, -1); // Append the album art + gchar * album_uuid = koto_album_get_uuid(album); // Get the album UUID + + if (g_hash_table_contains(self->albums_to_component, album_uuid)) { // Already added this album + return; + } KotoAlbumView * album_view = koto_album_view_new(album); // Create our new album view GtkWidget * album_view_main = koto_album_view_get_main(album_view); gtk_flow_box_insert(GTK_FLOW_BOX(self->album_list), album_view_main, -1); // Append the album view to the album list + g_hash_table_replace(self->albums_to_component, album_uuid, album_view_main); } -void koto_artist_view_add_artist( +void koto_artist_view_handle_album_added( + KotoArtist * artist, + KotoAlbum * album, + gpointer user_data +) { + KotoArtistView * self = user_data; + + if (!KOTO_IS_ARTIST_VIEW(self)) { + return; + } + + if (!KOTO_IS_ARTIST(artist)) { + return; + } + + if (!KOTO_IS_ALBUM(album)) { + return; + } + + if (g_strcmp0(koto_artist_get_uuid(self->artist), koto_artist_get_uuid(artist)) != 0) { // Not the same artist + return; + } + + koto_artist_view_add_album(self, album); // Add the album if necessary +} + +void koto_artist_view_handle_album_removed( + KotoArtist * artist, + gchar * album_uuid, + gpointer user_data +) { + KotoArtistView * self = user_data; + + if (!KOTO_IS_ARTIST_VIEW(self)) { + return; + } + + if (g_strcmp0(koto_artist_get_uuid(self->artist), koto_artist_get_uuid(artist)) != 0) { // Not the same artist + return; + } + + GtkWidget * album_view = g_hash_table_lookup(self->albums_to_component, album_uuid); // Get the album view if it exists + + if (!GTK_IS_WIDGET(album_view)) { // Not a widget + return; + } + + gtk_flow_box_remove(GTK_FLOW_BOX(self->album_list), album_view); // Remove the album + g_hash_table_remove(self->albums_to_component, album_uuid); // Remove the album from our hash table +} + +void koto_artist_view_set_artist( KotoArtistView * self, KotoArtist * artist ) { - if (artist == NULL) { + if (!KOTO_IS_ARTIST_VIEW(self)) { // Not an Artist view + return; + } + + if (!KOTO_IS_ARTIST(artist)) { return; } self->artist = artist; - - if (!self->constructed) { - return; - } - - GList * albums = koto_artist_get_albums(self->artist); // Get the albums - - GList * a; - - for (a = albums; a != NULL; a = a->next) { - KotoAlbum * 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 - } - } + g_signal_connect(artist, "album-added", G_CALLBACK(koto_artist_view_handle_album_added), self); + g_signal_connect(artist, "album-removed", G_CALLBACK(koto_artist_view_handle_album_removed), self); } GtkWidget * koto_artist_view_get_main(KotoArtistView * self) { return self->scrolled_window; } -KotoArtistView * koto_artist_view_new() { - return g_object_new(KOTO_TYPE_ARTIST_VIEW, NULL); +KotoArtistView * koto_artist_view_new(KotoArtist * artist) { + return g_object_new( + KOTO_TYPE_ARTIST_VIEW, + "artist", + artist, + NULL + ); } diff --git a/src/pages/music/artist-view.h b/src/pages/music/artist-view.h index 3601a32..15442d6 100644 --- a/src/pages/music/artist-view.h +++ b/src/pages/music/artist-view.h @@ -27,13 +27,26 @@ G_BEGIN_DECLS G_DECLARE_FINAL_TYPE(KotoArtistView, koto_artist_view, KOTO, ARTIST_VIEW, GObject) -KotoArtistView* koto_artist_view_new(); +KotoArtistView* koto_artist_view_new(KotoArtist * artist); + void koto_artist_view_add_album( KotoArtistView * self, KotoAlbum * album ); -void koto_artist_view_add_artist( +void koto_artist_view_handle_album_added( + KotoArtist * artist, + KotoAlbum * album, + gpointer user_data +); + +void koto_artist_view_handle_album_removed( + KotoArtist * artist, + gchar * album_uuid, + gpointer user_data +); + +void koto_artist_view_set_artist( KotoArtistView * self, KotoArtist * artist ); diff --git a/src/pages/music/disc-view.c b/src/pages/music/disc-view.c index e74c82a..a8487d5 100644 --- a/src/pages/music/disc-view.c +++ b/src/pages/music/disc-view.c @@ -18,6 +18,7 @@ #include #include "../../components/koto-action-bar.h" #include "../../db/cartographer.h" +#include "../../indexer/track-helpers.h" #include "../../indexer/structs.h" #include "../../koto-track-item.h" #include "../../koto-utils.h" @@ -33,7 +34,9 @@ struct _KotoDiscView { GtkWidget * label; GtkWidget * list; - guint * disc_number; + GHashTable * tracks_to_items; + + guint disc_number; }; G_DEFINE_TYPE(KotoDiscView, koto_disc_view, GTK_TYPE_BOX); @@ -48,6 +51,7 @@ enum { static GParamSpec * props[N_PROPERTIES] = { NULL, }; + static void koto_disc_view_get_property( GObject * obj, guint prop_id, @@ -133,6 +137,8 @@ static void koto_disc_view_set_property( } static void koto_disc_view_init(KotoDiscView * self) { + self->tracks_to_items = g_hash_table_new(g_str_hash, g_str_equal); + gtk_widget_add_css_class(GTK_WIDGET(self), "disc-view"); gtk_widget_set_can_focus(GTK_WIDGET(self), FALSE); self->header = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); @@ -153,6 +159,7 @@ static void koto_disc_view_init(KotoDiscView * self) { self->list = gtk_list_box_new(); // Create our list of our tracks gtk_list_box_set_activate_on_single_click(GTK_LIST_BOX(self->list), FALSE); 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_disc_view_sort_list_box_rows, self, NULL); gtk_widget_add_css_class(self->list, "track-list"); gtk_widget_set_can_focus(self->list, FALSE); gtk_widget_set_focusable(self->list, FALSE); @@ -162,24 +169,37 @@ static void koto_disc_view_init(KotoDiscView * self) { g_signal_connect(self->list, "selected-rows-changed", G_CALLBACK(koto_disc_view_handle_selected_rows_changed), self); } -void koto_disc_view_list_tracks( - gpointer data, - gpointer selfptr +void koto_disc_view_add_track( + KotoDiscView * self, + KotoTrack * track ) { - KotoDiscView * self = (KotoDiscView*) selfptr; - KotoTrack * track = koto_cartographer_get_track_by_uuid(koto_maps, (gchar*) data); // Get the track by its UUID + if (!KOTO_IS_DISC_VIEW(self)) { // If this is not a disc view + return; + } - guint * disc_number; + if (!KOTO_IS_TRACK(track)) { // If this is not a track + return; + } - g_object_get(track, "cd", &disc_number, NULL); // get the disc number + if (self->disc_number != koto_track_get_disc_number(track)) { // Not the same disc + return; + } - if (GPOINTER_TO_UINT(self->disc_number) != GPOINTER_TO_UINT(disc_number)) { // Track does not belong to this CD + gchar * track_uuid = koto_track_get_uuid(track); // Get the track UUID + + if (g_hash_table_contains(self->tracks_to_items, track_uuid)) { // Already have added this track return; } KotoTrackItem * track_item = koto_track_item_new(track); // Create our new track item + if (!KOTO_IS_TRACK_ITEM(track_item)) { // Failed to create a track item for this track + g_warning("Failed to build track item for track with UUID %s", track_uuid); + return; + } + gtk_list_box_append(GTK_LIST_BOX(self->list), GTK_WIDGET(track_item)); // Add to our tracks list box + g_hash_table_replace(self->tracks_to_items, track_uuid, track_item); // Add to our hash table so we know we have added this } void koto_disc_view_handle_selected_rows_changed( @@ -215,6 +235,28 @@ void koto_disc_view_handle_selected_rows_changed( koto_action_bar_toggle_reveal(action_bar, TRUE); // Show the action bar } +void koto_disc_view_remove_track( + KotoDiscView * self, + gchar * track_uuid +) { + if (!KOTO_IS_DISC_VIEW(self)) { // If this is not a disc view + return; + } + + if (!koto_utils_is_string_valid(track_uuid)) { // If our UUID is not a valid + return; + } + + KotoTrackItem * track_item = g_hash_table_lookup(self->tracks_to_items, track_uuid); // Get the track item + + if (!KOTO_IS_TRACK_ITEM(track_item)) { // Track item not valid + return; + } + + gtk_list_box_remove(GTK_LIST_BOX(self->list), GTK_WIDGET(track_item)); // Remove the item from the listbox + g_hash_table_remove(self->tracks_to_items, track_uuid); // Remove the track from the hashtable +} + void koto_disc_view_set_album( KotoDiscView * self, KotoAlbum * album @@ -228,8 +270,6 @@ void koto_disc_view_set_album( } self->album = album; - - g_list_foreach(koto_album_get_tracks(self->album), koto_disc_view_list_tracks, self); } void koto_disc_view_set_disc_number( @@ -240,7 +280,7 @@ void koto_disc_view_set_disc_number( return; } - self->disc_number = GUINT_TO_POINTER(disc_number); + self->disc_number = disc_number; gchar * disc_label = g_strdup_printf("Disc %u", disc_number); @@ -256,9 +296,19 @@ void koto_disc_view_set_disc_label_visible( (visible) ? gtk_widget_show(self->header) : gtk_widget_hide(self->header); } +gint koto_disc_view_sort_list_box_rows( + GtkListBoxRow * row1, + GtkListBoxRow * row2, + gpointer user_data +) { + KotoTrackItem * track1_item = KOTO_TRACK_ITEM(gtk_list_box_row_get_child(row1)); + KotoTrackItem * track2_item = KOTO_TRACK_ITEM(gtk_list_box_row_get_child(row2)); + return koto_track_helpers_sort_track_items(track1_item, track2_item, user_data); +} + KotoDiscView * koto_disc_view_new( KotoAlbum * album, - guint * disc_number + guint disc_number ) { return g_object_new( KOTO_TYPE_DISC_VIEW, diff --git a/src/pages/music/disc-view.h b/src/pages/music/disc-view.h index 31462b5..35e7bcd 100644 --- a/src/pages/music/disc-view.h +++ b/src/pages/music/disc-view.h @@ -27,17 +27,21 @@ G_BEGIN_DECLS G_DECLARE_FINAL_TYPE(KotoDiscView, koto_disc_view, KOTO, DISC_VIEW, GtkBox) -KotoDiscView* koto_disc_view_new(KotoAlbum * album, guint * disc); +KotoDiscView* koto_disc_view_new( + KotoAlbum * album, + guint disc +); + +void koto_disc_view_add_track( + KotoDiscView * self, + KotoTrack * track +); + void koto_disc_view_handle_selected_rows_changed( GtkListBox * box, gpointer user_data ); -void koto_disc_view_list_tracks( - gpointer data, - gpointer selfptr -); - void koto_disc_view_set_album( KotoDiscView * self, KotoAlbum * album @@ -53,4 +57,10 @@ void koto_disc_view_set_disc_number( guint disc_number ); +gint koto_disc_view_sort_list_box_rows( + GtkListBoxRow * row1, + GtkListBoxRow * row2, + gpointer user_data +); + G_END_DECLS diff --git a/src/pages/music/music-local.c b/src/pages/music/music-local.c index 1671609..fae261c 100644 --- a/src/pages/music/music-local.c +++ b/src/pages/music/music-local.c @@ -31,6 +31,7 @@ struct _KotoPageMusicLocal { GtkWidget * scrolled_window; GtkWidget * artist_list; GtkWidget * stack; + GHashTable * artist_name_to_buttons; gboolean constructed; }; @@ -54,6 +55,7 @@ static void koto_page_music_local_class_init(KotoPageMusicLocalClass * c) { static void koto_page_music_local_init(KotoPageMusicLocal * self) { self->constructed = FALSE; + self->artist_name_to_buttons = g_hash_table_new(g_str_hash, g_str_equal); gtk_widget_add_css_class(GTK_WIDGET(self), "page-music-local"); gtk_widget_set_hexpand(GTK_WIDGET(self), TRUE); @@ -79,6 +81,9 @@ static void koto_page_music_local_init(KotoPageMusicLocal * self) { gtk_widget_set_hexpand(self->stack, TRUE); gtk_widget_set_vexpand(self->stack, TRUE); gtk_box_append(GTK_BOX(self), self->stack); + + g_signal_connect(koto_maps, "artist-added", G_CALLBACK(koto_page_music_local_handle_artist_added), self); + g_signal_connect(koto_maps, "artist-removed", G_CALLBACK(koto_page_music_local_handle_artist_removed), self); } static void koto_page_music_local_constructed(GObject * obj) { @@ -92,17 +97,35 @@ void koto_page_music_local_add_artist( KotoPageMusicLocal * self, KotoArtist * artist ) { - gchar * artist_name; + if (!KOTO_IS_PAGE_MUSIC_LOCAL(self)) { // Not the local page + return; + } + + if (!KOTO_IS_ARTIST(artist)) { // Not an artist + return; + } + + gchar * artist_name = koto_artist_get_name(artist); // Get the artist name + + if (!GTK_IS_STACK(self->stack)) { + return; + } + + GtkWidget * stack_child = gtk_stack_get_child_by_name(GTK_STACK(self->stack), artist_name); + + if (GTK_IS_WIDGET(stack_child)) { // Already have a page for this artist + g_free(artist_name); + return; + } - g_object_get(artist, "name", &artist_name, NULL); KotoButton * artist_button = koto_button_new_plain(artist_name); - gtk_list_box_prepend(GTK_LIST_BOX(self->artist_list), GTK_WIDGET(artist_button)); + g_hash_table_replace(self->artist_name_to_buttons, artist_name, artist_button); // Add the KotoButton for this artist to the hash table, that way we can reference and remove it when we remove the artist - KotoArtistView * artist_view = koto_artist_view_new(); // Create our new artist view + KotoArtistView * artist_view = koto_artist_view_new(artist); // Create our new artist view - koto_artist_view_add_artist(artist_view, artist); // Add the artist gtk_stack_add_named(GTK_STACK(self->stack), koto_artist_view_get_main(artist_view), artist_name); + g_free(artist_name); } void koto_page_music_local_go_to_artist_by_name( @@ -153,24 +176,51 @@ void koto_page_music_local_handle_artist_click( koto_page_music_local_go_to_artist_by_name(self, artist_name); } -void koto_page_music_local_build_ui(KotoPageMusicLocal * self) { - if (!self->constructed) { +void koto_page_music_local_handle_artist_added( + KotoCartographer * carto, + KotoArtist * artist, + gpointer user_data +) { + (void) carto; + KotoPageMusicLocal * self = user_data; + + if (!KOTO_IS_PAGE_MUSIC_LOCAL(self)) { // Not the page return; } - GHashTableIter artist_list_iter; - gpointer artist_key; - gpointer artist_data; + if (!KOTO_IS_ARTIST(artist)) { // Not an artist + return; + } - GHashTable * artists = koto_cartographer_get_artists(koto_maps); // Get all the artists + if (koto_artist_get_lib_type(artist) != KOTO_LIBRARY_TYPE_MUSIC) { // Not in our music library + return; + } - 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 - KotoArtist * artist = artist_data; // Cast our artist_data as an artist + koto_page_music_local_add_artist(self, artist); // Add the artist if needed +} - if (KOTO_IS_ARTIST(artist)) { // Is an artist - koto_page_music_local_add_artist(self, artist); - } +void koto_page_music_local_handle_artist_removed( + KotoCartographer * carto, + gchar * artist_uuid, + gchar * artist_name, + gpointer user_data +) { + (void) carto; + (void) artist_uuid; + + KotoPageMusicLocal * self = user_data; + + GtkWidget * existing_artist_page = gtk_stack_get_child_by_name(GTK_STACK(self->stack), artist_name); + + // TODO: Navigate away from artist if we are currently looking at it + if (GTK_IS_WIDGET(existing_artist_page)) { // Page exists + gtk_stack_remove(GTK_STACK(self->stack), existing_artist_page); // Remove the artist page + } + + KotoButton * btn = g_hash_table_lookup(self->artist_name_to_buttons, artist_name); + + if (KOTO_IS_BUTTON(btn)) { // If we have a button for this artist + gtk_list_box_remove(GTK_LIST_BOX(self->artist_list), GTK_WIDGET(btn)); // Remove the button } } diff --git a/src/pages/music/music-local.h b/src/pages/music/music-local.h index b7b7f6c..1bef6d5 100644 --- a/src/pages/music/music-local.h +++ b/src/pages/music/music-local.h @@ -40,6 +40,19 @@ void koto_page_music_local_handle_artist_click( gpointer data ); +void koto_page_music_local_handle_artist_added( + KotoCartographer * carto, + KotoArtist * artist, + gpointer user_data +); + +void koto_page_music_local_handle_artist_removed( + KotoCartographer * carto, + gchar * artist_uuid, + gchar * artist_name, + gpointer user_data +); + void koto_page_music_local_go_to_artist_by_name( KotoPageMusicLocal * self, gchar * artist_name @@ -50,8 +63,6 @@ void koto_page_music_local_go_to_artist_by_uuid( gchar * artist_uuid ); -void koto_page_music_local_build_ui(KotoPageMusicLocal * self); - int koto_page_music_local_sort_artists( GtkListBoxRow * artist1, GtkListBoxRow * artist2, diff --git a/src/pages/playlist/list.c b/src/pages/playlist/list.c index c2a948d..cae3ac3 100644 --- a/src/pages/playlist/list.c +++ b/src/pages/playlist/list.c @@ -269,7 +269,7 @@ void koto_playlist_page_bind_track_item( KotoAlbum * album = koto_cartographer_get_album_by_uuid(koto_maps, album_uuid); if (KOTO_IS_ALBUM(album)) { - gtk_label_set_label(GTK_LABEL(track_album_label), koto_album_get_album_name(album)); // Get the name of the album and set it to the label + gtk_label_set_label(GTK_LABEL(track_album_label), koto_album_get_name(album)); // Get the name of the album and set it to the label } KotoArtist * artist = koto_cartographer_get_artist_by_uuid(koto_maps, artist_uuid); diff --git a/src/playback/engine.c b/src/playback/engine.c index cd24651..c7c0ad2 100644 --- a/src/playback/engine.c +++ b/src/playback/engine.c @@ -200,6 +200,7 @@ static void koto_playback_engine_class_init(KotoPlaybackEngineClass * c) { static void koto_playback_engine_init(KotoPlaybackEngine * self) { self->current_track = NULL; + current_playlist = koto_current_playlist_new(); // Ensure our global current playlist is set self->player = gst_pipeline_new("player"); self->playbin = gst_element_factory_make("playbin", NULL); @@ -480,7 +481,7 @@ void koto_playback_engine_set_track_by_uuid( gchar * track_uuid, gboolean playing_specific_track ) { - if (track_uuid == NULL) { + if (!koto_utils_is_string_valid(track_uuid)) { // If this is not a valid track uuid string return; } @@ -492,9 +493,7 @@ void koto_playback_engine_set_track_by_uuid( self->current_track = track; - gchar * track_file_path = NULL; - - g_object_get(track, "path", &track_file_path, NULL); // Get the path to the track + gchar * track_file_path = koto_track_get_path(self->current_track); // Get the most optimal path for the track given the libraries it is in koto_playback_engine_stop(self); // Stop current track diff --git a/src/playlist/playlist.c b/src/playlist/playlist.c index 844c98a..58a99d8 100644 --- a/src/playlist/playlist.c +++ b/src/playlist/playlist.c @@ -18,8 +18,8 @@ #include #include #include -#include #include "../db/cartographer.h" +#include "../db/db.h" #include "../koto-utils.h" #include "playlist.h" @@ -372,17 +372,7 @@ void koto_playlist_commit(KotoPlaylist * self) { 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); + new_transaction(commit_op, "Failed to save playlist", FALSE); } void koto_playlist_commit_tracks( @@ -660,7 +650,7 @@ gint koto_playlist_model_sort_by_track( return 0; // Just consider them as equal } - return g_utf8_collate(koto_album_get_album_name(first_album), koto_album_get_album_name(second_album)); + return g_utf8_collate(koto_album_get_name(first_album), koto_album_get_name(second_album)); } if (model == KOTO_PREFERRED_MODEL_TYPE_SORT_BY_ARTIST) { // Sort by artist name diff --git a/theme/pages/_music-local.scss b/theme/pages/_music-local.scss index b044777..ac257fd 100644 --- a/theme/pages/_music-local.scss +++ b/theme/pages/_music-local.scss @@ -16,13 +16,6 @@ & > stack { & > .artist-view { & > viewport > .artist-view-content { - & > .album-strip { - margin-bottom: $itempadding; - & > flowboxchild { - margin-right: $itempadding; - } - } - & > .album-list { & > flowboxchild > .album-view { & > overlay {