feat: initial database creation, loading
fixes for cartographer and automatically add track to album when adding to artist
This commit is contained in:
parent
72bbcaba9e
commit
62c99ee67c
18 changed files with 515 additions and 108 deletions
|
@ -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)
|
||||
|
|
|
@ -2,18 +2,60 @@
|
|||
|
||||
#include <QDir>
|
||||
#include <QStandardPaths>
|
||||
#include <QTextStream>
|
||||
#include <filesystem>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
KotoConfig::KotoConfig() {
|
||||
// Define our application's config location
|
||||
auto configDir = QDir(QStandardPaths::writableLocation(QStandardPaths::StandardLocation::AppConfigLocation));
|
||||
auto configDirPath = configDir.absolutePath();
|
||||
this->i_configDirPath = configDirPath;
|
||||
this->i_libraries = QList<KotoLibraryConfig*>();
|
||||
|
||||
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<KotoLibraryConfig*> KotoConfig::getLibraries() {
|
||||
return this->i_libraries;
|
||||
}
|
||||
|
||||
void KotoConfig::parseConfigFile(std::string filePath) {
|
||||
auto data = toml::parse(filePath);
|
||||
std::optional<toml::value> 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<std::vector<toml::value>>(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<KotoLibraryConfig> 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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,28 @@
|
|||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
|
||||
#include "library.hpp"
|
||||
#include "ui_prefs.hpp"
|
||||
|
||||
class KotoConfig {
|
||||
public:
|
||||
KotoConfig();
|
||||
~KotoConfig();
|
||||
std::vector<KotoLibraryConfig> getLibraries();
|
||||
static KotoConfig& instance();
|
||||
static KotoConfig* create() { return &instance(); }
|
||||
void save();
|
||||
|
||||
QString getConfigDirPath();
|
||||
QList<KotoLibraryConfig*> getLibraries();
|
||||
KotoUiPreferences* getUiPreferences();
|
||||
|
||||
private:
|
||||
std::vector<KotoLibraryConfig> i_libraries;
|
||||
KotoUiPreferences * i_uiPreferences;
|
||||
void bootstrap();
|
||||
void parseConfigFile(std::string filePath);
|
||||
|
||||
QString i_configDirPath;
|
||||
QString i_configPath;
|
||||
QList<KotoLibraryConfig*> i_libraries;
|
||||
KotoUiPreferences* i_uiPreferences;
|
||||
};
|
||||
|
|
|
@ -3,9 +3,10 @@
|
|||
#include <QDebug>
|
||||
#include <string>
|
||||
|
||||
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<std::string>(v, "name");
|
||||
this->i_path = toml::find<std::string>(v, "path");
|
||||
this->i_type = libraryTypeFromString(toml::find<std::string>(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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
KotoLibraryType getType();
|
||||
toml::ordered_value serialize();
|
||||
|
||||
private:
|
||||
std::string i_name;
|
||||
fs::path i_path;
|
||||
KotoLibraryType i_type;
|
||||
};
|
||||
|
|
|
@ -1,23 +1,24 @@
|
|||
#include "ui_prefs.hpp"
|
||||
|
||||
KotoUiPreferences::KotoUiPreferences(std::optional<toml::value> 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<toml::value> v) {
|
||||
// No UI prefs provided
|
||||
if (!v.has_value()) return;
|
||||
toml::value& uiPrefs = v.value();
|
||||
this->i_albumInfoShowDescription = toml::find_or<bool>(uiPrefs, "album_info_show_description", false);
|
||||
this->i_albumInfoShowGenre = toml::find_or<bool>(uiPrefs, "album_info_show_genre", false);
|
||||
this->i_albumInfoShowNarrator = toml::find_or<bool>(uiPrefs, "album_info_show_narrator", false);
|
||||
this->i_albumInfoShowYear = toml::find_or<bool>(uiPrefs, "album_info_show_year", false);
|
||||
this->i_lastUsedVolume = toml::find_or<float>(uiPrefs, "last_used_volume", 0.5);
|
||||
}
|
||||
auto showDescription = toml::find_or<bool>(uiPrefs, "album_info_show_description", false);
|
||||
auto showGenre = toml::find_or<bool>(uiPrefs, "album_info_show_genre", false);
|
||||
auto showNarrator = toml::find_or<bool>(uiPrefs, "album_info_show_narrator", false);
|
||||
auto showYear = toml::find_or<bool>(uiPrefs, "album_info_show_year", false);
|
||||
auto lastUsedVolume = toml::find_or<float>(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;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
class KotoUiPreferences {
|
||||
public:
|
||||
KotoUiPreferences();
|
||||
KotoUiPreferences(std::optional<toml::value> 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);
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
#include <iostream>
|
||||
|
||||
#include "database.hpp"
|
||||
#include "structs.hpp"
|
||||
|
||||
KotoAlbum::KotoAlbum() {
|
||||
|
@ -5,8 +8,17 @@ KotoAlbum::KotoAlbum() {
|
|||
this->tracks = QList<KotoTrack*>();
|
||||
}
|
||||
|
||||
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<KotoTrack*> KotoAlbum::getTracks() {
|
|||
return QList {this->tracks};
|
||||
}
|
||||
|
||||
int KotoAlbum::getYear() {
|
||||
std::optional<int> KotoAlbum::getYear() {
|
||||
return this->year;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
#include <QSqlQuery>
|
||||
|
||||
#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<KotoAlbum*> KotoArtist::getAlbums() {
|
||||
|
|
|
@ -29,11 +29,19 @@ std::optional<KotoAlbum*> Cartographer::getAlbum(QUuid uuid) {
|
|||
return album ? std::optional {album} : std::nullopt;
|
||||
}
|
||||
|
||||
QList<KotoAlbum*> Cartographer::getAlbums() {
|
||||
return this->i_albums.values();
|
||||
}
|
||||
|
||||
std::optional<KotoArtist*> Cartographer::getArtist(QUuid uuid) {
|
||||
auto artist = this->i_artists.value(uuid, nullptr);
|
||||
return artist ? std::optional {artist} : std::nullopt;
|
||||
}
|
||||
|
||||
QList<KotoArtist*> Cartographer::getArtists() {
|
||||
return this->i_artists.values();
|
||||
}
|
||||
|
||||
std::optional<KotoArtist*> 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<KotoTrack*> Cartographer::getTrack(QUuid uuid) {
|
|||
auto track = this->i_tracks.value(uuid, nullptr);
|
||||
return track ? std::optional {track} : std::nullopt;
|
||||
}
|
||||
|
||||
QList<KotoTrack*> Cartographer::getTracks() {
|
||||
return this->i_tracks.values();
|
||||
}
|
||||
|
|
|
@ -17,10 +17,12 @@ class Cartographer {
|
|||
void addArtist(KotoArtist* artist);
|
||||
void addTrack(KotoTrack* track);
|
||||
std::optional<KotoAlbum*> getAlbum(QUuid uuid);
|
||||
//.std::optional<KotoAlbum*> getAlbum(QString name);
|
||||
QList<KotoAlbum*> getAlbums();
|
||||
std::optional<KotoArtist*> getArtist(QUuid uuid);
|
||||
QList<KotoArtist*> getArtists();
|
||||
std::optional<KotoArtist*> getArtist(QString name);
|
||||
std::optional<KotoTrack*> getTrack(QUuid uuid);
|
||||
QList<KotoTrack*> getTracks();
|
||||
|
||||
private:
|
||||
QHash<QUuid, KotoAlbum*> i_albums;
|
||||
|
|
102
desktop/datalake/database.cpp
Normal file
102
desktop/datalake/database.cpp
Normal file
|
@ -0,0 +1,102 @@
|
|||
#include "database.hpp"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QSqlQuery>
|
||||
#include <iostream>
|
||||
|
||||
#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);
|
||||
}
|
||||
}
|
21
desktop/datalake/database.hpp
Normal file
21
desktop/datalake/database.hpp
Normal file
|
@ -0,0 +1,21 @@
|
|||
#pragma once
|
||||
|
||||
#include <QSqlDatabase>
|
||||
|
||||
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;
|
||||
};
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
#include <string>
|
||||
|
||||
#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<KotoArtist*> i_artists;
|
||||
QList<KotoTrack*> i_tracks;
|
||||
QString i_root;
|
||||
};
|
||||
|
||||
void indexAllLibraries();
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
#pragma once
|
||||
#include <KFileMetaData/SimpleExtractionResult>
|
||||
#include <QFileInfo>
|
||||
#include <QList>
|
||||
#include <QSqlQuery>
|
||||
#include <QSqlRecord>
|
||||
#include <QString>
|
||||
#include <QUuid>
|
||||
|
||||
|
@ -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<KotoAlbum*> getAlbums();
|
||||
std::optional<KotoAlbum*> getAlbumByName(QString name);
|
||||
QString getName();
|
||||
|
@ -39,12 +43,13 @@ class KotoArtist {
|
|||
class KotoAlbum {
|
||||
public:
|
||||
KotoAlbum();
|
||||
static KotoAlbum* fromDb();
|
||||
static KotoAlbum* fromDb(const QSqlQuery& query, const QSqlRecord& record);
|
||||
~KotoAlbum();
|
||||
|
||||
QUuid uuid;
|
||||
QUuid artist_uuid;
|
||||
|
||||
void commit();
|
||||
QString getAlbumArtPath();
|
||||
QString getDescription();
|
||||
QList<QString> getGenres();
|
||||
|
@ -52,7 +57,7 @@ class KotoAlbum {
|
|||
QString getPath();
|
||||
QString getTitle();
|
||||
QList<KotoTrack*> getTracks();
|
||||
int getYear();
|
||||
std::optional<int> getYear();
|
||||
|
||||
void addTrack(KotoTrack* track);
|
||||
void removeTrack(KotoTrack* track);
|
||||
|
@ -68,7 +73,7 @@ class KotoAlbum {
|
|||
QString title;
|
||||
QString description;
|
||||
QString narrator;
|
||||
int year;
|
||||
std::optional<int> year;
|
||||
|
||||
QList<QString> genres;
|
||||
QList<KotoTrack*> 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<QUuid> album_uuid;
|
||||
QUuid artist_uuid;
|
||||
QUuid uuid;
|
||||
|
||||
void commit();
|
||||
int getDuration();
|
||||
QStringList getGenres();
|
||||
QString getLyrics();
|
||||
|
|
|
@ -1,19 +1,32 @@
|
|||
#include <QFileInfo>
|
||||
#include <iostream>
|
||||
|
||||
#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<KotoArtist*>();
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
#include <QGuiApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQuickStyle>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
#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();
|
||||
std::thread([]() {
|
||||
Cartographer::create();
|
||||
auto config = KotoLibraryConfig("Music", "/home/joshua/Music");
|
||||
auto indexExample = FileIndexer(&config);
|
||||
indexExample.index();
|
||||
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();
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue