koto/src/indexer/album.c
Joshua Strobl 381cc9ce4c Implement Album and Track metadata for duration and genres.
For tracks, we will fetch the duration from the ID3 tag information, alongside genres. These genres will be used for multiple purposes, e.g. search. At the moment however, it is used in koto_album_add_track to collate a list of genres that apply to the album based on the contents of it.

We can have multiple genres, we separate them by semi-column as is the case for TagLib. We will attempt to filter and rename some select genres to enforce consistency.

Changed position and playback position from guint to guint64.

Implemented multiple koto utility functions:

- koto_utils_join_string_list
- koto_utils_string_contains_substring
- koto_utils_string_to_string_list

Drastically simplified koto_utils_replace_string_all.

Implement koto_track_helpers_init to initialize our hashtable.

Fixed a segfault during first load of Koto where we were attempting to get the playback engine last used volume in koto_config_save, when the KotoPlaybackEngine was not yet initialized. Default it to 1.0.
2021-07-08 18:37:52 +03:00

682 lines
17 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 "../db/db.h"
#include "../playlist/current.h"
#include "../playlist/playlist.h"
#include "../koto-utils.h"
#include "structs.h"
#include "track-helpers.h"
extern KotoCartographer * koto_maps;
extern KotoCurrentPlaylist * current_playlist;
extern magic_t magic_cookie;
extern sqlite3 * koto_db;
enum {
PROP_0,
PROP_UUID,
PROP_DO_INITIAL_INDEX,
PROP_ALBUM_NAME,
PROP_ART_PATH,
PROP_ARTIST_UUID,
PROP_ALBUM_PREPARED_GENRES,
N_PROPERTIES
};
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 * genres;
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,
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_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
);
props[PROP_ALBUM_PREPARED_GENRES] = g_param_spec_string(
"preparsed-genres",
"Preparsed Genres",
"Preparsed Genres",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_WRITABLE
);
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->genres = NULL;
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 (!KOTO_IS_ALBUM(self)) { // Not an album
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) { // Have added it already
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);
GList * track_genres = koto_track_get_genres(track); // Get the genres for the track
GList * current_genre_list;
gchar * existing_genres_as_string = koto_utils_join_string_list(self->genres, ";");
for (current_genre_list = track_genres; current_genre_list != NULL; current_genre_list = current_genre_list->next) { // Iterate over each item in the track genres
gchar * track_genre = current_genre_list->data; // Get this genre
if (koto_utils_string_contains_substring(existing_genres_as_string, track_genre)) { // Genres list contains this genre
continue;
}
self->genres = g_list_append(self->genres, g_strdup(track_genre)); // Duplicate the genre and add it to our list
}
g_signal_emit(
self,
album_signals[SIGNAL_TRACK_ADDED],
0,
track
);
}
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 * genres_string = koto_utils_join_string_list(self->genres, ";");
gchar * commit_op = g_strdup_printf(
"INSERT INTO albums(id, artist_id, name, art_path, genres)"
"VALUES('%s', '%s', quote(\"%s\"), quote(\"%s\"), '%s')"
"ON CONFLICT(id) DO UPDATE SET artist_id=excluded.artist_id, name=excluded.name, art_path=excluded.art_path, genres=excluded.genres;",
self->uuid,
self->artist_uuid,
self->name,
self->art_path,
genres_string
);
new_transaction(commit_op, "Failed to write our album to the database", FALSE);
g_free(genres_string);
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);
}
}
void koto_album_find_album_art(KotoAlbum * self) {
if (self->has_album_art) { // If we already have album art
return;
}
gchar * optimal_album_path = koto_album_get_path(self);
DIR * dir = opendir(optimal_album_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", 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
((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
}
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
g_free(album_art_no_ext);
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);
}
GList * koto_album_get_genres(KotoAlbum * self) {
if (!KOTO_IS_ALBUM(self)) { // Not an album
return NULL;
}
return self->genres;
}
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_ALBUM_NAME:
g_value_set_string(val, self->name);
break;
case PROP_ART_PATH:
g_value_set_string(val, koto_album_get_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_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;
case PROP_ALBUM_PREPARED_GENRES:
koto_album_set_preparsed_genres(self, g_strdup(g_value_get_string(val)));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
}
gchar * koto_album_get_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_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
}
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;
}
return self->tracks; // Return the tracks
}
gchar * koto_album_get_uuid(KotoAlbum * self) {
if (!KOTO_IS_ALBUM(self)) { // Not an album
return NULL;
}
return self->uuid; // Return the UUID
}
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_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
) {
if (!KOTO_IS_ALBUM(self)) { // Not an album
return;
}
if (!KOTO_IS_TRACK(track)) { // Not a track
return;
}
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(
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
}
void koto_album_set_preparsed_genres(
KotoAlbum * self,
gchar * genrelist
) {
if (!KOTO_IS_ALBUM(self)) { // Not an album
return;
}
if (!koto_utils_is_string_valid(genrelist)) { // If it is an empty string
return;
}
GList * preparsed_genres_list = koto_utils_string_to_string_list(genrelist, ";");
if (g_list_length(preparsed_genres_list) == 0) { // No genres
g_list_free(preparsed_genres_list);
return;
}
// TODO: Do a pass on in first memory optimization phase to ensure string elements are freed.
g_list_free_full(self->genres, NULL); // Free the existing genres list
self->genres = preparsed_genres_list;
};
KotoAlbum * koto_album_new(gchar * artist_uuid) {
if (!koto_utils_is_string_valid(artist_uuid)) { // Invalid artist UUID provided
return NULL;
}
KotoAlbum * album = g_object_new(
KOTO_TYPE_ALBUM,
"artist-uuid",
artist_uuid,
"uuid",
g_strdup(g_uuid_string_random()),
"do-initial-index",
TRUE,
NULL
);
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
);
}