From 15625855611c5f5c4b65dd30435f26757a0dc72f Mon Sep 17 00:00:00 2001 From: Jonas Kvinge Date: Thu, 6 Sep 2018 20:04:29 +0200 Subject: [PATCH] Added support for reading lyrics from tags Also fixed tagreader crash when saving tags to MP3 files --- Changelog | 2 + data/data.qrc | 3 +- data/schema/schema.sql | 4 +- ext/libstrawberry-tagreader/tagreader.cpp | 156 ++++++++++++------ ext/libstrawberry-tagreader/tagreader.h | 1 + .../tagreadermessages.proto | 37 +++-- src/context/contextview.cpp | 2 +- src/core/database.cpp | 2 +- src/core/organiseformat.cpp | 3 + src/core/song.cpp | 11 ++ src/core/song.h | 4 +- 11 files changed, 148 insertions(+), 77 deletions(-) diff --git a/Changelog b/Changelog index cf230de68..3a99d8761 100644 --- a/Changelog +++ b/Changelog @@ -20,6 +20,8 @@ Unreleased: * Made xine enabled only for window debug * Removed dead code * Added DSF and DSDIFF/DFF support + * Fixed tagreader crash when saving tags to MP3 files + * Added support for reading/writing lyrics to tags Version 0.2.1: diff --git a/data/data.qrc b/data/data.qrc index 2a80823ad..a24df5e34 100644 --- a/data/data.qrc +++ b/data/data.qrc @@ -1,7 +1,8 @@ schema/schema.sql - schema/schema-1.sql + schema/schema-1.sql + schema/schema-2.sql schema/device-schema.sql style/strawberry.css misc/playing_tooltip.txt diff --git a/data/schema/schema.sql b/data/schema/schema.sql index 83143be52..26e815cf2 100644 --- a/data/schema/schema.sql +++ b/data/schema/schema.sql @@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS schema_version ( DELETE FROM schema_version; -INSERT INTO schema_version (version) VALUES (1); +INSERT INTO schema_version (version) VALUES (2); CREATE TABLE IF NOT EXISTS directories ( path TEXT NOT NULL, @@ -35,6 +35,7 @@ CREATE TABLE IF NOT EXISTS songs ( performer TEXT NOT NULL, grouping TEXT NOT NULL, comment TEXT NOT NULL, + lyrics TEXT NOT NULL, beginning INTEGER NOT NULL DEFAULT 0, length INTEGER NOT NULL DEFAULT 0, @@ -109,6 +110,7 @@ CREATE TABLE IF NOT EXISTS playlist_items ( performer TEXT NOT NULL, grouping TEXT NOT NULL, comment TEXT NOT NULL, + lyrics TEXT NOT NULL, beginning INTEGER NOT NULL DEFAULT 0, length INTEGER NOT NULL DEFAULT 0, diff --git a/ext/libstrawberry-tagreader/tagreader.cpp b/ext/libstrawberry-tagreader/tagreader.cpp index 109d40d9d..452055c23 100644 --- a/ext/libstrawberry-tagreader/tagreader.cpp +++ b/ext/libstrawberry-tagreader/tagreader.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include "taglib/id3v2frame.h" @@ -157,15 +158,14 @@ void TagReader::ReadFile(const QString &filename, pb::tagreader::SongMetadata *s return; } + song->set_filetype(GuessFileType(fileref.get())); + if (fileref->audioProperties()) { song->set_bitrate(fileref->audioProperties()->bitrate()); song->set_samplerate(fileref->audioProperties()->sampleRate()); song->set_length_nanosec(fileref->audioProperties()->length() * kNsecPerSec); } - // Get the filetype if we can - song->set_filetype(GuessFileType(fileref.get())); - TagLib::Tag *tag = fileref->tag(); if (tag) { Decode(tag->title(), nullptr, song->mutable_title()); @@ -179,6 +179,7 @@ void TagReader::ReadFile(const QString &filename, pb::tagreader::SongMetadata *s QString disc; QString compilation; + QString lyrics; // Handle all the files which have VorbisComments (Ogg, OPUS, ...) in the same way; // apart, so we keep specific behavior for some formats by adding another "else if" block below. @@ -189,9 +190,29 @@ void TagReader::ReadFile(const QString &filename, pb::tagreader::SongMetadata *s if (!tag->pictureList().isEmpty()) song->set_art_automatic(kEmbeddedCover); #endif } + + if (TagLib::FLAC::File *file = dynamic_cast(fileref->file())) { + + song->set_bitdepth(file->audioProperties()->bitsPerSample()); + + if ( file->xiphComment() ) { + ParseOggTag(file->xiphComment()->fieldListMap(), nullptr, &disc, &compilation, song); +#ifdef TAGLIB_HAS_FLAC_PICTURELIST + if (!file->pictureList().isEmpty()) { + song->set_art_automatic(kEmbeddedCover); + } +#endif + } + Decode(tag->comment(), nullptr, song->mutable_comment()); + } + + else if (TagLib::WavPack::File *file = dynamic_cast(fileref->file())) { + song->set_bitdepth(file->audioProperties()->bitsPerSample()); + Decode(tag->comment(), nullptr, song->mutable_comment()); + } + + else if (TagLib::MPEG::File *file = dynamic_cast(fileref->file())) { - if (TagLib::MPEG::File *file = dynamic_cast(fileref->file())) { - if (file->ID3v2Tag()) { const TagLib::ID3v2::FrameListMap &map = file->ID3v2Tag()->frameListMap(); @@ -245,22 +266,9 @@ void TagReader::ReadFile(const QString &filename, pb::tagreader::SongMetadata *s } } - else if (TagLib::FLAC::File *file = dynamic_cast(fileref->file())) { - song->set_bitdepth(file->audioProperties()->bitsPerSample()); - - if ( file->xiphComment() ) { - ParseOggTag(file->xiphComment()->fieldListMap(), nullptr, &disc, &compilation, song); -#ifdef TAGLIB_HAS_FLAC_PICTURELIST - if (!file->pictureList().isEmpty()) { - song->set_art_automatic(kEmbeddedCover); - } -#endif - } - Decode(tag->comment(), nullptr, song->mutable_comment()); - } else if (TagLib::MP4::File *file = dynamic_cast(fileref->file())) { - + song->set_bitdepth(file->audioProperties()->bitsPerSample()); if (file->tag()) { @@ -347,6 +355,8 @@ void TagReader::ReadFile(const QString &filename, pb::tagreader::SongMetadata *s song->set_compilation(compilation.toInt() == 1); } + if (!lyrics.isEmpty()) song->set_lyrics(lyrics.toStdString()); + // Set integer fields to -1 if they're not valid #define SetDefault(field) if (song->field() <= 0) { song->set_##field(-1); } SetDefault(track); @@ -438,6 +448,11 @@ void TagReader::ParseOggTag(const TagLib::Ogg::FieldListMap &map, const QTextCod if (!map["FMPS_PLAYCOUNT"].isEmpty() && song->playcount() <= 0) song->set_playcount(TStringToQString( map["FMPS_PLAYCOUNT"].front() ).trimmed().toFloat()); + if (!map["LYRICS"].isEmpty()) + Decode(map["LYRICS"].front(), codec, song->mutable_lyrics()); + else if (!map["UNSYNCEDLYRICS"].isEmpty()) + Decode(map["UNSYNCEDLYRICS"].front(), codec, song->mutable_lyrics()); + } void TagReader::SetVorbisComments(TagLib::Ogg::XiphComment *vorbis_comments, const pb::tagreader::SongMetadata &song) const { @@ -453,6 +468,8 @@ void TagReader::SetVorbisComments(TagLib::Ogg::XiphComment *vorbis_comments, con vorbis_comments->addField("ALBUMARTIST", StdStringToTaglibString(song.albumartist()), true); vorbis_comments->removeField("ALBUM ARTIST"); + vorbis_comments->addField("LYRICS", StdStringToTaglibString(song.lyrics()), true); + vorbis_comments->removeField("UNSYNCEDLYRICS"); } @@ -481,15 +498,11 @@ pb::tagreader::SongMetadata_Type TagReader::GuessFileType(TagLib::FileRef *filer } bool TagReader::SaveFile(const QString &filename, const pb::tagreader::SongMetadata &song) const { - - if (filename.isNull()) return false; + if (filename.isNull() || filename.isEmpty()) return false; qLog(Debug) << "Saving tags to" << filename; - - std::unique_ptr fileref(factory_->GetFileRef(filename)); - - if (!fileref || fileref->isNull()) // The file probably doesn't exist - return false; + std::unique_ptr fileref(factory_->GetFileRef(filename));; + if (!fileref || fileref->isNull()) return false; fileref->tag()->setTitle(StdStringToTaglibString(song.title())); fileref->tag()->setArtist(StdStringToTaglibString(song.artist())); @@ -501,6 +514,7 @@ bool TagReader::SaveFile(const QString &filename, const pb::tagreader::SongMetad if (TagLib::MPEG::File *file = dynamic_cast(fileref->file())) { TagLib::ID3v2::Tag *tag = file->ID3v2Tag(true); + if (!tag) return false; SetTextFrame("TPOS", song.disc() <= 0 -1 ? QString() : QString::number(song.disc()), tag); SetTextFrame("TCOM", song.composer(), tag); SetTextFrame("TIT1", song.grouping(), tag); @@ -508,6 +522,7 @@ bool TagReader::SaveFile(const QString &filename, const pb::tagreader::SongMetad // 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); + SetUnsyncLyricsFrame(song.lyrics(), tag); } else if (TagLib::FLAC::File *file = dynamic_cast(fileref->file())) { TagLib::Ogg::XiphComment *tag = file->xiphComment(); @@ -589,10 +604,15 @@ void TagReader::SetTextFrame(const char *id, const std::string &value, TagLib::I frames_buffer.push_back(frame.render()); } - // add frame takes ownership and clears the memory - TagLib::ID3v2::TextIdentificationFrame *frame; - frame->setText(StdStringToTaglibString(value)); - tag->addFrame(frame); + // Update and add the frames + for (int lyrics_index = 0; lyrics_index < frames_buffer.size(); lyrics_index++) { + TagLib::ID3v2::TextIdentificationFrame* frame = new TagLib::ID3v2::TextIdentificationFrame(frames_buffer.at(lyrics_index)); + if (lyrics_index == 0) { + frame->setText(StdStringToTaglibString(value)); + } + // add frame takes ownership and clears the memory + tag->addFrame(frame); + } } @@ -619,23 +639,26 @@ QByteArray TagReader::LoadEmbeddedArt(const QString &filename) const { if (ref.isNull() || !ref.file()) return QByteArray(); - // MP3 - TagLib::MPEG::File *file = dynamic_cast(ref.file()); - if (file && file->ID3v2Tag()) { - TagLib::ID3v2::FrameList apic_frames = file->ID3v2Tag()->frameListMap()["APIC"]; - if (apic_frames.isEmpty()) - return QByteArray(); +#ifdef TAGLIB_HAS_FLAC_PICTURELIST + // FLAC + TagLib::FLAC::File *flac_file = dynamic_cast(ref.file()); + if (flac_file && flac_file->xiphComment()) { + TagLib::List pics = flac_file->pictureList(); + if (!pics.isEmpty()) { + // Use the first picture in the file - this could be made cleverer and + // pick the front cover if it's present. - TagLib::ID3v2::AttachedPictureFrame *pic = static_cast(apic_frames.front()); + std::list::iterator it = pics.begin(); + TagLib::FLAC::Picture *picture = *it; - return QByteArray((const char*) pic->picture().data(), pic->picture().size()); + return QByteArray(picture->data().data(), picture->data().size()); + } } +#endif - // Ogg vorbis/speex + // Ogg Vorbis / Speex TagLib::Ogg::XiphComment *xiph_comment = dynamic_cast(ref.file()->tag()); - if (xiph_comment) { - TagLib::Ogg::FieldListMap map = xiph_comment->fieldListMap(); #if TAGLIB_MAJOR_VERSION <= 1 && TAGLIB_MINOR_VERSION < 11 @@ -678,22 +701,17 @@ QByteArray TagReader::LoadEmbeddedArt(const QString &filename) const { return QByteArray::fromBase64(map["COVERART"].toString().toCString()); } -#ifdef TAGLIB_HAS_FLAC_PICTURELIST - // Flac - TagLib::FLAC::File *flac_file = dynamic_cast(ref.file()); - if (flac_file && flac_file->xiphComment()) { - TagLib::List pics = flac_file->pictureList(); - if (!pics.isEmpty()) { - // Use the first picture in the file - this could be made cleverer and - // pick the front cover if it's present. + // MP3 + TagLib::MPEG::File *file = dynamic_cast(ref.file()); + if (file && file->ID3v2Tag()) { + TagLib::ID3v2::FrameList apic_frames = file->ID3v2Tag()->frameListMap()["APIC"]; + if (apic_frames.isEmpty()) + return QByteArray(); - std::list::iterator it = pics.begin(); - TagLib::FLAC::Picture *picture = *it; + TagLib::ID3v2::AttachedPictureFrame *pic = static_cast(apic_frames.front()); - return QByteArray(picture->data().data(), picture->data().size()); - } + return QByteArray((const char*) pic->picture().data(), pic->picture().size()); } -#endif // MP4/AAC TagLib::MP4::File *aac_file = dynamic_cast(ref.file()); @@ -715,3 +733,33 @@ QByteArray TagReader::LoadEmbeddedArt(const QString &filename) const { return QByteArray(); } + +void TagReader::SetUnsyncLyricsFrame(const std::string& value, TagLib::ID3v2::Tag* tag) const { + + TagLib::ByteVector id_vector("USLT"); + QVector frames_buffer; + + // Store and clear existing frames + while (tag->frameListMap().contains(id_vector) && tag->frameListMap()[id_vector].size() != 0) { + frames_buffer.push_back(tag->frameListMap()[id_vector].front()->render()); + tag->removeFrame(tag->frameListMap()[id_vector].front()); + } + + // If no frames stored create empty frame + if (frames_buffer.isEmpty()) { + TagLib::ID3v2::UnsynchronizedLyricsFrame frame(TagLib::String::UTF8); + frame.setDescription("Clementine editor"); + frames_buffer.push_back(frame.render()); + } + + // Update and add the frames + for (int lyrics_index = 0; lyrics_index < frames_buffer.size(); lyrics_index++) { + TagLib::ID3v2::UnsynchronizedLyricsFrame* frame = new TagLib::ID3v2::UnsynchronizedLyricsFrame(frames_buffer.at(lyrics_index)); + if (lyrics_index == 0) { + frame->setText(StdStringToTaglibString(value)); + } + // add frame takes ownership and clears the memory + tag->addFrame(frame); + } + +} diff --git a/ext/libstrawberry-tagreader/tagreader.h b/ext/libstrawberry-tagreader/tagreader.h index 21fb998e1..3cd94d688 100644 --- a/ext/libstrawberry-tagreader/tagreader.h +++ b/ext/libstrawberry-tagreader/tagreader.h @@ -71,6 +71,7 @@ class TagReader { void SetTextFrame(const char *id, const QString &value, TagLib::ID3v2::Tag *tag) const; void SetTextFrame(const char *id, const std::string &value, TagLib::ID3v2::Tag *tag) const; + void SetUnsyncLyricsFrame(const std::string& value, TagLib::ID3v2::Tag* tag) const; private: diff --git a/ext/libstrawberry-tagreader/tagreadermessages.proto b/ext/libstrawberry-tagreader/tagreadermessages.proto index 068b55f91..c54206c15 100644 --- a/ext/libstrawberry-tagreader/tagreadermessages.proto +++ b/ext/libstrawberry-tagreader/tagreadermessages.proto @@ -26,7 +26,7 @@ message SongMetadata { } optional bool valid = 1; - + optional string title = 2; optional string album = 3; optional string artist = 4; @@ -41,26 +41,27 @@ message SongMetadata { optional string performer = 13; optional string grouping = 14; optional string comment = 15; - - optional uint64 length_nanosec = 16; - - optional int32 bitrate = 17; - optional int32 samplerate = 18; - optional int32 bitdepth = 19; + optional string lyrics = 16; - optional string url = 20; - optional string basefilename = 21; - optional Type filetype = 22; - optional int32 filesize = 23; - optional int32 mtime = 24; - optional int32 ctime = 25; + optional uint64 length_nanosec = 17; - optional int32 playcount = 26; - optional int32 skipcount = 27; - optional int32 lastplayed = 28; + optional int32 bitrate = 18; + optional int32 samplerate = 19; + optional int32 bitdepth = 20; - optional bool suspicious_tags = 29; - optional string art_automatic = 30; + optional string url = 21; + optional string basefilename = 22; + optional Type filetype = 23; + optional int32 filesize = 24; + optional int32 mtime = 25; + optional int32 ctime = 26; + + optional int32 playcount = 27; + optional int32 skipcount = 28; + optional int32 lastplayed = 29; + + optional bool suspicious_tags = 30; + optional string art_automatic = 31; } diff --git a/src/context/contextview.cpp b/src/context/contextview.cpp index 1618b31cb..61ba8b9ba 100644 --- a/src/context/contextview.cpp +++ b/src/context/contextview.cpp @@ -192,7 +192,7 @@ void ContextView::SongChanged(const Song &song) { image_previous_ = image_original_; prev_artist_ = song_playing_.artist(); - lyrics_ = QString(); + lyrics_ = song.lyrics(); song_playing_ = song; song_ = song; UpdateSong(); diff --git a/src/core/database.cpp b/src/core/database.cpp index 8b22e330c..99b13fb35 100644 --- a/src/core/database.cpp +++ b/src/core/database.cpp @@ -52,7 +52,7 @@ #include "scopedtransaction.h" const char *Database::kDatabaseFilename = "strawberry.db"; -const int Database::kSchemaVersion = 1; +const int Database::kSchemaVersion = 2; const char *Database::kMagicAllSongsTables = "%allsongstables"; int Database::sNextConnectionId = 1; diff --git a/src/core/organiseformat.cpp b/src/core/organiseformat.cpp index 604675b58..b807a7ec9 100644 --- a/src/core/organiseformat.cpp +++ b/src/core/organiseformat.cpp @@ -65,6 +65,7 @@ const QStringList OrganiseFormat::kKnownTags = QStringList() << "title" << "extension" << "performer" << "grouping" + << "lyrics" << "originalyear"; // From http://en.wikipedia.org/wiki/8.3_filename#Directory_table @@ -200,6 +201,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 f8146b6b7..8a6eb2d0d 100644 --- a/src/core/song.cpp +++ b/src/core/song.cpp @@ -77,6 +77,7 @@ const QStringList Song::kColumns = QStringList() << "title" << "performer" << "grouping" << "comment" + << "lyrics" << "beginning" << "length" @@ -157,6 +158,7 @@ struct Song::Private : public QSharedData { QString performer_; QString grouping_; QString comment_; + QString lyrics_; qint64 beginning_; qint64 end_; @@ -265,6 +267,7 @@ int Song::effective_originalyear() const { } const QString &Song::genre() const { return d->genre_; } const QString &Song::comment() const { return d->comment_; } +const QString &Song::lyrics() const { return d->lyrics_; } bool Song::is_compilation() const { return (d->compilation_ || d->compilation_detected_ || d->compilation_on_) && ! d->compilation_off_; } @@ -318,6 +321,7 @@ 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_comment(const QString &v) { d->comment_ = v; } +void Song::set_lyrics(const QString &v) { d->lyrics_ = v; } void Song::set_beginning_nanosec(qint64 v) { d->beginning_ = qMax(0ll, v); } void Song::set_end_nanosec(qint64 v) { d->end_ = v; } @@ -463,6 +467,7 @@ void Song::InitFromProtobuf(const pb::tagreader::SongMetadata &pb) { d->originalyear_ = pb.originalyear(); d->genre_ = QStringFromStdString(pb.genre()); d->comment_ = QStringFromStdString(pb.comment()); + d->lyrics_ = QStringFromStdString(pb.lyrics()); d->compilation_ = pb.compilation(); d->skipcount_ = pb.skipcount(); d->lastplayed_ = pb.lastplayed(); @@ -502,6 +507,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_year(d->year_); @@ -590,6 +596,9 @@ void Song::InitFromQuery(const SqlRow &q, bool reliable_metadata, int col) { else if (Song::kColumns.value(i) == "comment") { d->comment_ = tostr(x); } + else if (Song::kColumns.value(i) == "lyrics") { + d->comment_ = tostr(x); + } else if (Song::kColumns.value(i) == "beginning") { d->beginning_ = q.value(x).isNull() ? 0 : q.value(x).toLongLong(); @@ -929,6 +938,7 @@ void Song::BindToQuery(QSqlQuery *query) const { query->bindValue(":performer", strval(d->performer_)); query->bindValue(":grouping", strval(d->grouping_)); query->bindValue(":comment", strval(d->comment_)); + query->bindValue(":lyrics", strval(d->lyrics_)); query->bindValue(":beginning", d->beginning_); query->bindValue(":length", intval(length_nanosec())); @@ -1062,6 +1072,7 @@ bool Song::IsMetadataEqual(const Song &other) const { d->originalyear_ == other.d->originalyear_ && d->genre_ == other.d->genre_ && d->comment_ == other.d->comment_ && + d->lyrics_ == other.d->lyrics_ && d->compilation_ == other.d->compilation_ && d->beginning_ == other.d->beginning_ && length_nanosec() == other.length_nanosec() && diff --git a/src/core/song.h b/src/core/song.h index b8e6b6167..81355f17e 100644 --- a/src/core/song.h +++ b/src/core/song.h @@ -167,6 +167,7 @@ class Song { const QString &performer() const; const QString &grouping() const; const QString &comment() const; + const QString &lyrics() const; int playcount() const; int skipcount() const; @@ -250,7 +251,8 @@ class Song { void set_performer(const QString &v); void set_grouping(const QString &v); void set_comment(const QString &v); - + void set_lyrics(const QString &v); + void set_beginning_nanosec(qint64 v); void set_end_nanosec(qint64 v); void set_length_nanosec(qint64 v);