Implement initial audiobook UX (some of which is a bit of a WIP).

- Renamed various components and moved them to src/components.
- Renamed KOTO_PREFERRED_MODEL* to KOTO_PREFERRED_PLAYLIST*
- Renamed koto string utility functions to always be prefixed with koto_utils_string_ for consistency.
- Added configuration options for show / hiding various album information, as well as preferred sort type.
- Changed db schema to reflect various metadata changes (sorry).
- Implemented genre, narrator, year aggregation from KotoTrack to KotoAlbum for use in KotoAlbumInfo and audiobooks.
- Rearchitected our playlist functionality for KotoAlbums to always have an inner KotoPlaylist that is used.
- Added various getters / setters for new koto_album functionality.
- Implement aggregation of KotoAlbum pointer aggregation in the KotoArtist as a GQueue and GListStore instead of GList so we can get all the albums associated with an artist and use the GListStore for the audiobook view.
- Implement some initial album sorting in Artists (more work to do on this front).
- Many improvements to file indexing logic for CD and position detection, various new koto_track_helpers.
- Add new logic for knowing when to hide playlists given we generate them for each Album now.
- Fix missing updates of KotoPlaylist in KotoNav.
- Added playback position to KotoPlayerbar, renamed bar refs to self.
- New Playlist state saving.
- Updated track ticking logic for track in KotoPlaybackEngine.
- Fixed playback position detection in our KotoPlaybackEngine by swapping from GST_FORMAT_DEFAULT to GST_FORMAT_TIME.
- Changed our get_progress to divide by GST_SECOND.
- Fixed missing type checks in various KotoPlaybackEngine functions.

Fixes #13. Fixes #14. Fixes #15.
This commit is contained in:
Joshua Strobl 2021-08-10 19:18:46 +03:00
parent 93f3f45adf
commit 77b4e900e6
86 changed files with 4926 additions and 824 deletions

View file

