Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1222,6 +1222,7 @@ add_library(
src/library/dao/analysisdao.cpp
src/library/dao/autodjcratesdao.cpp
src/library/dao/cuedao.cpp
src/library/dao/genredao.cpp
src/library/dao/directorydao.cpp
src/library/dao/libraryhashdao.cpp
src/library/dao/playlistdao.cpp
Expand Down Expand Up @@ -1586,6 +1587,7 @@ add_library(
src/widget/weffectpushbutton.cpp
src/widget/weffectselector.cpp
src/widget/wfindonwebmenu.cpp
src/widget/wgenretaginput.cpp
src/widget/whotcuebutton.cpp
src/widget/wimagestore.cpp
src/widget/wkey.cpp
Expand Down
39 changes: 39 additions & 0 deletions res/schema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -584,4 +584,43 @@ reapplying those migrations.
UPDATE library SET filetype='aiff' WHERE filetype='aif';
</sql>
</revision>
<revision version="40" min_compatible="3">
<description>
Multi-Genre Support (Flat Model):
- Adds a 'genres' table for a centralized, flat list of genre names.
- Adds a 'genre_tracks' junction table for many-to-many track-genre associations.
- Migrates existing single genres from 'library.genre' to the new tables.
</description>
<sql>
CREATE TABLE IF NOT EXISTS genres (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL COLLATE NOCASE
);

CREATE UNIQUE INDEX IF NOT EXISTS idx_genres_name_unique ON genres(name);

CREATE TABLE IF NOT EXISTS genre_tracks (
track_id INTEGER NOT NULL,
genre_id INTEGER NOT NULL,
PRIMARY KEY (track_id, genre_id),
FOREIGN KEY (track_id) REFERENCES library(id) ON DELETE CASCADE,
FOREIGN KEY (genre_id) REFERENCES genres(id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_genre_tracks_track_id ON genre_tracks(track_id);
CREATE INDEX IF NOT EXISTS idx_genre_tracks_genre_id ON genre_tracks(genre_id);

INSERT INTO genres (name)
SELECT DISTINCT TRIM(genre)
FROM library
WHERE genre IS NOT NULL AND TRIM(genre) != '';

INSERT INTO genre_tracks (track_id, genre_id)
SELECT T.id, G.id
FROM library T
JOIN genres G ON TRIM(T.genre) = G.name COLLATE NOCASE
WHERE T.genre IS NOT NULL AND TRIM(T.genre) != '';
</sql>
</revision>

</schema>
2 changes: 1 addition & 1 deletion src/database/mixxxdb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
const QString MixxxDb::kDefaultSchemaFile(":/schema.xml");

//static
const int MixxxDb::kRequiredSchemaVersion = 39;
const int MixxxDb::kRequiredSchemaVersion = 40;

namespace {

Expand Down
144 changes: 144 additions & 0 deletions src/library/dao/genredao.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#include "library/dao/genredao.h"

#include <QDebug>
#include <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>
#include <QSqlRecord>
#include <QVariant>
#include <QtDebug>

#include "library/queryutil.h"
#include "moc_genredao.cpp"
#include "util/db/dbconnection.h"

GenreDao::GenreDao(QObject* parent)
: QObject(parent) {
}

void GenreDao::initialize(const QSqlDatabase& database) {
DAO::initialize(database);
loadGenres2QVL(m_genreData);
}

void GenreDao::loadGenres2QVL(QVariantList& genreData) {
genreData.clear();
QSqlQuery query(m_database);
query.prepare("SELECT id, name FROM genres ORDER BY name ASC");

if (query.exec()) {
while (query.next()) {
QVariantMap entry;
entry["id"] = query.value("id").toInt();
entry["name"] = query.value("name").toString();
genreData.append(entry);
}
qDebug() << "GenreDao::loadGenres2QVL loaded" << genreData.size() << "genres";
} else {
qWarning() << "GenreDao::loadGenres2QVL failed:" << query.lastError();
}
}

QString GenreDao::getDisplayGenreNameForGenreID(const QString& rawGenre) const {
return rawGenre;
}

QMap<QString, QString> GenreDao::getAllGenres() {
QMap<QString, QString> genreMap;
QSqlQuery query(m_database);
query.prepare("SELECT id, name FROM genres");

if (query.exec()) {
while (query.next()) {
int id = query.value("id").toInt();
QString name = query.value("name").toString();
genreMap.insert(QString::number(id), name);
}
} else {
qWarning() << "GenreDao::getAllGenres failed:" << query.lastError();
}

return genreMap;
}

QStringList GenreDao::getAllGenreNames() const {
QStringList names;
for (const QVariant& entry : m_genreData) {
QVariantMap map = entry.toMap();
names << map["name"].toString();
}
return names;
}

QStringList GenreDao::getGenresForTrack(TrackId trackId) const {
QStringList genres;
QSqlQuery query(m_database);
query.prepare(
"SELECT g.name FROM genres g "
"JOIN genre_tracks gt ON g.id = gt.genre_id "
"WHERE gt.track_id = ? ORDER BY g.name");
query.bindValue(0, trackId.toVariant());

if (query.exec()) {
while (query.next()) {
genres << query.value(0).toString();
}
}

return genres;
}

bool GenreDao::setGenresForTrack(TrackId trackId, const QStringList& genreNames) {
if (!m_database.transaction()) {
return false;
}

QSqlQuery deleteQuery(m_database);
deleteQuery.prepare("DELETE FROM genre_tracks WHERE track_id = ?");
deleteQuery.bindValue(0, trackId.toVariant());

if (!deleteQuery.exec()) {
m_database.rollback();
return false;
}

for (const QString& genreName : genreNames) {
if (genreName.trimmed().isEmpty())
continue;

int genreId = createGenre(genreName.trimmed());
if (genreId > 0) {
QSqlQuery insertQuery(m_database);
insertQuery.prepare("INSERT INTO genre_tracks (track_id, genre_id) VALUES (?, ?)");
insertQuery.bindValue(0, trackId.toVariant());
insertQuery.bindValue(1, genreId);

if (!insertQuery.exec()) {
m_database.rollback();
return false;
}
}
}

return m_database.commit();
}

int GenreDao::createGenre(const QString& genreName) {
QSqlQuery checkQuery(m_database);
checkQuery.prepare("SELECT id FROM genres WHERE name = ? COLLATE NOCASE");
checkQuery.bindValue(0, genreName);

if (checkQuery.exec() && checkQuery.next()) {
return checkQuery.value(0).toInt();
}

QSqlQuery insertQuery(m_database);
insertQuery.prepare("INSERT INTO genres (name) VALUES (?)");
insertQuery.bindValue(0, genreName);

if (insertQuery.exec()) {
return insertQuery.lastInsertId().toInt();
}

return -1;
}
35 changes: 35 additions & 0 deletions src/library/dao/genredao.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#pragma once

#include <QMap>
#include <QRegularExpression>
#include <QString>
#include <QVariantList>

#include "library/dao/dao.h"
#include "preferences/usersettings.h"
#include "track/trackid.h"
#include "util/class.h"

class QSqlDatabase;

class GenreDao : public QObject, public virtual DAO {
Q_OBJECT
public:
explicit GenreDao(QObject* parent = nullptr);
~GenreDao() override = default;

void initialize(const QSqlDatabase& database) override;
void loadGenres2QVL(QVariantList& genreData);
QString getDisplayGenreNameForGenreID(const QString& rawGenre) const;
QMap<QString, QString> getAllGenres();

QStringList getAllGenreNames() const;
QStringList getGenresForTrack(TrackId trackId) const;
bool setGenresForTrack(TrackId trackId, const QStringList& genreNames);
int createGenre(const QString& genreName);

private:
QVariantList m_genreData;

DISALLOW_COPY_AND_ASSIGN(GenreDao);
};
11 changes: 7 additions & 4 deletions src/library/dao/trackdao.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "library/coverartutils.h"
#include "library/dao/analysisdao.h"
#include "library/dao/cuedao.h"
#include "library/dao/genredao.h"
#include "library/dao/libraryhashdao.h"
#include "library/dao/playlistdao.h"
#include "library/dao/trackschema.h"
Expand Down Expand Up @@ -89,13 +90,15 @@ QSet<QString> collectTrackLocations(FwdSqlQuery& query) {
} // anonymous namespace

TrackDAO::TrackDAO(CueDAO& cueDao,
PlaylistDAO& playlistDao,
AnalysisDao& analysisDao,
LibraryHashDAO& libraryHashDao,
UserSettingsPointer pConfig)
PlaylistDAO& playlistDao,
AnalysisDao& analysisDao,
GenreDao& genreDao,
LibraryHashDAO& libraryHashDao,
UserSettingsPointer pConfig)
: m_cueDao(cueDao),
m_playlistDao(playlistDao),
m_analysisDao(analysisDao),
m_genreDao(genreDao),
m_libraryHashDao(libraryHashDao),
m_pConfig(pConfig),
m_trackLocationIdColumn(UndefinedRecordIndex),
Expand Down
3 changes: 3 additions & 0 deletions src/library/dao/trackdao.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class PlaylistDAO;
class AnalysisDao;
class CueDAO;
class LibraryHashDAO;
class GenreDao;

namespace mixxx {
class FileInfo;
Expand All @@ -41,6 +42,7 @@ class TrackDAO : public QObject, public virtual DAO, public virtual GlobalTrackC
CueDAO& cueDao,
PlaylistDAO& playlistDao,
AnalysisDao& analysisDao,
GenreDao& genreDao,
LibraryHashDAO& libraryHashDao,
UserSettingsPointer pConfig);
~TrackDAO() override;
Expand Down Expand Up @@ -202,6 +204,7 @@ class TrackDAO : public QObject, public virtual DAO, public virtual GlobalTrackC
CueDAO& m_cueDao;
PlaylistDAO& m_playlistDao;
AnalysisDao& m_analysisDao;
GenreDao& m_genreDao;
LibraryHashDAO& m_libraryHashDao;

const UserSettingsPointer m_pConfig;
Expand Down
Loading