/* album.c * * Copyright 2021 Joshua Strobl * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include #include "../db/cartographer.h" #include "../playlist/current.h" #include "../playlist/playlist.h" #include "structs.h" #include "koto-utils.h" extern KotoCartographer * koto_maps; extern KotoCurrentPlaylist * current_playlist; extern sqlite3 * koto_db; struct _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, N_PROPERTIES }; static GParamSpec * props[N_PROPERTIES] = { NULL, }; static void koto_album_get_property( GObject * obj, guint prop_id, GValue * val, GParamSpec * spec ); static void koto_album_set_property( GObject * obj, guint prop_id, const GValue * val, GParamSpec * spec ); static void koto_album_class_init(KotoAlbumClass * c) { GObjectClass * gobject_class; gobject_class = G_OBJECT_CLASS(c); gobject_class->set_property = koto_album_set_property; gobject_class->get_property = koto_album_get_property; props[PROP_UUID] = g_param_spec_string( "uuid", "UUID to Album in database", "UUID to Album in database", NULL, G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE ); props[PROP_DO_INITIAL_INDEX] = g_param_spec_boolean( "do-initial-index", "Do an initial indexing operating instead of pulling from the database", "Do an initial indexing operating instead of pulling from the database", FALSE, 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", "Name of Album", NULL, G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE ); props[PROP_ART_PATH] = g_param_spec_string( "art-path", "Path to Artwork", "Path to Artwork", NULL, G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE ); props[PROP_ARTIST_UUID] = g_param_spec_string( "artist-uuid", "UUID of Artist associated with Album", "UUID of Artist associated with Album", NULL, G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE ); g_object_class_install_properties(gobject_class, N_PROPERTIES, props); } static void koto_album_init(KotoAlbum * self) { self->has_album_art = FALSE; self->tracks = NULL; } void koto_album_add_track( KotoAlbum * self, KotoTrack * track ) { if (track == NULL) { // Not a file return; } gchar * track_uuid; g_object_get(track, "uuid", &track_uuid, NULL); 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); } } void koto_album_commit(KotoAlbum * self) { if (self->art_path == NULL) { // If art_path isn't defined when committing koto_album_set_album_art(self, ""); // Set to an empty string } 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;", 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); if (rc != SQLITE_OK) { g_warning("Failed to write our album to the database: %s", commit_op_errmsg); } 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) { return; } if (magic_load(magic_cookie, NULL) != 0) { magic_close(magic_cookie); return; } DIR * dir = opendir(self->path); // Attempt to open our directory if (dir == NULL) { return; } struct dirent * entry; while ((entry = readdir(dir))) { if (entry->d_type != DT_REG) { // Not a regular file continue; // SKIP } 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", self->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 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 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; } g_free(album_art_no_ext); g_free(lower_art); } 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( GObject * obj, guint prop_id, GValue * val, GParamSpec * spec ) { KotoAlbum * self = KOTO_ALBUM(obj); switch (prop_id) { case PROP_UUID: g_value_set_string(val, self->uuid); break; 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)); break; case PROP_ARTIST_UUID: g_value_set_string(val, self->artist_uuid); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); break; } } static void koto_album_set_property( GObject * obj, guint prop_id, const GValue * val, GParamSpec * spec ) { KotoAlbum * self = KOTO_ALBUM(obj); switch (prop_id) { case PROP_UUID: self->uuid = g_strdup(g_value_get_string(val)); g_object_notify_by_pspec(G_OBJECT(self), props[PROP_UUID]); break; case PROP_DO_INITIAL_INDEX: self->do_initial_index = g_value_get_boolean(val); break; case PROP_PATH: // Path to the album koto_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; case PROP_ART_PATH: // Path to art koto_album_set_album_art(self, g_value_get_string(val)); break; case PROP_ARTIST_UUID: koto_album_set_artist_uuid(self, g_value_get_string(val)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); break; } } gchar * koto_album_get_album_art(KotoAlbum * self) { if (!KOTO_IS_ALBUM(self)) { // Not an album return g_strdup(""); } 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) { if (!KOTO_IS_ALBUM(self)) { // Not an album return NULL; } if (!koto_utils_is_string_valid(self->name)) { // Not set return NULL; } return g_strdup(self->name); // Return duplicate of the name } gchar * koto_album_get_album_uuid(KotoAlbum * self) { if (!KOTO_IS_ALBUM(self)) { // Not an album return NULL; } if (!koto_utils_is_string_valid(self->uuid)) { // Not set return NULL; } return g_strdup(self->uuid); // Return a duplicate of the UUID } GList * koto_album_get_tracks(KotoAlbum * self) { if (!KOTO_IS_ALBUM(self)) { // Not an album return NULL; } return self->tracks; // Return } void koto_album_set_album_art( KotoAlbum * self, const gchar * album_art ) { if (!KOTO_IS_ALBUM(self)) { // Not an album return; } if (album_art == NULL) { // Not valid album art return; } if (self->art_path != NULL) { g_free(self->art_path); } self->art_path = g_strdup(album_art); self->has_album_art = TRUE; } void koto_album_remove_file( KotoAlbum * self, KotoTrack * track ) { if (!KOTO_IS_ALBUM(self)) { // Not an album return; } if (track == NULL) { // Not a file return; } gchar * track_uuid; g_object_get(track, "parsed-name", &track_uuid, NULL); self->tracks = g_list_remove(self->tracks, track_uuid); } void koto_album_set_album_name( KotoAlbum * self, const gchar * album_name ) { if (!KOTO_IS_ALBUM(self)) { // Not an album return; } if (album_name == NULL) { // Not valid album name return; } if (self->name != NULL) { g_free(self->name); } self->name = g_strdup(album_name); } void koto_album_set_artist_uuid( KotoAlbum * self, const gchar * artist_uuid ) { if (!KOTO_IS_ALBUM(self)) { // Not an album return; } if (artist_uuid == NULL) { return; } if (self->artist_uuid != NULL) { g_free(self->artist_uuid); } self->artist_uuid = g_strdup(artist_uuid); } void koto_album_set_as_current_playlist(KotoAlbum * self) { if (!KOTO_IS_ALBUM(self)) { // Not an album return; } if (self->tracks == NULL) { // No files to add to the playlist return; } KotoPlaylist * new_album_playlist = koto_playlist_new(); // Create a new playlist g_object_set(new_album_playlist, "ephemeral", TRUE, NULL); // Set as ephemeral / temporary // The following section effectively reverses our tracks, so the first is now last. // It then adds them in reverse order, since our playlist add function will prepend to our queue // This enables the preservation and defaulting of "newest" first everywhere else, while in albums preserving the rightful order of the album // e.g. first track (0) being added last is actually first in the playlist's tracks GList * reversed_tracks = g_list_copy(self->tracks); // Copy our tracks so we can reverse the order reversed_tracks = g_list_reverse(reversed_tracks); // Actually reverse it GList * t; for (t = reversed_tracks; t != NULL; t = t->next) { // For each of the tracks gchar * track_uuid = t->data; koto_playlist_add_track_by_uuid(new_album_playlist, track_uuid, FALSE, FALSE); // Add the UUID, skip commit to table since it is temporary } g_list_free(t); g_list_free(reversed_tracks); koto_playlist_apply_model(new_album_playlist, KOTO_PREFERRED_MODEL_TYPE_DEFAULT); // Ensure we are using our default model 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; } 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", artist_uuid, "uuid", 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; } KotoAlbum * koto_album_new_with_uuid( KotoArtist * artist, const gchar * uuid ) { gchar * artist_uuid = NULL; g_object_get(artist, "uuid", &artist_uuid, NULL); return g_object_new( KOTO_TYPE_ALBUM, "artist-uuid", artist, "uuid", g_strdup(uuid), "do-initial-index", FALSE, NULL ); }