@ -39,7 +39,11 @@ struct _KotoTrack {
guint cd;
guint64 position;
guint64 duration;
guint64 * playback_position;
gchar * description;
gchar * narrator;
guint64 playback_position;
guint64 year;
GList * genres;
gboolean do_initial_index;
@ -57,6 +61,9 @@ enum {
PROP_CD,
PROP_POSITION,
PROP_DURATION,
PROP_DESCRIPTION,
PROP_NARRATOR,
PROP_YEAR,
PROP_PLAYBACK_POSITION,
PROP_PREPARSED_GENRES,
N_PROPERTIES
@ -157,6 +164,32 @@ static void koto_track_class_init(KotoTrackClass * c) {
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
props[PROP_DESCRIPTION] = g_param_spec_string(
"description",
"Description of Track, typically show notes for a podcast episode",
"Description of Track, typically show notes for a podcast episode",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
props[PROP_NARRATOR] = g_param_spec_string(
"narrator",
"Narrator, typically of an Audiobook",
"Narrator, typically of an Audiobook",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
props[PROP_YEAR] = g_param_spec_uint64(
"year",
"Year",
"Year",
0,
G_MAXUINT64,
0,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
props[PROP_PLAYBACK_POSITION] = g_param_spec_uint64(
"playback-position",
"Current playback position",
@ -179,10 +212,13 @@ static void koto_track_class_init(KotoTrackClass * c) {
}
static void koto_track_init(KotoTrack * self) {
self->description = NULL; // Initialize our description
self->duration = 0; // Initialize our duration
self->genres = NULL; // Initialize our genres list
self->narrator = NULL, // Initialize our narrator
self->paths = g_hash_table_new(g_str_hash, g_str_equal); // Create our hash table of paths
self->position = 0; // Initialize our duration
self->year = 0; // Initialize our year
}
static void koto_track_get_property(
@ -209,11 +245,20 @@ static void koto_track_get_property(
case PROP_CD:
g_value_set_uint(val, self->cd);
break;
case PROP_DESCRIPTION:
g_value_set_string(val, self->description);
break;
case PROP_NARRATOR:
g_value_set_string(val, self->narrator);
break;
case PROP_YEAR:
g_value_set_uint64(val, self->year);
break;
case PROP_POSITION:
g_value_set_uint(val, self->position);
g_value_set_uint64(val, self->position);
break;
case PROP_PLAYBACK_POSITION:
g_value_set_uint(val, GPOINTER_TO_UINT(self->playback_position));
g_value_set_uint64(val, koto_track_get_playback_position(self));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
@ -254,11 +299,20 @@ static void koto_track_set_property(
koto_track_set_position(self, g_value_get_uint64(val));
break;
case PROP_PLAYBACK_POSITION:
self->playback_position = GUINT_TO_POINTER(g_value_get_uint64(val));
koto_track_set_playback_position(self, g_value_get_uint64(val));
break;
case PROP_DURATION:
koto_track_set_duration(self, g_value_get_uint64(val));
break;
case PROP_DESCRIPTION:
koto_track_set_description(self, g_value_get_string(val));
break;
case PROP_NARRATOR:
koto_track_set_narrator(self, g_strdup(g_value_get_string(val)));
break;
case PROP_YEAR:
koto_track_set_year(self, g_value_get_uint64(val));
break;
case PROP_PREPARSED_GENRES:
koto_track_set_preparsed_genres(self, g_strdup(g_value_get_string(val)));
break;
@ -273,14 +327,10 @@ void koto_track_commit(KotoTrack * self) {
return;
}
if (!koto_utils_is_string_valid(self->artist_uuid)) { // No valid required artist UUID
if (!koto_utils_string_is_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, duration, genres)" \
"VALUES('%s', '%s', '%s', quote(\"%s\"), %d, %d, %d, '%s')" \
"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, duration=excluded.duration, genres=excluded.genres;";
@ -292,7 +342,7 @@ void koto_track_commit(KotoTrack * self) {
commit_msg,
self->uuid,
self->artist_uuid,
self->album_uuid,
koto_utils_string_get_valid(self->album_uuid),
g_strescape(self->parsed_name, NULL),
(int) self->cd,
(int) self->position,
@ -326,6 +376,10 @@ void koto_track_commit(KotoTrack * self) {
}
}
gchar * koto_track_get_description(KotoTrack * self) {
return KOTO_IS_TRACK(self) ? g_strdup(self->description) : NULL;
}
guint koto_track_get_disc_number(KotoTrack * self) {
return KOTO_IS_TRACK(self) ? self->cd : 1;
}
@ -347,14 +401,14 @@ GVariant * koto_track_get_metadata_vardict(KotoTrack * self) {
KotoArtist * artist = koto_cartographer_get_artist_by_uuid(koto_maps, self->artist_uuid);
gchar * artist_name = koto_artist_get_name(artist);
if (koto_utils_is_string_valid(self->album_uuid)) { // Have an album associated
if (koto_utils_string_is_valid(self->album_uuid)) { // Have an album associated
KotoAlbum * album = koto_cartographer_get_album_by_uuid(koto_maps, self->album_uuid);
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
if (koto_utils_string_is_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));
}
@ -366,7 +420,7 @@ GVariant * koto_track_get_metadata_vardict(KotoTrack * self) {
g_variant_builder_add(builder, "{sv}", "mpris:trackid", g_variant_new_string(self->uuid));
if (koto_utils_is_string_valid(artist_name)) { // Valid artist name
if (koto_utils_string_is_valid(artist_name)) { // Valid artist name
GVariant * artist_name_variant;
GVariantBuilder * artist_list_builder = g_variant_builder_new(G_VARIANT_TYPE("as"));
g_variant_builder_add(artist_list_builder, "s", artist_name);
@ -388,11 +442,11 @@ GVariant * koto_track_get_metadata_vardict(KotoTrack * self) {
}
gchar * koto_track_get_name(KotoTrack * self) {
if (!KOTO_IS_TRACK(self)) { // Not a track
return NULL;
}
return KOTO_IS_TRACK(self) ? g_strdup(self->parsed_name) : NULL;
}
return g_strdup(self->parsed_name);
gchar * koto_track_get_narrator(KotoTrack * self) {
return KOTO_IS_TRACK(self) ? g_strdup(self->narrator) : NULL;
}
gchar * koto_track_get_path(KotoTrack * self) {
@ -420,6 +474,10 @@ gchar * koto_track_get_path(KotoTrack * self) {
return path;
}
guint64 koto_track_get_playback_position(KotoTrack * self) {
return KOTO_IS_TRACK(self) ? self->playback_position : 0;
}
guint64 koto_track_get_position(KotoTrack * self) {
return KOTO_IS_TRACK(self) ? self->position : 0;
}
@ -433,13 +491,13 @@ gchar * koto_track_get_uniqueish_key(KotoTrack * self) {
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)
if (koto_utils_string_is_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)) {
if (koto_utils_string_is_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)
}
}
@ -456,6 +514,14 @@ 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
}
guint64 koto_track_get_year(KotoTrack * self) {
if (!KOTO_IS_TRACK(self)) {
return 0;
}
return self->year;
}
void koto_track_remove_from_playlist(
KotoTrack * self,
gchar * playlist_uuid
@ -487,7 +553,7 @@ void koto_track_set_album_uuid(
gchar * uuid = g_strdup(album_uuid);
if (!koto_utils_is_string_valid(uuid)) { // If this is not a valid string
if (!koto_utils_string_is_valid(uuid)) { // If this is not a valid string
return;
}
@ -497,19 +563,17 @@ void koto_track_set_album_uuid(
void koto_track_save_to_playlist(
KotoTrack * self,
gchar * playlist_uuid,
gint current
gchar * playlist_uuid
) {
if (!KOTO_IS_TRACK(self)) {
return;
}
gchar * commit_op = g_strdup_printf(
"INSERT INTO playlist_tracks(playlist_id, track_id, current)"
"VALUES('%s', '%s', quote(\"%d\"))",
"INSERT INTO playlist_tracks(playlist_id, track_id)"
"VALUES('%s', '%s')",
playlist_uuid,
self->uuid,
current
self->uuid
);
new_transaction(commit_op, "Failed to save track to playlist", FALSE);
@ -531,6 +595,30 @@ void koto_track_set_cd(
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_CD]);
}
void koto_track_set_description(
KotoTrack * self,
const gchar * description
) {
if (!KOTO_IS_TRACK(self)) {
return;
}
if (!koto_utils_string_is_valid(description)) {
return;
}
if (g_strcmp0(self->description, description) == 0) { // Same description
return;
}
if (koto_utils_string_is_valid(self->description)) {
g_free(self->description); // Free the existing narrator
}
self->description = g_strdup(description); // Duplicate our description
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_DESCRIPTION]);
}
void koto_track_set_duration(
KotoTrack * self,
guint64 duration
@ -544,6 +632,7 @@ void koto_track_set_duration(
}
self->duration = duration;
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_DURATION]);
}
void koto_track_set_genres(
@ -554,7 +643,7 @@ void koto_track_set_genres(
return;
}
if (!koto_utils_is_string_valid((gchar*) genrelist)) { // If it is an empty string
if (!koto_utils_string_is_valid((gchar*) genrelist)) { // If it is an empty string
return;
}
@ -570,7 +659,7 @@ void koto_track_set_genres(
gchar * genre = genres[i]; // Get the genre
gchar * lowercased_genre = g_utf8_strdown(g_strstrip(genre), -1); // Lowercase the genre
gchar * lowercased_hyphenated_genre = koto_utils_replace_string_all(lowercased_genre, " ", "-");
gchar * lowercased_hyphenated_genre = koto_utils_string_replace_all(lowercased_genre, " ", "-");
g_free(lowercased_genre); // Free the lowercase genre string since we no longer need it
gchar * corrected_genre = koto_track_helpers_get_corrected_genre(lowercased_hyphenated_genre); // Get any corrected genre
@ -585,6 +674,30 @@ void koto_track_set_genres(
g_strfreev(genres); // Free the list of genres locally
}
void koto_track_set_narrator(
KotoTrack * self,
const gchar * narrator
) {
if (!KOTO_IS_TRACK(self)) {
return;
}
if (!koto_utils_string_is_valid(narrator)) {
return;
}
if (g_strcmp0(self->narrator, narrator) == 0) { // Same narrator
return;
}
if (koto_utils_string_is_valid(self->narrator)) {
g_free(self->narrator); // Free the existing narrator
}
self->narrator = g_strdup(narrator); // Duplicate our narrator
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_NARRATOR]);
}
void koto_track_set_parsed_name(
KotoTrack * self,
gchar * new_parsed_name
@ -593,11 +706,11 @@ void koto_track_set_parsed_name(
return;
}
if (!koto_utils_is_string_valid(new_parsed_name)) {
if (!koto_utils_string_is_valid(new_parsed_name)) {
return;
}
gboolean have_existing_name = koto_utils_is_string_valid(self->parsed_name);
gboolean have_existing_name = koto_utils_string_is_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
@ -620,7 +733,7 @@ void koto_track_set_path(
return;
}
if (!koto_utils_is_string_valid(fixed_path)) { // Not a valid path
if (!koto_utils_string_is_valid(fixed_path)) { // Not a valid path
return;
}
@ -635,10 +748,25 @@ void koto_track_set_path(
}
}
void koto_track_set_playback_position(
KotoTrack * self,
guint64 position
) {
if (!KOTO_IS_TRACK(self)) { // Not a track
return;
}
self->playback_position = position;
}
void koto_track_set_position(
KotoTrack * self,
guint64 pos
) {
if (!KOTO_IS_TRACK(self)) { // Not a track
return;
}
if (pos == 0) { // No position change really
return;
}
@ -647,29 +775,48 @@ void koto_track_set_position(
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_POSITION]);
}
void koto_track_set_year(
KotoTrack * self,
guint64 year
) {
if (!KOTO_IS_TRACK(self)) {
return;
}
self->year = year;
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_YEAR]);
}
void koto_track_update_metadata(KotoTrack * self) {
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
if (!koto_utils_string_is_valid(optimal_track_path)) { // Not a valid string
return;
}
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
TagLib_Tag * tag = taglib_file_tag(t_file); // Get our tag
koto_track_set_genres(self, taglib_tag_genre(tag)); // Set our genres to any genres listed for the track
koto_track_set_position(self, (uint) taglib_tag_track(tag)); // Get the track, convert to uint and cast as a pointer
koto_track_set_year(self, (guint64) taglib_tag_year(tag)); // Get the track year and convert it to guint64
const TagLib_AudioProperties * tag_props = taglib_file_audioproperties(t_file); // Get the audio properties of the file
koto_track_set_duration(self, taglib_audioproperties_length(tag_props)); // Get the length of the track and set it as our duration
} else { // Failed to get tag info
}
if (self->position == 0) { // Failed to get tag info or got the tag info but position is zero
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
g_free(optimal_track_path);
}
void koto_track_set_preparsed_genres(
@ -680,7 +827,7 @@ void koto_track_set_preparsed_genres(
return;
}
if (!koto_utils_is_string_valid(genrelist)) { // If it is an empty string
if (!koto_utils_string_is_valid(genrelist)) { // If it is an empty string
return;
}