Extend tag support: performer, grouping

The transaction handling while upgrading the database schema had to be revised.
Furthermore some QSqlQuery statements needed to be finished properly.

Fixes issue 2556
This commit is contained in:
Uwe Klotz 2013-03-03 13:00:24 +01:00 committed by David Sansome
parent d083f38f54
commit a6d3b48231
31 changed files with 300 additions and 54 deletions

View File

@ -343,6 +343,7 @@
<file>schema/schema-42.sql</file>
<file>schema/schema-43.sql</file>
<file>schema/schema-44.sql</file>
<file>schema/schema-45.sql</file>
<file>schema/schema-4.sql</file>
<file>schema/schema-5.sql</file>
<file>schema/schema-6.sql</file>

View File

@ -42,11 +42,14 @@ CREATE TABLE jamendo.songs (
unavailable INTEGER DEFAULT 0,
effective_albumartist TEXT,
etag TEXT
etag TEXT,
performer TEXT,
grouping TEXT
);
CREATE VIRTUAL TABLE jamendo.songs_fts USING fts3(
ftstitle, ftsalbum, ftsartist, ftsalbumartist, ftscomposer, ftsgenre, ftscomment,
ftstitle, ftsalbum, ftsartist, ftsalbumartist, ftscomposer, ftsperformer, ftsgrouping, ftsgenre, ftscomment,
tokenize=unicode
);

24
data/schema/schema-45.sql Normal file
View File

@ -0,0 +1,24 @@
CREATE VIRTUAL TABLE playlist_items_fts USING fts3(
ftstitle, ftsalbum, ftsartist, ftsalbumartist, ftscomposer, ftsgenre, ftscomment,
tokenize=unicode
);
DELETE FROM %allsongstables_fts;
DROP TABLE %allsongstables_fts;
ALTER TABLE %allsongstables ADD COLUMN performer TEXT;
ALTER TABLE %allsongstables ADD COLUMN grouping TEXT;
CREATE VIRTUAL TABLE %allsongstables_fts USING fts3(
ftstitle, ftsalbum, ftsartist, ftsalbumartist, ftscomposer, ftsperformer, ftsgrouping, ftsgenre, ftscomment,
tokenize=unicode
);
INSERT INTO %allsongstables_fts (ROWID, ftstitle, ftsalbum, ftsartist, ftsalbumartist, ftscomposer, ftsperformer, ftsgrouping, ftsgenre, ftscomment)
SELECT ROWID, title, album, artist, albumartist, composer, performer, grouping, genre, comment
FROM %allsongstables;
UPDATE schema_version SET version=45;

View File

