diff --git a/data/data.qrc b/data/data.qrc index 0269dd706..b5e8a9d41 100644 --- a/data/data.qrc +++ b/data/data.qrc @@ -343,6 +343,7 @@ schema/schema-42.sql schema/schema-43.sql schema/schema-44.sql + schema/schema-45.sql schema/schema-4.sql schema/schema-5.sql schema/schema-6.sql diff --git a/data/schema/jamendo.sql b/data/schema/jamendo.sql index 42a85908f..0c914365b 100644 --- a/data/schema/jamendo.sql +++ b/data/schema/jamendo.sql @@ -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 ); diff --git a/data/schema/schema-45.sql b/data/schema/schema-45.sql new file mode 100644 index 000000000..058d4599b --- /dev/null +++ b/data/schema/schema-45.sql @@ -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; + diff --git a/ext/libclementine-tagreader/tagreader.cpp b/ext/libclementine-tagreader/tagreader.cpp index 5cf43c789..69a7d9f29 100644 --- a/ext/libclementine-tagreader/tagreader.cpp +++ b/ext/libclementine-tagreader/tagreader.cpp @@ -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(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"); } diff --git a/ext/libclementine-tagreader/tagreadermessages.proto b/ext/libclementine-tagreader/tagreadermessages.proto index 5b4e4b7f4..0cd8971f7 100644 --- a/ext/libclementine-tagreader/tagreadermessages.proto +++ b/ext/libclementine-tagreader/tagreadermessages.proto @@ -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 { diff --git a/src/core/database.cpp b/src/core/database.cpp index ccbabe394..15e936f8e 100644 --- a/src/core/database.cpp +++ b/src/core/database.cpp @@ -37,7 +37,7 @@ #include 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(&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(&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); diff --git a/src/core/database.h b/src/core/database.h index 1bc570d68..cc409cfe1 100644 --- a/src/core/database.h +++ b/src/core/database.h @@ -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; diff --git a/src/core/mpris1.cpp b/src/core/mpris1.cpp index b2f63f8bf..368e284ed 100644 --- a/src/core/mpris1.cpp +++ b/src/core/mpris1.cpp @@ -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); } diff --git a/src/core/organiseformat.cpp b/src/core/organiseformat.cpp index 15e074c2c..2e932817a 100644 --- a/src/core/organiseformat.cpp +++ b/src/core/organiseformat.cpp @@ -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()); diff --git a/src/core/song.cpp b/src/core/song.cpp index 6afc9459f..08af33ae3 100644 --- a/src/core/song.cpp +++ b/src/core/song.cpp @@ -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_) && diff --git a/src/core/song.h b/src/core/song.h index 240ddfd89..f5310e1ad 100644 --- a/src/core/song.h +++ b/src/core/song.h @@ -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); diff --git a/src/devices/devicedatabasebackend.cpp b/src/devices/devicedatabasebackend.cpp index 74749b8b4..a7ace7425 100644 --- a/src/devices/devicedatabasebackend.cpp +++ b/src/devices/devicedatabasebackend.cpp @@ -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; diff --git a/src/globalsearch/globalsearchmodel.cpp b/src/globalsearch/globalsearchmodel.cpp index bd4fc9beb..d1433b228 100644 --- a/src/globalsearch/globalsearchmodel.cpp +++ b/src/globalsearch/globalsearchmodel.cpp @@ -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(); diff --git a/src/library/groupbydialog.cpp b/src/library/groupbydialog.cpp index 3ece9c15c..eea1dd448 100644 --- a/src/library/groupbydialog.cpp +++ b/src/library/groupbydialog.cpp @@ -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())); diff --git a/src/library/librarymodel.cpp b/src/library/librarymodel.cpp index 0b3f61503..2c73fd14e 100644 --- a/src/library/librarymodel.cpp +++ b/src/library/librarymodel.cpp @@ -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(); diff --git a/src/library/librarymodel.h b/src/library/librarymodel.h index fb238c0ec..6fdd5e665 100644 --- a/src/library/librarymodel.h +++ b/src/library/librarymodel.h @@ -80,6 +80,8 @@ class LibraryModel : public SimpleTreeModel { GroupBy_Genre = 6, GroupBy_AlbumArtist = 7, GroupBy_FileType = 8, + GroupBy_Performer = 9, + GroupBy_Grouping = 10, }; struct Grouping { diff --git a/src/playlist/playlist.cpp b/src/playlist/playlist.cpp index 0209c4cc8..27b2a6fe8 100644 --- a/src/playlist/playlist.cpp +++ b/src/playlist/playlist.cpp @@ -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"); diff --git a/src/playlist/playlist.h b/src/playlist/playlist.h index e1c655dc7..16e9de037 100644 --- a/src/playlist/playlist.h +++ b/src/playlist/playlist.h @@ -90,6 +90,8 @@ class Playlist : public QAbstractListModel { Column_Album, Column_AlbumArtist, Column_Composer, + Column_Performer, + Column_Grouping, Column_Length, Column_Track, Column_Disc, diff --git a/src/playlist/playlistdelegates.cpp b/src/playlist/playlistdelegates.cpp index 1dfedb9b7..752b556eb 100644 --- a/src/playlist/playlistdelegates.cpp +++ b/src/playlist/playlistdelegates.cpp @@ -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; diff --git a/src/playlist/playlistfilter.cpp b/src/playlist/playlistfilter.cpp index 2158972d2..d96dc0d3f 100644 --- a/src/playlist/playlistfilter.cpp +++ b/src/playlist/playlistfilter.cpp @@ -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; diff --git a/src/playlist/playlistview.cpp b/src/playlist/playlistview.cpp index d3539c5db..8281f8fce 100644 --- a/src/playlist/playlistview.cpp +++ b/src/playlist/playlistview.cpp @@ -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); diff --git a/src/smartplaylists/searchterm.cpp b/src/smartplaylists/searchterm.cpp index 4a373d190..ddc6b59fa 100644 --- a/src/smartplaylists/searchterm.cpp +++ b/src/smartplaylists/searchterm.cpp @@ -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); diff --git a/src/smartplaylists/searchterm.h b/src/smartplaylists/searchterm.h index 07fbd89a6..fd628ad88 100644 --- a/src/smartplaylists/searchterm.h +++ b/src/smartplaylists/searchterm.h @@ -32,6 +32,8 @@ public: Field_Album, Field_AlbumArtist, Field_Composer, + Field_Performer, + Field_Grouping, Field_Length, Field_Track, Field_Disc, diff --git a/src/ui/edittagdialog.cpp b/src/ui/edittagdialog.cpp index 7e81d8c82..2eabf8770 100644 --- a/src/ui/edittagdialog.cpp +++ b/src/ui/edittagdialog.cpp @@ -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()); diff --git a/src/ui/edittagdialog.ui b/src/ui/edittagdialog.ui index 01fe91e81..5b5d8ee9a 100644 --- a/src/ui/edittagdialog.ui +++ b/src/ui/edittagdialog.ui @@ -779,17 +779,37 @@ - + - Genre + Performer - genre + performer - + + + true + + + false + + + + + + + Grouping + + + grouping + + + + + true @@ -799,17 +819,17 @@ - + - Comment + Genre - comment + genre - - + + true @@ -818,7 +838,7 @@ - + Complete tags automatically @@ -835,6 +855,26 @@ + + + + Comment + + + comment + + + + + + + true + + + false + + + diff --git a/src/ui/mainwindow.cpp b/src/ui/mainwindow.cpp index d960c6dd1..372d7e756 100644 --- a/src/ui/mainwindow.cpp +++ b/src/ui/mainwindow.cpp @@ -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); diff --git a/src/ui/notificationssettingspage.cpp b/src/ui/notificationssettingspage.cpp index 1991a0928..ef05f15ac 100644 --- a/src/ui/notificationssettingspage.cpp +++ b/src/ui/notificationssettingspage.cpp @@ -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); diff --git a/src/ui/notificationssettingspage.ui b/src/ui/notificationssettingspage.ui index 5ffec8033..7cc4e52df 100644 --- a/src/ui/notificationssettingspage.ui +++ b/src/ui/notificationssettingspage.ui @@ -378,6 +378,22 @@ Add song composer tag + + + %performer% + + + Add song performer tag + + + + + %grouping% + + + Add song grouping tag + + %disc% diff --git a/src/ui/organisedialog.cpp b/src/ui/organisedialog.cpp index cafb43278..bb786115e 100644 --- a/src/ui/organisedialog.cpp +++ b/src/ui/organisedialog.cpp @@ -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"; diff --git a/src/widgets/osd.cpp b/src/widgets/osd.cpp index 26586ec13..9e40ff1d9 100644 --- a/src/widgets/osd.cpp +++ b/src/widgets/osd.cpp @@ -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%") { diff --git a/tests/organiseformat_test.cpp b/tests/organiseformat_test.cpp index 0c333f7d1..4afb98725 100644 --- a/tests/organiseformat_test.cpp +++ b/tests/organiseformat_test.cpp @@ -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_)); }