From 62c99ee67c9a26536a91f721c0e904a8a79b5201 Mon Sep 17 00:00:00 2001 From: Joshua Strobl Date: Sat, 5 Oct 2024 00:03:50 +0300 Subject: [PATCH] feat: initial database creation, loading fixes for cartographer and automatically add track to album when adding to artist --- desktop/CMakeLists.txt | 24 +++---- desktop/config/config.cpp | 81 ++++++++++++++++++++---- desktop/config/config.hpp | 28 +++++--- desktop/config/library.cpp | 37 ++++++++++- desktop/config/library.hpp | 22 +++++-- desktop/config/ui_prefs.cpp | 59 +++++++++++++---- desktop/config/ui_prefs.hpp | 3 + desktop/datalake/album.cpp | 38 ++++++++++- desktop/datalake/artist.cpp | 27 +++++++- desktop/datalake/cartographer.cpp | 12 ++++ desktop/datalake/cartographer.hpp | 12 ++-- desktop/datalake/database.cpp | 102 ++++++++++++++++++++++++++++++ desktop/datalake/database.hpp | 21 ++++++ desktop/datalake/indexer.cpp | 18 +++--- desktop/datalake/indexer.hpp | 5 +- desktop/datalake/structs.hpp | 38 ++++++----- desktop/datalake/track.cpp | 54 ++++++++++++++-- desktop/main.cpp | 42 ++++++++---- 18 files changed, 515 insertions(+), 108 deletions(-) create mode 100644 desktop/datalake/database.cpp create mode 100644 desktop/datalake/database.hpp diff --git a/desktop/CMakeLists.txt b/desktop/CMakeLists.txt index 492f4df..bada5af 100644 --- a/desktop/CMakeLists.txt +++ b/desktop/CMakeLists.txt @@ -1,4 +1,4 @@ -find_package(Qt6 6.4 REQUIRED COMPONENTS Quick QuickControls2) +find_package(Qt6 6.4 REQUIRED COMPONENTS Quick QuickControls2 Sql) find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE) find_package(KF6Baloo) find_package(KF6FileMetaData) @@ -10,33 +10,33 @@ include(ECMQmlModule) qt_standard_project_setup() -qt_add_executable(koto - main.cpp +qt_add_executable(com.github.joshstrobl.koto config/config.cpp config/library.cpp config/ui_prefs.cpp - datalake/indexer.cpp - datalake/track.cpp datalake/album.cpp datalake/artist.cpp datalake/cartographer.cpp - datalake/cartographer.hpp + datalake/database.cpp + datalake/indexer.cpp + datalake/track.cpp + main.cpp ) -ecm_add_qml_module(koto URI "com.github.joshstrobl.koto" GENERATE_PLUGIN_SOURCE) +ecm_add_qml_module(com.github.joshstrobl.koto URI "com.github.joshstrobl.koto" GENERATE_PLUGIN_SOURCE) -ecm_target_qml_sources(koto +ecm_target_qml_sources(com.github.joshstrobl.koto SOURCES qml/PrimaryNavigation.qml qml/HomePage.qml qml/Main.qml ) -target_link_libraries(koto - PRIVATE KF6::Baloo KF6::FileMetaData Qt6::Quick Qt6::QuickControls2 +target_link_libraries(com.github.joshstrobl.koto + PRIVATE KF6::Baloo KF6::FileMetaData Qt6::Quick Qt6::QuickControls2 Qt6::Sql ) install(FILES com.github.joshstrobl.koto.desktop DESTINATION ${KDE_INSTALL_APPDIR}) -install(TARGETS koto ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) +install(TARGETS com.github.joshstrobl.koto ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) -ecm_finalize_qml_module(koto) +ecm_finalize_qml_module(com.github.joshstrobl.koto) diff --git a/desktop/config/config.cpp b/desktop/config/config.cpp index cbea6ab..8fb1adb 100644 --- a/desktop/config/config.cpp +++ b/desktop/config/config.cpp @@ -2,18 +2,60 @@ #include #include +#include #include + namespace fs = std::filesystem; KotoConfig::KotoConfig() { // Define our application's config location - auto configDir = QDir(QStandardPaths::writableLocation(QStandardPaths::StandardLocation::AppConfigLocation)); - auto configDirPath = configDir.absolutePath(); + auto configDir = QDir(QStandardPaths::writableLocation(QStandardPaths::StandardLocation::AppConfigLocation)); + auto configDirPath = configDir.absolutePath(); + this->i_configDirPath = configDirPath; + this->i_libraries = QList(); + fs::path filePath {}; auto configPathStd = configDirPath.toStdString(); filePath /= configPathStd; filePath /= "config.toml"; + this->i_configPath = QString {filePath.c_str()}; + if (QFileInfo::exists(i_configPath)) { + this->parseConfigFile(filePath); + } else { + this->bootstrap(); + } +} + +KotoConfig& KotoConfig::instance() { + static KotoConfig _instance; + return _instance; +} + +void KotoConfig::bootstrap() { + this->i_uiPreferences = new KotoUiPreferences(); + + auto musicDir = QDir(QStandardPaths::writableLocation(QStandardPaths::StandardLocation::MusicLocation)); + auto musicLibrary = new KotoLibraryConfig("Music", musicDir.absolutePath().toStdString(), KotoLibraryType::Music); + + this->i_libraries.append(musicLibrary); + + this->save(); +} + +QString KotoConfig::getConfigDirPath() { + return QString {this->i_configDirPath}; +} + +KotoUiPreferences* KotoConfig::getUiPreferences() { + return this->i_uiPreferences; +} + +QList KotoConfig::getLibraries() { + return this->i_libraries; +} + +void KotoConfig::parseConfigFile(std::string filePath) { auto data = toml::parse(filePath); std::optional ui_prefs; @@ -22,22 +64,35 @@ KotoConfig::KotoConfig() { if (ui_prefs_at.is_table()) ui_prefs = ui_prefs_at.as_table(); } - auto prefs = KotoUiPreferences(ui_prefs); - this->i_uiPreferences = &prefs; + auto prefs = new KotoUiPreferences(ui_prefs); + this->i_uiPreferences = prefs; - this->i_libraries = {}; for (const auto& lib_value : toml::find>(data, "libraries")) { - auto lib = KotoLibraryConfig(lib_value); - this->i_libraries.push_back(lib); + auto lib = new KotoLibraryConfig(lib_value); + this->i_libraries.append(lib); } } -KotoConfig::~KotoConfig() {} +void KotoConfig::save() { + toml::ordered_value config_table(toml::ordered_table {}); + config_table["preferences.ui"] = this->i_uiPreferences->serialize(); -KotoUiPreferences* KotoConfig::getUiPreferences() { - return this->i_uiPreferences; -} + toml::ordered_value libraries_array(toml::ordered_array {}); + for (auto lib : this->i_libraries) { + auto lib_table = lib->serialize(); + libraries_array.push_back(lib_table); + } + config_table["libraries"] = libraries_array; -std::vector KotoConfig::getLibraries() { - return this->i_libraries; + auto configContent = toml::format(config_table); + + auto config_dir = QDir {this->i_configDirPath}; + if (!config_dir.exists()) config_dir.mkpath("."); + auto config_file = QFile {this->i_configPath}; + + auto out = QTextStream {&config_file}; + if (config_file.open(QIODevice::WriteOnly | QIODevice::Text)) { + out << configContent.c_str(); + config_file.close(); + } } diff --git a/desktop/config/config.hpp b/desktop/config/config.hpp index 667de7f..1acb3ce 100644 --- a/desktop/config/config.hpp +++ b/desktop/config/config.hpp @@ -1,18 +1,28 @@ #pragma once -#include +#include +#include + #include "library.hpp" #include "ui_prefs.hpp" class KotoConfig { - public: - KotoConfig(); - ~KotoConfig(); - std::vector getLibraries(); - KotoUiPreferences * getUiPreferences(); + public: + KotoConfig(); + static KotoConfig& instance(); + static KotoConfig* create() { return &instance(); } + void save(); - private: - std::vector i_libraries; - KotoUiPreferences * i_uiPreferences; + QString getConfigDirPath(); + QList getLibraries(); + KotoUiPreferences* getUiPreferences(); + private: + void bootstrap(); + void parseConfigFile(std::string filePath); + + QString i_configDirPath; + QString i_configPath; + QList i_libraries; + KotoUiPreferences* i_uiPreferences; }; diff --git a/desktop/config/library.cpp b/desktop/config/library.cpp index 0c3fa18..3022366 100644 --- a/desktop/config/library.cpp +++ b/desktop/config/library.cpp @@ -3,9 +3,10 @@ #include #include -KotoLibraryConfig::KotoLibraryConfig(std::string name, fs::path path) { +KotoLibraryConfig::KotoLibraryConfig(std::string name, fs::path path, KotoLibraryType type) { this->i_name = name; this->i_path = path; + this->i_type = type; qDebug() << "Library: " << this->i_name.c_str() << " at " << this->i_path.c_str(); } @@ -14,6 +15,7 @@ KotoLibraryConfig::~KotoLibraryConfig() {} KotoLibraryConfig::KotoLibraryConfig(const toml::value& v) { this->i_name = toml::find(v, "name"); this->i_path = toml::find(v, "path"); + this->i_type = libraryTypeFromString(toml::find(v, "type")); } std::string KotoLibraryConfig::getName() { @@ -23,3 +25,36 @@ std::string KotoLibraryConfig::getName() { fs::path KotoLibraryConfig::getPath() { return this->i_path; } + +KotoLibraryType KotoLibraryConfig::getType() { + return this->i_type; +} + +toml::ordered_value KotoLibraryConfig::serialize() { + toml::ordered_value library_table(toml::ordered_table {}); + library_table["name"] = this->i_name; + library_table["path"] = this->i_path.string(); + auto stringifiedType = libraryTypeToString(this->i_type); + library_table["type"] = stringifiedType; + return library_table; +} + +std::string libraryTypeToString(KotoLibraryType type) { + switch (type) { + case KotoLibraryType::Audiobooks: + return std::string {"audiobooks"}; + case KotoLibraryType::Music: + return std::string {"music"}; + case KotoLibraryType::Podcasts: + return std::string {"podcasts"}; + default: + return std::string {"unknown"}; + } +} + +KotoLibraryType libraryTypeFromString(const std::string& type) { + if (type == "audiobooks") return KotoLibraryType::Audiobooks; + if (type == "music") return KotoLibraryType::Music; + if (type == "podcasts") return KotoLibraryType::Podcasts; + throw std::invalid_argument("Unknown KotoLibraryType: " + type); +} diff --git a/desktop/config/library.hpp b/desktop/config/library.hpp index 1baf089..bfbb16c 100644 --- a/desktop/config/library.hpp +++ b/desktop/config/library.hpp @@ -6,15 +6,27 @@ namespace fs = std::filesystem; +enum class KotoLibraryType { + Audiobooks, + Music, + Podcasts, +}; + +KotoLibraryType libraryTypeFromString(const std::string& type); +std::string libraryTypeToString(KotoLibraryType type); + class KotoLibraryConfig { public: - KotoLibraryConfig(std::string name, fs::path path); + KotoLibraryConfig(std::string name, fs::path path, KotoLibraryType type); KotoLibraryConfig(const toml::value& v); ~KotoLibraryConfig(); - std::string getName(); - fs::path getPath(); + std::string getName(); + fs::path getPath(); + KotoLibraryType getType(); + toml::ordered_value serialize(); private: - std::string i_name; - fs::path i_path; + std::string i_name; + fs::path i_path; + KotoLibraryType i_type; }; diff --git a/desktop/config/ui_prefs.cpp b/desktop/config/ui_prefs.cpp index dfc8065..24bad2f 100644 --- a/desktop/config/ui_prefs.cpp +++ b/desktop/config/ui_prefs.cpp @@ -1,23 +1,24 @@ #include "ui_prefs.hpp" -KotoUiPreferences::KotoUiPreferences(std::optional v) { - this->i_albumInfoShowDescription = true; - this->i_albumInfoShowGenre = true; - this->i_albumInfoShowNarrator = true; - this->i_albumInfoShowYear = true; - this->i_lastUsedVolume = 0.5; +KotoUiPreferences::KotoUiPreferences() + : i_albumInfoShowDescription(true), i_albumInfoShowGenre(true), i_albumInfoShowNarrator(true), i_albumInfoShowYear(true), i_lastUsedVolume(0.5) {} +KotoUiPreferences::KotoUiPreferences(std::optional v) { // No UI prefs provided if (!v.has_value()) return; - toml::value& uiPrefs = v.value(); - this->i_albumInfoShowDescription = toml::find_or(uiPrefs, "album_info_show_description", false); - this->i_albumInfoShowGenre = toml::find_or(uiPrefs, "album_info_show_genre", false); - this->i_albumInfoShowNarrator = toml::find_or(uiPrefs, "album_info_show_narrator", false); - this->i_albumInfoShowYear = toml::find_or(uiPrefs, "album_info_show_year", false); - this->i_lastUsedVolume = toml::find_or(uiPrefs, "last_used_volume", 0.5); -} + toml::value& uiPrefs = v.value(); + auto showDescription = toml::find_or(uiPrefs, "album_info_show_description", false); + auto showGenre = toml::find_or(uiPrefs, "album_info_show_genre", false); + auto showNarrator = toml::find_or(uiPrefs, "album_info_show_narrator", false); + auto showYear = toml::find_or(uiPrefs, "album_info_show_year", false); + auto lastUsedVolume = toml::find_or(uiPrefs, "last_used_volume", 0.5); -KotoUiPreferences::~KotoUiPreferences() {} + this->setAlbumInfoShowDescription(showDescription); + this->setAlbumInfoShowGenre(showGenre); + this->setAlbumInfoShowNarrator(showNarrator); + this->setAlbumInfoShowYear(showYear); + this->setLastUsedVolume(lastUsedVolume); +} bool KotoUiPreferences::getAlbumInfoShowDescription() { return this->i_albumInfoShowDescription; @@ -38,3 +39,33 @@ bool KotoUiPreferences::getAlbumInfoShowYear() { float KotoUiPreferences::getLastUsedVolume() { return this->i_lastUsedVolume; } + +toml::ordered_value KotoUiPreferences::serialize() { + toml::ordered_value ui_prefs_table(toml::ordered_table {}); + ui_prefs_table["album_info_show_description"] = this->i_albumInfoShowDescription; + ui_prefs_table["album_info_show_genre"] = this->i_albumInfoShowGenre; + ui_prefs_table["album_info_show_narrator"] = this->i_albumInfoShowNarrator; + ui_prefs_table["album_info_show_year"] = this->i_albumInfoShowYear; + ui_prefs_table["last_used_volume"] = this->i_lastUsedVolume; + return ui_prefs_table; +} + +void KotoUiPreferences::setAlbumInfoShowDescription(bool show) { + this->i_albumInfoShowDescription = show; +} + +void KotoUiPreferences::setAlbumInfoShowGenre(bool show) { + this->i_albumInfoShowGenre = show; +} + +void KotoUiPreferences::setAlbumInfoShowNarrator(bool show) { + this->i_albumInfoShowNarrator = show; +} + +void KotoUiPreferences::setAlbumInfoShowYear(bool show) { + this->i_albumInfoShowYear = show; +} + +void KotoUiPreferences::setLastUsedVolume(float volume) { + this->i_lastUsedVolume = volume; +} diff --git a/desktop/config/ui_prefs.hpp b/desktop/config/ui_prefs.hpp index 58f6141..a5a5978 100644 --- a/desktop/config/ui_prefs.hpp +++ b/desktop/config/ui_prefs.hpp @@ -8,6 +8,7 @@ class KotoUiPreferences { public: + KotoUiPreferences(); KotoUiPreferences(std::optional v); ~KotoUiPreferences(); @@ -17,6 +18,8 @@ class KotoUiPreferences { bool getAlbumInfoShowYear(); float getLastUsedVolume(); + toml::ordered_value serialize(); + void setAlbumInfoShowDescription(bool show); void setAlbumInfoShowGenre(bool show); void setAlbumInfoShowNarrator(bool show); diff --git a/desktop/datalake/album.cpp b/desktop/datalake/album.cpp index 082ad2c..1550206 100644 --- a/desktop/datalake/album.cpp +++ b/desktop/datalake/album.cpp @@ -1,3 +1,6 @@ +#include + +#include "database.hpp" #include "structs.hpp" KotoAlbum::KotoAlbum() { @@ -5,8 +8,17 @@ KotoAlbum::KotoAlbum() { this->tracks = QList(); } -KotoAlbum* KotoAlbum::fromDb() { - return new KotoAlbum(); +KotoAlbum* KotoAlbum::fromDb(const QSqlQuery& query, const QSqlRecord& record) { + KotoAlbum* album = new KotoAlbum(); + album->uuid = QUuid {query.value(record.indexOf("id")).toString()}; + album->artist_uuid = QUuid {query.value(record.indexOf("artist_id")).toString()}; + album->title = QString {query.value(record.indexOf("name")).toString()}; + album->year = query.value(record.indexOf("year")).toInt(); + album->description = QString {query.value(record.indexOf("description")).toString()}; + album->narrator = QString {query.value(record.indexOf("narrator")).toString()}; + album->album_art_path = QString {query.value(record.indexOf("art_path")).toString()}; + album->genres = QList {query.value(record.indexOf("genres")).toString().split(", ")}; + return album; } KotoAlbum::~KotoAlbum() { @@ -18,6 +30,26 @@ void KotoAlbum::addTrack(KotoTrack* track) { this->tracks.append(track); } +void KotoAlbum::commit() { + QSqlQuery query(KotoDatabase::instance().getDatabase()); + + query.prepare( + "INSERT INTO albums(id, artist_id, name, description, narrator, art_path, genres, year) " + "VALUES (:id, :artist_id, :name, :description, :narrator, :art_path, :genres, :year) " + "ON CONFLICT(id) DO UPDATE SET artist_id = :artist_id, name = :name, description = :description, narrator = :narrator, art_path = " + ":art_path, genres = :genres, year = :year"); + + query.bindValue(":id", this->uuid.toString()); + query.bindValue(":artist_id", this->artist_uuid.toString()); + query.bindValue(":name", this->title); + query.bindValue(":year", this->year.value_or(NULL)); + query.bindValue(":description", this->description); + query.bindValue(":art_path", this->album_art_path); + query.bindValue(":narrator", this->narrator); + query.bindValue(":genres", this->genres.join(", ")); + query.exec(); +} + QString KotoAlbum::getAlbumArtPath() { return QString {this->album_art_path}; } @@ -46,7 +78,7 @@ QList KotoAlbum::getTracks() { return QList {this->tracks}; } -int KotoAlbum::getYear() { +std::optional KotoAlbum::getYear() { return this->year; } diff --git a/desktop/datalake/artist.cpp b/desktop/datalake/artist.cpp index f473b71..a72f6d8 100644 --- a/desktop/datalake/artist.cpp +++ b/desktop/datalake/artist.cpp @@ -1,11 +1,18 @@ +#include + +#include "database.hpp" #include "structs.hpp" KotoArtist::KotoArtist() { this->uuid = QUuid::createUuid(); } -KotoArtist* KotoArtist::fromDb() { - return new KotoArtist(); +KotoArtist* KotoArtist::fromDb(const QSqlQuery& query, const QSqlRecord& record) { + KotoArtist* artist = new KotoArtist(); + artist->uuid = QUuid {query.value(record.indexOf("id")).toString()}; + artist->name = QString {query.value(record.indexOf("name")).toString()}; + artist->path = QString {query.value(record.indexOf("art_path")).toString()}; + return artist; } KotoArtist::~KotoArtist() { @@ -21,6 +28,22 @@ void KotoArtist::addAlbum(KotoAlbum* album) { void KotoArtist::addTrack(KotoTrack* track) { this->tracks.append(track); + if (!track->album_uuid.has_value()) return; + for (auto album : this->albums) { + if (album->uuid == track->album_uuid.value()) { + album->addTrack(track); + return; + } + } +} + +void KotoArtist::commit() { + QSqlQuery query(KotoDatabase::instance().getDatabase()); + query.prepare("INSERT INTO artists(id, name, art_path) VALUES (:id, :name, :art_path) ON CONFLICT(id) DO UPDATE SET name = :name, art_path = :art_path"); + query.bindValue(":id", this->uuid.toString()); + query.bindValue(":name", this->name); + query.bindValue(":art_path", this->path); + query.exec(); } QList KotoArtist::getAlbums() { diff --git a/desktop/datalake/cartographer.cpp b/desktop/datalake/cartographer.cpp index 3c830a5..fe1eccb 100644 --- a/desktop/datalake/cartographer.cpp +++ b/desktop/datalake/cartographer.cpp @@ -29,11 +29,19 @@ std::optional Cartographer::getAlbum(QUuid uuid) { return album ? std::optional {album} : std::nullopt; } +QList Cartographer::getAlbums() { + return this->i_albums.values(); +} + std::optional Cartographer::getArtist(QUuid uuid) { auto artist = this->i_artists.value(uuid, nullptr); return artist ? std::optional {artist} : std::nullopt; } +QList Cartographer::getArtists() { + return this->i_artists.values(); +} + std::optional Cartographer::getArtist(QString name) { auto artist = this->i_artists_by_name.value(name, nullptr); return artist ? std::optional {artist} : std::nullopt; @@ -43,3 +51,7 @@ std::optional Cartographer::getTrack(QUuid uuid) { auto track = this->i_tracks.value(uuid, nullptr); return track ? std::optional {track} : std::nullopt; } + +QList Cartographer::getTracks() { + return this->i_tracks.values(); +} diff --git a/desktop/datalake/cartographer.hpp b/desktop/datalake/cartographer.hpp index ca7e2e5..27f5813 100644 --- a/desktop/datalake/cartographer.hpp +++ b/desktop/datalake/cartographer.hpp @@ -13,14 +13,16 @@ class Cartographer { static Cartographer& instance(); static Cartographer* create() { return &instance(); } - void addAlbum(KotoAlbum* album); - void addArtist(KotoArtist* artist); - void addTrack(KotoTrack* track); - std::optional getAlbum(QUuid uuid); - //.std::optional getAlbum(QString name); + void addAlbum(KotoAlbum* album); + void addArtist(KotoArtist* artist); + void addTrack(KotoTrack* track); + std::optional getAlbum(QUuid uuid); + QList getAlbums(); std::optional getArtist(QUuid uuid); + QList getArtists(); std::optional getArtist(QString name); std::optional getTrack(QUuid uuid); + QList getTracks(); private: QHash i_albums; diff --git a/desktop/datalake/database.cpp b/desktop/datalake/database.cpp new file mode 100644 index 0000000..f470ccd --- /dev/null +++ b/desktop/datalake/database.cpp @@ -0,0 +1,102 @@ +#include "database.hpp" + +#include +#include +#include +#include +#include + +#include "cartographer.hpp" +#include "config/config.hpp" + +KotoDatabase::KotoDatabase() { + QString dbPath = QDir(KotoConfig::instance().getConfigDirPath()).filePath("koto.db"); + this->shouldBootstrap = !QFileInfo::exists(dbPath); + + this->db = QSqlDatabase::addDatabase("QSQLITE"); + std::cout << "Database path: " << dbPath.toStdString() << std::endl; + + this->db.setDatabaseName(dbPath); +} + +KotoDatabase& KotoDatabase::instance() { + static KotoDatabase _instance; + return _instance; +} + +void KotoDatabase::connect() { + if (!this->db.open()) { + std::cerr << "Failed to open database" << std::endl; + QCoreApplication::quit(); + } + + if (this->shouldBootstrap) this->bootstrap(); +} + +void KotoDatabase::disconnect() { + this->db.close(); +} + +QSqlDatabase KotoDatabase::getDatabase() { + return this->db; +} + +bool KotoDatabase::requiredBootstrap() { + return this->shouldBootstrap; +} + +void KotoDatabase::bootstrap() { + QSqlQuery query(this->db); + + query.exec("CREATE TABLE IF NOT EXISTS artists(id string UNIQUE PRIMARY KEY, name string, art_path string);"); + query.exec( + "CREATE TABLE IF NOT EXISTS albums(id string UNIQUE PRIMARY KEY, artist_id string, name string, description string, narrator string, art_path string, " + "genres strings, year int, FOREIGN KEY(artist_id) REFERENCES artists(id) ON DELETE CASCADE);"); + query.exec( + "CREATE TABLE IF NOT EXISTS tracks(id string UNIQUE PRIMARY KEY, artist_id string, album_id string, name string, disc int, position int, duration int, " + "genres string, FOREIGN KEY(artist_id) REFERENCES artists(id) ON DELETE CASCADE, FOREIGN KEY (album_id) REFERENCES albums(id) ON DELETE CASCADE);"); + query.exec( + "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);"); + query.exec( + "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);"); + query.exec( + "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);"); + query.exec( + "CREATE TABLE IF NOT EXISTS playlist_meta(id string UNIQUE PRIMARY KEY, name string, art_path string, preferred_model int, album_id string, track_id " + "string, " + "playback_position_of_track int);"); + query.exec( + "CREATE TABLE IF NOT EXISTS playlist_tracks(position INTEGER PRIMARY KEY AUTOINCREMENT, playlist_id string, track_id string, FOREIGN KEY(playlist_id) " + "REFERENCES playlist_meta(id), FOREIGN KEY(track_id) REFERENCES tracks(id) ON DELETE CASCADE);"); +} + +void KotoDatabase::load() { + QSqlQuery query(this->db); + + query.exec("SELECT * FROM artists;"); + while (query.next()) { + KotoArtist* artist = KotoArtist::fromDb(query, query.record()); + Cartographer::instance().addArtist(artist); + } + + query.exec("SELECT * FROM albums;"); + while (query.next()) { + KotoAlbum* album = KotoAlbum::fromDb(query, query.record()); + auto artist = Cartographer::instance().getArtist(album->artist_uuid); + if (artist.has_value()) { artist.value()->addAlbum(album); } + Cartographer::instance().addAlbum(album); + } + + query.exec("SELECT * FROM tracks;"); + while (query.next()) { + KotoTrack* track = KotoTrack::fromDb(query, query.record()); + auto artist = Cartographer::instance().getArtist(track->artist_uuid); + if (artist.has_value()) { artist.value()->addTrack(track); } + Cartographer::instance().addTrack(track); + } +} diff --git a/desktop/datalake/database.hpp b/desktop/datalake/database.hpp new file mode 100644 index 0000000..b5df848 --- /dev/null +++ b/desktop/datalake/database.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include + +class KotoDatabase { + public: + KotoDatabase(); + static KotoDatabase& instance(); + static KotoDatabase* create() { return &instance(); } + + void connect(); + void disconnect(); + QSqlDatabase getDatabase(); + void load(); + bool requiredBootstrap(); + + private: + void bootstrap(); + bool shouldBootstrap; + QSqlDatabase db; +}; diff --git a/desktop/datalake/indexer.cpp b/desktop/datalake/indexer.cpp index cb05764..00cdfdb 100644 --- a/desktop/datalake/indexer.cpp +++ b/desktop/datalake/indexer.cpp @@ -34,6 +34,7 @@ void FileIndexer::index() { auto artist = new KotoArtist(); artist->setName(info.fileName()); artist->setPath(path); + artist->commit(); this->i_artists.append(artist); Cartographer::instance().addArtist(artist); continue; @@ -51,6 +52,7 @@ void FileIndexer::index() { artist->addAlbum(album); } + album->commit(); Cartographer::instance().addAlbum(album); continue; } @@ -67,22 +69,20 @@ void FileIndexer::index() { if (!result.types().contains(KFileMetaData::Type::Audio)) { continue; } - auto track = KotoTrack::fromMetadata(result); - track->setPath(path); + auto track = KotoTrack::fromMetadata(result, info); this->i_tracks.append(track); + track->commit(); Cartographer::instance().addTrack(track); } else if (mime.name().startsWith("image/")) { // This is an image, TODO add cover art to album } } +} - std::cout << "===== Summary =====" << std::endl; - for (auto artist : this->i_artists) { - std::cout << "Artist: " << artist->getName().toStdString() << std::endl; - for (auto album : artist->getAlbums()) { - std::cout << " Album: " << album->getTitle().toStdString() << std::endl; - for (auto track : album->getTracks()) { std::cout << " Track: " << track->getTitle().toStdString() << std::endl; } - } +void indexAllLibraries() { + for (auto library : KotoConfig::instance().getLibraries()) { + auto indexer = new FileIndexer(library); + indexer->index(); } } diff --git a/desktop/datalake/indexer.hpp b/desktop/datalake/indexer.hpp index ed2e48e..4ae06da 100644 --- a/desktop/datalake/indexer.hpp +++ b/desktop/datalake/indexer.hpp @@ -3,7 +3,7 @@ #include #include "cartographer.hpp" -#include "config/library.hpp" +#include "config/config.hpp" #include "structs.hpp" class FileIndexer { @@ -18,8 +18,9 @@ class FileIndexer { void index(); protected: - void indexDirectory(QString path, int depth); QList i_artists; QList i_tracks; QString i_root; }; + +void indexAllLibraries(); diff --git a/desktop/datalake/structs.hpp b/desktop/datalake/structs.hpp index 1a08f6d..5514d64 100644 --- a/desktop/datalake/structs.hpp +++ b/desktop/datalake/structs.hpp @@ -1,6 +1,9 @@ #pragma once #include +#include #include +#include +#include #include #include @@ -11,13 +14,14 @@ class KotoTrack; class KotoArtist { public: KotoArtist(); - static KotoArtist* fromDb(); + static KotoArtist* fromDb(const QSqlQuery& query, const QSqlRecord& record); ~KotoArtist(); QUuid uuid; void addAlbum(KotoAlbum* album); void addTrack(KotoTrack* track); + void commit(); QList getAlbums(); std::optional getAlbumByName(QString name); QString getName(); @@ -39,20 +43,21 @@ class KotoArtist { class KotoAlbum { public: KotoAlbum(); - static KotoAlbum* fromDb(); + static KotoAlbum* fromDb(const QSqlQuery& query, const QSqlRecord& record); ~KotoAlbum(); QUuid uuid; QUuid artist_uuid; - QString getAlbumArtPath(); - QString getDescription(); - QList getGenres(); - QString getNarrator(); - QString getPath(); - QString getTitle(); - QList getTracks(); - int getYear(); + void commit(); + QString getAlbumArtPath(); + QString getDescription(); + QList getGenres(); + QString getNarrator(); + QString getPath(); + QString getTitle(); + QList getTracks(); + std::optional getYear(); void addTrack(KotoTrack* track); void removeTrack(KotoTrack* track); @@ -65,10 +70,10 @@ class KotoAlbum { void setYear(int num); private: - QString title; - QString description; - QString narrator; - int year; + QString title; + QString description; + QString narrator; + std::optional year; QList genres; QList tracks; @@ -80,14 +85,15 @@ class KotoAlbum { class KotoTrack { public: KotoTrack(); // No-op constructor - static KotoTrack* fromDb(); - static KotoTrack* fromMetadata(const KFileMetaData::SimpleExtractionResult& metadata); + static KotoTrack* fromDb(const QSqlQuery& query, const QSqlRecord& record); + static KotoTrack* fromMetadata(const KFileMetaData::SimpleExtractionResult& metadata, const QFileInfo& info); ~KotoTrack(); std::optional album_uuid; QUuid artist_uuid; QUuid uuid; + void commit(); int getDuration(); QStringList getGenres(); QString getLyrics(); diff --git a/desktop/datalake/track.cpp b/desktop/datalake/track.cpp index 2b70fa4..3aedcdf 100644 --- a/desktop/datalake/track.cpp +++ b/desktop/datalake/track.cpp @@ -1,19 +1,32 @@ +#include #include #include "cartographer.hpp" +#include "database.hpp" #include "structs.hpp" KotoTrack::KotoTrack() { this->uuid = QUuid::createUuid(); } -KotoTrack* KotoTrack::fromDb() { - return new KotoTrack(); +KotoTrack* KotoTrack::fromDb(const QSqlQuery& query, const QSqlRecord& record) { + KotoTrack* track = new KotoTrack(); + track->uuid = QUuid {query.value(record.indexOf("id")).toString()}; + auto artist_id = query.value(record.indexOf("artist_id")); + if (!artist_id.isNull()) { track->artist_uuid = QUuid {artist_id.toString()}; } + + auto album_id = query.value(record.indexOf("album_id")); + if (!album_id.isNull()) { track->album_uuid = QUuid {album_id.toString()}; } + + track->title = QString {query.value(record.indexOf("name")).toString()}; + track->disc_number = query.value(record.indexOf("disc")).toInt(); + track->track_number = query.value(record.indexOf("position")).toInt(); + track->duration = query.value(record.indexOf("duration")).toInt(); + track->genres = QList {query.value(record.indexOf("genres")).toString().split(", ")}; + return track; } -KotoTrack::~KotoTrack() {} - -KotoTrack* KotoTrack::fromMetadata(const KFileMetaData::SimpleExtractionResult& metadata) { +KotoTrack* KotoTrack::fromMetadata(const KFileMetaData::SimpleExtractionResult& metadata, const QFileInfo& info) { auto props = metadata.properties(); KotoTrack* track = new KotoTrack(); track->disc_number = props.value(KFileMetaData::Property::DiscNumber, 0).toInt(); @@ -26,10 +39,18 @@ KotoTrack* KotoTrack::fromMetadata(const KFileMetaData::SimpleExtractionResult& track->lyrics = props.value(KFileMetaData::Property::Lyrics).toString(); track->narrator = props.value(KFileMetaData::Property::Performer).toString(); - track->title = props.value(KFileMetaData::Property::Title).toString(); + track->path = info.absolutePath(); track->track_number = props.value(KFileMetaData::Property::TrackNumber, 0).toInt(); track->year = props.value(KFileMetaData::Property::ReleaseYear, 0).toInt(); + auto titleResult = props.value(KFileMetaData::Property::Title); + if (titleResult.isValid() && !titleResult.isNull()) { + track->title = titleResult.toString(); + } else { + // TODO: mirror the same logic we had for cleaning up file name to determine track name, position, chapter, artist, etc. + track->title = info.fileName(); + } + auto artistResult = props.value(KFileMetaData::Property::Artist); auto artistOptional = std::optional(); if (artistResult.isValid()) { @@ -60,6 +81,27 @@ KotoTrack* KotoTrack::fromMetadata(const KFileMetaData::SimpleExtractionResult& return track; } +KotoTrack::~KotoTrack() {} + +void KotoTrack::commit() { + QSqlQuery query(KotoDatabase::instance().getDatabase()); + query.prepare( + "INSERT INTO tracks(id, artist_id, album_id, name, disc, position, duration, genres) " + "VALUES (:id, :artist_id, :album_id, :name, :disc, :position, :duration, :genres) " + "ON CONFLICT(id) DO UPDATE SET artist_id = :artist_id, album_id = :album_id, name = :name, disc = :disc, position = :position, duration = :duration, " + "genres = :genres"); + query.bindValue(":id", this->uuid.toString()); + query.bindValue(":artist_id", !this->artist_uuid.isNull() ? this->artist_uuid.toString() : NULL); + query.bindValue(":album_id", this->album_uuid.has_value() ? this->album_uuid.value().toString() : NULL); + query.bindValue(":name", this->title); + query.bindValue(":disc", this->disc_number); + query.bindValue(":position", this->track_number); + query.bindValue(":duration", this->duration); + query.bindValue(":genres", this->genres.join(", ")); + + query.exec(); +} + int KotoTrack::getDuration() { return this->duration; } diff --git a/desktop/main.cpp b/desktop/main.cpp index 9e5bcd0..f4d6b01 100644 --- a/desktop/main.cpp +++ b/desktop/main.cpp @@ -1,9 +1,11 @@ #include #include #include +#include #include -#include "config/library.hpp" +#include "config/config.hpp" +#include "datalake/database.hpp" #include "datalake/indexer.hpp" int main(int argc, char* argv[]) { @@ -18,15 +20,33 @@ int main(int argc, char* argv[]) { if (engine.rootObjects().isEmpty()) { return -1; } - // std::thread([]() { - // auto config = KotoLibraryConfig("Music", "/home/joshua/Music"); - // - // auto indexExample = FileIndexer(&config); - // indexExample.index(); - // }).detach(); - Cartographer::create(); - auto config = KotoLibraryConfig("Music", "/home/joshua/Music"); - auto indexExample = FileIndexer(&config); - indexExample.index(); + std::thread([]() { + Cartographer::create(); + KotoConfig::create(); + KotoDatabase::create(); + + KotoDatabase::instance().connect(); + + // If we needed to bootstrap, index all libraries, otherwise load the database + if (KotoDatabase::instance().requiredBootstrap()) { + indexAllLibraries(); + } else { + KotoDatabase::instance().load(); + std::cout << "===== Summary =====" << std::endl; + for (auto artist : Cartographer::instance().getArtists()) { + std::cout << "Artist: " << artist->getName().toStdString() << std::endl; + for (auto album : artist->getAlbums()) { + std::cout << " Album: " << album->getTitle().toStdString() << std::endl; + for (auto track : album->getTracks()) { std::cout << " Track: " << track->getTitle().toStdString() << std::endl; } + } + } + std::cout << "===== Tracks without albums and/or artists =====" << std::endl; + for (auto track : Cartographer::instance().getTracks()) { + if (track->album_uuid.has_value()) continue; + std::cout << "Track: " << track->getTitle().toStdString() << std::endl; + } + } + }).detach(); + return app.exec(); }