From 258ae281d841e9ac891954b74a447d701334b747 Mon Sep 17 00:00:00 2001 From: Martin Babutzka Date: Fri, 10 Apr 2015 21:05:07 +0200 Subject: [PATCH] IDv3 tag lyrics support. Squashed the following commits: 5c723ad commit: Fix: Includes alpha sort 15ac350 commit: Fix: Updated Database::kSchemaVersion to 49. 767a26a commit: Fixed small code style issue. Added schema-49 to data.qrc instead schema-48 bf6aa64 commit: fixup! Modified async handling of CollapsibleInfoPane as recommended by Andreas. Display of IDv2 tag lyrics works now. c1f97e9 commit: fixup! Added support to read/display the ID tag lyrics in MP3 files: c946b1d commit: Added support to read/display the ID tag lyrics in MP3 files: -Added schema to the database to store it -Added readers/writers for ID tags -Added readers/writers for the database to the song class -Added the taglyricsinfoprovider to show the lyrics in songinfo --- data/data.qrc | 1 + data/schema/schema-49.sql | 3 ++ ext/libclementine-tagreader/tagreader.cpp | 10 +++++ .../tagreadermessages.proto | 1 + src/CMakeLists.txt | 2 + src/core/database.cpp | 2 +- src/core/metatypes.cpp | 24 ++++++------ src/core/mpris1.cpp | 1 + src/core/organiseformat.cpp | 5 ++- src/core/song.cpp | 20 +++++++--- src/core/song.h | 2 + src/songinfo/songinfofetcher.cpp | 9 +++-- src/songinfo/songinfoview.cpp | 2 + src/songinfo/taglyricsinfoprovider.cpp | 39 +++++++++++++++++++ src/songinfo/taglyricsinfoprovider.h | 30 ++++++++++++++ src/ui/mainwindow.cpp | 1 + src/ui/organisedialog.cpp | 11 +++--- src/widgets/osd.cpp | 2 + 18 files changed, 138 insertions(+), 27 deletions(-) create mode 100644 data/schema/schema-49.sql create mode 100644 src/songinfo/taglyricsinfoprovider.cpp create mode 100644 src/songinfo/taglyricsinfoprovider.h diff --git a/data/data.qrc b/data/data.qrc index afd87697b..01092b218 100644 --- a/data/data.qrc +++ b/data/data.qrc @@ -387,6 +387,7 @@ schema/schema-46.sql schema/schema-47.sql schema/schema-48.sql + schema/schema-49.sql schema/schema-4.sql schema/schema-5.sql schema/schema-6.sql diff --git a/data/schema/schema-49.sql b/data/schema/schema-49.sql new file mode 100644 index 000000000..1a48579e9 --- /dev/null +++ b/data/schema/schema-49.sql @@ -0,0 +1,3 @@ +ALTER TABLE %allsongstables ADD COLUMN lyrics TEXT; + +UPDATE schema_version SET version=49; diff --git a/ext/libclementine-tagreader/tagreader.cpp b/ext/libclementine-tagreader/tagreader.cpp index b3eb8faa1..e87a4b210 100644 --- a/ext/libclementine-tagreader/tagreader.cpp +++ b/ext/libclementine-tagreader/tagreader.cpp @@ -143,6 +143,7 @@ void TagReader::ReadFile(const QString& filename, QString disc; QString compilation; + QString lyrics; // Handle all the files which have VorbisComments (Ogg, OPUS, ...) in the same // way; @@ -188,6 +189,12 @@ void TagReader::ReadFile(const QString& filename, compilation = TStringToQString(map["TCMP"].front()->toString()).trimmed(); + if (!map["USLT"].isEmpty()) { + lyrics = TStringToQString((map["USLT"].front())->toString()).trimmed(); + qLog(Debug) << "Read ULST lyrics " << lyrics; + } else if (!map["SYLT"].isEmpty()) + lyrics = TStringToQString((map["SYLT"].front())->toString()).trimmed(); + if (!map["APIC"].isEmpty()) song->set_art_automatic(kEmbeddedCover); // Find a suitable comment tag. For now we ignore iTunNORM comments. @@ -369,6 +376,8 @@ void TagReader::ReadFile(const QString& filename, song->set_compilation(compilation.toInt() == 1); } + if (!lyrics.isEmpty()) song->set_lyrics(lyrics.toStdString()); + if (fileref->audioProperties()) { song->set_bitrate(fileref->audioProperties()->bitrate()); song->set_samplerate(fileref->audioProperties()->sampleRate()); @@ -617,6 +626,7 @@ bool TagReader::SaveFile(const QString& filename, SetTextFrame("TCOM", song.composer(), tag); SetTextFrame("TIT1", song.grouping(), tag); SetTextFrame("TOPE", song.performer(), tag); + SetTextFrame("USLT", song.lyrics(), tag); // Skip TPE1 (which is the artist) here because we already set it SetTextFrame("TPE2", song.albumartist(), tag); SetTextFrame("TCMP", std::string(song.compilation() ? "1" : "0"), tag); diff --git a/ext/libclementine-tagreader/tagreadermessages.proto b/ext/libclementine-tagreader/tagreadermessages.proto index f90dbceab..225e03403 100644 --- a/ext/libclementine-tagreader/tagreadermessages.proto +++ b/ext/libclementine-tagreader/tagreadermessages.proto @@ -51,6 +51,7 @@ message SongMetadata { optional string etag = 30; optional string performer = 31; optional string grouping = 32; + optional string lyrics = 33; } message ReadFileRequest { diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 355b605db..7642a30f9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -313,6 +313,7 @@ set(SOURCES songinfo/songkickconcerts.cpp songinfo/songkickconcertwidget.cpp songinfo/songplaystats.cpp + songinfo/taglyricsinfoprovider.cpp songinfo/ultimatelyricslyric.cpp songinfo/ultimatelyricsprovider.cpp songinfo/ultimatelyricsreader.cpp @@ -605,6 +606,7 @@ set(HEADERS songinfo/songkickconcerts.h songinfo/songkickconcertwidget.h songinfo/songplaystats.h + songinfo/taglyricsinfoprovider.h songinfo/ultimatelyricslyric.h songinfo/ultimatelyricsprovider.h songinfo/ultimatelyricsreader.h diff --git a/src/core/database.cpp b/src/core/database.cpp index e3bbee56a..3daab7a75 100644 --- a/src/core/database.cpp +++ b/src/core/database.cpp @@ -47,7 +47,7 @@ #include const char* Database::kDatabaseFilename = "clementine.db"; -const int Database::kSchemaVersion = 48; +const int Database::kSchemaVersion = 49; const char* Database::kMagicAllSongsTables = "%allsongstables"; int Database::sNextConnectionId = 1; diff --git a/src/core/metatypes.cpp b/src/core/metatypes.cpp index daf5b71f7..f02b4d51c 100644 --- a/src/core/metatypes.cpp +++ b/src/core/metatypes.cpp @@ -32,11 +32,12 @@ #include "globalsearch/searchprovider.h" #include "internet/digitally/digitallyimportedclient.h" #include "internet/core/geolocator.h" +#include "internet/podcasts/podcastepisode.h" +#include "internet/podcasts/podcast.h" #include "internet/somafm/somafmservice.h" #include "library/directory.h" #include "playlist/playlist.h" -#include "internet/podcasts/podcastepisode.h" -#include "internet/podcasts/podcast.h" +#include "songinfo/collapsibleinfopane.h" #include "ui/equalizer.h" #ifdef HAVE_VK @@ -53,6 +54,7 @@ class GstEnginePipeline; class QNetworkReply; void RegisterMetaTypes() { + qRegisterMetaType("CollapsibleInfoPane::Data"); qRegisterMetaType("ColumnAlignmentMap"); qRegisterMetaType("const char*"); qRegisterMetaType("CoverSearchResult"); @@ -74,16 +76,16 @@ void RegisterMetaTypes() { qRegisterMetaType("PlaylistItemPtr"); qRegisterMetaType("PodcastEpisodeList"); qRegisterMetaType("PodcastList"); - qRegisterMetaType >("QList"); - qRegisterMetaType >("QList"); + qRegisterMetaType>("QList"); + qRegisterMetaType>("QList"); qRegisterMetaType( "PlaylistSequence::RepeatMode"); qRegisterMetaType( "PlaylistSequence::ShuffleMode"); - qRegisterMetaType >("QList"); - qRegisterMetaType >("QList"); - qRegisterMetaType >("QList"); - qRegisterMetaType >("QList"); + qRegisterMetaType>("QList"); + qRegisterMetaType>("QList"); + qRegisterMetaType>("QList"); + qRegisterMetaType>("QList"); qRegisterMetaType("QNetworkCookie"); qRegisterMetaType("QNetworkReply*"); qRegisterMetaType("QNetworkReply**"); @@ -97,12 +99,12 @@ void RegisterMetaTypes() { qRegisterMetaTypeStreamOperators( "DigitallyImportedClient::Channel"); qRegisterMetaTypeStreamOperators("Equalizer::Params"); - qRegisterMetaTypeStreamOperators >("ColumnAlignmentMap"); + qRegisterMetaTypeStreamOperators>("ColumnAlignmentMap"); qRegisterMetaTypeStreamOperators( "SomaFMService::Stream"); qRegisterMetaType("SubdirectoryList"); qRegisterMetaType("Subdirectory"); - qRegisterMetaType >("QList"); + qRegisterMetaType>("QList"); #ifdef HAVE_VK qRegisterMetaType("MusicOwner"); @@ -113,7 +115,7 @@ void RegisterMetaTypes() { qDBusRegisterMetaType(); qDBusRegisterMetaType(); qDBusRegisterMetaType(); - qDBusRegisterMetaType >(); + qDBusRegisterMetaType>(); qDBusRegisterMetaType(); qDBusRegisterMetaType(); qDBusRegisterMetaType(); diff --git a/src/core/mpris1.cpp b/src/core/mpris1.cpp index 6f95b5af4..c56355341 100644 --- a/src/core/mpris1.cpp +++ b/src/core/mpris1.cpp @@ -331,6 +331,7 @@ QVariantMap Mpris1::GetMetadata(const Song& song) { AddMetadata("composer", song.composer(), &ret); AddMetadata("performer", song.performer(), &ret); AddMetadata("grouping", song.grouping(), &ret); + AddMetadata("lyrics", song.lyrics(), &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 6d6215561..8be7b730e 100644 --- a/src/core/organiseformat.cpp +++ b/src/core/organiseformat.cpp @@ -50,7 +50,8 @@ const QStringList OrganiseFormat::kKnownTags = QStringList() << "title" << "samplerate" << "extension" << "performer" - << "grouping"; + << "grouping" + << "lyrics"; // From http://en.wikipedia.org/wiki/8.3_filename#Directory_table const char OrganiseFormat::kInvalidFatCharacters[] = "\"*/\\:<>?|"; @@ -191,6 +192,8 @@ QString OrganiseFormat::TagValue(const QString& tag, const Song& song) const { value = song.performer(); else if (tag == "grouping") value = song.grouping(); + else if (tag == "lyrics") + value = song.lyrics(); else if (tag == "genre") value = song.genre(); else if (tag == "comment") diff --git a/src/core/song.cpp b/src/core/song.cpp index 36ac7354c..b5a617167 100644 --- a/src/core/song.cpp +++ b/src/core/song.cpp @@ -111,7 +111,8 @@ const QStringList Song::kColumns = QStringList() << "title" << "effective_albumartist" << "etag" << "performer" - << "grouping"; + << "grouping" + << "lyrics"; const QString Song::kColumnSpec = Song::kColumns.join(", "); const QString Song::kBindSpec = @@ -151,6 +152,7 @@ struct Song::Private : public QSharedData { QString composer_; QString performer_; QString grouping_; + QString lyrics_; int track_; int disc_; float bpm_; @@ -278,6 +280,7 @@ const QString& Song::playlist_albumartist() const { const QString& Song::composer() const { return d->composer_; } const QString& Song::performer() const { return d->performer_; } const QString& Song::grouping() const { return d->grouping_; } +const QString& Song::lyrics() const { return d->lyrics_; } int Song::track() const { return d->track_; } int Song::disc() const { return d->disc_; } float Song::bpm() const { return d->bpm_; } @@ -334,6 +337,7 @@ 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_lyrics(const QString& v) { d->lyrics_ = 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; } @@ -490,6 +494,7 @@ void Song::InitFromProtobuf(const pb::tagreader::SongMetadata& pb) { d->composer_ = QStringFromStdString(pb.composer()); d->performer_ = QStringFromStdString(pb.performer()); d->grouping_ = QStringFromStdString(pb.grouping()); + d->lyrics_ = QStringFromStdString(pb.lyrics()); d->track_ = pb.track(); d->disc_ = pb.disc(); d->bpm_ = pb.bpm(); @@ -535,6 +540,7 @@ void Song::ToProtobuf(pb::tagreader::SongMetadata* pb) const { pb->set_composer(DataCommaSizeFromQString(d->composer_)); pb->set_performer(DataCommaSizeFromQString(d->performer_)); pb->set_grouping(DataCommaSizeFromQString(d->grouping_)); + pb->set_lyrics(DataCommaSizeFromQString(d->lyrics_)); pb->set_track(d->track_); pb->set_disc(d->disc_); pb->set_bpm(d->bpm_); @@ -557,7 +563,7 @@ void Song::ToProtobuf(pb::tagreader::SongMetadata* pb) const { pb->set_filesize(d->filesize_); pb->set_suspicious_tags(d->suspicious_tags_); pb->set_art_automatic(DataCommaSizeFromQString(d->art_automatic_)); - pb->set_type(static_cast< ::pb::tagreader::SongMetadata_Type>(d->filetype_)); + pb->set_type(static_cast<::pb::tagreader::SongMetadata_Type>(d->filetype_)); } void Song::InitFromQuery(const SqlRow& q, bool reliable_metadata, int col) { @@ -625,6 +631,7 @@ void Song::InitFromQuery(const SqlRow& q, bool reliable_metadata, int col) { d->performer_ = tostr(col + 38); d->grouping_ = tostr(col + 39); + d->lyrics_ = tostr(col + 40); InitArtManual(); @@ -910,9 +917,8 @@ void Song::BindToQuery(QSqlQuery* query) const { if (Application::kIsPortable && Utilities::UrlOnSameDriveAsClementine(d->url_)) { - query->bindValue(":filename", - Utilities:: - GetRelativePathToClementineBin(d->url_).toEncoded()); + query->bindValue(":filename", Utilities::GetRelativePathToClementineBin( + d->url_).toEncoded()); } else { query->bindValue(":filename", d->url_.toEncoded()); } @@ -950,6 +956,7 @@ void Song::BindToQuery(QSqlQuery* query) const { query->bindValue(":performer", strval(d->performer_)); query->bindValue(":grouping", strval(d->grouping_)); + query->bindValue(":lyrics", strval(d->lyrics_)); #undef intval #undef notnullintval @@ -1058,7 +1065,8 @@ bool Song::IsMetadataEqual(const Song& other) const { d->samplerate_ == other.d->samplerate_ && d->art_automatic_ == other.d->art_automatic_ && d->art_manual_ == other.d->art_manual_ && - d->cue_path_ == other.d->cue_path_; + d->cue_path_ == other.d->cue_path_ && + d->lyrics_ == other.d->lyrics_; } bool Song::IsEditable() const { diff --git a/src/core/song.h b/src/core/song.h index 938429fbd..d3c08b71e 100644 --- a/src/core/song.h +++ b/src/core/song.h @@ -171,6 +171,7 @@ class Song { const QString& composer() const; const QString& performer() const; const QString& grouping() const; + const QString& lyrics() const; int track() const; int disc() const; float bpm() const; @@ -249,6 +250,7 @@ class Song { void set_composer(const QString& v); void set_performer(const QString& v); void set_grouping(const QString& v); + void set_lyrics(const QString& v); void set_track(int v); void set_disc(int v); void set_bpm(float v); diff --git a/src/songinfo/songinfofetcher.cpp b/src/songinfo/songinfofetcher.cpp index f9a6f0463..165914df5 100644 --- a/src/songinfo/songinfofetcher.cpp +++ b/src/songinfo/songinfofetcher.cpp @@ -32,10 +32,13 @@ SongInfoFetcher::SongInfoFetcher(QObject* parent) void SongInfoFetcher::AddProvider(SongInfoProvider* provider) { providers_ << provider; - connect(provider, SIGNAL(ImageReady(int, QUrl)), SLOT(ImageReady(int, QUrl))); + connect(provider, SIGNAL(ImageReady(int, QUrl)), SLOT(ImageReady(int, QUrl)), + Qt::QueuedConnection); connect(provider, SIGNAL(InfoReady(int, CollapsibleInfoPane::Data)), - SLOT(InfoReady(int, CollapsibleInfoPane::Data))); - connect(provider, SIGNAL(Finished(int)), SLOT(ProviderFinished(int))); + SLOT(InfoReady(int, CollapsibleInfoPane::Data)), + Qt::QueuedConnection); + connect(provider, SIGNAL(Finished(int)), SLOT(ProviderFinished(int)), + Qt::QueuedConnection); } int SongInfoFetcher::FetchInfo(const Song& metadata) { diff --git a/src/songinfo/songinfoview.cpp b/src/songinfo/songinfoview.cpp index 076512ae0..7e8ddb347 100644 --- a/src/songinfo/songinfoview.cpp +++ b/src/songinfo/songinfoview.cpp @@ -18,6 +18,7 @@ #include "config.h" #include "songinfoprovider.h" #include "songinfoview.h" +#include "taglyricsinfoprovider.h" #include "ultimatelyricsprovider.h" #include "ultimatelyricsreader.h" @@ -48,6 +49,7 @@ SongInfoView::SongInfoView(QWidget* parent) #ifdef HAVE_LIBLASTFM fetcher_->AddProvider(new LastfmTrackInfoProvider); #endif + fetcher_->AddProvider(new TagLyricsInfoProvider); } SongInfoView::~SongInfoView() {} diff --git a/src/songinfo/taglyricsinfoprovider.cpp b/src/songinfo/taglyricsinfoprovider.cpp new file mode 100644 index 000000000..587eb3040 --- /dev/null +++ b/src/songinfo/taglyricsinfoprovider.cpp @@ -0,0 +1,39 @@ +/* This file is part of Clementine. + Copyright 2010, David Sansome + + Clementine is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Clementine is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Clementine. If not, see . +*/ + +#include "songinfotextview.h" +#include "taglyricsinfoprovider.h" +#include "core/logging.h" + +void TagLyricsInfoProvider::FetchInfo(int id, const Song& metadata) { + QString lyrics; + lyrics = metadata.lyrics(); + + if (!lyrics.isEmpty()) { + CollapsibleInfoPane::Data data; + data.id_ = "tag/lyrics"; + data.title_ = tr("Lyrics from the ID3v2 tag"); + data.type_ = CollapsibleInfoPane::Data::Type_Lyrics; + + SongInfoTextView* editor = new SongInfoTextView; + editor->setPlainText(lyrics); + data.contents_ = editor; + + emit InfoReady(id, data); + } + emit Finished(id); +} diff --git a/src/songinfo/taglyricsinfoprovider.h b/src/songinfo/taglyricsinfoprovider.h new file mode 100644 index 000000000..cb9e7aca2 --- /dev/null +++ b/src/songinfo/taglyricsinfoprovider.h @@ -0,0 +1,30 @@ +/* This file is part of Clementine. + Copyright 2010, David Sansome + + Clementine is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Clementine is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Clementine. If not, see . +*/ + +#ifndef TAGLYRICSINFOPROVIDER_H +#define TAGLYRICSINFOPROVIDER_H + +#include "songinfoprovider.h" + +class TagLyricsInfoProvider : public SongInfoProvider { + Q_OBJECT + + public: + void FetchInfo(int id, const Song& metadata); +}; + +#endif // TAGLYRICSINFOPROVIDER_H diff --git a/src/ui/mainwindow.cpp b/src/ui/mainwindow.cpp index 50f9689f3..1956dbb75 100644 --- a/src/ui/mainwindow.cpp +++ b/src/ui/mainwindow.cpp @@ -2675,6 +2675,7 @@ void MainWindow::HandleNotificationPreview(OSD::Behaviour type, QString line1, fake.set_genre("Classical"); fake.set_composer("Anonymous"); fake.set_performer("Anonymous"); + fake.set_lyrics("None"); fake.set_track(1); fake.set_disc(1); fake.set_year(2011); diff --git a/src/ui/organisedialog.cpp b/src/ui/organisedialog.cpp index 250b1992b..b744d0439 100644 --- a/src/ui/organisedialog.cpp +++ b/src/ui/organisedialog.cpp @@ -65,6 +65,7 @@ OrganiseDialog::OrganiseDialog(TaskManager* task_manager, QWidget* parent) tags[tr("Composer")] = "composer"; tags[tr("Performer")] = "performer"; tags[tr("Grouping")] = "grouping"; + tags[tr("Lyrics")] = "lyrics"; tags[tr("Track")] = "track"; tags[tr("Disc")] = "disc"; tags[tr("BPM")] = "bpm"; @@ -315,7 +316,8 @@ void OrganiseDialog::showEvent(QShowEvent*) { ui_->replace_spaces->setChecked(s.value("replace_spaces", false).toBool()); ui_->replace_the->setChecked(s.value("replace_the", false).toBool()); ui_->overwrite->setChecked(s.value("overwrite", false).toBool()); - ui_->mark_as_listened->setChecked(s.value("mark_as_listened", false).toBool()); + ui_->mark_as_listened->setChecked( + s.value("mark_as_listened", false).toBool()); ui_->eject_after->setChecked(s.value("eject_after", false).toBool()); QString destination = s.value("destination").toString(); @@ -349,12 +351,11 @@ void OrganiseDialog::accept() { const bool copy = ui_->aftercopying->currentIndex() == 0; Organise* organise = new Organise( task_manager_, storage, format_, copy, ui_->overwrite->isChecked(), - ui_->mark_as_listened->isChecked(), - new_songs_info_, ui_->eject_after->isChecked()); + ui_->mark_as_listened->isChecked(), new_songs_info_, + ui_->eject_after->isChecked()); connect(organise, SIGNAL(Finished(QStringList)), SLOT(OrganiseFinished(QStringList))); - connect(organise, SIGNAL(FileCopied(int)), - this, SIGNAL(FileCopied(int))); + connect(organise, SIGNAL(FileCopied(int)), this, SIGNAL(FileCopied(int))); organise->Start(); QDialog::accept(); diff --git a/src/widgets/osd.cpp b/src/widgets/osd.cpp index 87f6ddb23..a1cbf117e 100644 --- a/src/widgets/osd.cpp +++ b/src/widgets/osd.cpp @@ -337,6 +337,8 @@ QString OSD::ReplaceVariable(const QString& variable, const Song& song) { return song.performer(); } else if (variable == "%grouping%") { return song.grouping(); + } else if (variable == "%lyrics%") { + return song.lyrics(); } else if (variable == "%length%") { return song.PrettyLength(); } else if (variable == "%disc%") {