mirror of
https://github.com/strawberrymusicplayer/strawberry
synced 2025-01-09 06:44:24 +01:00
e8be0adf37
TagLib will have created a valid ID3v2 tag on this file by this point in the code, due to the way it handles the tag() method for WAV::File. Thus the null pointer check is redundant and the hasID3v2() call is at best redundant and at worst will cause tags to not save when they otherwise should have
1666 lines
67 KiB
C++
1666 lines
67 KiB
C++
/* This file is part of Strawberry.
|
|
Copyright 2013, David Sansome <me@davidsansome.com>
|
|
Copyright 2018-2023, Jonas Kvinge <jonas@jkvinge.net>
|
|
|
|
Strawberry 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.
|
|
|
|
Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include "tagreadertaglib.h"
|
|
|
|
#include <string>
|
|
#include <memory>
|
|
#include <algorithm>
|
|
#include <sys/stat.h>
|
|
|
|
#include <taglib/taglib.h>
|
|
#include <taglib/fileref.h>
|
|
#include <taglib/tbytevector.h>
|
|
#include <taglib/tfile.h>
|
|
#include <taglib/tlist.h>
|
|
#include <taglib/tstring.h>
|
|
#include <taglib/tstringlist.h>
|
|
#include <taglib/tpropertymap.h>
|
|
#include <taglib/audioproperties.h>
|
|
#include <taglib/xiphcomment.h>
|
|
#include <taglib/tag.h>
|
|
#include <taglib/apetag.h>
|
|
#include <taglib/apeitem.h>
|
|
#include <taglib/apeproperties.h>
|
|
#include <taglib/id3v2tag.h>
|
|
#include <taglib/id3v2frame.h>
|
|
#include <taglib/attachedpictureframe.h>
|
|
#include <taglib/textidentificationframe.h>
|
|
#include <taglib/unsynchronizedlyricsframe.h>
|
|
#include <taglib/popularimeterframe.h>
|
|
#include <taglib/uniquefileidentifierframe.h>
|
|
#include <taglib/commentsframe.h>
|
|
#include <taglib/flacfile.h>
|
|
#include <taglib/oggflacfile.h>
|
|
#include <taglib/flacproperties.h>
|
|
#include <taglib/flacpicture.h>
|
|
#include <taglib/vorbisfile.h>
|
|
#include <taglib/speexfile.h>
|
|
#include <taglib/wavfile.h>
|
|
#include <taglib/wavpackfile.h>
|
|
#include <taglib/wavpackproperties.h>
|
|
#include <taglib/aifffile.h>
|
|
#include <taglib/asffile.h>
|
|
#include <taglib/asftag.h>
|
|
#include <taglib/asfattribute.h>
|
|
#include <taglib/asfproperties.h>
|
|
#include <taglib/mp4file.h>
|
|
#include <taglib/mp4tag.h>
|
|
#include <taglib/mp4item.h>
|
|
#include <taglib/mp4coverart.h>
|
|
#include <taglib/mp4properties.h>
|
|
#include <taglib/mpcfile.h>
|
|
#include <taglib/mpegfile.h>
|
|
#include <taglib/opusfile.h>
|
|
#include <taglib/trueaudiofile.h>
|
|
#include <taglib/apefile.h>
|
|
#include <taglib/modfile.h>
|
|
#include <taglib/s3mfile.h>
|
|
#include <taglib/xmfile.h>
|
|
#include <taglib/itfile.h>
|
|
#ifdef HAVE_TAGLIB_DSFFILE
|
|
# include <taglib/dsffile.h>
|
|
#endif
|
|
#ifdef HAVE_TAGLIB_DSDIFFFILE
|
|
# include <taglib/dsdifffile.h>
|
|
#endif
|
|
|
|
#include <QtGlobal>
|
|
#include <QVector>
|
|
#include <QByteArray>
|
|
#include <QString>
|
|
#include <QStringList>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QUrl>
|
|
#include <QDateTime>
|
|
#include <QtDebug>
|
|
|
|
#include "core/logging.h"
|
|
#include "core/messagehandler.h"
|
|
#include "utilities/timeconstants.h"
|
|
|
|
class FileRefFactory {
|
|
public:
|
|
FileRefFactory() = default;
|
|
virtual ~FileRefFactory() = default;
|
|
virtual TagLib::FileRef *GetFileRef(const QString &filename) = 0;
|
|
|
|
private:
|
|
Q_DISABLE_COPY(FileRefFactory)
|
|
};
|
|
|
|
class TagLibFileRefFactory : public FileRefFactory {
|
|
public:
|
|
TagLibFileRefFactory() = default;
|
|
TagLib::FileRef *GetFileRef(const QString &filename) override {
|
|
#ifdef Q_OS_WIN32
|
|
return new TagLib::FileRef(filename.toStdWString().c_str());
|
|
#else
|
|
return new TagLib::FileRef(QFile::encodeName(filename).constData());
|
|
#endif
|
|
}
|
|
|
|
private:
|
|
Q_DISABLE_COPY(TagLibFileRefFactory)
|
|
};
|
|
|
|
namespace {
|
|
|
|
TagLib::String StdStringToTaglibString(const std::string &s) {
|
|
return TagLib::String(s.c_str(), TagLib::String::UTF8);
|
|
}
|
|
|
|
TagLib::String QStringToTaglibString(const QString &s) {
|
|
return TagLib::String(s.toUtf8().constData(), TagLib::String::UTF8);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
namespace {
|
|
|
|
constexpr char kID3v2_AcoustID_ID[] = "Acoustid Id";
|
|
constexpr char kID3v2_AcoustID_Fingerprint[] = "Acoustid Fingerprint";
|
|
constexpr char kID3v2_MusicBrainz_AlbumArtistID[] = "MusicBrainz Album Artist Id";
|
|
constexpr char kID3v2_MusicBrainz_ArtistID[] = "MusicBrainz Artist Id";
|
|
constexpr char kID3v2_MusicBrainz_OriginalArtistID[] = "MusicBrainz Original Artist Id";
|
|
constexpr char kID3v2_MusicBrainz_AlbumID[] = "MusicBrainz Album Id";
|
|
constexpr char kID3v2_MusicBrainz_OriginalAlbumID[] = "MusicBrainz Original Album Id";
|
|
constexpr char kID3v2_MusicBrainz_RecordingID[] = "MUSICBRAINZ_TRACKID";
|
|
constexpr char kID3v2_MusicBrainz_TrackID[] = "MusicBrainz Release Track Id";
|
|
constexpr char kID3v2_MusicBrainz_DiscID[] = "MusicBrainz Disc Id";
|
|
constexpr char kID3v2_MusicBrainz_ReleaseGroupID[] = "MusicBrainz Release Group Id";
|
|
constexpr char kID3v2_MusicBrainz_WorkID[] = "MusicBrainz Work Id";
|
|
|
|
constexpr char kMP4_OriginalYear_ID[] = "----:com.apple.iTunes:ORIGINAL YEAR";
|
|
constexpr char kMP4_FMPS_Playcount_ID[] = "----:com.apple.iTunes:FMPS_Playcount";
|
|
constexpr char kMP4_FMPS_Rating_ID[] = "----:com.apple.iTunes:FMPS_Rating";
|
|
constexpr char kMP4_AcoustID_ID[] = "----:com.apple.iTunes:Acoustid Id";
|
|
constexpr char kMP4_AcoustID_Fingerprint[] = "----:com.apple.iTunes:Acoustid Fingerprint";
|
|
constexpr char kMP4_MusicBrainz_AlbumArtistID[] = "----:com.apple.iTunes:MusicBrainz Album Artist Id";
|
|
constexpr char kMP4_MusicBrainz_ArtistID[] = "----:com.apple.iTunes:MusicBrainz Artist Id";
|
|
constexpr char kMP4_MusicBrainz_OriginalArtistID[] = "----:com.apple.iTunes:MusicBrainz Original Artist Id";
|
|
constexpr char kMP4_MusicBrainz_AlbumID[] = "----:com.apple.iTunes:MusicBrainz Album Id";
|
|
constexpr char kMP4_MusicBrainz_OriginalAlbumID[] = "----:com.apple.iTunes:MusicBrainz Original Album Id";
|
|
constexpr char kMP4_MusicBrainz_RecordingID[] = "----:com.apple.iTunes:MusicBrainz Track Id";
|
|
constexpr char kMP4_MusicBrainz_TrackID[] = "----:com.apple.iTunes:MusicBrainz Release Track Id";
|
|
constexpr char kMP4_MusicBrainz_DiscID[] = "----:com.apple.iTunes:MusicBrainz Disc Id";
|
|
constexpr char kMP4_MusicBrainz_ReleaseGroupID[] = "----:com.apple.iTunes:MusicBrainz Release Group Id";
|
|
constexpr char kMP4_MusicBrainz_WorkID[] = "----:com.apple.iTunes:MusicBrainz Work Id";
|
|
|
|
constexpr char kASF_OriginalDate_ID[] = "WM/OriginalReleaseTime";
|
|
constexpr char kASF_OriginalYear_ID[] = "WM/OriginalReleaseYear";
|
|
constexpr char kASF_AcoustID_ID[] = "Acoustid/Id";
|
|
constexpr char kASF_AcoustID_Fingerprint[] = "Acoustid/Fingerprint";
|
|
constexpr char kASF_MusicBrainz_AlbumArtistID[] = "MusicBrainz/Album Artist Id";
|
|
constexpr char kASF_MusicBrainz_ArtistID[] = "MusicBrainz/Artist Id";
|
|
constexpr char kASF_MusicBrainz_OriginalArtistID[] = "MusicBrainz/Original Artist Id";
|
|
constexpr char kASF_MusicBrainz_AlbumID[] = "MusicBrainz/Album Id";
|
|
constexpr char kASF_MusicBrainz_OriginalAlbumID[] = "MusicBrainz/Original Album Id";
|
|
constexpr char kASF_MusicBrainz_RecordingID[] = "MusicBrainz/Track Id";
|
|
constexpr char kASF_MusicBrainz_TrackID[] = "MusicBrainz/Release Track Id";
|
|
constexpr char kASF_MusicBrainz_DiscID[] = "MusicBrainz/Disc Id";
|
|
constexpr char kASF_MusicBrainz_ReleaseGroupID[] = "MusicBrainz/Release Group Id";
|
|
constexpr char kASF_MusicBrainz_WorkID[] = "MusicBrainz/Work Id";
|
|
|
|
} // namespace
|
|
|
|
|
|
TagReaderTagLib::TagReaderTagLib() : factory_(new TagLibFileRefFactory) {}
|
|
|
|
TagReaderTagLib::~TagReaderTagLib() {
|
|
delete factory_;
|
|
}
|
|
|
|
bool TagReaderTagLib::IsMediaFile(const QString &filename) const {
|
|
|
|
qLog(Debug) << "Checking for valid file" << filename;
|
|
|
|
std::unique_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
|
|
return fileref && !fileref->isNull() && fileref->file() && fileref->tag();
|
|
|
|
}
|
|
|
|
spb::tagreader::SongMetadata_FileType TagReaderTagLib::GuessFileType(TagLib::FileRef *fileref) const {
|
|
|
|
if (dynamic_cast<TagLib::RIFF::WAV::File*>(fileref->file())) return spb::tagreader::SongMetadata_FileType_WAV;
|
|
if (dynamic_cast<TagLib::FLAC::File*>(fileref->file())) return spb::tagreader::SongMetadata_FileType_FLAC;
|
|
if (dynamic_cast<TagLib::WavPack::File*>(fileref->file())) return spb::tagreader::SongMetadata_FileType_WAVPACK;
|
|
if (dynamic_cast<TagLib::Ogg::FLAC::File*>(fileref->file())) return spb::tagreader::SongMetadata_FileType_OGGFLAC;
|
|
if (dynamic_cast<TagLib::Ogg::Vorbis::File*>(fileref->file())) return spb::tagreader::SongMetadata_FileType_OGGVORBIS;
|
|
if (dynamic_cast<TagLib::Ogg::Opus::File*>(fileref->file())) return spb::tagreader::SongMetadata_FileType_OGGOPUS;
|
|
if (dynamic_cast<TagLib::Ogg::Speex::File*>(fileref->file())) return spb::tagreader::SongMetadata_FileType_OGGSPEEX;
|
|
if (dynamic_cast<TagLib::MPEG::File*>(fileref->file())) return spb::tagreader::SongMetadata_FileType_MPEG;
|
|
if (dynamic_cast<TagLib::MP4::File*>(fileref->file())) return spb::tagreader::SongMetadata_FileType_MP4;
|
|
if (dynamic_cast<TagLib::ASF::File*>(fileref->file())) return spb::tagreader::SongMetadata_FileType_ASF;
|
|
if (dynamic_cast<TagLib::RIFF::AIFF::File*>(fileref->file())) return spb::tagreader::SongMetadata_FileType_AIFF;
|
|
if (dynamic_cast<TagLib::MPC::File*>(fileref->file())) return spb::tagreader::SongMetadata_FileType_MPC;
|
|
if (dynamic_cast<TagLib::TrueAudio::File*>(fileref->file())) return spb::tagreader::SongMetadata_FileType_TRUEAUDIO;
|
|
if (dynamic_cast<TagLib::APE::File*>(fileref->file())) return spb::tagreader::SongMetadata_FileType_APE;
|
|
if (dynamic_cast<TagLib::Mod::File*>(fileref->file())) return spb::tagreader::SongMetadata_FileType_MOD;
|
|
if (dynamic_cast<TagLib::S3M::File*>(fileref->file())) return spb::tagreader::SongMetadata_FileType_S3M;
|
|
if (dynamic_cast<TagLib::XM::File*>(fileref->file())) return spb::tagreader::SongMetadata_FileType_XM;
|
|
if (dynamic_cast<TagLib::IT::File*>(fileref->file())) return spb::tagreader::SongMetadata_FileType_IT;
|
|
#ifdef HAVE_TAGLIB_DSFFILE
|
|
if (dynamic_cast<TagLib::DSF::File*>(fileref->file())) return spb::tagreader::SongMetadata_FileType_DSF;
|
|
#endif
|
|
#ifdef HAVE_TAGLIB_DSDIFFFILE
|
|
if (dynamic_cast<TagLib::DSDIFF::File*>(fileref->file())) return spb::tagreader::SongMetadata_FileType_DSDIFF;
|
|
#endif
|
|
|
|
return spb::tagreader::SongMetadata_FileType_UNKNOWN;
|
|
|
|
}
|
|
|
|
bool TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const {
|
|
|
|
const QByteArray url(QUrl::fromLocalFile(filename).toEncoded());
|
|
const QFileInfo fileinfo(filename);
|
|
|
|
qLog(Debug) << "Reading tags from" << filename;
|
|
|
|
const QByteArray basefilename = fileinfo.fileName().toUtf8();
|
|
song->set_basefilename(basefilename.constData(), basefilename.length());
|
|
song->set_url(url.constData(), url.size());
|
|
song->set_filesize(fileinfo.size());
|
|
|
|
song->set_mtime(fileinfo.lastModified().isValid() ? std::max(fileinfo.lastModified().toSecsSinceEpoch(), 0LL) : 0LL);
|
|
song->set_ctime(fileinfo.birthTime().isValid() ? std::max(fileinfo.birthTime().toSecsSinceEpoch(), 0LL) : fileinfo.lastModified().isValid() ? std::max(fileinfo.lastModified().toSecsSinceEpoch(), 0LL) : 0LL);
|
|
|
|
if (song->ctime() <= 0) {
|
|
song->set_ctime(song->mtime());
|
|
}
|
|
|
|
song->set_lastseen(QDateTime::currentSecsSinceEpoch());
|
|
|
|
std::unique_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
|
|
if (fileref->isNull()) {
|
|
qLog(Info) << "TagLib hasn't been able to read" << filename << "file";
|
|
return false;
|
|
}
|
|
|
|
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()->lengthInMilliseconds() * kNsecPerMsec);
|
|
}
|
|
|
|
TagLib::Tag *tag = fileref->tag();
|
|
if (tag) {
|
|
TStringToStdString(tag->title(), song->mutable_title());
|
|
TStringToStdString(tag->artist(), song->mutable_artist()); // TPE1
|
|
TStringToStdString(tag->album(), song->mutable_album());
|
|
TStringToStdString(tag->genre(), song->mutable_genre());
|
|
song->set_year(static_cast<int>(tag->year()));
|
|
song->set_track(static_cast<int>(tag->track()));
|
|
song->set_valid(true);
|
|
}
|
|
|
|
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.
|
|
if (TagLib::Ogg::XiphComment *xiph_comment = dynamic_cast<TagLib::Ogg::XiphComment*>(fileref->file()->tag())) {
|
|
ParseOggTag(xiph_comment->fieldListMap(), &disc, &compilation, song);
|
|
TagLib::List<TagLib::FLAC::Picture*> pictures = xiph_comment->pictureList();
|
|
if (!pictures.isEmpty()) {
|
|
for (TagLib::FLAC::Picture *picture : pictures) {
|
|
if (picture->type() == TagLib::FLAC::Picture::FrontCover && picture->data().size() > 0) {
|
|
song->set_art_embedded(true);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (TagLib::FLAC::File *file_flac = dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
|
|
song->set_bitdepth(file_flac->audioProperties()->bitsPerSample());
|
|
if (file_flac->xiphComment()) {
|
|
ParseOggTag(file_flac->xiphComment()->fieldListMap(), &disc, &compilation, song);
|
|
TagLib::List<TagLib::FLAC::Picture*> pictures = file_flac->pictureList();
|
|
if (!pictures.isEmpty()) {
|
|
for (TagLib::FLAC::Picture *picture : pictures) {
|
|
if (picture->type() == TagLib::FLAC::Picture::FrontCover && picture->data().size() > 0) {
|
|
song->set_art_embedded(true);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (tag) TStringToStdString(tag->comment(), song->mutable_comment());
|
|
}
|
|
|
|
else if (TagLib::WavPack::File *file_wavpack = dynamic_cast<TagLib::WavPack::File*>(fileref->file())) {
|
|
song->set_bitdepth(file_wavpack->audioProperties()->bitsPerSample());
|
|
if (file_wavpack->APETag()) {
|
|
ParseAPETag(file_wavpack->APETag()->itemListMap(), &disc, &compilation, song);
|
|
}
|
|
if (tag) TStringToStdString(tag->comment(), song->mutable_comment());
|
|
}
|
|
|
|
else if (TagLib::APE::File *file_ape = dynamic_cast<TagLib::APE::File*>(fileref->file())) {
|
|
if (file_ape->APETag()) {
|
|
ParseAPETag(file_ape->APETag()->itemListMap(), &disc, &compilation, song);
|
|
}
|
|
song->set_bitdepth(file_ape->audioProperties()->bitsPerSample());
|
|
if (tag) TStringToStdString(tag->comment(), song->mutable_comment());
|
|
}
|
|
|
|
else if (TagLib::MPEG::File *file_mpeg = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
|
|
if (file_mpeg->hasID3v2Tag()) {
|
|
ParseID3v2Tag(file_mpeg->ID3v2Tag(), &disc, &compilation, song);
|
|
}
|
|
}
|
|
|
|
else if (TagLib::MP4::File *file_mp4 = dynamic_cast<TagLib::MP4::File*>(fileref->file())) {
|
|
|
|
song->set_bitdepth(file_mp4->audioProperties()->bitsPerSample());
|
|
|
|
if (file_mp4->tag()) {
|
|
TagLib::MP4::Tag *mp4_tag = file_mp4->tag();
|
|
|
|
// Find album artists
|
|
if (mp4_tag->item("aART").isValid()) {
|
|
TagLib::StringList album_artists = mp4_tag->item("aART").toStringList();
|
|
if (!album_artists.isEmpty()) {
|
|
TStringToStdString(album_artists.front(), song->mutable_albumartist());
|
|
}
|
|
}
|
|
|
|
// Find album cover art
|
|
if (mp4_tag->item("covr").isValid()) {
|
|
song->set_art_embedded(true);
|
|
}
|
|
|
|
if (mp4_tag->item("disk").isValid()) {
|
|
disc = TStringToQString(TagLib::String::number(mp4_tag->item("disk").toIntPair().first));
|
|
}
|
|
|
|
if (mp4_tag->item("\251wrt").isValid()) {
|
|
TStringToStdString(mp4_tag->item("\251wrt").toStringList().toString(", "), song->mutable_composer());
|
|
}
|
|
if (mp4_tag->item("\251grp").isValid()) {
|
|
TStringToStdString(mp4_tag->item("\251grp").toStringList().toString(" "), song->mutable_grouping());
|
|
}
|
|
if (mp4_tag->item("\251lyr").isValid()) {
|
|
TStringToStdString(mp4_tag->item("\251lyr").toStringList().toString(" "), song->mutable_lyrics());
|
|
}
|
|
|
|
if (mp4_tag->item(kMP4_OriginalYear_ID).isValid()) {
|
|
song->set_originalyear(TStringToQString(mp4_tag->item(kMP4_OriginalYear_ID).toStringList().toString('\n')).left(4).toInt());
|
|
}
|
|
|
|
if (mp4_tag->item("cpil").isValid()) {
|
|
song->set_compilation(mp4_tag->item("cpil").toBool());
|
|
}
|
|
|
|
{
|
|
TagLib::MP4::Item item = mp4_tag->item(kMP4_FMPS_Playcount_ID);
|
|
if (item.isValid()) {
|
|
const int playcount = TStringToQString(item.toStringList().toString('\n')).toInt();
|
|
if (song->playcount() <= 0 && playcount > 0) {
|
|
song->set_playcount(static_cast<uint>(playcount));
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
TagLib::MP4::Item item = mp4_tag->item(kMP4_FMPS_Rating_ID);
|
|
if (item.isValid()) {
|
|
const float rating = TStringToQString(item.toStringList().toString('\n')).toFloat();
|
|
if (song->rating() <= 0 && rating > 0) {
|
|
song->set_rating(rating);
|
|
}
|
|
}
|
|
}
|
|
|
|
TStringToStdString(mp4_tag->comment(), song->mutable_comment());
|
|
|
|
if (mp4_tag->contains(kMP4_AcoustID_ID)) {
|
|
TStringToStdString(mp4_tag->item(kMP4_AcoustID_ID).toStringList().toString(), song->mutable_acoustid_id());
|
|
}
|
|
if (mp4_tag->contains(kMP4_AcoustID_Fingerprint)) {
|
|
TStringToStdString(mp4_tag->item(kMP4_AcoustID_Fingerprint).toStringList().toString(), song->mutable_acoustid_fingerprint());
|
|
}
|
|
|
|
if (mp4_tag->contains(kMP4_MusicBrainz_AlbumArtistID)) {
|
|
TStringToStdString(mp4_tag->item(kMP4_MusicBrainz_AlbumArtistID).toStringList().toString(), song->mutable_musicbrainz_album_artist_id());
|
|
}
|
|
if (mp4_tag->contains(kMP4_MusicBrainz_ArtistID)) {
|
|
TStringToStdString(mp4_tag->item(kMP4_MusicBrainz_ArtistID).toStringList().toString(), song->mutable_musicbrainz_artist_id());
|
|
}
|
|
if (mp4_tag->contains(kMP4_MusicBrainz_OriginalArtistID)) {
|
|
TStringToStdString(mp4_tag->item(kMP4_MusicBrainz_OriginalArtistID).toStringList().toString(), song->mutable_musicbrainz_original_artist_id());
|
|
}
|
|
if (mp4_tag->contains(kMP4_MusicBrainz_AlbumID)) {
|
|
TStringToStdString(mp4_tag->item(kMP4_MusicBrainz_AlbumID).toStringList().toString(), song->mutable_musicbrainz_album_id());
|
|
}
|
|
if (mp4_tag->contains(kMP4_MusicBrainz_OriginalAlbumID)) {
|
|
TStringToStdString(mp4_tag->item(kMP4_MusicBrainz_OriginalAlbumID).toStringList().toString(), song->mutable_musicbrainz_original_album_id());
|
|
}
|
|
if (mp4_tag->contains(kMP4_MusicBrainz_RecordingID)) {
|
|
TStringToStdString(mp4_tag->item(kMP4_MusicBrainz_RecordingID).toStringList().toString(), song->mutable_musicbrainz_recording_id());
|
|
}
|
|
if (mp4_tag->contains(kMP4_MusicBrainz_TrackID)) {
|
|
TStringToStdString(mp4_tag->item(kMP4_MusicBrainz_TrackID).toStringList().toString(), song->mutable_musicbrainz_track_id());
|
|
}
|
|
if (mp4_tag->contains(kMP4_MusicBrainz_DiscID)) {
|
|
TStringToStdString(mp4_tag->item(kMP4_MusicBrainz_DiscID).toStringList().toString(), song->mutable_musicbrainz_disc_id());
|
|
}
|
|
if (mp4_tag->contains(kMP4_MusicBrainz_ReleaseGroupID)) {
|
|
TStringToStdString(mp4_tag->item(kMP4_MusicBrainz_ReleaseGroupID).toStringList().toString(), song->mutable_musicbrainz_release_group_id());
|
|
}
|
|
if (mp4_tag->contains(kMP4_MusicBrainz_WorkID)) {
|
|
TStringToStdString(mp4_tag->item(kMP4_MusicBrainz_WorkID).toStringList().toString(), song->mutable_musicbrainz_work_id());
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
else if (TagLib::ASF::File *file_asf = dynamic_cast<TagLib::ASF::File*>(fileref->file())) {
|
|
|
|
song->set_bitdepth(file_asf->audioProperties()->bitsPerSample());
|
|
|
|
if (file_asf->tag()) {
|
|
TStringToStdString(file_asf->tag()->comment(), song->mutable_comment());
|
|
|
|
const TagLib::ASF::AttributeListMap &attributes_map = file_asf->tag()->attributeListMap();
|
|
|
|
if (attributes_map.contains(kASF_OriginalDate_ID)) {
|
|
const TagLib::ASF::AttributeList &attributes = attributes_map[kASF_OriginalDate_ID];
|
|
if (!attributes.isEmpty()) {
|
|
song->set_originalyear(TStringToQString(attributes.front().toString()).left(4).toInt());
|
|
}
|
|
}
|
|
else if (attributes_map.contains(kASF_OriginalYear_ID)) {
|
|
const TagLib::ASF::AttributeList &attributes = attributes_map[kASF_OriginalYear_ID];
|
|
if (!attributes.isEmpty()) {
|
|
song->set_originalyear(TStringToQString(attributes.front().toString()).left(4).toInt());
|
|
}
|
|
}
|
|
|
|
if (attributes_map.contains("FMPS/Playcount")) {
|
|
const TagLib::ASF::AttributeList &attributes = attributes_map["FMPS/Playcount"];
|
|
if (!attributes.isEmpty()) {
|
|
int playcount = TStringToQString(attributes.front().toString()).toInt();
|
|
if (song->playcount() <= 0 && playcount > 0) {
|
|
song->set_playcount(static_cast<uint>(playcount));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (attributes_map.contains("FMPS/Rating")) {
|
|
const TagLib::ASF::AttributeList &attributes = attributes_map["FMPS/Rating"];
|
|
if (!attributes.isEmpty()) {
|
|
float rating = TStringToQString(attributes.front().toString()).toFloat();
|
|
if (song->rating() <= 0 && rating > 0) {
|
|
song->set_rating(rating);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (attributes_map.contains(kASF_AcoustID_ID)) {
|
|
TStringToStdString(attributes_map[kASF_AcoustID_ID].front().toString(), song->mutable_acoustid_id());
|
|
}
|
|
if (attributes_map.contains(kASF_AcoustID_Fingerprint)) {
|
|
TStringToStdString(attributes_map[kASF_AcoustID_Fingerprint].front().toString(), song->mutable_acoustid_fingerprint());
|
|
}
|
|
|
|
if (attributes_map.contains(kASF_MusicBrainz_AlbumArtistID)) {
|
|
TStringToStdString(attributes_map[kASF_MusicBrainz_AlbumArtistID].front().toString(), song->mutable_musicbrainz_album_artist_id());
|
|
}
|
|
if (attributes_map.contains(kASF_MusicBrainz_ArtistID)) {
|
|
TStringToStdString(attributes_map[kASF_MusicBrainz_ArtistID].front().toString(), song->mutable_musicbrainz_artist_id());
|
|
}
|
|
if (attributes_map.contains(kASF_MusicBrainz_OriginalArtistID)) {
|
|
TStringToStdString(attributes_map[kASF_MusicBrainz_OriginalArtistID].front().toString(), song->mutable_musicbrainz_original_artist_id());
|
|
}
|
|
if (attributes_map.contains(kASF_MusicBrainz_AlbumID)) {
|
|
TStringToStdString(attributes_map[kASF_MusicBrainz_AlbumID].front().toString(), song->mutable_musicbrainz_album_id());
|
|
}
|
|
if (attributes_map.contains(kASF_MusicBrainz_OriginalAlbumID)) {
|
|
TStringToStdString(attributes_map[kASF_MusicBrainz_OriginalAlbumID].front().toString(), song->mutable_musicbrainz_original_album_id());
|
|
}
|
|
if (attributes_map.contains(kASF_MusicBrainz_RecordingID)) {
|
|
TStringToStdString(attributes_map[kASF_MusicBrainz_RecordingID].front().toString(), song->mutable_musicbrainz_recording_id());
|
|
}
|
|
if (attributes_map.contains(kASF_MusicBrainz_TrackID)) {
|
|
TStringToStdString(attributes_map[kASF_MusicBrainz_TrackID].front().toString(), song->mutable_musicbrainz_track_id());
|
|
}
|
|
if (attributes_map.contains(kASF_MusicBrainz_DiscID)) {
|
|
TStringToStdString(attributes_map[kASF_MusicBrainz_DiscID].front().toString(), song->mutable_musicbrainz_disc_id());
|
|
}
|
|
if (attributes_map.contains(kASF_MusicBrainz_ReleaseGroupID)) {
|
|
TStringToStdString(attributes_map[kASF_MusicBrainz_ReleaseGroupID].front().toString(), song->mutable_musicbrainz_release_group_id());
|
|
}
|
|
if (attributes_map.contains(kASF_MusicBrainz_WorkID)) {
|
|
TStringToStdString(attributes_map[kASF_MusicBrainz_WorkID].front().toString(), song->mutable_musicbrainz_work_id());
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (TagLib::MPC::File *file_mpc = dynamic_cast<TagLib::MPC::File*>(fileref->file())) {
|
|
if (file_mpc->APETag()) {
|
|
ParseAPETag(file_mpc->APETag()->itemListMap(), &disc, &compilation, song);
|
|
}
|
|
if (tag) TStringToStdString(tag->comment(), song->mutable_comment());
|
|
}
|
|
|
|
else if (TagLib::RIFF::WAV::File *file_wav = dynamic_cast<TagLib::RIFF::WAV::File*>(fileref->file())) {
|
|
if (file_wav->hasID3v2Tag()) {
|
|
ParseID3v2Tag(file_wav->ID3v2Tag(), &disc, &compilation, song);
|
|
}
|
|
}
|
|
|
|
else if (tag) {
|
|
TStringToStdString(tag->comment(), song->mutable_comment());
|
|
}
|
|
|
|
if (!disc.isEmpty()) {
|
|
const qint64 i = disc.indexOf(QLatin1Char('/'));
|
|
if (i != -1) {
|
|
// disc.right( i ).toInt() is total number of discs, we don't use this at the moment
|
|
song->set_disc(disc.left(i).toInt());
|
|
}
|
|
else {
|
|
song->set_disc(disc.toInt());
|
|
}
|
|
}
|
|
|
|
if (compilation.isEmpty()) {
|
|
// well, it wasn't set, but if the artist is VA assume it's a compilation
|
|
const QString albumartist = QString::fromUtf8(song->albumartist().data(), static_cast<qint64>(song->albumartist().size()));
|
|
const QString artist = QString::fromUtf8(song->artist().data(), static_cast<qint64>(song->artist().size()));
|
|
if (artist.compare(QLatin1String("various artists")) == 0 || albumartist.compare(QLatin1String("various artists")) == 0) {
|
|
song->set_compilation(true);
|
|
}
|
|
}
|
|
else {
|
|
song->set_compilation(compilation.toInt() == 1);
|
|
}
|
|
|
|
if (!lyrics.isEmpty()) song->set_lyrics(lyrics.toStdString());
|
|
|
|
// Set integer fields to -1 if they're not valid
|
|
|
|
if (song->track() <= 0) { song->set_track(-1); }
|
|
if (song->disc() <= 0) { song->set_disc(-1); }
|
|
if (song->year() <= 0) { song->set_year(-1); }
|
|
if (song->originalyear() <= 0) { song->set_originalyear(-1); }
|
|
if (song->samplerate() <= 0) { song->set_samplerate(-1); }
|
|
if (song->bitdepth() <= 0) { song->set_bitdepth(-1); }
|
|
if (song->bitrate() <= 0) { song->set_bitrate(-1); }
|
|
if (song->lastplayed() <= 0) { song->set_lastplayed(-1); }
|
|
|
|
return song->filetype() != spb::tagreader::SongMetadata_FileType_UNKNOWN;
|
|
|
|
}
|
|
|
|
void TagReaderTagLib::TStringToStdString(const TagLib::String &tag, std::string *output) {
|
|
|
|
const QString tmp = TStringToQString(tag).trimmed();
|
|
const QByteArray data = tmp.toUtf8();
|
|
output->assign(data.constData(), data.size());
|
|
|
|
}
|
|
|
|
void TagReaderTagLib::ParseID3v2Tag(TagLib::ID3v2::Tag *tag, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const {
|
|
|
|
TagLib::ID3v2::FrameListMap map = tag->frameListMap();
|
|
|
|
if (map.contains("TPOS")) *disc = TStringToQString(map["TPOS"].front()->toString()).trimmed();
|
|
if (map.contains("TCOM")) TStringToStdString(map["TCOM"].front()->toString(), song->mutable_composer());
|
|
|
|
// content group
|
|
if (map.contains("TIT1")) TStringToStdString(map["TIT1"].front()->toString(), song->mutable_grouping());
|
|
|
|
// original artist/performer
|
|
if (map.contains("TOPE")) TStringToStdString(map["TOPE"].front()->toString(), song->mutable_performer());
|
|
|
|
// Skip TPE1 (which is the artist) here because we already fetched it
|
|
|
|
// non-standard: Apple, Microsoft
|
|
if (map.contains("TPE2")) TStringToStdString(map["TPE2"].front()->toString(), song->mutable_albumartist());
|
|
|
|
if (map.contains("TCMP")) *compilation = TStringToQString(map["TCMP"].front()->toString()).trimmed();
|
|
|
|
if (map.contains("TDOR")) {
|
|
song->set_originalyear(map["TDOR"].front()->toString().substr(0, 4).toInt());
|
|
}
|
|
else if (map.contains("TORY")) {
|
|
song->set_originalyear(map["TORY"].front()->toString().substr(0, 4).toInt());
|
|
}
|
|
|
|
if (map.contains("USLT")) {
|
|
TStringToStdString(map["USLT"].front()->toString(), song->mutable_lyrics());
|
|
}
|
|
else if (map.contains("SYLT")) {
|
|
TStringToStdString(map["SYLT"].front()->toString(), song->mutable_lyrics());
|
|
}
|
|
|
|
if (map.contains("APIC")) song->set_art_embedded(true);
|
|
|
|
// Find a suitable comment tag. For now we ignore iTunNORM comments.
|
|
for (uint i = 0; i < map["COMM"].size(); ++i) {
|
|
const TagLib::ID3v2::CommentsFrame *frame = dynamic_cast<const TagLib::ID3v2::CommentsFrame*>(map["COMM"][i]);
|
|
|
|
if (frame && TStringToQString(frame->description()) != QStringLiteral("iTunNORM")) {
|
|
TStringToStdString(frame->text(), song->mutable_comment());
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (TagLib::ID3v2::UserTextIdentificationFrame *frame_fmps_playcount = TagLib::ID3v2::UserTextIdentificationFrame::find(tag, "FMPS_Playcount")) {
|
|
TagLib::StringList frame_field_list = frame_fmps_playcount->fieldList();
|
|
if (frame_field_list.size() > 1) {
|
|
int playcount = TStringToQString(frame_field_list[1]).toInt();
|
|
if (song->playcount() <= 0 && playcount > 0) {
|
|
song->set_playcount(playcount);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (TagLib::ID3v2::UserTextIdentificationFrame *frame_fmps_rating = TagLib::ID3v2::UserTextIdentificationFrame::find(tag, "FMPS_Rating")) {
|
|
TagLib::StringList frame_field_list = frame_fmps_rating->fieldList();
|
|
if (frame_field_list.size() > 1) {
|
|
float rating = TStringToQString(frame_field_list[1]).toFloat();
|
|
if (song->rating() <= 0 && rating > 0 && rating <= 1.0) {
|
|
song->set_rating(rating);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (map.contains("POPM")) {
|
|
const TagLib::ID3v2::PopularimeterFrame *frame = dynamic_cast<const TagLib::ID3v2::PopularimeterFrame*>(map["POPM"].front());
|
|
if (frame) {
|
|
if (song->playcount() <= 0 && frame->counter() > 0) {
|
|
song->set_playcount(frame->counter());
|
|
}
|
|
if (song->rating() <= 0 && frame->rating() > 0) {
|
|
song->set_rating(ConvertPOPMRating(frame->rating()));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (map.contains("UFID")) {
|
|
for (uint i = 0; i < map["UFID"].size(); ++i) {
|
|
if (TagLib::ID3v2::UniqueFileIdentifierFrame *frame = dynamic_cast<TagLib::ID3v2::UniqueFileIdentifierFrame*>(map["UFID"][i])) {
|
|
const TagLib::PropertyMap property_map = frame->asProperties();
|
|
if (property_map.contains(kID3v2_MusicBrainz_RecordingID)) {
|
|
TStringToStdString(property_map[kID3v2_MusicBrainz_RecordingID].toString(), song->mutable_musicbrainz_recording_id());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (map.contains("TXXX")) {
|
|
for (uint i = 0; i < map["TXXX"].size(); ++i) {
|
|
if (TagLib::ID3v2::UserTextIdentificationFrame *frame = dynamic_cast<TagLib::ID3v2::UserTextIdentificationFrame*>(map["TXXX"][i])) {
|
|
const TagLib::StringList frame_field_list = frame->fieldList();
|
|
if (frame_field_list.size() != 2) continue;
|
|
if (frame->description() == kID3v2_AcoustID_ID) {
|
|
TStringToStdString(frame_field_list.back(), song->mutable_acoustid_id());
|
|
}
|
|
if (frame->description() == kID3v2_AcoustID_Fingerprint) {
|
|
TStringToStdString(frame_field_list.back(), song->mutable_acoustid_fingerprint());
|
|
}
|
|
if (frame->description() == kID3v2_MusicBrainz_AlbumArtistID) {
|
|
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_album_artist_id());
|
|
}
|
|
if (frame->description() == kID3v2_MusicBrainz_ArtistID) {
|
|
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_artist_id());
|
|
}
|
|
if (frame->description() == kID3v2_MusicBrainz_OriginalArtistID) {
|
|
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_original_artist_id());
|
|
}
|
|
if (frame->description() == kID3v2_MusicBrainz_AlbumID) {
|
|
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_album_id());
|
|
}
|
|
if (frame->description() == kID3v2_MusicBrainz_OriginalAlbumID) {
|
|
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_original_album_id());
|
|
}
|
|
if (frame->description() == kID3v2_MusicBrainz_TrackID) {
|
|
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_track_id());
|
|
}
|
|
if (frame->description() == kID3v2_MusicBrainz_DiscID) {
|
|
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_disc_id());
|
|
}
|
|
if (frame->description() == kID3v2_MusicBrainz_ReleaseGroupID) {
|
|
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_release_group_id());
|
|
}
|
|
if (frame->description() == kID3v2_MusicBrainz_WorkID) {
|
|
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_work_id());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void TagReaderTagLib::ParseOggTag(const TagLib::Ogg::FieldListMap &map, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const {
|
|
|
|
if (map.contains("COMPOSER")) TStringToStdString(map["COMPOSER"].front(), song->mutable_composer());
|
|
if (map.contains("PERFORMER")) TStringToStdString(map["PERFORMER"].front(), song->mutable_performer());
|
|
if (map.contains("CONTENT GROUP")) TStringToStdString(map["CONTENT GROUP"].front(), song->mutable_grouping());
|
|
if (map.contains("GROUPING")) TStringToStdString(map["GROUPING"].front(), song->mutable_grouping());
|
|
|
|
if (map.contains("ALBUMARTIST")) TStringToStdString(map["ALBUMARTIST"].front(), song->mutable_albumartist());
|
|
else if (map.contains("ALBUM ARTIST")) TStringToStdString(map["ALBUM ARTIST"].front(), song->mutable_albumartist());
|
|
|
|
if (map.contains("ORIGINALDATE")) song->set_originalyear(TStringToQString(map["ORIGINALDATE"].front()).left(4).toInt());
|
|
else if (map.contains("ORIGINALYEAR")) song->set_originalyear(TStringToQString(map["ORIGINALYEAR"].front()).toInt());
|
|
|
|
if (map.contains("DISCNUMBER")) *disc = TStringToQString(map["DISCNUMBER"].front()).trimmed();
|
|
if (map.contains("COMPILATION")) *compilation = TStringToQString(map["COMPILATION"].front()).trimmed();
|
|
if (map.contains("COVERART")) song->set_art_embedded(true);
|
|
if (map.contains("METADATA_BLOCK_PICTURE")) song->set_art_embedded(true);
|
|
|
|
if (map.contains("FMPS_PLAYCOUNT") && song->playcount() <= 0) {
|
|
const int playcount = TStringToQString(map["FMPS_PLAYCOUNT"].front()).trimmed().toInt();
|
|
song->set_playcount(static_cast<uint>(playcount));
|
|
}
|
|
if (map.contains("FMPS_RATING") && song->rating() <= 0) song->set_rating(TStringToQString(map["FMPS_RATING"].front()).trimmed().toFloat());
|
|
|
|
if (map.contains("LYRICS")) TStringToStdString(map["LYRICS"].front(), song->mutable_lyrics());
|
|
else if (map.contains("UNSYNCEDLYRICS")) TStringToStdString(map["UNSYNCEDLYRICS"].front(), song->mutable_lyrics());
|
|
|
|
if (map.contains("ACOUSTID_ID")) TStringToStdString(map["ACOUSTID_ID"].front(), song->mutable_acoustid_id());
|
|
if (map.contains("ACOUSTID_FINGERPRINT")) TStringToStdString(map["ACOUSTID_FINGERPRINT"].front(), song->mutable_acoustid_fingerprint());
|
|
|
|
if (map.contains("MUSICBRAINZ_ALBUMARTISTID")) TStringToStdString(map["MUSICBRAINZ_ALBUMARTISTID"].front(), song->mutable_musicbrainz_album_artist_id());
|
|
if (map.contains("MUSICBRAINZ_ARTISTID")) TStringToStdString(map["MUSICBRAINZ_ARTISTID"].front(), song->mutable_musicbrainz_artist_id());
|
|
if (map.contains("MUSICBRAINZ_ORIGINALARTISTID")) TStringToStdString(map["MUSICBRAINZ_ORIGINALARTISTID"].front(), song->mutable_musicbrainz_original_artist_id());
|
|
if (map.contains("MUSICBRAINZ_ALBUMID")) TStringToStdString(map["MUSICBRAINZ_ALBUMID"].front(), song->mutable_musicbrainz_album_id());
|
|
if (map.contains("MUSICBRAINZ_ORIGINALALBUMID")) TStringToStdString(map["MUSICBRAINZ_ORIGINALALBUMID"].front(), song->mutable_musicbrainz_original_album_id());
|
|
if (map.contains("MUSICBRAINZ_TRACKID")) TStringToStdString(map["MUSICBRAINZ_TRACKID"].front(), song->mutable_musicbrainz_recording_id());
|
|
if (map.contains("MUSICBRAINZ_RELEASETRACKID")) TStringToStdString(map["MUSICBRAINZ_RELEASETRACKID"].front(), song->mutable_musicbrainz_track_id());
|
|
if (map.contains("MUSICBRAINZ_DISCID")) TStringToStdString(map["MUSICBRAINZ_DISCID"].front(), song->mutable_musicbrainz_disc_id());
|
|
if (map.contains("MUSICBRAINZ_RELEASEGROUPID")) TStringToStdString(map["MUSICBRAINZ_RELEASEGROUPID"].front(), song->mutable_musicbrainz_release_group_id());
|
|
if (map.contains("MUSICBRAINZ_WORKID")) TStringToStdString(map["MUSICBRAINZ_WORKID"].front(), song->mutable_musicbrainz_work_id());
|
|
|
|
}
|
|
|
|
void TagReaderTagLib::ParseAPETag(const TagLib::APE::ItemListMap &map, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const {
|
|
|
|
TagLib::APE::ItemListMap::ConstIterator it = map.find("ALBUM ARTIST");
|
|
if (it != map.end()) {
|
|
TagLib::StringList album_artists = it->second.values();
|
|
if (!album_artists.isEmpty()) {
|
|
TStringToStdString(album_artists.front(), song->mutable_albumartist());
|
|
}
|
|
}
|
|
|
|
if (map.find("COVER ART (FRONT)") != map.end()) song->set_art_embedded(true);
|
|
if (map.contains("COMPILATION")) {
|
|
*compilation = TStringToQString(TagLib::String::number(map["COMPILATION"].toString().toInt()));
|
|
}
|
|
|
|
if (map.contains("DISC")) {
|
|
*disc = TStringToQString(TagLib::String::number(map["DISC"].toString().toInt()));
|
|
}
|
|
|
|
if (map.contains("PERFORMER")) {
|
|
TStringToStdString(map["PERFORMER"].values().toString(", "), song->mutable_performer());
|
|
}
|
|
|
|
if (map.contains("COMPOSER")) {
|
|
TStringToStdString(map["COMPOSER"].values().toString(", "), song->mutable_composer());
|
|
}
|
|
|
|
if (map.contains("GROUPING")) {
|
|
TStringToStdString(map["GROUPING"].values().toString(" "), song->mutable_grouping());
|
|
}
|
|
|
|
if (map.contains("LYRICS")) {
|
|
TStringToStdString(map["LYRICS"].toString(), song->mutable_lyrics());
|
|
}
|
|
|
|
if (map.contains("FMPS_PLAYCOUNT")) {
|
|
const int playcount = TStringToQString(map["FMPS_PLAYCOUNT"].toString()).toInt();
|
|
if (song->playcount() <= 0 && playcount > 0) {
|
|
song->set_playcount(static_cast<uint>(playcount));
|
|
}
|
|
}
|
|
|
|
if (map.contains("FMPS_RATING")) {
|
|
const float rating = TStringToQString(map["FMPS_RATING"].toString()).toFloat();
|
|
if (song->rating() <= 0 && rating > 0) {
|
|
song->set_rating(rating);
|
|
}
|
|
}
|
|
|
|
if (map.contains("ACOUSTID_ID")) TStringToStdString(map["ACOUSTID_ID"].toString(), song->mutable_acoustid_id());
|
|
if (map.contains("ACOUSTID_FINGERPRINT")) TStringToStdString(map["ACOUSTID_FINGERPRINT"].toString(), song->mutable_acoustid_fingerprint());
|
|
|
|
if (map.contains("MUSICBRAINZ_ALBUMARTISTID")) TStringToStdString(map["MUSICBRAINZ_ALBUMARTISTID"].toString(), song->mutable_musicbrainz_album_artist_id());
|
|
if (map.contains("MUSICBRAINZ_ARTISTID")) TStringToStdString(map["MUSICBRAINZ_ARTISTID"].toString(), song->mutable_musicbrainz_artist_id());
|
|
if (map.contains("MUSICBRAINZ_ORIGINALARTISTID")) TStringToStdString(map["MUSICBRAINZ_ORIGINALARTISTID"].toString(), song->mutable_musicbrainz_original_artist_id());
|
|
if (map.contains("MUSICBRAINZ_ALBUMID")) TStringToStdString(map["MUSICBRAINZ_ALBUMID"].toString(), song->mutable_musicbrainz_album_id());
|
|
if (map.contains("MUSICBRAINZ_ORIGINALALBUMID")) TStringToStdString(map["MUSICBRAINZ_ORIGINALALBUMID"].toString(), song->mutable_musicbrainz_original_album_id());
|
|
if (map.contains("MUSICBRAINZ_TRACKID")) TStringToStdString(map["MUSICBRAINZ_TRACKID"].toString(), song->mutable_musicbrainz_recording_id());
|
|
if (map.contains("MUSICBRAINZ_RELEASETRACKID")) TStringToStdString(map["MUSICBRAINZ_RELEASETRACKID"].toString(), song->mutable_musicbrainz_track_id());
|
|
if (map.contains("MUSICBRAINZ_DISCID")) TStringToStdString(map["MUSICBRAINZ_DISCID"].toString(), song->mutable_musicbrainz_disc_id());
|
|
if (map.contains("MUSICBRAINZ_RELEASEGROUPID")) TStringToStdString(map["MUSICBRAINZ_RELEASEGROUPID"].toString(), song->mutable_musicbrainz_release_group_id());
|
|
if (map.contains("MUSICBRAINZ_WORKID")) TStringToStdString(map["MUSICBRAINZ_WORKID"].toString(), song->mutable_musicbrainz_work_id());
|
|
|
|
}
|
|
|
|
void TagReaderTagLib::SetVorbisComments(TagLib::Ogg::XiphComment *vorbis_comment, const spb::tagreader::SongMetadata &song) const {
|
|
|
|
vorbis_comment->addField("COMPOSER", StdStringToTaglibString(song.composer()), true);
|
|
vorbis_comment->addField("PERFORMER", StdStringToTaglibString(song.performer()), true);
|
|
vorbis_comment->addField("GROUPING", StdStringToTaglibString(song.grouping()), true);
|
|
vorbis_comment->addField("DISCNUMBER", QStringToTaglibString(song.disc() <= 0 ? QString() : QString::number(song.disc())), true);
|
|
vorbis_comment->addField("COMPILATION", QStringToTaglibString(song.compilation() ? QStringLiteral("1") : QString()), true);
|
|
|
|
// Try to be coherent, the two forms are used but the first one is preferred
|
|
|
|
vorbis_comment->addField("ALBUMARTIST", StdStringToTaglibString(song.albumartist()), true);
|
|
vorbis_comment->removeFields("ALBUM ARTIST");
|
|
|
|
vorbis_comment->addField("LYRICS", StdStringToTaglibString(song.lyrics()), true);
|
|
vorbis_comment->removeFields("UNSYNCEDLYRICS");
|
|
|
|
}
|
|
|
|
bool TagReaderTagLib::SaveFile(const spb::tagreader::SaveFileRequest &request) const {
|
|
|
|
if (request.filename().empty()) return false;
|
|
|
|
const QString filename = QString::fromUtf8(request.filename().data(), static_cast<qint64>(request.filename().size()));
|
|
const spb::tagreader::SongMetadata song = request.metadata();
|
|
const bool save_tags = request.has_save_tags() && request.save_tags();
|
|
const bool save_playcount = request.has_save_playcount() && request.save_playcount();
|
|
const bool save_rating = request.has_save_rating() && request.save_rating();
|
|
const bool save_cover = request.has_save_cover() && request.save_cover();
|
|
|
|
QStringList save_tags_options;
|
|
if (save_tags) {
|
|
save_tags_options << QStringLiteral("tags");
|
|
}
|
|
if (save_playcount) {
|
|
save_tags_options << QStringLiteral("playcount");
|
|
}
|
|
if (save_rating) {
|
|
save_tags_options << QStringLiteral("rating");
|
|
}
|
|
if (save_cover) {
|
|
save_tags_options << QStringLiteral("embedded cover");
|
|
}
|
|
|
|
qLog(Debug) << "Saving" << save_tags_options.join(QStringLiteral(", ")) << "to" << filename;
|
|
|
|
const Cover cover = LoadCoverFromRequest(request);
|
|
|
|
std::unique_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
|
|
if (!fileref || fileref->isNull()) return false;
|
|
|
|
if (save_tags) {
|
|
fileref->tag()->setTitle(song.title().empty() ? TagLib::String() : StdStringToTaglibString(song.title()));
|
|
fileref->tag()->setArtist(song.artist().empty() ? TagLib::String() : StdStringToTaglibString(song.artist()));
|
|
fileref->tag()->setAlbum(song.album().empty() ? TagLib::String() : StdStringToTaglibString(song.album()));
|
|
fileref->tag()->setGenre(song.genre().empty() ? TagLib::String() : StdStringToTaglibString(song.genre()));
|
|
fileref->tag()->setComment(song.comment().empty() ? TagLib::String() : StdStringToTaglibString(song.comment()));
|
|
fileref->tag()->setYear(song.year() <= 0 ? 0 : song.year());
|
|
fileref->tag()->setTrack(song.track() <= 0 ? 0 : song.track());
|
|
}
|
|
|
|
bool is_flac = false;
|
|
if (TagLib::FLAC::File *file_flac = dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
|
|
is_flac = true;
|
|
TagLib::Ogg::XiphComment *xiph_comment = file_flac->xiphComment(true);
|
|
if (!xiph_comment) return false;
|
|
if (save_tags) {
|
|
SetVorbisComments(xiph_comment, song);
|
|
}
|
|
if (save_playcount) {
|
|
SetPlaycount(xiph_comment, song);
|
|
}
|
|
if (save_rating) {
|
|
SetRating(xiph_comment, song);
|
|
}
|
|
if (save_cover) {
|
|
SetEmbeddedArt(file_flac, xiph_comment, cover.data, cover.mime_type);
|
|
}
|
|
}
|
|
|
|
else if (TagLib::WavPack::File *file_wavpack = dynamic_cast<TagLib::WavPack::File*>(fileref->file())) {
|
|
TagLib::APE::Tag *tag = file_wavpack->APETag(true);
|
|
if (!tag) return false;
|
|
if (save_tags) {
|
|
SaveAPETag(tag, song);
|
|
}
|
|
if (save_playcount) {
|
|
SetPlaycount(tag, song);
|
|
}
|
|
if (save_rating) {
|
|
SetRating(tag, song);
|
|
}
|
|
}
|
|
|
|
else if (TagLib::APE::File *file_ape = dynamic_cast<TagLib::APE::File*>(fileref->file())) {
|
|
TagLib::APE::Tag *tag = file_ape->APETag(true);
|
|
if (!tag) return false;
|
|
if (save_tags) {
|
|
SaveAPETag(tag, song);
|
|
}
|
|
if (save_playcount) {
|
|
SetPlaycount(tag, song);
|
|
}
|
|
if (save_rating) {
|
|
SetRating(tag, song);
|
|
}
|
|
}
|
|
|
|
else if (TagLib::MPC::File *file_mpc = dynamic_cast<TagLib::MPC::File*>(fileref->file())) {
|
|
TagLib::APE::Tag *tag = file_mpc->APETag(true);
|
|
if (!tag) return false;
|
|
if (save_tags) {
|
|
SaveAPETag(tag, song);
|
|
}
|
|
if (save_playcount) {
|
|
SetPlaycount(tag, song);
|
|
}
|
|
if (save_rating) {
|
|
SetRating(tag, song);
|
|
}
|
|
}
|
|
|
|
else if (TagLib::MPEG::File *file_mpeg = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
|
|
TagLib::ID3v2::Tag *tag = file_mpeg->ID3v2Tag(true);
|
|
if (!tag) return false;
|
|
if (save_tags) {
|
|
SaveID3v2Tag(tag, song);
|
|
}
|
|
if (save_playcount) {
|
|
SetPlaycount(tag, song);
|
|
}
|
|
if (save_rating) {
|
|
SetRating(tag, song);
|
|
}
|
|
if (save_cover) {
|
|
SetEmbeddedArt(tag, cover.data, cover.mime_type);
|
|
}
|
|
}
|
|
|
|
else if (TagLib::MP4::File *file_mp4 = dynamic_cast<TagLib::MP4::File*>(fileref->file())) {
|
|
TagLib::MP4::Tag *tag = file_mp4->tag();
|
|
if (!tag) return false;
|
|
if (save_tags) {
|
|
tag->setItem("disk", TagLib::MP4::Item(song.disc() <= 0 - 1 ? 0 : song.disc(), 0));
|
|
tag->setItem("\251wrt", TagLib::StringList(TagLib::String(song.composer(), TagLib::String::UTF8)));
|
|
tag->setItem("\251grp", TagLib::StringList(TagLib::String(song.grouping(), TagLib::String::UTF8)));
|
|
tag->setItem("\251lyr", TagLib::StringList(TagLib::String(song.lyrics(), TagLib::String::UTF8)));
|
|
tag->setItem("aART", TagLib::StringList(TagLib::String(song.albumartist(), TagLib::String::UTF8)));
|
|
tag->setItem("cpil", TagLib::MP4::Item(song.compilation()));
|
|
}
|
|
if (save_playcount) {
|
|
SetPlaycount(tag, song);
|
|
}
|
|
if (save_rating) {
|
|
SetRating(tag, song);
|
|
}
|
|
if (save_cover) {
|
|
SetEmbeddedArt(file_mp4, tag, cover.data, cover.mime_type);
|
|
}
|
|
}
|
|
|
|
else if (TagLib::RIFF::WAV::File *file_wav = dynamic_cast<TagLib::RIFF::WAV::File*>(fileref->file())) {
|
|
TagLib::ID3v2::Tag *tag = file_wav->ID3v2Tag();
|
|
if (save_tags) {
|
|
SaveID3v2Tag(tag, song);
|
|
}
|
|
if (save_playcount) {
|
|
SetPlaycount(tag, song);
|
|
}
|
|
if (save_rating) {
|
|
SetRating(tag, song);
|
|
}
|
|
if (save_cover) {
|
|
SetEmbeddedArt(tag, cover.data, cover.mime_type);
|
|
}
|
|
}
|
|
|
|
// 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 above.
|
|
if (!is_flac) {
|
|
if (TagLib::Ogg::XiphComment *xiph_comment = dynamic_cast<TagLib::Ogg::XiphComment*>(fileref->file()->tag())) {
|
|
if (save_tags) {
|
|
SetVorbisComments(xiph_comment, song);
|
|
}
|
|
if (save_playcount) {
|
|
SetPlaycount(xiph_comment, song);
|
|
}
|
|
if (save_rating) {
|
|
SetRating(xiph_comment, song);
|
|
}
|
|
if (save_cover) {
|
|
SetEmbeddedArt(xiph_comment, cover.data, cover.mime_type);
|
|
}
|
|
}
|
|
}
|
|
|
|
const bool success = fileref->save();
|
|
#ifdef Q_OS_LINUX
|
|
if (success) {
|
|
// Linux: inotify doesn't seem to notice the change to the file unless we change the timestamps as well. (this is what touch does)
|
|
utimensat(0, QFile::encodeName(filename).constData(), nullptr, 0);
|
|
}
|
|
#endif // Q_OS_LINUX
|
|
|
|
return success;
|
|
|
|
}
|
|
|
|
void TagReaderTagLib::SaveID3v2Tag(TagLib::ID3v2::Tag *tag, const spb::tagreader::SongMetadata &song) const {
|
|
|
|
SetTextFrame("TPOS", song.disc() <= 0 ? QString() : QString::number(song.disc()), tag);
|
|
SetTextFrame("TCOM", song.composer().empty() ? std::string() : song.composer(), tag);
|
|
SetTextFrame("TIT1", song.grouping().empty() ? std::string() : song.grouping(), tag);
|
|
SetTextFrame("TOPE", song.performer().empty() ? std::string() : song.performer(), tag);
|
|
// Skip TPE1 (which is the artist) here because we already set it
|
|
SetTextFrame("TPE2", song.albumartist().empty() ? std::string() : song.albumartist(), tag);
|
|
SetTextFrame("TCMP", song.compilation() ? QString::number(1) : QString(), tag);
|
|
SetUnsyncLyricsFrame(song.lyrics().empty() ? std::string() : song.lyrics(), tag);
|
|
|
|
}
|
|
|
|
void TagReaderTagLib::SaveAPETag(TagLib::APE::Tag *tag, const spb::tagreader::SongMetadata &song) const {
|
|
|
|
tag->setItem("album artist", TagLib::APE::Item("album artist", TagLib::StringList(song.albumartist().c_str())));
|
|
tag->addValue("disc", QStringToTaglibString(song.disc() <= 0 ? QString() : QString::number(song.disc())), true);
|
|
tag->setItem("composer", TagLib::APE::Item("composer", TagLib::StringList(song.composer().c_str())));
|
|
tag->setItem("grouping", TagLib::APE::Item("grouping", TagLib::StringList(song.grouping().c_str())));
|
|
tag->setItem("performer", TagLib::APE::Item("performer", TagLib::StringList(song.performer().c_str())));
|
|
tag->setItem("lyrics", TagLib::APE::Item("lyrics", TagLib::String(song.lyrics())));
|
|
tag->addValue("compilation", QStringToTaglibString(song.compilation() ? QString::number(1) : QString()), true);
|
|
|
|
}
|
|
|
|
void TagReaderTagLib::SetTextFrame(const char *id, const QString &value, TagLib::ID3v2::Tag *tag) const {
|
|
|
|
const QByteArray utf8(value.toUtf8());
|
|
SetTextFrame(id, std::string(utf8.constData(), utf8.length()), tag);
|
|
|
|
}
|
|
|
|
void TagReaderTagLib::SetTextFrame(const char *id, const std::string &value, TagLib::ID3v2::Tag *tag) const {
|
|
|
|
TagLib::ByteVector id_vector(id);
|
|
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 (value.empty()) return;
|
|
|
|
// If no frames stored create empty frame
|
|
if (frames_buffer.isEmpty()) {
|
|
TagLib::ID3v2::TextIdentificationFrame frame(id_vector, TagLib::String::UTF8);
|
|
frames_buffer.push_back(frame.render());
|
|
}
|
|
|
|
// Update and add the frames
|
|
for (int i = 0; i < frames_buffer.size(); ++i) {
|
|
TagLib::ID3v2::TextIdentificationFrame *frame = new TagLib::ID3v2::TextIdentificationFrame(frames_buffer.at(i));
|
|
if (i == 0) {
|
|
frame->setText(StdStringToTaglibString(value));
|
|
}
|
|
// add frame takes ownership and clears the memory
|
|
tag->addFrame(frame);
|
|
}
|
|
|
|
}
|
|
|
|
void TagReaderTagLib::SetUserTextFrame(const QString &description, const QString &value, TagLib::ID3v2::Tag *tag) const {
|
|
|
|
const QByteArray descr_utf8(description.toUtf8());
|
|
const QByteArray value_utf8(value.toUtf8());
|
|
SetUserTextFrame(std::string(descr_utf8.constData(), descr_utf8.length()), std::string(value_utf8.constData(), value_utf8.length()), tag);
|
|
|
|
}
|
|
|
|
void TagReaderTagLib::SetUserTextFrame(const std::string &description, const std::string &value, TagLib::ID3v2::Tag *tag) const {
|
|
|
|
const TagLib::String t_description = StdStringToTaglibString(description);
|
|
TagLib::ID3v2::UserTextIdentificationFrame *frame = TagLib::ID3v2::UserTextIdentificationFrame::find(tag, t_description);
|
|
if (frame) {
|
|
tag->removeFrame(frame);
|
|
}
|
|
|
|
// Create and add a new frame
|
|
frame = new TagLib::ID3v2::UserTextIdentificationFrame(TagLib::String::UTF8);
|
|
frame->setDescription(t_description);
|
|
frame->setText(StdStringToTaglibString(value));
|
|
tag->addFrame(frame);
|
|
|
|
}
|
|
|
|
void TagReaderTagLib::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 (value.empty()) return;
|
|
|
|
// 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 i = 0; i < frames_buffer.size(); ++i) {
|
|
TagLib::ID3v2::UnsynchronizedLyricsFrame *frame = new TagLib::ID3v2::UnsynchronizedLyricsFrame(frames_buffer.at(i));
|
|
if (i == 0) {
|
|
frame->setText(StdStringToTaglibString(value));
|
|
}
|
|
// add frame takes ownership and clears the memory
|
|
tag->addFrame(frame);
|
|
}
|
|
|
|
}
|
|
|
|
QByteArray TagReaderTagLib::LoadEmbeddedArt(const QString &filename) const {
|
|
|
|
if (filename.isEmpty()) return QByteArray();
|
|
|
|
qLog(Debug) << "Loading art from" << filename;
|
|
|
|
#ifdef Q_OS_WIN32
|
|
TagLib::FileRef fileref(filename.toStdWString().c_str());
|
|
#else
|
|
TagLib::FileRef fileref(QFile::encodeName(filename).constData());
|
|
#endif
|
|
|
|
if (fileref.isNull() || !fileref.file()) return QByteArray();
|
|
|
|
// FLAC
|
|
if (TagLib::FLAC::File *flac_file = dynamic_cast<TagLib::FLAC::File*>(fileref.file())) {
|
|
if (flac_file->xiphComment()) {
|
|
TagLib::List<TagLib::FLAC::Picture*> pictures = flac_file->pictureList();
|
|
if (!pictures.isEmpty()) {
|
|
for (TagLib::FLAC::Picture *picture : pictures) {
|
|
if (picture->type() == TagLib::FLAC::Picture::FrontCover && picture->data().size() > 0) {
|
|
QByteArray data(picture->data().data(), picture->data().size());
|
|
if (!data.isEmpty()) {
|
|
return data;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// WavPack
|
|
if (TagLib::WavPack::File *wavpack_file = dynamic_cast<TagLib::WavPack::File*>(fileref.file())) {
|
|
if (wavpack_file->APETag()) {
|
|
return LoadEmbeddedAPEArt(wavpack_file->APETag()->itemListMap());
|
|
}
|
|
}
|
|
|
|
// APE
|
|
if (TagLib::APE::File *ape_file = dynamic_cast<TagLib::APE::File*>(fileref.file())) {
|
|
if (ape_file->APETag()) {
|
|
return LoadEmbeddedAPEArt(ape_file->APETag()->itemListMap());
|
|
}
|
|
}
|
|
|
|
// MPC
|
|
if (TagLib::MPC::File *mpc_file = dynamic_cast<TagLib::MPC::File*>(fileref.file())) {
|
|
if (mpc_file->APETag()) {
|
|
return LoadEmbeddedAPEArt(mpc_file->APETag()->itemListMap());
|
|
}
|
|
}
|
|
|
|
// Ogg Vorbis / Opus / Speex
|
|
if (TagLib::Ogg::XiphComment *xiph_comment = dynamic_cast<TagLib::Ogg::XiphComment*>(fileref.file()->tag())) {
|
|
TagLib::Ogg::FieldListMap map = xiph_comment->fieldListMap();
|
|
|
|
TagLib::List<TagLib::FLAC::Picture*> pictures = xiph_comment->pictureList();
|
|
if (!pictures.isEmpty()) {
|
|
for (TagLib::FLAC::Picture *picture : pictures) {
|
|
if (picture->type() == TagLib::FLAC::Picture::FrontCover && picture->data().size() > 0) {
|
|
QByteArray data(picture->data().data(), picture->data().size());
|
|
if (!data.isEmpty()) {
|
|
return data;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ogg lacks a definitive standard for embedding cover art, but it seems b64 encoding a field called COVERART is the general convention
|
|
if (map.contains("COVERART")) {
|
|
return QByteArray::fromBase64(map["COVERART"].toString().toCString());
|
|
}
|
|
|
|
return QByteArray();
|
|
}
|
|
|
|
// MP3
|
|
if (TagLib::MPEG::File *file_mp3 = dynamic_cast<TagLib::MPEG::File*>(fileref.file())) {
|
|
if (file_mp3->ID3v2Tag()) {
|
|
TagLib::ID3v2::FrameList apic_frames = file_mp3->ID3v2Tag()->frameListMap()["APIC"];
|
|
if (apic_frames.isEmpty()) {
|
|
return QByteArray();
|
|
}
|
|
|
|
TagLib::ID3v2::AttachedPictureFrame *picture = static_cast<TagLib::ID3v2::AttachedPictureFrame*>(apic_frames.front());
|
|
|
|
return QByteArray(reinterpret_cast<const char*>(picture->picture().data()), picture->picture().size());
|
|
}
|
|
}
|
|
|
|
// MP4/AAC
|
|
if (TagLib::MP4::File *aac_file = dynamic_cast<TagLib::MP4::File*>(fileref.file())) {
|
|
TagLib::MP4::Tag *tag = aac_file->tag();
|
|
if (tag && tag->item("covr").isValid()) {
|
|
const TagLib::MP4::CoverArtList &art_list = tag->item("covr").toCoverArtList();
|
|
|
|
if (!art_list.isEmpty()) {
|
|
// Just take the first one for now
|
|
const TagLib::MP4::CoverArt &art = art_list.front();
|
|
return QByteArray(art.data().data(), art.data().size());
|
|
}
|
|
}
|
|
}
|
|
|
|
return QByteArray();
|
|
|
|
}
|
|
|
|
QByteArray TagReaderTagLib::LoadEmbeddedAPEArt(const TagLib::APE::ItemListMap &map) const {
|
|
|
|
QByteArray ret;
|
|
|
|
TagLib::APE::ItemListMap::ConstIterator it = map.find("COVER ART (FRONT)");
|
|
if (it != map.end()) {
|
|
TagLib::ByteVector data = it->second.binaryData();
|
|
|
|
int pos = data.find('\0') + 1;
|
|
if ((pos > 0) && (static_cast<uint>(pos) < data.size())) {
|
|
ret = QByteArray(data.data() + pos, data.size() - pos);
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
void TagReaderTagLib::SetEmbeddedArt(TagLib::FLAC::File *flac_file, TagLib::Ogg::XiphComment *xiph_comment, const QByteArray &data, const QString &mime_type) const {
|
|
|
|
(void)xiph_comment;
|
|
|
|
flac_file->removePictures();
|
|
|
|
if (!data.isEmpty()) {
|
|
TagLib::FLAC::Picture *picture = new TagLib::FLAC::Picture();
|
|
picture->setType(TagLib::FLAC::Picture::FrontCover);
|
|
picture->setMimeType(QStringToTString(mime_type));
|
|
picture->setData(TagLib::ByteVector(data.constData(), data.size()));
|
|
flac_file->addPicture(picture);
|
|
}
|
|
|
|
}
|
|
|
|
void TagReaderTagLib::SetEmbeddedArt(TagLib::Ogg::XiphComment *xiph_comment, const QByteArray &data, const QString &mime_type) const {
|
|
|
|
xiph_comment->removeAllPictures();
|
|
|
|
if (!data.isEmpty()) {
|
|
TagLib::FLAC::Picture *picture = new TagLib::FLAC::Picture();
|
|
picture->setType(TagLib::FLAC::Picture::FrontCover);
|
|
picture->setMimeType(QStringToTString(mime_type));
|
|
picture->setData(TagLib::ByteVector(data.constData(), data.size()));
|
|
xiph_comment->addPicture(picture);
|
|
}
|
|
|
|
}
|
|
|
|
void TagReaderTagLib::SetEmbeddedArt(TagLib::ID3v2::Tag *tag, const QByteArray &data, const QString &mime_type) const {
|
|
|
|
// Remove existing covers
|
|
TagLib::ID3v2::FrameList apiclist = tag->frameListMap()["APIC"];
|
|
for (TagLib::ID3v2::FrameList::ConstIterator it = apiclist.begin(); it != apiclist.end(); ++it) {
|
|
TagLib::ID3v2::AttachedPictureFrame *frame = dynamic_cast<TagLib::ID3v2::AttachedPictureFrame*>(*it);
|
|
tag->removeFrame(frame, false);
|
|
}
|
|
|
|
if (!data.isEmpty()) {
|
|
// Add new cover
|
|
TagLib::ID3v2::AttachedPictureFrame *frontcover = nullptr;
|
|
frontcover = new TagLib::ID3v2::AttachedPictureFrame("APIC");
|
|
frontcover->setType(TagLib::ID3v2::AttachedPictureFrame::FrontCover);
|
|
frontcover->setMimeType(QStringToTString(mime_type));
|
|
frontcover->setPicture(TagLib::ByteVector(data.constData(), data.size()));
|
|
tag->addFrame(frontcover);
|
|
}
|
|
|
|
}
|
|
|
|
void TagReaderTagLib::SetEmbeddedArt(TagLib::MP4::File *aac_file, TagLib::MP4::Tag *tag, const QByteArray &data, const QString &mime_type) const {
|
|
|
|
(void)aac_file;
|
|
|
|
TagLib::MP4::CoverArtList covers;
|
|
if (data.isEmpty()) {
|
|
if (tag->contains("covr")) tag->removeItem("covr");
|
|
}
|
|
else {
|
|
TagLib::MP4::CoverArt::Format cover_format = TagLib::MP4::CoverArt::Format::JPEG;
|
|
if (mime_type == QStringLiteral("image/jpeg")) {
|
|
cover_format = TagLib::MP4::CoverArt::Format::JPEG;
|
|
}
|
|
else if (mime_type == QStringLiteral("image/png")) {
|
|
cover_format = TagLib::MP4::CoverArt::Format::PNG;
|
|
}
|
|
else {
|
|
return;
|
|
}
|
|
covers.append(TagLib::MP4::CoverArt(cover_format, TagLib::ByteVector(data.constData(), data.size())));
|
|
tag->setItem("covr", covers);
|
|
}
|
|
|
|
}
|
|
|
|
bool TagReaderTagLib::SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtRequest &request) const {
|
|
|
|
if (request.filename().empty()) return false;
|
|
|
|
const QString filename = QString::fromUtf8(request.filename().data(), static_cast<qint64>(request.filename().size()));
|
|
|
|
qLog(Debug) << "Saving art to" << filename;
|
|
|
|
const Cover cover = LoadCoverFromRequest(request);
|
|
|
|
#ifdef Q_OS_WIN32
|
|
TagLib::FileRef fileref(filename.toStdWString().c_str());
|
|
#else
|
|
TagLib::FileRef fileref(QFile::encodeName(filename).constData());
|
|
#endif
|
|
|
|
if (fileref.isNull() || !fileref.file()) return false;
|
|
|
|
// FLAC
|
|
if (TagLib::FLAC::File *flac_file = dynamic_cast<TagLib::FLAC::File*>(fileref.file())) {
|
|
TagLib::Ogg::XiphComment *xiph_comment = flac_file->xiphComment(true);
|
|
if (!xiph_comment) return false;
|
|
SetEmbeddedArt(flac_file, xiph_comment, cover.data, cover.mime_type);
|
|
}
|
|
|
|
// Ogg Vorbis / Opus / Speex
|
|
else if (TagLib::Ogg::XiphComment *xiph_comment = dynamic_cast<TagLib::Ogg::XiphComment*>(fileref.file()->tag())) {
|
|
SetEmbeddedArt(xiph_comment, cover.data, cover.mime_type);
|
|
}
|
|
|
|
// MP3
|
|
else if (TagLib::MPEG::File *file_mp3 = dynamic_cast<TagLib::MPEG::File*>(fileref.file())) {
|
|
TagLib::ID3v2::Tag *tag = file_mp3->ID3v2Tag();
|
|
if (!tag) return false;
|
|
SetEmbeddedArt(tag, cover.data, cover.mime_type);
|
|
}
|
|
|
|
// MP4/AAC
|
|
else if (TagLib::MP4::File *aac_file = dynamic_cast<TagLib::MP4::File*>(fileref.file())) {
|
|
TagLib::MP4::Tag *tag = aac_file->tag();
|
|
if (!tag) return false;
|
|
SetEmbeddedArt(aac_file, tag, cover.data, cover.mime_type);
|
|
}
|
|
|
|
// Not supported.
|
|
else return false;
|
|
|
|
const bool success = fileref.file()->save();
|
|
#ifdef Q_OS_LINUX
|
|
if (success) {
|
|
// Linux: inotify doesn't seem to notice the change to the file unless we change the timestamps as well. (this is what touch does)
|
|
utimensat(0, QFile::encodeName(filename).constData(), nullptr, 0);
|
|
}
|
|
#endif // Q_OS_LINUX
|
|
|
|
return success;
|
|
|
|
}
|
|
|
|
TagLib::ID3v2::PopularimeterFrame *TagReaderTagLib::GetPOPMFrameFromTag(TagLib::ID3v2::Tag *tag) {
|
|
|
|
TagLib::ID3v2::PopularimeterFrame *frame = nullptr;
|
|
|
|
const TagLib::ID3v2::FrameListMap &map = tag->frameListMap();
|
|
if (map.contains("POPM")) {
|
|
frame = dynamic_cast<TagLib::ID3v2::PopularimeterFrame*>(map["POPM"].front());
|
|
}
|
|
|
|
if (!frame) {
|
|
frame = new TagLib::ID3v2::PopularimeterFrame();
|
|
tag->addFrame(frame);
|
|
}
|
|
|
|
return frame;
|
|
|
|
}
|
|
|
|
void TagReaderTagLib::SetPlaycount(TagLib::Ogg::XiphComment *xiph_comment, const spb::tagreader::SongMetadata &song) const {
|
|
|
|
if (song.playcount() > 0) {
|
|
xiph_comment->addField("FMPS_PLAYCOUNT", TagLib::String::number(static_cast<int>(song.playcount())), true);
|
|
}
|
|
else {
|
|
xiph_comment->removeFields("FMPS_PLAYCOUNT");
|
|
}
|
|
|
|
}
|
|
|
|
void TagReaderTagLib::SetPlaycount(TagLib::APE::Tag *tag, const spb::tagreader::SongMetadata &song) const {
|
|
|
|
if (song.playcount() > 0) {
|
|
tag->setItem("FMPS_Playcount", TagLib::APE::Item("FMPS_Playcount", TagLib::String::number(static_cast<int>(song.playcount()))));
|
|
}
|
|
else {
|
|
tag->removeItem("FMPS_Playcount");
|
|
}
|
|
|
|
}
|
|
|
|
void TagReaderTagLib::SetPlaycount(TagLib::ID3v2::Tag *tag, const spb::tagreader::SongMetadata &song) const {
|
|
|
|
SetUserTextFrame(QStringLiteral("FMPS_Playcount"), QString::number(song.playcount()), tag);
|
|
TagLib::ID3v2::PopularimeterFrame *frame = GetPOPMFrameFromTag(tag);
|
|
if (frame) {
|
|
frame->setCounter(song.playcount());
|
|
}
|
|
|
|
}
|
|
|
|
void TagReaderTagLib::SetPlaycount(TagLib::MP4::Tag *tag, const spb::tagreader::SongMetadata &song) const {
|
|
|
|
if (song.playcount() > 0) {
|
|
tag->setItem(kMP4_FMPS_Playcount_ID, TagLib::MP4::Item(TagLib::String::number(static_cast<int>(song.playcount()))));
|
|
}
|
|
else {
|
|
tag->removeItem(kMP4_FMPS_Playcount_ID);
|
|
}
|
|
|
|
}
|
|
|
|
void TagReaderTagLib::SetPlaycount(TagLib::ASF::Tag *tag, const spb::tagreader::SongMetadata &song) const {
|
|
|
|
if (song.playcount() > 0) {
|
|
tag->setAttribute("FMPS/Playcount", TagLib::ASF::Attribute(QStringToTaglibString(QString::number(song.playcount()))));
|
|
}
|
|
else {
|
|
tag->removeItem("FMPS/Playcount");
|
|
}
|
|
|
|
}
|
|
|
|
bool TagReaderTagLib::SaveSongPlaycountToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const {
|
|
|
|
if (filename.isEmpty()) return false;
|
|
|
|
qLog(Debug) << "Saving song playcount to" << filename;
|
|
|
|
std::unique_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
|
|
if (!fileref || fileref->isNull()) return false;
|
|
|
|
if (TagLib::FLAC::File *flac_file = dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
|
|
TagLib::Ogg::XiphComment *xiph_comment = flac_file->xiphComment(true);
|
|
if (!xiph_comment) return false;
|
|
SetPlaycount(xiph_comment, song);
|
|
}
|
|
else if (TagLib::WavPack::File *wavpack_file = dynamic_cast<TagLib::WavPack::File*>(fileref->file())) {
|
|
TagLib::APE::Tag *tag = wavpack_file->APETag(true);
|
|
if (!tag) return false;
|
|
SetPlaycount(tag, song);
|
|
}
|
|
else if (TagLib::APE::File *ape_file = dynamic_cast<TagLib::APE::File*>(fileref->file())) {
|
|
TagLib::APE::Tag *tag = ape_file->APETag(true);
|
|
if (!tag) return false;
|
|
SetPlaycount(tag, song);
|
|
}
|
|
else if (TagLib::Ogg::XiphComment *xiph_comment = dynamic_cast<TagLib::Ogg::XiphComment*>(fileref->file()->tag())) {
|
|
SetPlaycount(xiph_comment, song);
|
|
}
|
|
else if (TagLib::MPEG::File *mpeg_file = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
|
|
TagLib::ID3v2::Tag *tag = mpeg_file->ID3v2Tag(true);
|
|
if (!tag) return false;
|
|
SetPlaycount(tag, song);
|
|
}
|
|
else if (TagLib::MP4::File *mp4_file = dynamic_cast<TagLib::MP4::File*>(fileref->file())) {
|
|
TagLib::MP4::Tag *tag = mp4_file->tag();
|
|
if (!tag) return false;
|
|
SetPlaycount(tag, song);
|
|
}
|
|
else if (TagLib::MPC::File *mpc_file = dynamic_cast<TagLib::MPC::File*>(fileref->file())) {
|
|
TagLib::APE::Tag *tag = mpc_file->APETag(true);
|
|
if (!tag) return false;
|
|
SetPlaycount(tag, song);
|
|
}
|
|
else if (TagLib::ASF::File *asf_file = dynamic_cast<TagLib::ASF::File*>(fileref->file())) {
|
|
TagLib::ASF::Tag *tag = asf_file->tag();
|
|
if (!tag) return false;
|
|
if (song.playcount() > 0) {
|
|
tag->addAttribute("FMPS/Playcount", TagLib::ASF::Attribute(QStringToTaglibString(QString::number(song.playcount()))));
|
|
}
|
|
}
|
|
else {
|
|
return true;
|
|
}
|
|
|
|
bool success = fileref->save();
|
|
#ifdef Q_OS_LINUX
|
|
if (success) {
|
|
// Linux: inotify doesn't seem to notice the change to the file unless we change the timestamps as well. (this is what touch does)
|
|
utimensat(0, QFile::encodeName(filename).constData(), nullptr, 0);
|
|
}
|
|
#endif // Q_OS_LINUX
|
|
|
|
return success;
|
|
|
|
}
|
|
|
|
void TagReaderTagLib::SetRating(TagLib::Ogg::XiphComment *xiph_comment, const spb::tagreader::SongMetadata &song) const {
|
|
|
|
if (song.rating() > 0.0F) {
|
|
xiph_comment->addField("FMPS_RATING", QStringToTaglibString(QString::number(song.rating())), true);
|
|
}
|
|
else {
|
|
xiph_comment->removeFields("FMPS_RATING");
|
|
}
|
|
|
|
}
|
|
|
|
void TagReaderTagLib::SetRating(TagLib::APE::Tag *tag, const spb::tagreader::SongMetadata &song) const {
|
|
|
|
if (song.rating() > 0.0F) {
|
|
tag->setItem("FMPS_Rating", TagLib::APE::Item("FMPS_Rating", TagLib::StringList(QStringToTaglibString(QString::number(song.rating())))));
|
|
}
|
|
else {
|
|
tag->removeItem("FMPS_Rating");
|
|
}
|
|
|
|
}
|
|
|
|
void TagReaderTagLib::SetRating(TagLib::ID3v2::Tag *tag, const spb::tagreader::SongMetadata &song) const {
|
|
|
|
SetUserTextFrame(QStringLiteral("FMPS_Rating"), QString::number(song.rating()), tag);
|
|
TagLib::ID3v2::PopularimeterFrame *frame = GetPOPMFrameFromTag(tag);
|
|
if (frame) {
|
|
frame->setRating(ConvertToPOPMRating(song.rating()));
|
|
}
|
|
|
|
}
|
|
|
|
void TagReaderTagLib::SetRating(TagLib::MP4::Tag *tag, const spb::tagreader::SongMetadata &song) const {
|
|
|
|
tag->setItem(kMP4_FMPS_Rating_ID, TagLib::StringList(QStringToTaglibString(QString::number(song.rating()))));
|
|
|
|
}
|
|
|
|
void TagReaderTagLib::SetRating(TagLib::ASF::Tag *tag, const spb::tagreader::SongMetadata &song) const {
|
|
|
|
tag->addAttribute("FMPS/Rating", TagLib::ASF::Attribute(QStringToTaglibString(QString::number(song.rating()))));
|
|
|
|
}
|
|
|
|
bool TagReaderTagLib::SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const {
|
|
|
|
if (filename.isNull()) return false;
|
|
|
|
qLog(Debug) << "Saving song rating to" << filename;
|
|
|
|
if (song.rating() < 0) {
|
|
return true;
|
|
}
|
|
|
|
std::unique_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
|
|
|
|
if (!fileref || fileref->isNull()) return false;
|
|
|
|
if (TagLib::FLAC::File *flac_file = dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
|
|
TagLib::Ogg::XiphComment *xiph_comment = flac_file->xiphComment(true);
|
|
if (!xiph_comment) return false;
|
|
SetRating(xiph_comment, song);
|
|
}
|
|
else if (TagLib::WavPack::File *wavpack_file = dynamic_cast<TagLib::WavPack::File*>(fileref->file())) {
|
|
TagLib::APE::Tag *tag = wavpack_file->APETag(true);
|
|
if (!tag) return false;
|
|
SetRating(tag, song);
|
|
}
|
|
else if (TagLib::APE::File *ape_file = dynamic_cast<TagLib::APE::File*>(fileref->file())) {
|
|
TagLib::APE::Tag *tag = ape_file->APETag(true);
|
|
if (!tag) return false;
|
|
SetRating(tag, song);
|
|
}
|
|
else if (TagLib::Ogg::XiphComment *xiph_comment = dynamic_cast<TagLib::Ogg::XiphComment*>(fileref->file()->tag())) {
|
|
SetRating(xiph_comment, song);
|
|
}
|
|
else if (TagLib::MPEG::File *mpeg_file = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
|
|
TagLib::ID3v2::Tag *tag = mpeg_file->ID3v2Tag(true);
|
|
if (!tag) return false;
|
|
SetRating(tag, song);
|
|
}
|
|
else if (TagLib::MP4::File *mp4_file = dynamic_cast<TagLib::MP4::File*>(fileref->file())) {
|
|
TagLib::MP4::Tag *tag = mp4_file->tag();
|
|
if (!tag) return false;
|
|
SetRating(tag, song);
|
|
}
|
|
else if (TagLib::ASF::File *asf_file = dynamic_cast<TagLib::ASF::File*>(fileref->file())) {
|
|
TagLib::ASF::Tag *tag = asf_file->tag();
|
|
if (!tag) return false;
|
|
SetRating(tag, song);
|
|
}
|
|
else if (TagLib::MPC::File *mpc_file = dynamic_cast<TagLib::MPC::File*>(fileref->file())) {
|
|
TagLib::APE::Tag *tag = mpc_file->APETag(true);
|
|
if (!tag) return false;
|
|
SetRating(tag, song);
|
|
}
|
|
else {
|
|
return true;
|
|
}
|
|
|
|
const bool success = fileref->save();
|
|
#ifdef Q_OS_LINUX
|
|
if (success) {
|
|
// Linux: inotify doesn't seem to notice the change to the file unless we change the timestamps as well. (this is what touch does)
|
|
utimensat(0, QFile::encodeName(filename).constData(), nullptr, 0);
|
|
}
|
|
#endif // Q_OS_LINUX
|
|
|
|
return success;
|
|
|
|
}
|