Added support for reading lyrics from tags

Also fixed tagreader crash when saving tags to MP3 files
This commit is contained in:
Jonas Kvinge 2018-09-06 20:04:29 +02:00
parent 0a64a2a394
commit 1562585561
11 changed files with 148 additions and 77 deletions

View File

@ -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:

View File

@ -1,7 +1,8 @@
<RCC>
<qresource prefix="/">
<file>schema/schema.sql</file>
<file>schema/schema-1.sql</file>
<file>schema/schema-1.sql</file>
<file>schema/schema-2.sql</file>
<file>schema/device-schema.sql</file>
<file>style/strawberry.css</file>
<file>misc/playing_tooltip.txt</file>

View File

@ -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,

View File

@ -39,6 +39,7 @@
#include <taglib/textidentificationframe.h>
#include <taglib/xiphcomment.h>
#include <taglib/commentsframe.h>
#include <taglib/unsynchronizedlyricsframe.h>
#include <taglib/tag.h>
#include <taglib/id3v2tag.h>
#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<TagLib::FLAC::File *>(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<TagLib::WavPack::File *>(fileref->file())) {
song->set_bitdepth(file->audioProperties()->bitsPerSample());
Decode(tag->comment(), nullptr, song->mutable_comment());
}
else if (TagLib::MPEG::File *file = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
if (TagLib::MPEG::File *file = dynamic_cast<TagLib::MPEG::File*>(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<TagLib::FLAC::File *>(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<TagLib::MP4::File*>(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<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
if (!fileref || fileref->isNull()) // The file probably doesn't exist
return false;
std::unique_ptr<TagLib::FileRef> 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<TagLib::MPEG::File*>(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<TagLib::FLAC::File*>(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<TagLib::MPEG::File*>(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<TagLib::FLAC::File*>(ref.file());
if (flac_file && flac_file->xiphComment()) {
TagLib::List<TagLib::FLAC::Picture*> 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<TagLib::ID3v2::AttachedPictureFrame*>(apic_frames.front());
std::list<TagLib::FLAC::Picture*>::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<TagLib::Ogg::XiphComment*>(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<TagLib::FLAC::File*>(ref.file());
if (flac_file && flac_file->xiphComment()) {
TagLib::List<TagLib::FLAC::Picture*> 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<TagLib::MPEG::File*>(ref.file());
if (file && file->ID3v2Tag()) {
TagLib::ID3v2::FrameList apic_frames = file->ID3v2Tag()->frameListMap()["APIC"];
if (apic_frames.isEmpty())
return QByteArray();
std::list<TagLib::FLAC::Picture*>::iterator it = pics.begin();
TagLib::FLAC::Picture *picture = *it;
TagLib::ID3v2::AttachedPictureFrame *pic = static_cast<TagLib::ID3v2::AttachedPictureFrame*>(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<TagLib::MP4::File*>(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<TagLib::ByteVector> 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);
}
}

View File

@ -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:

View File

@ -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;
}

View File

@ -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();

View File

@ -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;

View File

@ -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")

View File

@ -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() &&

View File

@ -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);