koto/src/indexer/album.c
Joshua Strobl bfe4891620 Start cleanup of KotoLibrary logic and decoupling other components like Music Local from a specific library.
Fix the displaying of discs and tracks in an album on initial index due to missing cartographer add track call.

Cleanup lots of double empty newlines. Updated ptr spacing in uncrustify config to enforce consistency in pointer char (`*`) spacing.
2021-05-27 17:02:19 +03:00

681 lines
16 KiB
C

/* 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 <glib-2.0/glib.h>
#include <magic.h>
#include <sqlite3.h>
#include <stdio.h>
#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
);
}