@ -161,6 +161,12 @@ void TagReader::ReadFile(const QString& filename,
if (!map["TCOM"].isEmpty())
Decode(map["TCOM"].front()->toString(), NULL, song->mutable_composer());
if (!map["TIT1"].isEmpty()) // content group
Decode(map["TIT1"].front()->toString(), NULL, song->mutable_grouping());
if (!map["TPE1"].isEmpty()) // ID3v2: lead performer/soloist
Decode(map["TPE1"].front()->toString(), NULL, song->mutable_performer());
if (!map["TPE2"].isEmpty()) // non-standard: Apple, Microsoft
Decode(map["TPE2"].front()->toString(), NULL, song->mutable_albumartist());
@ -260,6 +266,9 @@ void TagReader::ReadFile(const QString& filename,
if(items.contains("\251wrt")) {
Decode(items["\251wrt"].toStringList().toString(", "), NULL, song->mutable_composer());
}
if(items.contains("\251grp")) {
Decode(items["\251grp"].toStringList().toString(" "), NULL, song->mutable_grouping());
}
Decode(mp4_tag->comment(), NULL, song->mutable_comment());
}
}
@ -398,6 +407,10 @@ void TagReader::ParseOggTag(const TagLib::Ogg::FieldListMap& map,
pb::tagreader::SongMetadata* song) const {
if (!map["COMPOSER"].isEmpty())
Decode(map["COMPOSER"].front(), codec, song->mutable_composer());
if (!map["PERFORMER"].isEmpty())
Decode(map["PERFORMER"].front(), codec, song->mutable_performer());
if (!map["CONTENT GROUP"].isEmpty())
Decode(map["CONTENT GROUP"].front(), codec, song->mutable_grouping());
if (!map["ALBUMARTIST"].isEmpty()) {
Decode(map["ALBUMARTIST"].front(), codec, song->mutable_albumartist());
@ -428,6 +441,8 @@ void TagReader::SetVorbisComments(TagLib::Ogg::XiphComment* vorbis_comments,
const pb::tagreader::SongMetadata& song) const {
vorbis_comments->addField("COMPOSER", StdStringToTaglibString(song.composer()), true);
vorbis_comments->addField("PERFORMER", StdStringToTaglibString(song.performer()), true);
vorbis_comments->addField("CONTENT GROUP", StdStringToTaglibString(song.grouping()), true);
vorbis_comments->addField("BPM", QStringToTaglibString(song.bpm() <= 0 -1 ? QString() : QString::number(song.bpm())), true);
vorbis_comments->addField("DISCNUMBER", QStringToTaglibString(song.disc() <= 0 -1 ? QString() : QString::number(song.disc())), true);
vorbis_comments->addField("COMPILATION", StdStringToTaglibString(song.compilation() ? "1" : "0"), true);
@ -501,6 +516,8 @@ bool TagReader::SaveFile(const QString& filename,
SetTextFrame("TPOS", song.disc() <= 0 -1 ? QString() : QString::number(song.disc()), tag);
SetTextFrame("TBPM", song.bpm() <= 0 -1 ? QString() : QString::number(song.bpm()), tag);
SetTextFrame("TCOM", song.composer(), tag);
SetTextFrame("TIT1", song.grouping(), tag);
SetTextFrame("TPE1", song.performer(), tag);
SetTextFrame("TPE2", song.albumartist(), tag);
SetTextFrame("TCMP", std::string(song.compilation() ? "1" : "0"), tag);
} else if (TagLib::FLAC::File* file = dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
@ -511,6 +528,7 @@ bool TagReader::SaveFile(const QString& filename,
tag->itemListMap()["disk"] = TagLib::MP4::Item(song.disc() <= 0 -1 ? 0 : song.disc(), 0);
tag->itemListMap()["tmpo"] = TagLib::StringList(song.bpm() <= 0 -1 ? "0" : TagLib::String::number(song.bpm()));
tag->itemListMap()["\251wrt"] = TagLib::StringList(song.composer());
tag->itemListMap()["\251grp"] = TagLib::StringList(song.grouping());
tag->itemListMap()["aART"] = TagLib::StringList(song.albumartist());
tag->itemListMap()["cpil"] = TagLib::StringList(song.compilation() ? "1" : "0");
}

View File

@ -49,6 +49,8 @@ message SongMetadata {
optional string art_automatic = 28;
optional Type type = 29;
optional string etag = 30;
optional string performer = 31;
optional string grouping = 32;
}
message ReadFileRequest {

View File

@ -37,7 +37,7 @@
#include <QVariant>
const char* Database::kDatabaseFilename = "clementine.db";
const int Database::kSchemaVersion = 44;
const int Database::kSchemaVersion = 45;
const char* Database::kMagicAllSongsTables = "%allsongstables";
int Database::sNextConnectionId = 1;
@ -369,12 +369,16 @@ QSqlDatabase Database::Connect() {
// Find Sqlite3 functions in the Qt plugin.
StaticInit();
QSqlQuery set_fts_tokenizer("SELECT fts3_tokenizer(:name, :pointer)", db);
set_fts_tokenizer.bindValue(":name", "unicode");
set_fts_tokenizer.bindValue(":pointer", QByteArray(
reinterpret_cast<const char*>(&sFTSTokenizer), sizeof(&sFTSTokenizer)));
if (!set_fts_tokenizer.exec()) {
qLog(Warning) << "Couldn't register FTS3 tokenizer";
{
QSqlQuery set_fts_tokenizer("SELECT fts3_tokenizer(:name, :pointer)", db);
set_fts_tokenizer.bindValue(":name", "unicode");
set_fts_tokenizer.bindValue(":pointer", QByteArray(
reinterpret_cast<const char*>(&sFTSTokenizer), sizeof(&sFTSTokenizer)));
if (!set_fts_tokenizer.exec()) {
qLog(Warning) << "Couldn't register FTS3 tokenizer";
}
// Implicit invocation of ~QSqlQuery() when leaving the scope
// to release any remaining database locks!
}
if (db.tables().count() == 0) {
@ -410,9 +414,8 @@ QSqlDatabase Database::Connect() {
QSqlQuery q(QString("SELECT ROWID FROM %1.sqlite_master"
" WHERE type='table'").arg(key), db);
if (!q.exec() || !q.next()) {
ScopedTransaction t(&db);
ExecFromFile(attached_databases_[key].schema_, db, 0);
t.Commit();
q.finish();
ExecSchemaCommandsFromFile(db, attached_databases_[key].schema_, 0);
}
}
@ -421,10 +424,14 @@ QSqlDatabase Database::Connect() {
void Database::UpdateMainSchema(QSqlDatabase* db) {
// Get the database's schema version
QSqlQuery q("SELECT version FROM schema_version", *db);
int schema_version = 0;
if (q.next())
schema_version = q.value(0).toInt();
{
QSqlQuery q("SELECT version FROM schema_version", *db);
if (q.next())
schema_version = q.value(0).toInt();
// Implicit invocation of ~QSqlQuery() when leaving the scope
// to release any remaining database locks!
}
startup_schema_version_ = schema_version;
@ -478,13 +485,12 @@ void Database::UpdateDatabaseSchema(int version, QSqlDatabase &db) {
filename = ":/schema/schema.sql";
else
filename = QString(":/schema/schema-%1.sql").arg(version);
ScopedTransaction t(&db);
if (version == 31) {
// This version used to do a bad job of converting filenames in the songs
// table to file:// URLs. Now we do it properly here instead.
ScopedTransaction t(&db);
UrlEncodeFilenameColumn("songs", db);
UrlEncodeFilenameColumn("playlist_items", db);
@ -493,30 +499,32 @@ void Database::UpdateDatabaseSchema(int version, QSqlDatabase &db) {
UrlEncodeFilenameColumn(table, db);
}
}
qLog(Debug) << "Applying database schema update" << version
<< "from" << filename;
ExecSchemaCommandsFromFile(db, filename, version - 1, &t);
t.Commit();
} else {
qLog(Debug) << "Applying database schema update" << version
<< "from" << filename;
ExecSchemaCommandsFromFile(db, filename, version - 1);
}
qLog(Debug) << "Applying database schema update" << version
<< "from" << filename;
ExecFromFile(filename, db, version - 1);
t.Commit();
}
void Database::UrlEncodeFilenameColumn(const QString& table, QSqlDatabase& db) {
QSqlQuery select(QString("SELECT ROWID, filename FROM %1").arg(table), db);
QSqlQuery update(QString("UPDATE %1 SET filename=:filename WHERE ROWID=:id").arg(table), db);
select.exec();
if (CheckErrors(select)) return;
while (select.next()) {
const int rowid = select.value(0).toInt();
const QString filename = select.value(1).toString();
if (filename.isEmpty() || filename.contains("://")) {
continue;
}
const QUrl url = QUrl::fromLocalFile(filename);
update.bindValue(":filename", url.toEncoded());
update.bindValue(":id", rowid);
update.exec();
@ -524,30 +532,50 @@ void Database::UrlEncodeFilenameColumn(const QString& table, QSqlDatabase& db) {
}
}
void Database::ExecFromFile(const QString &filename, QSqlDatabase &db,
int schema_version) {
void Database::ExecSchemaCommandsFromFile(QSqlDatabase& db,
QString const& filename,
int schema_version,
ScopedTransaction const* outerTransaction) {
// Open and read the database schema
QFile schema_file(filename);
if (!schema_file.open(QIODevice::ReadOnly))
qFatal("Couldn't open schema file %s", filename.toUtf8().constData());
ExecCommands(QString::fromUtf8(schema_file.readAll()), db, schema_version);
ExecSchemaCommands(db, QString::fromUtf8(schema_file.readAll()), schema_version, outerTransaction);
}
void Database::ExecCommands(const QString& schema, QSqlDatabase& db,
int schema_version) {
void Database::ExecSchemaCommands(QSqlDatabase& db,
QString const& schema,
int schema_version,
ScopedTransaction const* outerTransaction) {
// Run each command
QStringList commands(schema.split(";\n\n"));
QStringList const schemaCommands(schema.split(";\n\n"));
// We don't want this list to reflect possible DB schema changes
// so we initialize it before executing any statements.
QStringList tables = SongsTables(db, schema_version);
// If no outer transaction is provided the song tables need to
// be queried before beginning an inner transaction! Otherwise
// DROP TABLE commands on song tables may fail due to database
// locks.
QStringList const songTables(SongsTables(db, schema_version));
foreach (const QString& command, commands) {
if (0 == outerTransaction) {
ScopedTransaction innerTransaction(&db);
ExecSongTablesCommands(db, songTables, schemaCommands);
innerTransaction.Commit();
} else {
ExecSongTablesCommands(db, songTables, schemaCommands);
}
}
void Database::ExecSongTablesCommands(QSqlDatabase& db,
QStringList const& songTables,
QStringList const& commands) {
foreach (QString const& command, commands) {
// There are now lots of "songs" tables that need to have the same schema:
// songs, magnatune_songs, and device_*_songs. We allow a magic value
// in the schema files to update all songs tables at once.
if (command.contains(kMagicAllSongsTables)) {
foreach (const QString& table, tables) {
foreach (QString const& table, songTables) {
qLog(Info) << "Updating" << table << "for" << kMagicAllSongsTables;
QString new_command(command);
new_command.replace(kMagicAllSongsTables, table);

View File

@ -38,6 +38,7 @@ struct sqlite3_tokenizer_module;
}
class Application;
class ScopedTransaction;
class Database : public QObject {
Q_OBJECT
@ -55,8 +56,7 @@ class Database : public QObject {
QMutex* Mutex() { return &mutex_; }
void RecreateAttachedDb(const QString& database_name);
void ExecFromFile(const QString& filename, QSqlDatabase &db, int schema_version);
void ExecCommands(const QString& commands, QSqlDatabase &db, int schema_version);
void ExecSchemaCommands(QSqlDatabase &db, QString const& schema, int schema_version, ScopedTransaction const* outerTransaction = 0);
int startup_schema_version() const { return startup_schema_version_; }
int current_schema_version() const { return kSchemaVersion; }
@ -70,6 +70,9 @@ class Database : public QObject {
private:
void UpdateMainSchema(QSqlDatabase* db);
void ExecSchemaCommandsFromFile(QSqlDatabase &db, QString const& filename, int schema_version, ScopedTransaction const* outerTransaction = 0);
void ExecSongTablesCommands(QSqlDatabase &db, QStringList const& songTables, QStringList const& commands);
void UpdateDatabaseSchema(int version, QSqlDatabase& db);
void UrlEncodeFilenameColumn(const QString& table, QSqlDatabase& db);
QStringList SongsTables(QSqlDatabase& db, int schema_version) const;

View File

@ -342,6 +342,8 @@ QVariantMap Mpris1::GetMetadata(const Song& song) {
AddMetadata("audio-samplerate", song.samplerate(), &ret);
AddMetadata("bpm", song.bpm(), &ret);
AddMetadata("composer", song.composer(), &ret);
AddMetadata("performer", song.performer(), &ret);
AddMetadata("grouping", song.grouping(), &ret);
if (song.rating() != -1.0) {
AddMetadata("rating", song.rating() * 5, &ret);
}

View File

@ -27,7 +27,8 @@ const char* OrganiseFormat::kBlockPattern = "\\{([^{}]+)\\}";
const QStringList OrganiseFormat::kKnownTags = QStringList()
<< "title" << "album" << "artist" << "artistinitial" << "albumartist"
<< "composer" << "track" << "disc" << "bpm" << "year" << "genre"
<< "comment" << "length" << "bitrate" << "samplerate" << "extension";
<< "comment" << "length" << "bitrate" << "samplerate" << "extension"
<< "performer" << "grouping";
// From http://en.wikipedia.org/wiki/8.3_filename#Directory_table
const char* OrganiseFormat::kInvalidFatCharacters = "\"*/\\:<>?|";
@ -131,6 +132,8 @@ QString OrganiseFormat::TagValue(const QString &tag, const Song &song) const {
else if (tag == "album") value = song.album();
else if (tag == "artist") value = song.artist();
else if (tag == "composer") value = song.composer();
else if (tag == "performer") value = song.performer();
else if (tag == "grouping") value = song.grouping();
else if (tag == "genre") value = song.genre();
else if (tag == "comment") value = song.comment();
else if (tag == "year") value = QString::number(song.year());

View File

@ -73,7 +73,8 @@ const QStringList Song::kColumns = QStringList()
<< "art_manual" << "filetype" << "playcount" << "lastplayed" << "rating"
<< "forced_compilation_on" << "forced_compilation_off"
<< "effective_compilation" << "skipcount" << "score" << "beginning" << "length"
<< "cue_path" << "unavailable" << "effective_albumartist" << "etag";
<< "cue_path" << "unavailable" << "effective_albumartist" << "etag"
<< "performer" << "grouping";
const QString Song::kColumnSpec = Song::kColumns.join(", ");
const QString Song::kBindSpec = Utilities::Prepend(":", Song::kColumns).join(", ");
@ -82,7 +83,7 @@ const QString Song::kUpdateSpec = Utilities::Updateify(Song::kColumns).join(", "
const QStringList Song::kFtsColumns = QStringList()
<< "ftstitle" << "ftsalbum" << "ftsartist" << "ftsalbumartist"
<< "ftscomposer" << "ftsgenre" << "ftscomment";
<< "ftscomposer" << "ftsperformer" << "ftsgrouping" << "ftsgenre" << "ftscomment";
const QString Song::kFtsColumnSpec = Song::kFtsColumns.join(", ");
const QString Song::kFtsBindSpec = Utilities::Prepend(":", Song::kFtsColumns).join(", ");
@ -103,6 +104,8 @@ struct Song::Private : public QSharedData {
QString artist_;
QString albumartist_;
QString composer_;
QString performer_;
QString grouping_;
int track_;
int disc_;
float bpm_;
@ -234,6 +237,8 @@ const QString& Song::albumartist() const { return d->albumartist_; }
const QString& Song::effective_albumartist() const { return d->albumartist_.isEmpty() ? d->artist_ : d->albumartist_; }
const QString& Song::playlist_albumartist() const { return is_compilation() ? d->albumartist_ : effective_albumartist(); }
const QString& Song::composer() const { return d->composer_; }
const QString& Song::performer() const { return d->performer_; }
const QString& Song::grouping() const { return d->grouping_; }
int Song::track() const { return d->track_; }
int Song::disc() const { return d->disc_; }
float Song::bpm() const { return d->bpm_; }
@ -281,6 +286,8 @@ void Song::set_album(const QString& v) { d->album_ = v; }
void Song::set_artist(const QString& v) { d->artist_ = v; }
void Song::set_albumartist(const QString& v) { d->albumartist_ = v; }
void Song::set_composer(const QString& v) { d->composer_ = v; }
void Song::set_performer(const QString& v) { d->performer_ = v; }
void Song::set_grouping(const QString& v) { d->grouping_ = v; }
void Song::set_track(int v) { d->track_ = v; }
void Song::set_disc(int v) { d->disc_ = v; }
void Song::set_bpm(float v) { d->bpm_ = v; }
@ -396,6 +403,8 @@ void Song::InitFromProtobuf(const pb::tagreader::SongMetadata& pb) {
d->artist_ = QStringFromStdString(pb.artist());
d->albumartist_ = QStringFromStdString(pb.albumartist());
d->composer_ = QStringFromStdString(pb.composer());
d->performer_ = QStringFromStdString(pb.performer());
d->grouping_ = QStringFromStdString(pb.grouping());
d->track_ = pb.track();
d->disc_ = pb.disc();
d->bpm_ = pb.bpm();
@ -437,6 +446,8 @@ void Song::ToProtobuf(pb::tagreader::SongMetadata* pb) const {
pb->set_artist(DataCommaSizeFromQString(d->artist_));
pb->set_albumartist(DataCommaSizeFromQString(d->albumartist_));
pb->set_composer(DataCommaSizeFromQString(d->composer_));
pb->set_performer(DataCommaSizeFromQString(d->performer_));
pb->set_grouping(DataCommaSizeFromQString(d->grouping_));
pb->set_track(d->track_);
pb->set_disc(d->disc_);
pb->set_bpm(d->bpm_);
@ -523,6 +534,10 @@ void Song::InitFromQuery(const SqlRow& q, bool reliable_metadata, int col) {
d->unavailable_ = q.value(col + 35).toBool();
// effective_albumartist = 36
// etag = 37
d->performer_ = tostr(col + 38);
d->grouping_ = tostr(col + 39);
#undef tostr
#undef toint
@ -571,6 +586,7 @@ void Song::InitFromLastFM(const lastfm::Track& track) {
d->artist_ = QString::fromUtf8(track->artist);
d->albumartist_ = QString::fromUtf8(track->albumartist);
d->composer_ = QString::fromUtf8(track->composer);
d->grouping_ = QString::fromUtf8(track->grouping);
d->track_ = track->track_nr;
d->disc_ = track->cd_nr;
d->bpm_ = track->BPM;
@ -608,6 +624,7 @@ void Song::InitFromLastFM(const lastfm::Track& track) {
track->artist = strdup(d->artist_.toUtf8().constData());
track->albumartist = strdup(d->albumartist_.toUtf8().constData());
track->composer = strdup(d->composer_.toUtf8().constData());
track->grouping = strdup(d->grouping_.toUtf8().constData());
track->track_nr = d->track_;
track->cd_nr = d->disc_;
track->BPM = d->bpm_;
@ -1012,6 +1029,9 @@ void Song::BindToQuery(QSqlQuery *query) const {
query->bindValue(":etag", strval(d->etag_));
query->bindValue(":performer", strval(d->performer_));
query->bindValue(":grouping", strval(d->grouping_));
#undef intval
#undef notnullintval
#undef strval
@ -1023,6 +1043,8 @@ void Song::BindToFtsQuery(QSqlQuery *query) const {
query->bindValue(":ftsartist", d->artist_);
query->bindValue(":ftsalbumartist", d->albumartist_);
query->bindValue(":ftscomposer", d->composer_);
query->bindValue(":ftsperformer", d->performer_);
query->bindValue(":ftsgrouping", d->grouping_);
query->bindValue(":ftsgenre", d->genre_);
query->bindValue(":ftscomment", d->comment_);
}
@ -1114,6 +1136,8 @@ bool Song::IsMetadataEqual(const Song& other) const {
d->artist_ == other.d->artist_ &&
d->albumartist_ == other.d->albumartist_ &&
d->composer_ == other.d->composer_ &&
d->performer_ == other.d->performer_ &&
d->grouping_ == other.d->grouping_ &&
d->track_ == other.d->track_ &&
d->disc_ == other.d->disc_ &&
qFuzzyCompare(d->bpm_, other.d->bpm_) &&

View File

@ -161,6 +161,8 @@ class Song {
// compilations, but you do for normal albums:
const QString& playlist_albumartist() const;
const QString& composer() const;
const QString& performer() const;
const QString& grouping() const;
int track() const;
int disc() const;
float bpm() const;
@ -235,6 +237,8 @@ class Song {
void set_artist(const QString& v);
void set_albumartist(const QString& v);
void set_composer(const QString& v);
void set_performer(const QString& v);
void set_grouping(const QString& v);
void set_track(int v);
void set_disc(int v);
void set_bpm(float v);

View File

@ -90,7 +90,7 @@ int DeviceDatabaseBackend::AddDevice(const Device& device) {
QString schema = QString::fromUtf8(schema_file.readAll());
schema.replace("%deviceid", QString::number(id));
db_->ExecCommands(schema, db, 0);
db_->ExecSchemaCommands(db, schema, 0, &t);
t.Commit();
return id;

View File

@ -125,6 +125,8 @@ QStandardItem* GlobalSearchModel::BuildContainers(
break;
case LibraryModel::GroupBy_Composer: display_text = s.composer();
case LibraryModel::GroupBy_Performer: display_text = s.performer();
case LibraryModel::GroupBy_Grouping: display_text = s.grouping();
case LibraryModel::GroupBy_Genre: if (display_text.isNull()) display_text = s.genre();
case LibraryModel::GroupBy_Album:
unique_tag = s.album_id();

View File

@ -36,6 +36,8 @@ GroupByDialog::GroupByDialog(QWidget *parent)
mapping_.insert(Mapping(LibraryModel::GroupBy_Genre, 6));
mapping_.insert(Mapping(LibraryModel::GroupBy_Year, 7));
mapping_.insert(Mapping(LibraryModel::GroupBy_YearAlbum, 8));
mapping_.insert(Mapping(LibraryModel::GroupBy_Performer, 9));
mapping_.insert(Mapping(LibraryModel::GroupBy_Grouping, 10));
connect(ui_->button_box->button(QDialogButtonBox::Reset), SIGNAL(clicked()),
SLOT(Reset()));

View File

@ -180,6 +180,8 @@ void LibraryModel::SongsDiscovered(const SongList& songs) {
case GroupBy_Album: key = song.album(); break;
case GroupBy_Artist: key = song.artist(); break;
case GroupBy_Composer: key = song.composer(); break;
case GroupBy_Performer: key = song.performer(); break;
case GroupBy_Grouping: key = song.grouping(); break;
case GroupBy_Genre: key = song.genre(); break;
case GroupBy_AlbumArtist: key = song.effective_albumartist(); break;
case GroupBy_Year:
@ -256,6 +258,8 @@ QString LibraryModel::DividerKey(GroupBy type, LibraryItem* item) const {
case GroupBy_Album:
case GroupBy_Artist:
case GroupBy_Composer:
case GroupBy_Performer:
case GroupBy_Grouping:
case GroupBy_Genre:
case GroupBy_AlbumArtist:
case GroupBy_FileType: {
@ -289,6 +293,8 @@ QString LibraryModel::DividerDisplayText(GroupBy type, const QString& key) const
case GroupBy_Album:
case GroupBy_Artist:
case GroupBy_Composer:
case GroupBy_Performer:
case GroupBy_Grouping:
case GroupBy_Genre:
case GroupBy_AlbumArtist:
case GroupBy_FileType:
@ -714,6 +720,12 @@ void LibraryModel::InitQuery(GroupBy type, LibraryQuery* q) {
case GroupBy_Composer:
q->SetColumnSpec("DISTINCT composer");
break;
case GroupBy_Performer:
q->SetColumnSpec("DISTINCT performer");
break;
case GroupBy_Grouping:
q->SetColumnSpec("DISTINCT grouping");
break;
case GroupBy_YearAlbum:
q->SetColumnSpec("DISTINCT year, album");
break;
@ -762,6 +774,12 @@ void LibraryModel::FilterQuery(GroupBy type, LibraryItem* item, LibraryQuery* q)
case GroupBy_Composer:
q->AddWhere("composer", item->key);
break;
case GroupBy_Performer:
q->AddWhere("performer", item->key);
break;
case GroupBy_Grouping:
q->AddWhere("grouping", item->key);
break;
case GroupBy_Genre:
q->AddWhere("genre", item->key);
break;
@ -829,6 +847,8 @@ LibraryItem* LibraryModel::ItemFromQuery(GroupBy type,
break;
case GroupBy_Composer:
case GroupBy_Performer:
case GroupBy_Grouping:
case GroupBy_Genre:
case GroupBy_Album:
case GroupBy_AlbumArtist:
@ -883,6 +903,8 @@ LibraryItem* LibraryModel::ItemFromSong(GroupBy type,
break;
case GroupBy_Composer: item->key = s.composer();
case GroupBy_Performer: item->key = s.performer();
case GroupBy_Grouping: item->key = s.grouping();
case GroupBy_Genre: if (item->key.isNull()) item->key = s.genre();
case GroupBy_Album: if (item->key.isNull()) item->key = s.album();
case GroupBy_AlbumArtist: if (item->key.isNull()) item->key = s.effective_albumartist();

View File

@ -80,6 +80,8 @@ class LibraryModel : public SimpleTreeModel<LibraryItem> {
GroupBy_Genre = 6,
GroupBy_AlbumArtist = 7,
GroupBy_FileType = 8,
GroupBy_Performer = 9,
GroupBy_Grouping = 10,
};
struct Grouping {

View File

@ -174,6 +174,8 @@ bool Playlist::column_is_editable(Playlist::Column column) {
case Column_Album:
case Column_AlbumArtist:
case Column_Composer:
case Column_Performer:
case Column_Grouping:
case Column_Track:
case Column_Disc:
case Column_Year:
@ -209,6 +211,12 @@ bool Playlist::set_column_value(Song& song, Playlist::Column column,
case Column_Composer:
song.set_composer(value.toString());
break;
case Column_Performer:
song.set_performer(value.toString());
break;
case Column_Grouping:
song.set_grouping(value.toString());
break;
case Column_Track:
song.set_track(value.toInt());
break;
@ -271,6 +279,8 @@ QVariant Playlist::data(const QModelIndex& index, int role) const {
case Column_Genre: return song.genre();
case Column_AlbumArtist: return song.playlist_albumartist();
case Column_Composer: return song.composer();
case Column_Performer: return song.performer();
case Column_Grouping: return song.grouping();
case Column_Rating: return song.rating();
case Column_PlayCount: return song.playcount();
@ -1160,6 +1170,8 @@ bool Playlist::CompareItems(int column, Qt::SortOrder order,
case Column_Genre: strcmp(genre);
case Column_AlbumArtist: strcmp(playlist_albumartist);
case Column_Composer: strcmp(composer);
case Column_Performer: strcmp(performer);
case Column_Grouping: strcmp(grouping);
case Column_Rating: cmp(rating);
case Column_PlayCount: cmp(playcount);
@ -1199,6 +1211,8 @@ QString Playlist::column_name(Column column) {
case Column_Genre: return tr("Genre");
case Column_AlbumArtist: return tr("Album artist");
case Column_Composer: return tr("Composer");
case Column_Performer: return tr("Performer");
case Column_Grouping: return tr("Grouping");
case Column_Rating: return tr("Rating");
case Column_PlayCount: return tr("Play count");

View File

@ -90,6 +90,8 @@ class Playlist : public QAbstractListModel {
Column_Album,
Column_AlbumArtist,
Column_Composer,
Column_Performer,
Column_Grouping,
Column_Length,
Column_Track,
Column_Disc,

View File

@ -378,6 +378,8 @@ QString TagCompletionModel::database_column(Playlist::Column column) {
case Playlist::Column_Album: return "album";
case Playlist::Column_AlbumArtist: return "albumartist";
case Playlist::Column_Composer: return "composer";
case Playlist::Column_Performer: return "performer";
case Playlist::Column_Grouping: return "grouping";
case Playlist::Column_Genre: return "genre";
default:
qLog(Warning) << "Unknown column" << column;

View File

@ -33,6 +33,8 @@ PlaylistFilter::PlaylistFilter(QObject *parent)
column_names_["album"] = Playlist::Column_Album;
column_names_["albumartist"] = Playlist::Column_AlbumArtist;
column_names_["composer"] = Playlist::Column_Composer;
column_names_["performer"] = Playlist::Column_Performer;
column_names_["grouping"] = Playlist::Column_Grouping;
column_names_["length"] = Playlist::Column_Length;
column_names_["track"] = Playlist::Column_Track;
column_names_["disc"] = Playlist::Column_Disc;

View File

@ -192,6 +192,10 @@ void PlaylistView::SetItemDelegates(LibraryBackend* backend) {
new TagCompletionItemDelegate(this, backend, Playlist::Column_Genre));
setItemDelegateForColumn(Playlist::Column_Composer,
new TagCompletionItemDelegate(this, backend, Playlist::Column_Composer));
setItemDelegateForColumn(Playlist::Column_Performer,
new TagCompletionItemDelegate(this, backend, Playlist::Column_Performer));
setItemDelegateForColumn(Playlist::Column_Grouping,
new TagCompletionItemDelegate(this, backend, Playlist::Column_Grouping));
setItemDelegateForColumn(Playlist::Column_Length, new LengthItemDelegate(this));
setItemDelegateForColumn(Playlist::Column_Filesize, new SizeItemDelegate(this));
setItemDelegateForColumn(Playlist::Column_Filetype, new FileTypeItemDelegate(this));
@ -286,6 +290,8 @@ void PlaylistView::LoadGeometry() {
header_->HideSection(Playlist::Column_DateModified);
header_->HideSection(Playlist::Column_AlbumArtist);
header_->HideSection(Playlist::Column_Composer);
header_->HideSection(Playlist::Column_Performer);
header_->HideSection(Playlist::Column_Grouping);
header_->HideSection(Playlist::Column_Rating);
header_->HideSection(Playlist::Column_PlayCount);
header_->HideSection(Playlist::Column_SkipCount);

View File

@ -250,6 +250,8 @@ QString SearchTerm::FieldColumnName(Field field) {
case Field_Album: return "album";
case Field_AlbumArtist: return "albumartist";
case Field_Composer: return "composer";
case Field_Performer: return "performer";
case Field_Grouping: return "grouping";
case Field_Genre: return "genre";
case Field_Comment: return "comment";
case Field_Filepath: return "filename";
@ -280,6 +282,8 @@ QString SearchTerm::FieldName(Field field) {
case Field_Album: return Playlist::column_name(Playlist::Column_Album);
case Field_AlbumArtist: return Playlist::column_name(Playlist::Column_AlbumArtist);
case Field_Composer: return Playlist::column_name(Playlist::Column_Composer);
case Field_Performer: return Playlist::column_name(Playlist::Column_Performer);
case Field_Grouping: return Playlist::column_name(Playlist::Column_Grouping);
case Field_Genre: return Playlist::column_name(Playlist::Column_Genre);
case Field_Comment: return QObject::tr("Comment");
case Field_Filepath: return Playlist::column_name(Playlist::Column_Filename);

View File

@ -32,6 +32,8 @@ public:
Field_Album,
Field_AlbumArtist,
Field_Composer,
Field_Performer,
Field_Grouping,
Field_Length,
Field_Track,
Field_Disc,

View File

@ -186,6 +186,8 @@ EditTagDialog::EditTagDialog(Application* app, QWidget* parent)
new TagCompleter(app_->library_backend(), Playlist::Column_AlbumArtist, ui_->albumartist);
new TagCompleter(app_->library_backend(), Playlist::Column_Genre, ui_->genre);
new TagCompleter(app_->library_backend(), Playlist::Column_Composer, ui_->composer);
new TagCompleter(app_->library_backend(), Playlist::Column_Performer, ui_->performer);
new TagCompleter(app_->library_backend(), Playlist::Column_Grouping, ui_->grouping);
}
EditTagDialog::~EditTagDialog() {
@ -290,6 +292,8 @@ QVariant EditTagDialog::Data::value(const Song& song, const QString& id) {
if (id == "album") return song.album();
if (id == "albumartist") return song.albumartist();
if (id == "composer") return song.composer();
if (id == "performer") return song.performer();
if (id == "grouping") return song.grouping();
if (id == "genre") return song.genre();
if (id == "comment") return song.comment();
if (id == "track") return song.track();
@ -305,6 +309,8 @@ void EditTagDialog::Data::set_value(const QString& id, const QVariant& value) {
if (id == "album") current_.set_album(value.toString());
if (id == "albumartist") current_.set_albumartist(value.toString());
if (id == "composer") current_.set_composer(value.toString());
if (id == "performer") current_.set_performer(value.toString());
if (id == "grouping") current_.set_grouping(value.toString());
if (id == "genre") current_.set_genre(value.toString());
if (id == "comment") current_.set_comment(value.toString());
if (id == "track") current_.set_track(value.toInt());

View File

@ -779,17 +779,37 @@
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="genre_label">
<widget class="QLabel" name="performer_label">
<property name="text">
<string>Genre</string>
<string>Performer</string>
</property>
<property name="buddy">
<cstring>genre</cstring>
<cstring>performer</cstring>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="LineEdit" name="genre">
<widget class="LineEdit" name="performer">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="grouping_label">
<property name="text">
<string>Grouping</string>
</property>
<property name="buddy">
<cstring>grouping</cstring>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="LineEdit" name="grouping">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
@ -799,17 +819,17 @@
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="comment_label">
<widget class="QLabel" name="genre_label">
<property name="text">
<string>Comment</string>
<string>Genre</string>
</property>
<property name="buddy">
<cstring>comment</cstring>
<cstring>genre</cstring>
</property>
</widget>
</item>
<item row="7" column="1" colspan="3">
<widget class="TextEdit" name="comment">
<item row="7" column="1">
<widget class="LineEdit" name="genre">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
@ -818,7 +838,7 @@
</property>
</widget>
</item>
<item row="6" column="1">
<item row="9" column="1">
<widget class="QPushButton" name="fetch_tag">
<property name="text">
<string>Complete tags automatically</string>
@ -835,6 +855,26 @@
</property>
</widget>
</item>
<item row="11" column="0">
<widget class="QLabel" name="comment_label">
<property name="text">
<string>Comment</string>
</property>
<property name="buddy">
<cstring>comment</cstring>
</property>
</widget>
</item>
<item row="11" column="1" colspan="3">
<widget class="TextEdit" name="comment">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>

View File

@ -2289,6 +2289,7 @@ void MainWindow::HandleNotificationPreview(OSD::Behaviour type, QString line1, Q
fake.Init("Title", "Artist", "Album", 123);
fake.set_genre("Classical");
fake.set_composer("Anonymous");
fake.set_performer("Anonymous");
fake.set_track(1);
fake.set_disc(1);
fake.set_year(2011);

View File

@ -48,6 +48,8 @@ NotificationsSettingsPage::NotificationsSettingsPage(SettingsDialog* dialog)
menu->addAction(ui_->action_albumartist);
menu->addAction(ui_->action_year);
menu->addAction(ui_->action_composer);
menu->addAction(ui_->action_performer);
menu->addAction(ui_->action_grouping);
menu->addAction(ui_->action_length);
menu->addAction(ui_->action_disc);
menu->addAction(ui_->action_track);

View File

@ -378,6 +378,22 @@
<string>Add song composer tag</string>
</property>
</action>
<action name="action_performer">
<property name="text">
<string notr="true">%performer%</string>
</property>
<property name="toolTip">
<string>Add song performer tag</string>
</property>
</action>
<action name="action_grouping">
<property name="text">
<string notr="true">%grouping%</string>
</property>
<property name="toolTip">
<string>Add song grouping tag</string>
</property>
</action>
<action name="action_disc">
<property name="text">
<string notr="true">%disc%</string>

View File

@ -57,6 +57,8 @@ OrganiseDialog::OrganiseDialog(TaskManager* task_manager, QWidget *parent)
tags[tr("Artist's initial")] = "artistinitial";
tags[tr("Album artist")] = "albumartist";
tags[tr("Composer")] = "composer";
tags[tr("Performer")] = "performer";
tags[tr("Grouping")] = "grouping";
tags[tr("Track")] = "track";
tags[tr("Disc")] = "disc";
tags[tr("BPM")] = "bpm";

View File

@ -306,6 +306,10 @@ QString OSD::ReplaceVariable(const QString& variable, const Song& song) {
return song.PrettyYear();
} else if (variable == "%composer%") {
return song.composer();
} else if (variable == "%performer%") {
return song.performer();
} else if (variable == "%grouping%") {
return song.grouping();
} else if (variable == "%length%") {
return song.PrettyLength();
} else if (variable == "%disc%") {

View File

@ -38,6 +38,8 @@ TEST_F(OrganiseFormatTest, BasicReplace) {
song_.set_bpm(4.56);
song_.set_comment("comment");
song_.set_composer("composer");
song_.set_performer("performer");
song_.set_grouping("grouping");
song_.set_disc(789);
song_.set_genre("genre");
song_.set_length_nanosec(987 * kNsecPerSec);
@ -47,10 +49,11 @@ TEST_F(OrganiseFormatTest, BasicReplace) {
song_.set_year(2010);
format_.set_format("%album %albumartist %bitrate %bpm %comment %composer "
"%performer %grouping "
"%disc %genre %length %samplerate %title %track %year");
ASSERT_TRUE(format_.IsValid());
EXPECT_EQ("album albumartist 123 4.56 comment composer 789 genre 987 654 title 321 2010",
EXPECT_EQ("album albumartist 123 4.56 comment composer performer grouping 789 genre 987 654 title 321 2010",
format_.GetFilenameForSong(song_));
}