feat: initial database creation, loading

fixes for cartographer and automatically add track to album when adding to artist
This commit is contained in:
Joshua Strobl 2024-10-05 00:03:50 +03:00
parent 72bbcaba9e
commit 62c99ee67c
18 changed files with 515 additions and 108 deletions

View file

@ -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;
}

View file

@ -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() {

View file

@ -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();
}

View file

@ -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<KotoAlbum*> getAlbum(QUuid uuid);
//.std::optional<KotoAlbum*> getAlbum(QString name);
void addAlbum(KotoAlbum* album);
void addArtist(KotoArtist* artist);
void addTrack(KotoTrack* track);
std::optional<KotoAlbum*> getAlbum(QUuid uuid);
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;

View 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);
}
}

View 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;
};

View file

@ -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();
}
}

View file

@ -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();

View file

@ -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,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<QString> getGenres();
QString getNarrator();
QString getPath();
QString getTitle();
QList<KotoTrack*> getTracks();
int getYear();
void commit();
QString getAlbumArtPath();
QString getDescription();
QList<QString> getGenres();
QString getNarrator();
QString getPath();
QString getTitle();
QList<KotoTrack*> getTracks();
std::optional<int> 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<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();

View file

@ -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;
}