Save embedded cover in the same process as tags

Possible fix for #1158
This commit is contained in:
Jonas Kvinge 2023-03-18 20:03:07 +01:00
parent 394955a03f
commit e20cbe4170
42 changed files with 1205 additions and 723 deletions

View File

@ -46,6 +46,7 @@ target_link_libraries(libstrawberry-tagreader PRIVATE
${Protobuf_LIBRARIES}
${QtCore_LIBRARIES}
${QtNetwork_LIBRARIES}
${QtGui_LIBRARIES}
libstrawberry-common
)

View File

@ -19,6 +19,15 @@
#include <string>
#include <QByteArray>
#include <QString>
#include <QIODevice>
#include <QFile>
#include <QBuffer>
#include <QImage>
#include <QMimeDatabase>
#include "core/logging.h"
#include "tagreaderbase.h"
const std::string TagReaderBase::kEmbeddedCover = "(embedded)";
@ -28,7 +37,8 @@ TagReaderBase::~TagReaderBase() = default;
void TagReaderBase::Decode(const QString &tag, std::string *output) {
output->assign(DataCommaSizeFromQString(tag));
const QByteArray data = tag.toUtf8();
output->assign(data.constData(), data.size());
}
@ -55,3 +65,86 @@ int TagReaderBase::ConvertToPOPMRating(const float rating) {
return 0xFF;
}
QByteArray TagReaderBase::LoadCoverDataFromRequest(const spb::tagreader::SaveFileRequest &request) {
if (!request.has_save_cover() || !request.save_cover()) {
return QByteArray();
}
const QString song_filename = QString::fromUtf8(request.filename().data(), request.filename().size());
QString cover_filename;
if (request.has_cover_filename()) {
cover_filename = QString::fromUtf8(request.cover_filename().data(), request.cover_filename().size());
}
QByteArray cover_data;
if (request.has_cover_data()) {
cover_data = QByteArray(request.cover_data().data(), request.cover_data().size());
}
bool cover_is_jpeg = false;
if (request.has_cover_is_jpeg()) {
cover_is_jpeg = request.cover_is_jpeg();
}
return LoadCoverDataFromRequest(song_filename, cover_filename, cover_data, cover_is_jpeg);
}
QByteArray TagReaderBase::LoadCoverDataFromRequest(const spb::tagreader::SaveEmbeddedArtRequest &request) {
const QString song_filename = QString::fromUtf8(request.filename().data(), request.filename().size());
QString cover_filename;
if (request.has_cover_filename()) {
cover_filename = QString::fromUtf8(request.cover_filename().data(), request.cover_filename().size());
}
QByteArray cover_data;
if (request.has_cover_data()) {
cover_data = QByteArray(request.cover_data().data(), request.cover_data().size());
}
bool cover_is_jpeg = false;
if (request.has_cover_is_jpeg()) {
cover_is_jpeg = request.cover_is_jpeg();
}
return LoadCoverDataFromRequest(song_filename, cover_filename, cover_data, cover_is_jpeg);
}
QByteArray TagReaderBase::LoadCoverDataFromRequest(const QString &song_filename, const QString &cover_filename, QByteArray cover_data, const bool cover_is_jpeg) {
if (!cover_data.isEmpty() && cover_is_jpeg) {
qLog(Debug) << "Using cover from JPEG data for" << song_filename;
return cover_data;
}
if (cover_data.isEmpty() && !cover_filename.isEmpty()) {
qLog(Debug) << "Loading cover from" << cover_filename << "for" << song_filename;
QFile file(cover_filename);
if (!file.open(QIODevice::ReadOnly)) {
qLog(Error) << "Failed to open file" << cover_filename << "for reading:" << file.errorString();
return QByteArray();
}
cover_data = file.readAll();
file.close();
}
if (!cover_data.isEmpty()) {
if (QMimeDatabase().mimeTypeForData(cover_data).name() == "image/jpeg") {
qLog(Debug) << "Using cover from JPEG data for" << song_filename;
return cover_data;
}
// Convert image to JPEG.
qLog(Debug) << "Converting cover to JPEG data for" << song_filename;
QImage cover_image(cover_data);
cover_data.clear();
QBuffer buffer(&cover_data);
if (buffer.open(QIODevice::WriteOnly)) {
cover_image.save(&buffer, "JPEG");
buffer.close();
}
return cover_data;
}
return QByteArray();
}

View File

@ -27,9 +27,6 @@
#include "tagreadermessages.pb.h"
#define QStringFromStdString(x) QString::fromUtf8((x).data(), (x).size())
#define DataCommaSizeFromQString(x) (x).toUtf8().constData(), (x).toUtf8().length()
/*
* This class holds all useful methods to read and write tags from/to files.
* You should not use it directly in the main process but rather use a TagReaderWorker process (using TagReaderClient)
@ -42,10 +39,10 @@ class TagReaderBase {
virtual bool IsMediaFile(const QString &filename) const = 0;
virtual bool ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const = 0;
virtual bool SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const = 0;
virtual bool SaveFile(const spb::tagreader::SaveFileRequest &request) const = 0;
virtual QByteArray LoadEmbeddedArt(const QString &filename) const = 0;
virtual bool SaveEmbeddedArt(const QString &filename, const QByteArray &data) = 0;
virtual bool SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtRequest &request) const = 0;
virtual bool SaveSongPlaycountToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const = 0;
virtual bool SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const = 0;
@ -55,6 +52,12 @@ class TagReaderBase {
static float ConvertPOPMRating(const int POPM_rating);
static int ConvertToPOPMRating(const float rating);
static QByteArray LoadCoverDataFromRequest(const spb::tagreader::SaveFileRequest &request);
static QByteArray LoadCoverDataFromRequest(const spb::tagreader::SaveEmbeddedArtRequest &request);
private:
static QByteArray LoadCoverDataFromRequest(const QString &song_filename, const QString &cover_filename, QByteArray cover_data, const bool cover_is_jpeg);
protected:
static const std::string kEmbeddedCover;

View File

@ -278,7 +278,7 @@ bool TagReaderGME::ReadFile(const QString &filename, spb::tagreader::SongMetadat
return GME::ReadFile(fileinfo, song);
}
bool TagReaderGME::SaveFile(const QString&, const spb::tagreader::SongMetadata&) const {
bool TagReaderGME::SaveFile(const spb::tagreader::SaveFileRequest&) const {
return false;
}
@ -286,7 +286,7 @@ QByteArray TagReaderGME::LoadEmbeddedArt(const QString&) const {
return QByteArray();
}
bool TagReaderGME::SaveEmbeddedArt(const QString&, const QByteArray&) {
bool TagReaderGME::SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtRequest&) const {
return false;
}

View File

@ -107,10 +107,10 @@ class TagReaderGME : public TagReaderBase {
bool IsMediaFile(const QString &filename) const override;
bool ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const override;
bool SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
bool SaveFile(const spb::tagreader::SaveFileRequest &request) const override;
QByteArray LoadEmbeddedArt(const QString &filename) const override;
bool SaveEmbeddedArt(const QString &filename, const QByteArray &data) override;
bool SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtRequest &request) const override;
bool SaveSongPlaycountToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
bool SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;

View File

@ -77,6 +77,14 @@ message SongMetadata {
}
message IsMediaFileRequest {
optional string filename = 1;
}
message IsMediaFileResponse {
optional bool success = 1;
}
message ReadFileRequest {
optional string filename = 1;
}
@ -87,21 +95,20 @@ message ReadFileResponse {
message SaveFileRequest {
optional string filename = 1;
optional SongMetadata metadata = 2;
optional bool save_tags = 2;
optional bool save_playcount = 3;
optional bool save_rating = 4;
optional bool save_cover = 5;
optional SongMetadata metadata = 6;
optional string cover_filename = 7;
optional bytes cover_data = 8;
optional bool cover_is_jpeg = 9;
}
message SaveFileResponse {
optional bool success = 1;
}
message IsMediaFileRequest {
optional string filename = 1;
}
message IsMediaFileResponse {
optional bool success = 1;
}
message LoadEmbeddedArtRequest {
optional string filename = 1;
}
@ -112,7 +119,9 @@ message LoadEmbeddedArtResponse {
message SaveEmbeddedArtRequest {
optional string filename = 1;
optional bytes data = 2;
optional string cover_filename = 2;
optional bytes cover_data = 3;
optional bool cover_is_jpeg = 4;
}
message SaveEmbeddedArtResponse {

View File

@ -81,11 +81,12 @@
#endif
#include <QtGlobal>
#include <QFile>
#include <QFileInfo>
#include <QVector>
#include <QByteArray>
#include <QString>
#include <QStringList>
#include <QFile>
#include <QFileInfo>
#include <QUrl>
#include <QDateTime>
#include <QtDebug>
@ -193,7 +194,8 @@ bool TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMeta
qLog(Debug) << "Reading tags from" << filename;
song->set_basefilename(DataCommaSizeFromQString(fileinfo.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);
@ -494,7 +496,9 @@ bool TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMeta
if (compilation.isEmpty()) {
// well, it wasn't set, but if the artist is VA assume it's a compilation
if (QStringFromStdString(song->artist()).compare("various artists") == 0 || QStringFromStdString(song->albumartist()).compare("various artists") == 0) {
const QString albumartist = QString::fromUtf8(song->albumartist().data(), song->albumartist().size());
const QString artist = QString::fromUtf8(song->artist().data(), song->artist().size());
if (artist.compare("various artists") == 0 || albumartist.compare("various artists") == 0) {
song->set_compilation(true);
}
}
@ -521,8 +525,9 @@ bool TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMeta
void TagReaderTagLib::Decode(const TagLib::String &tag, std::string *output) {
QString tmp = TStringToQString(tag).trimmed();
output->assign(DataCommaSizeFromQString(tmp));
const QString tmp = TStringToQString(tag).trimmed();
const QByteArray data = tmp.toUtf8();
output->assign(data.constData(), data.size());
}
@ -624,79 +629,175 @@ void TagReaderTagLib::SetVorbisComments(TagLib::Ogg::XiphComment *vorbis_comment
}
bool TagReaderTagLib::SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const {
bool TagReaderTagLib::SaveFile(const spb::tagreader::SaveFileRequest &request) const {
if (filename.isEmpty()) return false;
if (request.filename().empty()) return false;
const QString filename = QString::fromUtf8(request.filename().data(), 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 << "tags";
}
if (save_playcount) {
save_tags_options << "playcount";
}
if (save_rating) {
save_tags_options << "rating";
}
if (save_cover) {
save_tags_options << "embedded cover";
}
qLog(Debug) << "Saving" << save_tags_options.join(", ") << "to" << filename;
const QByteArray cover_data = LoadCoverDataFromRequest(request);
qLog(Debug) << "Saving tags to" << filename;
std::unique_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));;
if (!fileref || fileref->isNull()) return false;
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 result = 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())) {
TagLib::Ogg::XiphComment *tag = file_flac->xiphComment(true);
if (!tag) return false;
SetVorbisComments(tag, song);
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);
}
}
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;
SaveAPETag(tag, song);
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;
SaveAPETag(tag, song);
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;
SaveAPETag(tag, song);
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;
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);
if (save_tags) {
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);
}
if (save_playcount) {
SetPlaycount(tag, song);
}
if (save_rating) {
SetRating(tag, song);
}
if (save_cover) {
SetEmbeddedArt(file_mpeg, tag, cover_data);
}
}
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;
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_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);
}
}
// 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 (TagLib::Ogg::XiphComment *xiph_comment = dynamic_cast<TagLib::Ogg::XiphComment*>(fileref->file()->tag())) {
SetVorbisComments(xiph_comment, song);
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);
}
}
}
result = fileref->save();
const bool result = fileref->save();
#ifdef Q_OS_LINUX
if (result) {
// Linux: inotify doesn't seem to notice the change to the file unless we change the timestamps as well. (this is what touch does)
@ -705,6 +806,7 @@ bool TagReaderTagLib::SaveFile(const QString &filename, const spb::tagreader::So
#endif // Q_OS_LINUX
return result;
}
void TagReaderTagLib::SaveAPETag(TagLib::APE::Tag *tag, const spb::tagreader::SongMetadata &song) const {
@ -939,12 +1041,84 @@ QByteArray TagReaderTagLib::LoadEmbeddedAPEArt(const TagLib::APE::ItemListMap &m
}
bool TagReaderTagLib::SaveEmbeddedArt(const QString &filename, const QByteArray &data) {
void TagReaderTagLib::SetEmbeddedArt(TagLib::FLAC::File *flac_file, TagLib::Ogg::XiphComment *xiph_comment, const QByteArray &data) const {
if (filename.isEmpty()) return false;
(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("image/jpeg");
picture->setData(TagLib::ByteVector(data.constData(), data.size()));
flac_file->addPicture(picture);
}
}
void TagReaderTagLib::SetEmbeddedArt(TagLib::Ogg::XiphComment *xiph_comment, const QByteArray &data) const {
xiph_comment->removeAllPictures();
if (!data.isEmpty()) {
TagLib::FLAC::Picture *picture = new TagLib::FLAC::Picture();
picture->setType(TagLib::FLAC::Picture::FrontCover);
picture->setMimeType("image/jpeg");
picture->setData(TagLib::ByteVector(data.constData(), data.size()));
xiph_comment->addPicture(picture);
}
}
void TagReaderTagLib::SetEmbeddedArt(TagLib::MPEG::File *file_mp3, TagLib::ID3v2::Tag *tag, const QByteArray &data) const {
(void)file_mp3;
// 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("image/jpeg");
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 {
(void)aac_file;
TagLib::MP4::CoverArtList covers;
if (data.isEmpty()) {
if (tag->contains("covr")) tag->removeItem("covr");
}
else {
covers.append(TagLib::MP4::CoverArt(TagLib::MP4::CoverArt::JPEG, 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(), request.filename().size());
qLog(Debug) << "Saving art to" << filename;
const QByteArray cover_data = LoadCoverDataFromRequest(request);
#ifdef Q_OS_WIN32
TagLib::FileRef fileref(filename.toStdWString().c_str());
#else
@ -955,65 +1129,28 @@ bool TagReaderTagLib::SaveEmbeddedArt(const QString &filename, const QByteArray
// FLAC
if (TagLib::FLAC::File *flac_file = dynamic_cast<TagLib::FLAC::File*>(fileref.file())) {
TagLib::Ogg::XiphComment *vorbis_comments = flac_file->xiphComment(true);
if (!vorbis_comments) return false;
flac_file->removePictures();
if (!data.isEmpty()) {
TagLib::FLAC::Picture *picture = new TagLib::FLAC::Picture();
picture->setType(TagLib::FLAC::Picture::FrontCover);
picture->setMimeType("image/jpeg");
picture->setData(TagLib::ByteVector(data.constData(), data.size()));
flac_file->addPicture(picture);
}
TagLib::Ogg::XiphComment *xiph_comment = flac_file->xiphComment(true);
if (!xiph_comment) return false;
SetEmbeddedArt(flac_file, xiph_comment, cover_data);
}
// Ogg Vorbis / Opus / Speex
else if (TagLib::Ogg::XiphComment *xiph_comment = dynamic_cast<TagLib::Ogg::XiphComment*>(fileref.file()->tag())) {
xiph_comment->removeAllPictures();
if (!data.isEmpty()) {
TagLib::FLAC::Picture *picture = new TagLib::FLAC::Picture();
picture->setType(TagLib::FLAC::Picture::FrontCover);
picture->setMimeType("image/jpeg");
picture->setData(TagLib::ByteVector(data.constData(), data.size()));
xiph_comment->addPicture(picture);
}
SetEmbeddedArt(xiph_comment, cover_data);
}
// 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;
// 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("image/jpeg");
frontcover->setPicture(TagLib::ByteVector(data.constData(), data.size()));
tag->addFrame(frontcover);
}
SetEmbeddedArt(file_mp3, tag, cover_data);
}
// 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;
TagLib::MP4::CoverArtList covers;
if (data.isEmpty()) {
if (tag->contains("covr")) tag->removeItem("covr");
}
else {
covers.append(TagLib::MP4::CoverArt(TagLib::MP4::CoverArt::JPEG, TagLib::ByteVector(data.constData(), data.size())));
tag->setItem("covr", covers);
}
SetEmbeddedArt(aac_file, tag, cover_data);
}
// Not supported.
@ -1041,6 +1178,60 @@ TagLib::ID3v2::PopularimeterFrame *TagReaderTagLib::GetPOPMFrameFromTag(TagLib::
}
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("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;
@ -1051,62 +1242,37 @@ bool TagReaderTagLib::SaveSongPlaycountToFile(const QString &filename, const spb
if (!fileref || fileref->isNull()) return false;
if (TagLib::FLAC::File *flac_file = dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
TagLib::Ogg::XiphComment *vorbis_comments = flac_file->xiphComment(true);
if (!vorbis_comments) return false;
if (song.playcount() > 0) {
vorbis_comments->addField("FMPS_PLAYCOUNT", TagLib::String::number(static_cast<int>(song.playcount())), true);
}
else {
vorbis_comments->removeFields("FMPS_PLAYCOUNT");
}
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;
if (song.playcount() > 0) {
tag->setItem("FMPS_Playcount", TagLib::APE::Item("FMPS_Playcount", TagLib::String::number(static_cast<int>(song.playcount()))));
}
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;
if (song.playcount() > 0) {
tag->setItem("FMPS_Playcount", TagLib::APE::Item("FMPS_Playcount", TagLib::String::number(static_cast<int>(song.playcount()))));
}
SetPlaycount(tag, song);
}
else if (TagLib::Ogg::XiphComment *xiph_comment = dynamic_cast<TagLib::Ogg::XiphComment*>(fileref->file()->tag())) {
if (song.playcount() > 0) {
xiph_comment->addField("FMPS_PLAYCOUNT", TagLib::String::number(static_cast<int>(song.playcount())), true);
}
else {
xiph_comment->removeFields("FMPS_PLAYCOUNT");
}
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;
if (song.playcount() > 0) {
SetUserTextFrame("FMPS_Playcount", QString::number(song.playcount()), tag);
TagLib::ID3v2::PopularimeterFrame *frame = GetPOPMFrameFromTag(tag);
if (frame) {
frame->setCounter(song.playcount());
}
}
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;
if (song.playcount() > 0) {
tag->setItem(kMP4_FMPS_Playcount_ID, TagLib::MP4::Item(TagLib::String::number(static_cast<int>(song.playcount()))));
}
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;
if (song.playcount() > 0) {
tag->setItem("FMPS_Playcount", TagLib::APE::Item("FMPS_Playcount", TagLib::String::number(static_cast<int>(song.playcount()))));
}
SetPlaycount(tag, song);
}
else if (TagLib::ASF::File *asf_file = dynamic_cast<TagLib::ASF::File*>(fileref->file())) {
TagLib::ASF::Tag *tag = asf_file->tag();
@ -1130,6 +1296,50 @@ bool TagReaderTagLib::SaveSongPlaycountToFile(const QString &filename, const spb
return ret;
}
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("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;
@ -1145,56 +1355,42 @@ bool TagReaderTagLib::SaveSongRatingToFile(const QString &filename, const spb::t
if (!fileref || fileref->isNull()) return false;
if (TagLib::FLAC::File *flac_file = dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
TagLib::Ogg::XiphComment *vorbis_comments = flac_file->xiphComment(true);
if (!vorbis_comments) return false;
if (song.rating() > 0) {
vorbis_comments->addField("FMPS_RATING", QStringToTaglibString(QString::number(song.rating())), true);
}
else {
vorbis_comments->removeFields("FMPS_RATING");
}
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;
tag->setItem("FMPS_Rating", TagLib::APE::Item("FMPS_Rating", TagLib::StringList(QStringToTaglibString(QString::number(song.rating())))));
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;
tag->setItem("FMPS_Rating", TagLib::APE::Item("FMPS_Rating", TagLib::StringList(QStringToTaglibString(QString::number(song.rating())))));
SetRating(tag, song);
}
else if (TagLib::Ogg::XiphComment *xiph_comment = dynamic_cast<TagLib::Ogg::XiphComment*>(fileref->file()->tag())) {
if (song.rating() > 0) {
xiph_comment->addField("FMPS_RATING", QStringToTaglibString(QString::number(song.rating())), true);
}
else {
xiph_comment->removeFields("FMPS_RATING");
}
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;
SetUserTextFrame("FMPS_Rating", QString::number(song.rating()), tag);
TagLib::ID3v2::PopularimeterFrame *frame = GetPOPMFrameFromTag(tag);
if (frame) {
frame->setRating(ConvertToPOPMRating(song.rating()));
}
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;
tag->setItem(kMP4_FMPS_Rating_ID, TagLib::StringList(QStringToTaglibString(QString::number(song.rating()))));
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;
tag->addAttribute("FMPS/Rating", TagLib::ASF::Attribute(QStringToTaglibString(QString::number(song.rating()))));
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;
tag->setItem("FMPS_Rating", TagLib::APE::Item("FMPS_Rating", TagLib::StringList(QStringToTaglibString(QString::number(song.rating())))));
SetRating(tag, song);
}
else {
return true;

View File

@ -29,8 +29,12 @@
#include <taglib/tstring.h>
#include <taglib/fileref.h>
#include <taglib/xiphcomment.h>
#include <taglib/flacfile.h>
#include <taglib/mpegfile.h>
#include <taglib/mp4file.h>
#include <taglib/apetag.h>
#include <taglib/apefile.h>
#include <taglib/asffile.h>
#include <taglib/id3v2tag.h>
#include <taglib/popularimeterframe.h>
@ -51,10 +55,10 @@ class TagReaderTagLib : public TagReaderBase {
bool IsMediaFile(const QString &filename) const override;
bool ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const override;
bool SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
bool SaveFile(const spb::tagreader::SaveFileRequest &request) const override;
QByteArray LoadEmbeddedArt(const QString &filename) const override;
bool SaveEmbeddedArt(const QString &filename, const QByteArray &data) override;
bool SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtRequest &request) const override;
bool SaveSongPlaycountToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
bool SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
@ -80,6 +84,23 @@ class TagReaderTagLib : public TagReaderBase {
static TagLib::ID3v2::PopularimeterFrame *GetPOPMFrameFromTag(TagLib::ID3v2::Tag *tag);
void SetPlaycount(TagLib::Ogg::XiphComment *xiph_comment, const spb::tagreader::SongMetadata &song) const;
void SetPlaycount(TagLib::APE::Tag *tag, const spb::tagreader::SongMetadata &song) const;
void SetPlaycount(TagLib::ID3v2::Tag *tag, const spb::tagreader::SongMetadata &song) const;
void SetPlaycount(TagLib::MP4::Tag *tag, const spb::tagreader::SongMetadata &song) const;
void SetPlaycount(TagLib::ASF::Tag *tag, const spb::tagreader::SongMetadata &song) const;
void SetRating(TagLib::Ogg::XiphComment *xiph_comment, const spb::tagreader::SongMetadata &song) const;
void SetRating(TagLib::APE::Tag *tag, const spb::tagreader::SongMetadata &song) const;
void SetRating(TagLib::ID3v2::Tag *tag, const spb::tagreader::SongMetadata &song) const;
void SetRating(TagLib::MP4::Tag *tag, const spb::tagreader::SongMetadata &song) const;
void SetRating(TagLib::ASF::Tag *tag, const spb::tagreader::SongMetadata &song) const;
void SetEmbeddedArt(TagLib::FLAC::File *flac_file, TagLib::Ogg::XiphComment *xiph_comment, const QByteArray &data) const;
void SetEmbeddedArt(TagLib::Ogg::XiphComment *xiph_comment, const QByteArray &data) const;
void SetEmbeddedArt(TagLib::MPEG::File *file_mp3, TagLib::ID3v2::Tag *tag, const QByteArray &data) const;
void SetEmbeddedArt(TagLib::MP4::File *aac_file, TagLib::MP4::Tag *tag, const QByteArray &data) const;
private:
FileRefFactory *factory_;

View File

@ -22,6 +22,7 @@
#include <string>
#include <memory>
#include <algorithm>
#include <vector>
#include <sys/stat.h>
#include <tagparser/mediafileinfo.h>
@ -79,7 +80,7 @@ bool TagReaderTagParser::IsMediaFile(const QString &filename) const {
}
const auto tracks = taginfo.tracks();
for (const auto track : tracks) {
for (TagParser::AbstractTrack *track : tracks) {
if (track->mediaType() == TagParser::MediaType::Audio) {
taginfo.close();
return true;
@ -102,8 +103,9 @@ bool TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongM
if (!fileinfo.exists() || fileinfo.suffix().compare("bak", Qt::CaseInsensitive) == 0) return false;
const QByteArray url(QUrl::fromLocalFile(filename).toEncoded());
const QByteArray basefilename = fileinfo.fileName().toUtf8();
song->set_basefilename(DataCommaSizeFromQString(fileinfo.fileName()));
song->set_basefilename(basefilename.constData(), basefilename.size());
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);
@ -154,8 +156,8 @@ bool TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongM
qLog(Debug) << QString::fromStdString(msg.message());
}
const auto tracks = taginfo.tracks();
for (const auto track : tracks) {
std::vector<TagParser::AbstractTrack*> tracks = taginfo.tracks();
for (TagParser::AbstractTrack *track : tracks) {
switch (track->format().general) {
case TagParser::GeneralMediaFormat::Flac:
song->set_filetype(spb::tagreader::SongMetadata_FileType::SongMetadata_FileType_FLAC);
@ -209,7 +211,7 @@ bool TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongM
return false;
}
for (const auto tag : taginfo.tags()) {
for (TagParser::Tag *tag : taginfo.tags()) {
song->set_albumartist(tag->value(TagParser::KnownField::AlbumArtist).toString(TagParser::TagTextEncoding::Utf8));
song->set_artist(tag->value(TagParser::KnownField::Artist).toString(TagParser::TagTextEncoding::Utf8));
song->set_album(tag->value(TagParser::KnownField::Album).toString(TagParser::TagTextEncoding::Utf8));
@ -256,11 +258,34 @@ bool TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongM
}
bool TagReaderTagParser::SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const {
bool TagReaderTagParser::SaveFile(const spb::tagreader::SaveFileRequest &request) const {
if (filename.isEmpty()) return false;
if (request.filename().empty()) return false;
qLog(Debug) << "Saving tags to" << filename;
const QString filename = QString::fromUtf8(request.filename().data(), 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 << "tags";
}
if (save_playcount) {
save_tags_options << "playcount";
}
if (save_rating) {
save_tags_options << "rating";
}
if (save_cover) {
save_tags_options << "embedded cover";
}
qLog(Debug) << "Saving" << save_tags_options.join(", ") << "to" << filename;
const QByteArray cover_data = LoadCoverDataFromRequest(request);
try {
TagParser::MediaFileInfo taginfo;
@ -295,22 +320,34 @@ bool TagReaderTagParser::SaveFile(const QString &filename, const spb::tagreader:
taginfo.createAppropriateTags();
}
for (const auto tag : taginfo.tags()) {
tag->setValue(TagParser::KnownField::AlbumArtist, TagParser::TagValue(song.albumartist(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Artist, TagParser::TagValue(song.artist(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Album, TagParser::TagValue(song.album(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Title, TagParser::TagValue(song.title(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Genre, TagParser::TagValue(song.genre(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Composer, TagParser::TagValue(song.composer(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Performers, TagParser::TagValue(song.performer(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Grouping, TagParser::TagValue(song.grouping(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Comment, TagParser::TagValue(song.comment(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Lyrics, TagParser::TagValue(song.lyrics(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::TrackPosition, TagParser::TagValue(song.track()));
tag->setValue(TagParser::KnownField::DiskPosition, TagParser::TagValue(song.disc()));
tag->setValue(TagParser::KnownField::RecordDate, TagParser::TagValue(song.year()));
tag->setValue(TagParser::KnownField::ReleaseDate, TagParser::TagValue(song.originalyear()));
for (TagParser::Tag *tag : taginfo.tags()) {
if (save_tags) {
tag->setValue(TagParser::KnownField::AlbumArtist, TagParser::TagValue(song.albumartist(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Artist, TagParser::TagValue(song.artist(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Album, TagParser::TagValue(song.album(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Title, TagParser::TagValue(song.title(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Genre, TagParser::TagValue(song.genre(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Composer, TagParser::TagValue(song.composer(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Performers, TagParser::TagValue(song.performer(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Grouping, TagParser::TagValue(song.grouping(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Comment, TagParser::TagValue(song.comment(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Lyrics, TagParser::TagValue(song.lyrics(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::TrackPosition, TagParser::TagValue(song.track()));
tag->setValue(TagParser::KnownField::DiskPosition, TagParser::TagValue(song.disc()));
tag->setValue(TagParser::KnownField::RecordDate, TagParser::TagValue(song.year()));
tag->setValue(TagParser::KnownField::ReleaseDate, TagParser::TagValue(song.originalyear()));
}
if (save_playcount) {
SaveSongPlaycountToFile(tag, song);
}
if (save_rating) {
SaveSongRatingToFile(tag, song);
}
if (save_cover) {
SaveEmbeddedArt(tag, cover_data);
}
}
taginfo.applyChanges(diag, progress);
taginfo.close();
@ -358,7 +395,7 @@ QByteArray TagReaderTagParser::LoadEmbeddedArt(const QString &filename) const {
return QByteArray();
}
for (const auto tag : taginfo.tags()) {
for (TagParser::Tag *tag : taginfo.tags()) {
if (!tag->value(TagParser::KnownField::Cover).empty() && tag->value(TagParser::KnownField::Cover).dataSize() > 0) {
QByteArray data(tag->value(TagParser::KnownField::Cover).dataPointer(), tag->value(TagParser::KnownField::Cover).dataSize());
taginfo.close();
@ -379,12 +416,22 @@ QByteArray TagReaderTagParser::LoadEmbeddedArt(const QString &filename) const {
}
bool TagReaderTagParser::SaveEmbeddedArt(const QString &filename, const QByteArray &data) {
void TagReaderTagParser::SaveEmbeddedArt(TagParser::Tag *tag, const QByteArray &data) const {
if (filename.isEmpty()) return false;
tag->setValue(TagParser::KnownField::Cover, TagParser::TagValue(data.toStdString()));
}
bool TagReaderTagParser::SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtRequest &request) const {
if (request.filename().empty()) return false;
const QString filename = QString::fromUtf8(request.filename().data(), request.filename().size());
qLog(Debug) << "Saving art to" << filename;
const QByteArray cover_data = LoadCoverDataFromRequest(request);
try {
TagParser::MediaFileInfo taginfo;
@ -415,8 +462,8 @@ bool TagReaderTagParser::SaveEmbeddedArt(const QString &filename, const QByteArr
taginfo.createAppropriateTags();
}
for (const auto tag : taginfo.tags()) {
tag->setValue(TagParser::KnownField::Cover, TagParser::TagValue(data.toStdString()));
for (TagParser::Tag *tag : taginfo.tags()) {
SaveEmbeddedArt(tag, cover_data);
}
taginfo.applyChanges(diag, progress);
@ -435,8 +482,16 @@ bool TagReaderTagParser::SaveEmbeddedArt(const QString &filename, const QByteArr
}
void TagReaderTagParser::SaveSongPlaycountToFile(TagParser::Tag*, const spb::tagreader::SongMetadata&) const {}
bool TagReaderTagParser::SaveSongPlaycountToFile(const QString&, const spb::tagreader::SongMetadata&) const { return false; }
void TagReaderTagParser::SaveSongRatingToFile(TagParser::Tag *tag, const spb::tagreader::SongMetadata &song) const {
tag->setValue(TagParser::KnownField::Rating, TagParser::TagValue(ConvertToPOPMRating(song.rating())));
}
bool TagReaderTagParser::SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const {
if (filename.isEmpty()) return false;
@ -476,9 +531,10 @@ bool TagReaderTagParser::SaveSongRatingToFile(const QString &filename, const spb
taginfo.createAppropriateTags();
}
for (const auto tag : taginfo.tags()) {
tag->setValue(TagParser::KnownField::Rating, TagParser::TagValue(ConvertToPOPMRating(song.rating())));
for (TagParser::Tag *tag : taginfo.tags()) {
SaveSongRatingToFile(tag, song);
}
taginfo.applyChanges(diag, progress);
taginfo.close();

View File

@ -22,6 +22,8 @@
#include <string>
#include <tagparser/tag.h>
#include <QByteArray>
#include <QString>
@ -40,14 +42,20 @@ class TagReaderTagParser : public TagReaderBase {
bool IsMediaFile(const QString &filename) const override;
bool ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const override;
bool SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
bool SaveFile(const spb::tagreader::SaveFileRequest &request) const override;
QByteArray LoadEmbeddedArt(const QString &filename) const override;
bool SaveEmbeddedArt(const QString &filename, const QByteArray &data) override;
bool SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtRequest &request) const override;
bool SaveSongPlaycountToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
bool SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
private:
void SaveSongPlaycountToFile(TagParser::Tag *tag, const spb::tagreader::SongMetadata &song) const;
void SaveSongRatingToFile(TagParser::Tag *tag, const spb::tagreader::SongMetadata &song) const;
void SaveEmbeddedArt(TagParser::Tag *tag, const QByteArray &data) const;
public:
Q_DISABLE_COPY(TagReaderTagParser)
};

View File

@ -56,36 +56,41 @@ void TagReaderWorker::DeviceClosed() {
bool TagReaderWorker::HandleMessage(const spb::tagreader::Message &message, spb::tagreader::Message &reply, TagReaderBase *reader) {
if (message.has_is_media_file_request()) {
bool success = reader->IsMediaFile(QStringFromStdString(message.is_media_file_request().filename()));
const QString filename = QString::fromUtf8(message.is_media_file_request().filename().data(), message.is_media_file_request().filename().size());
bool success = reader->IsMediaFile(filename);
reply.mutable_is_media_file_response()->set_success(success);
return success;
}
else if (message.has_read_file_request()) {
bool success = reader->ReadFile(QStringFromStdString(message.read_file_request().filename()), reply.mutable_read_file_response()->mutable_metadata());
const QString filename = QString::fromUtf8(message.read_file_request().filename().data(), message.read_file_request().filename().size());
bool success = reader->ReadFile(filename, reply.mutable_read_file_response()->mutable_metadata());
return success;
}
else if (message.has_save_file_request()) {
bool success = reader->SaveFile(QStringFromStdString(message.save_file_request().filename()), message.save_file_request().metadata());
bool success = reader->SaveFile(message.save_file_request());
reply.mutable_save_file_response()->set_success(success);
return success;
}
else if (message.has_load_embedded_art_request()) {
QByteArray data = reader->LoadEmbeddedArt(QStringFromStdString(message.load_embedded_art_request().filename()));
const QString filename = QString::fromUtf8(message.load_embedded_art_request().filename().data(), message.load_embedded_art_request().filename().size());
QByteArray data = reader->LoadEmbeddedArt(filename);
reply.mutable_load_embedded_art_response()->set_data(data.constData(), data.size());
return true;
}
else if (message.has_save_embedded_art_request()) {
bool success = reader->SaveEmbeddedArt(QStringFromStdString(message.save_embedded_art_request().filename()), QByteArray(message.save_embedded_art_request().data().data(), static_cast<qint64>(message.save_embedded_art_request().data().size())));
bool success = reader->SaveEmbeddedArt(message.save_embedded_art_request());
reply.mutable_save_embedded_art_response()->set_success(success);
return success;
}
else if (message.has_save_song_playcount_to_file_request()) {
bool success = reader->SaveSongPlaycountToFile(QStringFromStdString(message.save_song_playcount_to_file_request().filename()), message.save_song_playcount_to_file_request().metadata());
const QString filename = QString::fromUtf8(message.save_song_playcount_to_file_request().filename().data(), message.save_song_playcount_to_file_request().filename().size());
bool success = reader->SaveSongPlaycountToFile(filename, message.save_song_playcount_to_file_request().metadata());
reply.mutable_save_song_playcount_to_file_response()->set_success(success);
return success;
}
else if (message.has_save_song_rating_to_file_request()) {
bool success = reader->SaveSongRatingToFile(QStringFromStdString(message.save_song_rating_to_file_request().filename()), message.save_song_rating_to_file_request().metadata());
const QString filename = QString::fromUtf8(message.save_song_rating_to_file_request().filename().data(), message.save_song_rating_to_file_request().filename().size());
bool success = reader->SaveSongRatingToFile(filename, message.save_song_rating_to_file_request().metadata());
reply.mutable_save_song_rating_to_file_response()->set_success(success);
return success;
}

View File

@ -55,6 +55,7 @@ set(SOURCES
utilities/transliterate.cpp
utilities/xmlutils.cpp
utilities/filemanagerutils.cpp
utilities/coverutils.cpp
engine/enginetype.cpp
engine/enginebase.cpp

View File

@ -140,8 +140,12 @@ void CollectionBackend::IncrementSkipCountAsync(const int id, const float progre
QMetaObject::invokeMethod(this, "IncrementSkipCount", Qt::QueuedConnection, Q_ARG(int, id), Q_ARG(float, progress));
}
void CollectionBackend::ResetStatisticsAsync(const int id, const bool save_tags) {
QMetaObject::invokeMethod(this, "ResetStatistics", Qt::QueuedConnection, Q_ARG(int, id), Q_ARG(bool, save_tags));
void CollectionBackend::ResetPlayStatisticsAsync(const int id, const bool save_tags) {
QMetaObject::invokeMethod(this, "ResetPlayStatistics", Qt::QueuedConnection, Q_ARG(int, id), Q_ARG(bool, save_tags));
}
void CollectionBackend::ResetPlayStatisticsAsync(const QList<int> &id_list, const bool save_tags) {
QMetaObject::invokeMethod(this, "ResetPlayStatistics", Qt::QueuedConnection, Q_ARG(QList<int>, id_list), Q_ARG(bool, save_tags));
}
void CollectionBackend::LoadDirectories() {
@ -1776,23 +1780,48 @@ void CollectionBackend::IncrementSkipCount(const int id, const float progress) {
}
void CollectionBackend::ResetStatistics(const int id, const bool save_tags) {
void CollectionBackend::ResetPlayStatistics(const int id, const bool save_tags) {
if (id == -1) return;
ResetPlayStatistics(QList<int>() << id, save_tags);
}
void CollectionBackend::ResetPlayStatistics(const QList<int> &id_list, const bool save_tags) {
if (id_list.isEmpty()) return;
QStringList id_str_list;
id_str_list.reserve(id_list.count());
for (const int id : id_list) {
id_str_list << QString::number(id);
}
const bool success = ResetPlayStatistics(id_str_list);
if (success) {
const SongList songs = GetSongsById(id_list);
emit SongsStatisticsChanged(songs, save_tags);
}
}
bool CollectionBackend::ResetPlayStatistics(const QStringList &id_str_list) {
if (id_str_list.isEmpty()) return false;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
SqlQuery q(db);
q.prepare(QString("UPDATE %1 SET playcount = 0, skipcount = 0, lastplayed = -1 WHERE ROWID = :id").arg(songs_table_));
q.BindValue(":id", id);
q.prepare(QString("UPDATE %1 SET playcount = 0, skipcount = 0, lastplayed = -1 WHERE ROWID IN (:ids)").arg(songs_table_));
q.BindValue(":ids", id_str_list.join(","));
if (!q.Exec()) {
db_->ReportErrors(q);
return;
return false;
}
Song new_song = GetSongById(id, db);
emit SongsStatisticsChanged(SongList() << new_song, save_tags);
return true;
}

View File

@ -200,7 +200,8 @@ class CollectionBackend : public CollectionBackendInterface {
void IncrementPlayCountAsync(const int id);
void IncrementSkipCountAsync(const int id, const float progress);
void ResetStatisticsAsync(const int id, const bool save_tags = false);
void ResetPlayStatisticsAsync(const int id, const bool save_tags = false);
void ResetPlayStatisticsAsync(const QList<int> &id_list, const bool save_tags = false);
void DeleteAllAsync();
@ -236,7 +237,9 @@ class CollectionBackend : public CollectionBackendInterface {
void ForceCompilation(const QString &album, const QList<QString> &artists, const bool on);
void IncrementPlayCount(const int id);
void IncrementSkipCount(const int id, const float progress);
void ResetStatistics(const int id, const bool save_tags = false);
void ResetPlayStatistics(const int id, const bool save_tags = false);
void ResetPlayStatistics(const QList<int> &id_list, const bool save_tags = false);
bool ResetPlayStatistics(const QStringList &id_str_list);
void DeleteAll();
void SongPathChanged(const Song &song, const QFileInfo &new_file, const std::optional<int> new_collection_directory_id);

View File

@ -855,8 +855,8 @@ void CollectionWatcher::AddChangedSong(const QString &file, const Song &matching
changes << "metadata";
notify_new = true;
}
if (!matching_song.IsStatisticsEqual(new_song)) {
changes << "statistics";
if (!matching_song.IsPlayStatisticsEqual(new_song)) {
changes << "play statistics";
notify_new = true;
}
if (!matching_song.IsRatingEqual(new_song)) {

View File

@ -1169,7 +1169,6 @@ void MainWindow::ReloadAllSettings() {
collection_view_->ReloadSettings();
ui_->playlist->view()->ReloadSettings();
app_->playlist_manager()->playlist_container()->ReloadSettings();
app_->album_cover_loader()->ReloadSettings();
album_cover_choice_controller_->ReloadSettings();
context_view_->ReloadSettings();
file_view_->ReloadSettings();
@ -2152,7 +2151,7 @@ void MainWindow::SongSaveComplete(TagReaderReply *reply, const QPersistentModelI
if (reply->is_successful() && idx.isValid()) {
app_->playlist_manager()->current()->ReloadItems(QList<int>() << idx.row());
}
QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection);
reply->deleteLater();
}

View File

@ -52,7 +52,7 @@
#include "engine/enginebase.h"
#include "utilities/strutils.h"
#include "utilities/timeutils.h"
#include "utilities/cryptutils.h"
#include "utilities/coverutils.h"
#include "utilities/timeconstants.h"
#include "song.h"
#include "application.h"
@ -61,9 +61,6 @@
#include "sqlrow.h"
#include "tagreadermessages.pb.h"
#define QStringFromStdString(x) QString::fromUtf8((x).data(), (x).size())
#define DataCommaSizeFromQString(x) (x).toUtf8().constData(), (x).toUtf8().length()
const QStringList Song::kColumns = QStringList() << "title"
<< "album"
<< "artist"
@ -903,27 +900,27 @@ void Song::InitFromProtobuf(const spb::tagreader::SongMetadata &pb) {
d->init_from_file_ = true;
d->valid_ = pb.valid();
set_title(QStringFromStdString(pb.title()));
set_album(QStringFromStdString(pb.album()));
set_artist(QStringFromStdString(pb.artist()));
set_albumartist(QStringFromStdString(pb.albumartist()));
set_title(QString::fromUtf8(pb.title().data(), pb.title().size()));
set_album(QString::fromUtf8(pb.album().data(), pb.album().size()));
set_artist(QString::fromUtf8(pb.artist().data(), pb.artist().size()));
set_albumartist(QString::fromUtf8(pb.albumartist().data(), pb.albumartist().size()));
d->track_ = pb.track();
d->disc_ = pb.disc();
d->year_ = pb.year();
d->originalyear_ = pb.originalyear();
d->genre_ = QStringFromStdString(pb.genre());
d->genre_ = QString::fromUtf8(pb.genre().data(), pb.genre().size());
d->compilation_ = pb.compilation();
d->composer_ = QStringFromStdString(pb.composer());
d->performer_ = QStringFromStdString(pb.performer());
d->grouping_ = QStringFromStdString(pb.grouping());
d->comment_ = QStringFromStdString(pb.comment());
d->lyrics_ = QStringFromStdString(pb.lyrics());
d->composer_ = QString::fromUtf8(pb.composer().data(), pb.composer().size());
d->performer_ = QString::fromUtf8(pb.performer().data(), pb.performer().size());
d->grouping_ = QString::fromUtf8(pb.grouping().data(), pb.grouping().size());
d->comment_ = QString::fromUtf8(pb.comment().data(), pb.comment().size());
d->lyrics_ = QString::fromUtf8(pb.lyrics().data(), pb.lyrics().size());
set_length_nanosec(static_cast<qint64>(pb.length_nanosec()));
d->bitrate_ = pb.bitrate();
d->samplerate_ = pb.samplerate();
d->bitdepth_ = pb.bitdepth();
set_url(QUrl::fromEncoded(QByteArray(pb.url().data(), static_cast<qint64>(pb.url().size()))));
d->basefilename_ = QStringFromStdString(pb.basefilename());
d->basefilename_ = QString::fromUtf8(pb.basefilename().data(), pb.basefilename().size());
d->filetype_ = static_cast<FileType>(pb.filetype());
d->filesize_ = pb.filesize();
d->mtime_ = pb.mtime();
@ -956,27 +953,27 @@ void Song::ToProtobuf(spb::tagreader::SongMetadata *pb) const {
const QByteArray art_automatic(d->art_automatic_.toEncoded());
pb->set_valid(d->valid_);
pb->set_title(DataCommaSizeFromQString(d->title_));
pb->set_album(DataCommaSizeFromQString(d->album_));
pb->set_artist(DataCommaSizeFromQString(d->artist_));
pb->set_albumartist(DataCommaSizeFromQString(d->albumartist_));
pb->set_title(d->title_.toStdString());
pb->set_album(d->album_.toStdString());
pb->set_artist(d->artist_.toStdString());
pb->set_albumartist(d->albumartist_.toStdString());
pb->set_track(d->track_);
pb->set_disc(d->disc_);
pb->set_year(d->year_);
pb->set_originalyear(d->originalyear_);
pb->set_genre(DataCommaSizeFromQString(d->genre_));
pb->set_genre(d->genre_.toStdString());
pb->set_compilation(d->compilation_);
pb->set_composer(DataCommaSizeFromQString(d->composer_));
pb->set_performer(DataCommaSizeFromQString(d->performer_));
pb->set_grouping(DataCommaSizeFromQString(d->grouping_));
pb->set_comment(DataCommaSizeFromQString(d->comment_));
pb->set_lyrics(DataCommaSizeFromQString(d->lyrics_));
pb->set_composer(d->composer_.toStdString());
pb->set_performer(d->performer_.toStdString());
pb->set_grouping(d->grouping_.toStdString());
pb->set_comment(d->comment_.toStdString());
pb->set_lyrics(d->lyrics_.toStdString());
pb->set_length_nanosec(length_nanosec());
pb->set_bitrate(d->bitrate_);
pb->set_samplerate(d->samplerate_);
pb->set_bitdepth(d->bitdepth_);
pb->set_url(url.constData(), url.size());
pb->set_basefilename(DataCommaSizeFromQString(d->basefilename_));
pb->set_basefilename(d->basefilename_.toStdString());
pb->set_filetype(static_cast<spb::tagreader::SongMetadata_FileType>(d->filetype_));
pb->set_filesize(d->filesize_);
pb->set_mtime(d->mtime_);
@ -1080,7 +1077,7 @@ void Song::InitArtManual() {
// If we don't have an art, check if we have one in the cache
if (d->art_manual_.isEmpty() && d->art_automatic_.isEmpty() && !effective_albumartist().isEmpty() && !effective_album().isEmpty()) {
QString filename(Utilities::Sha1CoverHash(effective_albumartist(), effective_album()).toHex() + ".jpg");
QString filename(CoverUtils::Sha1CoverHash(effective_albumartist(), effective_album()).toHex() + ".jpg");
QString path(ImageCacheDir(d->source_) + "/" + filename);
if (QFile::exists(path)) {
d->art_manual_ = QUrl::fromLocalFile(path);
@ -1153,7 +1150,7 @@ void Song::InitFromItdb(Itdb_Track *track, const QString &prefix) {
QString cover_path = ImageCacheDir(Source::Device);
QDir dir(cover_path);
if (!dir.exists()) dir.mkpath(cover_path);
QString cover_file = cover_path + "/" + Utilities::Sha1CoverHash(effective_albumartist(), effective_album()).toHex() + ".jpg";
QString cover_file = cover_path + "/" + CoverUtils::Sha1CoverHash(effective_albumartist(), effective_album()).toHex() + ".jpg";
GError *error = nullptr;
if (dir.exists() && gdk_pixbuf_save(pixbuf, cover_file.toUtf8().constData(), "jpeg", &error, nullptr)) {
d->art_manual_ = QUrl::fromLocalFile(cover_file);
@ -1526,7 +1523,7 @@ bool Song::IsMetadataEqual(const Song &other) const {
d->cue_path_ == other.d->cue_path_;
}
bool Song::IsStatisticsEqual(const Song &other) const {
bool Song::IsPlayStatisticsEqual(const Song &other) const {
return d->playcount_ == other.d->playcount_ &&
d->skipcount_ == other.d->skipcount_ &&
@ -1556,7 +1553,7 @@ bool Song::IsArtEqual(const Song &other) const {
bool Song::IsAllMetadataEqual(const Song &other) const {
return IsMetadataEqual(other) &&
IsStatisticsEqual(other) &&
IsPlayStatisticsEqual(other) &&
IsRatingEqual(other) &&
IsFingerprintEqual(other) &&
IsArtEqual(other);

View File

@ -390,7 +390,7 @@ class Song {
// Comparison functions
bool IsMetadataEqual(const Song &other) const;
bool IsStatisticsEqual(const Song &other) const;
bool IsPlayStatisticsEqual(const Song &other) const;
bool IsRatingEqual(const Song &other) const;
bool IsFingerprintEqual(const Song &other) const;
bool IsArtEqual(const Song &other) const;

View File

@ -38,8 +38,6 @@
#include "tagreaderclient.h"
#include "settings/collectionsettingspage.h"
#define DataCommaSizeFromQString(x) (x).toUtf8().constData(), (x).toUtf8().length()
const char *TagReaderClient::kWorkerExecutableName = "strawberry-tagreader";
TagReaderClient *TagReaderClient::sInstance = nullptr;
@ -82,9 +80,10 @@ void TagReaderClient::WorkerFailedToStart() {
TagReaderReply *TagReaderClient::IsMediaFile(const QString &filename) {
spb::tagreader::Message message;
spb::tagreader::IsMediaFileRequest *req = message.mutable_is_media_file_request();
spb::tagreader::IsMediaFileRequest *request = message.mutable_is_media_file_request();
req->set_filename(DataCommaSizeFromQString(filename));
const QByteArray filename_data = filename.toUtf8();
request->set_filename(filename_data.constData(), filename_data.length());
return worker_pool_->SendMessageWithReply(&message);
@ -93,21 +92,35 @@ TagReaderReply *TagReaderClient::IsMediaFile(const QString &filename) {
TagReaderReply *TagReaderClient::ReadFile(const QString &filename) {
spb::tagreader::Message message;
spb::tagreader::ReadFileRequest *req = message.mutable_read_file_request();
spb::tagreader::ReadFileRequest *request = message.mutable_read_file_request();
req->set_filename(DataCommaSizeFromQString(filename));
const QByteArray filename_data = filename.toUtf8();
request->set_filename(filename_data.constData(), filename_data.length());
return worker_pool_->SendMessageWithReply(&message);
}
TagReaderReply *TagReaderClient::SaveFile(const QString &filename, const Song &metadata) {
TagReaderReply *TagReaderClient::SaveFile(const QString &filename, const Song &metadata, const SaveTags save_tags, const SavePlaycount save_playcount, const SaveRating save_rating, const SaveCoverOptions &save_cover_options) {
spb::tagreader::Message message;
spb::tagreader::SaveFileRequest *req = message.mutable_save_file_request();
spb::tagreader::SaveFileRequest *request = message.mutable_save_file_request();
req->set_filename(DataCommaSizeFromQString(filename));
metadata.ToProtobuf(req->mutable_metadata());
const QByteArray filename_data = filename.toUtf8();
request->set_filename(filename_data.constData(), filename_data.length());
request->set_save_tags(save_tags == SaveTags::On);
request->set_save_playcount(save_playcount == SavePlaycount::On);
request->set_save_rating(save_rating == SaveRating::On);
request->set_save_cover(save_cover_options.enabled);
request->set_cover_is_jpeg(save_cover_options.is_jpeg);
if (save_cover_options.cover_filename.length() > 0) {
const QByteArray cover_filename = filename.toUtf8();
request->set_cover_filename(cover_filename.constData(), cover_filename.length());
}
if (save_cover_options.cover_data.length() > 0) {
request->set_cover_data(save_cover_options.cover_data.constData(), save_cover_options.cover_data.length());
}
metadata.ToProtobuf(request->mutable_metadata());
ReplyType *reply = worker_pool_->SendMessageWithReply(&message);
@ -118,21 +131,31 @@ TagReaderReply *TagReaderClient::SaveFile(const QString &filename, const Song &m
TagReaderReply *TagReaderClient::LoadEmbeddedArt(const QString &filename) {
spb::tagreader::Message message;
spb::tagreader::LoadEmbeddedArtRequest *req = message.mutable_load_embedded_art_request();
spb::tagreader::LoadEmbeddedArtRequest *request = message.mutable_load_embedded_art_request();
req->set_filename(DataCommaSizeFromQString(filename));
const QByteArray filename_data = filename.toUtf8();
request->set_filename(filename_data.constData(), filename_data.length());
return worker_pool_->SendMessageWithReply(&message);
}
TagReaderReply *TagReaderClient::SaveEmbeddedArt(const QString &filename, const QByteArray &data) {
TagReaderReply *TagReaderClient::SaveEmbeddedArt(const QString &filename, const SaveCoverOptions &save_cover_options) {
spb::tagreader::Message message;
spb::tagreader::SaveEmbeddedArtRequest *req = message.mutable_save_embedded_art_request();
spb::tagreader::SaveEmbeddedArtRequest *request = message.mutable_save_embedded_art_request();
const QByteArray filename_data = filename.toUtf8();
request->set_filename(filename_data.constData(), filename_data.length());
request->set_cover_is_jpeg(save_cover_options.is_jpeg);
if (save_cover_options.cover_filename.length() > 0) {
const QByteArray cover_filename = filename.toUtf8();
request->set_cover_filename(cover_filename.constData(), cover_filename.length());
}
if (save_cover_options.cover_data.length() > 0) {
request->set_cover_data(save_cover_options.cover_data.constData(), save_cover_options.cover_data.length());
}
req->set_filename(DataCommaSizeFromQString(filename));
req->set_data(data.constData(), data.size());
return worker_pool_->SendMessageWithReply(&message);
@ -141,10 +164,11 @@ TagReaderReply *TagReaderClient::SaveEmbeddedArt(const QString &filename, const
TagReaderReply *TagReaderClient::UpdateSongPlaycount(const Song &metadata) {
spb::tagreader::Message message;
spb::tagreader::SaveSongPlaycountToFileRequest *req = message.mutable_save_song_playcount_to_file_request();
spb::tagreader::SaveSongPlaycountToFileRequest *request = message.mutable_save_song_playcount_to_file_request();
req->set_filename(DataCommaSizeFromQString(metadata.url().toLocalFile()));
metadata.ToProtobuf(req->mutable_metadata());
const QByteArray filename_data = metadata.url().toLocalFile().toUtf8();
request->set_filename(filename_data.constData(), filename_data.length());
metadata.ToProtobuf(request->mutable_metadata());
return worker_pool_->SendMessageWithReply(&message);
@ -162,10 +186,11 @@ void TagReaderClient::UpdateSongsPlaycount(const SongList &songs) {
TagReaderReply *TagReaderClient::UpdateSongRating(const Song &metadata) {
spb::tagreader::Message message;
spb::tagreader::SaveSongRatingToFileRequest *req = message.mutable_save_song_rating_to_file_request();
spb::tagreader::SaveSongRatingToFileRequest *request = message.mutable_save_song_rating_to_file_request();
req->set_filename(DataCommaSizeFromQString(metadata.url().toLocalFile()));
metadata.ToProtobuf(req->mutable_metadata());
const QByteArray filename_data = metadata.url().toLocalFile().toUtf8();
request->set_filename(filename_data.constData(), filename_data.length());
metadata.ToProtobuf(request->mutable_metadata());
return worker_pool_->SendMessageWithReply(&message);
@ -190,7 +215,7 @@ bool TagReaderClient::IsMediaFileBlocking(const QString &filename) {
if (reply->WaitForFinished()) {
ret = reply->message().is_media_file_response().success();
}
QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection);
reply->deleteLater();
return ret;
@ -204,21 +229,21 @@ void TagReaderClient::ReadFileBlocking(const QString &filename, Song *song) {
if (reply->WaitForFinished()) {
song->InitFromProtobuf(reply->message().read_file_response().metadata());
}
QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection);
reply->deleteLater();
}
bool TagReaderClient::SaveFileBlocking(const QString &filename, const Song &metadata) {
bool TagReaderClient::SaveFileBlocking(const QString &filename, const Song &metadata, const SaveTags save_tags, const SavePlaycount save_playcount, const SaveRating save_rating, const SaveCoverOptions &save_cover_options) {
Q_ASSERT(QThread::currentThread() != thread());
bool ret = false;
TagReaderReply *reply = SaveFile(filename, metadata);
TagReaderReply *reply = SaveFile(filename, metadata, save_tags, save_playcount, save_rating, save_cover_options);
if (reply->WaitForFinished()) {
ret = reply->message().save_file_response().success();
}
QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection);
reply->deleteLater();
return ret;
@ -235,7 +260,7 @@ QByteArray TagReaderClient::LoadEmbeddedArtBlocking(const QString &filename) {
const std::string &data_str = reply->message().load_embedded_art_response().data();
ret = QByteArray(data_str.data(), static_cast<qint64>(data_str.size()));
}
QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection);
reply->deleteLater();
return ret;
@ -252,23 +277,23 @@ QImage TagReaderClient::LoadEmbeddedArtAsImageBlocking(const QString &filename)
const std::string &data_str = reply->message().load_embedded_art_response().data();
ret.loadFromData(QByteArray(data_str.data(), static_cast<qint64>(data_str.size())));
}
QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection);
reply->deleteLater();
return ret;
}
bool TagReaderClient::SaveEmbeddedArtBlocking(const QString &filename, const QByteArray &data) {
bool TagReaderClient::SaveEmbeddedArtBlocking(const QString &filename, const SaveCoverOptions &save_cover_options) {
Q_ASSERT(QThread::currentThread() != thread());
bool success = false;
TagReaderReply *reply = SaveEmbeddedArt(filename, data);
TagReaderReply *reply = SaveEmbeddedArt(filename, save_cover_options);
if (reply->WaitForFinished()) {
success = reply->message().save_embedded_art_response().success();
}
QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection);
reply->deleteLater();
return success;

View File

@ -53,22 +53,48 @@ class TagReaderClient : public QObject {
void Start();
void ExitAsync();
ReplyType *ReadFile(const QString &filename);
ReplyType *SaveFile(const QString &filename, const Song &metadata);
enum class SaveTags {
Off,
On
};
enum class SavePlaycount {
Off,
On
};
enum class SaveRating {
Off,
On
};
class SaveCoverOptions {
public:
explicit SaveCoverOptions(const bool _enabled = false, const bool _is_jpeg = false, const QString &_cover_filename = QString(), const QByteArray &_cover_data = QByteArray()) : enabled(_enabled), is_jpeg(_is_jpeg), cover_filename(_cover_filename), cover_data(_cover_data) {}
explicit SaveCoverOptions(const QString &_cover_filename) : enabled(true), is_jpeg(false), cover_filename(_cover_filename) {}
explicit SaveCoverOptions(const QByteArray &_cover_data) : enabled(true), is_jpeg(false), cover_data(_cover_data) {}
bool enabled;
bool is_jpeg;
QString cover_filename;
QByteArray cover_data;
};
ReplyType *IsMediaFile(const QString &filename);
ReplyType *ReadFile(const QString &filename);
ReplyType *SaveFile(const QString &filename, const Song &metadata, const SaveTags save_tags = SaveTags(), const SavePlaycount save_playcount = SavePlaycount(), const SaveRating save_rating = SaveRating(), const SaveCoverOptions &save_cover_options = SaveCoverOptions());
ReplyType *LoadEmbeddedArt(const QString &filename);
ReplyType *SaveEmbeddedArt(const QString &filename, const QByteArray &data);
ReplyType *SaveEmbeddedArt(const QString &filename, const SaveCoverOptions &save_cover_options);
ReplyType *UpdateSongPlaycount(const Song &metadata);
ReplyType *UpdateSongRating(const Song &metadata);
// Convenience functions that call the above functions and wait for a response.
// These block the calling thread with a semaphore, and must NOT be called from the TagReaderClient's thread.
void ReadFileBlocking(const QString &filename, Song *song);
bool SaveFileBlocking(const QString &filename, const Song &metadata);
bool SaveFileBlocking(const QString &filename, const Song &metadata, const SaveTags save_tags = SaveTags(), const SavePlaycount save_playcount = SavePlaycount(), const SaveRating save_rating = SaveRating(), const SaveCoverOptions &save_cover_options = SaveCoverOptions());
bool IsMediaFileBlocking(const QString &filename);
QByteArray LoadEmbeddedArtBlocking(const QString &filename);
QImage LoadEmbeddedArtAsImageBlocking(const QString &filename);
bool SaveEmbeddedArtBlocking(const QString &filename, const QByteArray &data);
bool SaveEmbeddedArtBlocking(const QString &filename, const SaveCoverOptions &save_cover_options);
bool UpdateSongPlaycountBlocking(const Song &metadata);
bool UpdateSongRatingBlocking(const Song &metadata);

View File

@ -53,9 +53,12 @@
#include <QSettings>
#include <QtEvents>
#include "utilities/filenameconstants.h"
#include "utilities/strutils.h"
#include "utilities/mimeutils.h"
#include "utilities/imageutils.h"
#include "utilities/coveroptions.h"
#include "utilities/coverutils.h"
#include "core/application.h"
#include "core/song.h"
#include "core/iconloader.h"
@ -63,7 +66,6 @@
#include "collection/collectionfilteroptions.h"
#include "collection/collectionbackend.h"
#include "settings/collectionsettingspage.h"
#include "organize/organizeformat.h"
#include "internet/internetservices.h"
#include "internet/internetservice.h"
#include "albumcoverchoicecontroller.h"
@ -98,11 +100,6 @@ AlbumCoverChoiceController::AlbumCoverChoiceController(QWidget *parent)
separator2_(nullptr),
show_cover_(nullptr),
search_cover_auto_(nullptr),
save_cover_type_(CollectionSettingsPage::SaveCoverType::Cache),
save_cover_filename_(CollectionSettingsPage::SaveCoverFilename::Pattern),
cover_overwrite_(false),
cover_lowercase_(true),
cover_replace_spaces_(true),
save_embedded_cover_override_(false) {
cover_from_file_ = new QAction(IconLoader::Load("document-open"), tr("Load cover from disk..."), this);
@ -146,12 +143,12 @@ void AlbumCoverChoiceController::ReloadSettings() {
QSettings s;
s.beginGroup(CollectionSettingsPage::kSettingsGroup);
save_cover_type_ = static_cast<CollectionSettingsPage::SaveCoverType>(s.value("save_cover_type", static_cast<int>(CollectionSettingsPage::SaveCoverType::Cache)).toInt());
save_cover_filename_ = static_cast<CollectionSettingsPage::SaveCoverFilename>(s.value("save_cover_filename", static_cast<int>(CollectionSettingsPage::SaveCoverFilename::Pattern)).toInt());
cover_pattern_ = s.value("cover_pattern", "%albumartist-%album").toString();
cover_overwrite_ = s.value("cover_overwrite", false).toBool();
cover_lowercase_ = s.value("cover_lowercase", false).toBool();
cover_replace_spaces_ = s.value("cover_replace_spaces", false).toBool();
cover_options_.cover_type = static_cast<CoverOptions::CoverType>(s.value("save_cover_type", static_cast<int>(CoverOptions::CoverType::Cache)).toInt());
cover_options_.cover_filename = static_cast<CoverOptions::CoverFilename>(s.value("save_cover_filename", static_cast<int>(CoverOptions::CoverFilename::Pattern)).toInt());
cover_options_.cover_pattern = s.value("cover_pattern", "%albumartist-%album").toString();
cover_options_.cover_overwrite = s.value("cover_overwrite", false).toBool();
cover_options_.cover_lowercase = s.value("cover_lowercase", false).toBool();
cover_options_.cover_replace_spaces = s.value("cover_replace_spaces", false).toBool();
s.endGroup();
}
@ -214,14 +211,14 @@ QUrl AlbumCoverChoiceController::LoadCoverFromFile(Song *song) {
if (QImage(cover_file).isNull()) return QUrl();
switch (get_save_album_cover_type()) {
case CollectionSettingsPage::SaveCoverType::Embedded:
case CoverOptions::CoverType::Embedded:
if (song->save_embedded_cover_supported()) {
SaveCoverEmbeddedAutomatic(*song, cover_file);
return QUrl::fromLocalFile(Song::kEmbeddedCover);
}
[[fallthrough]];
case CollectionSettingsPage::SaveCoverType::Cache:
case CollectionSettingsPage::SaveCoverType::Album:{
case CoverOptions::CoverType::Cache:
case CoverOptions::CoverType::Album:{
QUrl cover_url = QUrl::fromLocalFile(cover_file);
SaveArtManualToSong(song, cover_url);
return cover_url;
@ -242,7 +239,7 @@ void AlbumCoverChoiceController::SaveCoverToFileManual(const Song &song, const A
initial_file_name = initial_file_name + "-" + (song.effective_album().isEmpty() ? tr("unknown") : song.effective_album()) + ".jpg";
initial_file_name = initial_file_name.toLower();
initial_file_name.replace(QRegularExpression("\\s"), "-");
initial_file_name.remove(OrganizeFormat::kInvalidFatCharacters);
initial_file_name.remove(QRegularExpression(QString(kInvalidFatCharactersRegex), QRegularExpression::CaseInsensitiveOption));
QString save_filename = QFileDialog::getSaveFileName(this, tr("Save album cover"), GetInitialPathForFileDialog(song, initial_file_name), tr(kSaveImageFileFilter) + ";;" + tr(kAllFilesFilter));
@ -613,12 +610,12 @@ QUrl AlbumCoverChoiceController::SaveCoverToFileAutomatic(const Song::Source sou
const AlbumCoverImageResult &result,
const bool force_overwrite) {
QString filepath = app_->album_cover_loader()->CoverFilePath(source, artist, album, album_id, album_dir, result.cover_url, "jpg");
QString filepath = CoverUtils::CoverFilePath(cover_options_, source, artist, album, album_id, album_dir, result.cover_url, "jpg");
if (filepath.isEmpty()) return QUrl();
QFile file(filepath);
// Don't overwrite when saving in album dir if the filename is set to pattern unless "force_overwrite" is set.
if (source == Song::Source::Collection && !cover_overwrite_ && !force_overwrite && get_save_album_cover_type() == CollectionSettingsPage::SaveCoverType::Album && save_cover_filename_ == CollectionSettingsPage::SaveCoverFilename::Pattern && file.exists()) {
if (source == Song::Source::Collection && !cover_options_.cover_overwrite && !force_overwrite && get_save_album_cover_type() == CoverOptions::CoverType::Album && cover_options_.cover_filename == CoverOptions::CoverFilename::Pattern && file.exists()) {
while (file.exists()) {
QFileInfo fileinfo(file.fileName());
file.setFileName(fileinfo.path() + "/0" + fileinfo.fileName());
@ -758,7 +755,7 @@ QUrl AlbumCoverChoiceController::SaveCover(Song *song, const QDropEvent *e) {
const QString suffix = QFileInfo(filename).suffix().toLower();
if (IsKnownImageExtension(suffix)) {
if (get_save_album_cover_type() == CollectionSettingsPage::SaveCoverType::Embedded && song->save_embedded_cover_supported()) {
if (get_save_album_cover_type() == CoverOptions::CoverType::Embedded && song->save_embedded_cover_supported()) {
SaveCoverEmbeddedAutomatic(*song, filename);
return QUrl::fromLocalFile(Song::kEmbeddedCover);
}
@ -784,7 +781,7 @@ QUrl AlbumCoverChoiceController::SaveCoverAutomatic(Song *song, const AlbumCover
QUrl cover_url;
switch(get_save_album_cover_type()) {
case CollectionSettingsPage::SaveCoverType::Embedded:{
case CoverOptions::CoverType::Embedded:{
if (song->save_embedded_cover_supported()) {
SaveCoverEmbeddedAutomatic(*song, result);
cover_url = QUrl::fromLocalFile(Song::kEmbeddedCover);
@ -792,8 +789,8 @@ QUrl AlbumCoverChoiceController::SaveCoverAutomatic(Song *song, const AlbumCover
}
}
[[fallthrough]];
case CollectionSettingsPage::SaveCoverType::Cache:
case CollectionSettingsPage::SaveCoverType::Album:{
case CoverOptions::CoverType::Cache:
case CoverOptions::CoverType::Album:{
cover_url = SaveCoverToFileAutomatic(song, result);
if (!cover_url.isEmpty()) SaveArtManualToSong(song, cover_url);
break;

View File

@ -38,6 +38,7 @@
#include <QMutex>
#include "core/song.h"
#include "utilities/coveroptions.h"
#include "settings/collectionsettingspage.h"
#include "albumcoverimageresult.h"
@ -69,8 +70,8 @@ class AlbumCoverChoiceController : public QWidget {
void Init(Application *app);
void ReloadSettings();
CollectionSettingsPage::SaveCoverType get_save_album_cover_type() const { return (save_embedded_cover_override_ ? CollectionSettingsPage::SaveCoverType::Embedded : save_cover_type_); }
CollectionSettingsPage::SaveCoverType get_collection_save_album_cover_type() const { return save_cover_type_; }
CoverOptions::CoverType get_save_album_cover_type() const { return (save_embedded_cover_override_ ? CoverOptions::CoverType::Embedded : cover_options_.cover_type); }
CoverOptions::CoverType get_collection_save_album_cover_type() const { return cover_options_.cover_type; }
// Getters for all QActions implemented by this controller.
@ -189,12 +190,7 @@ class AlbumCoverChoiceController : public QWidget {
QMap<quint64, Song> cover_save_tasks_;
QMutex mutex_cover_save_tasks_;
CollectionSettingsPage::SaveCoverType save_cover_type_;
CollectionSettingsPage::SaveCoverFilename save_cover_filename_;
QString cover_pattern_;
bool cover_overwrite_;
bool cover_lowercase_;
bool cover_replace_spaces_;
CoverOptions cover_options_;
bool save_embedded_cover_override_;
};

View File

@ -34,24 +34,18 @@
#include <QVariant>
#include <QByteArray>
#include <QString>
#include <QRegularExpression>
#include <QUrl>
#include <QFile>
#include <QImage>
#include <QPainter>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QSettings>
#include "core/networkaccessmanager.h"
#include "core/song.h"
#include "core/tagreaderclient.h"
#include "utilities/transliterate.h"
#include "utilities/mimeutils.h"
#include "utilities/cryptutils.h"
#include "utilities/imageutils.h"
#include "settings/collectionsettingspage.h"
#include "organize/organizeformat.h"
#include "albumcoverloader.h"
#include "albumcoverloaderoptions.h"
#include "albumcoverloaderresult.h"
@ -63,15 +57,9 @@ AlbumCoverLoader::AlbumCoverLoader(QObject *parent)
load_image_async_id_(1),
save_image_async_id_(1),
network_(new NetworkAccessManager(this)),
save_cover_type_(CollectionSettingsPage::SaveCoverType::Cache),
save_cover_filename_(CollectionSettingsPage::SaveCoverFilename::Pattern),
cover_overwrite_(false),
cover_lowercase_(true),
cover_replace_spaces_(true),
original_thread_(nullptr) {
original_thread_ = thread();
ReloadSettings();
}
@ -90,145 +78,6 @@ void AlbumCoverLoader::Exit() {
}
void AlbumCoverLoader::ReloadSettings() {
QSettings s;
s.beginGroup(CollectionSettingsPage::kSettingsGroup);
save_cover_type_ = static_cast<CollectionSettingsPage::SaveCoverType>(s.value("save_cover_type", static_cast<int>(CollectionSettingsPage::SaveCoverType::Cache)).toInt());
save_cover_filename_ = static_cast<CollectionSettingsPage::SaveCoverFilename>(s.value("save_cover_filename", static_cast<int>(CollectionSettingsPage::SaveCoverFilename::Pattern)).toInt());
cover_pattern_ = s.value("cover_pattern", "%albumartist-%album").toString();
cover_overwrite_ = s.value("cover_overwrite", false).toBool();
cover_lowercase_ = s.value("cover_lowercase", false).toBool();
cover_replace_spaces_ = s.value("cover_replace_spaces", false).toBool();
s.endGroup();
}
QString AlbumCoverLoader::AlbumCoverFilename(QString artist, QString album, const QString &extension) {
artist.remove('/').remove('\\');
album.remove('/').remove('\\');
QString filename = artist + "-" + album;
filename = Utilities::Transliterate(filename.toLower());
filename = filename.replace(' ', '-')
.replace("--", "-")
.remove(OrganizeFormat::kInvalidFatCharacters)
.simplified();
if (!extension.isEmpty()) {
filename.append('.');
filename.append(extension);
}
return filename;
}
QString AlbumCoverLoader::CoverFilePath(const Song &song, const QString &album_dir, const QUrl &cover_url, const QString &extension) {
return CoverFilePath(song.source(), song.effective_albumartist(), song.album(), song.album_id(), album_dir, cover_url, extension);
}
QString AlbumCoverLoader::CoverFilePath(const Song::Source source, const QString &artist, const QString &album, const QString &album_id, const QString &album_dir, const QUrl &cover_url, const QString &extension) {
QString path;
if (source == Song::Source::Collection && save_cover_type_ == CollectionSettingsPage::SaveCoverType::Album && !album_dir.isEmpty()) {
path = album_dir;
}
else {
path = Song::ImageCacheDir(source);
}
if (path.right(1) == QDir::separator() || path.right(1) == "/") {
path.chop(1);
}
QDir dir;
if (!dir.mkpath(path)) {
qLog(Error) << "Unable to create directory" << path;
path = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
}
QString filename;
if (source == Song::Source::Collection &&
save_cover_type_ == CollectionSettingsPage::SaveCoverType::Album &&
save_cover_filename_ == CollectionSettingsPage::SaveCoverFilename::Pattern &&
!cover_pattern_.isEmpty()) {
filename = CoverFilenameFromVariable(artist, album);
filename.remove(OrganizeFormat::kInvalidFatCharacters).remove('/').remove('\\');
if (cover_lowercase_) filename = filename.toLower();
if (cover_replace_spaces_) filename.replace(QRegularExpression("\\s"), "-");
if (!extension.isEmpty()) {
filename.append('.');
filename.append(extension);
}
}
if (filename.isEmpty()) {
filename = CoverFilenameFromSource(source, cover_url, artist, album, album_id, extension);
}
QString filepath(path + "/" + filename);
return filepath;
}
QString AlbumCoverLoader::CoverFilenameFromSource(const Song::Source source, const QUrl &cover_url, const QString &artist, const QString &album, const QString &album_id, const QString &extension) {
QString filename;
switch (source) {
case Song::Source::Tidal:
if (!album_id.isEmpty()) {
filename = album_id + "-" + cover_url.fileName();
break;
}
[[fallthrough]];
case Song::Source::Subsonic:
case Song::Source::Qobuz:
if (!album_id.isEmpty()) {
filename = album_id;
break;
}
[[fallthrough]];
case Song::Source::Collection:
case Song::Source::LocalFile:
case Song::Source::CDDA:
case Song::Source::Device:
case Song::Source::Stream:
case Song::Source::SomaFM:
case Song::Source::RadioParadise:
case Song::Source::Unknown:
filename = Utilities::Sha1CoverHash(artist, album).toHex();
break;
}
if (!extension.isEmpty()) {
filename.append('.');
filename.append(extension);
}
return filename;
}
QString AlbumCoverLoader::CoverFilenameFromVariable(const QString &artist, QString album, const QString &extension) {
album = album.remove(Song::kAlbumRemoveDisc);
QString filename(cover_pattern_);
filename.replace("%albumartist", artist);
filename.replace("%artist", artist);
filename.replace("%album", album);
if (!extension.isEmpty()) {
filename.append('.');
filename.append(extension);
}
return filename;
}
void AlbumCoverLoader::CancelTask(const quint64 id) {
QMutexLocker l(&mutex_load_image_async_);
@ -260,7 +109,7 @@ quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions &options,
Task task;
task.options = options;
task.song = song;
task.state = State_Manual;
task.state = State::Manual;
return EnqueueTask(task);
@ -276,7 +125,7 @@ quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions &options,
Task task;
task.options = options;
task.song = song;
task.state = State_Manual;
task.state = State::Manual;
return EnqueueTask(task);
@ -360,9 +209,9 @@ void AlbumCoverLoader::ProcessTask(Task *task) {
void AlbumCoverLoader::NextState(Task *task) {
if (task->state == State_Manual) {
if (task->state == State::Manual) {
// Try the automatic one next
task->state = State_Automatic;
task->state = State::Automatic;
ProcessTask(task);
}
else {
@ -382,13 +231,13 @@ AlbumCoverLoader::TryLoadResult AlbumCoverLoader::TryLoadImage(Task *task) {
// For local files and streams initialize art if found.
if ((task->song.source() == Song::Source::LocalFile || task->song.is_radio()) && !task->song.art_manual_is_valid() && !task->song.art_automatic_is_valid()) {
switch (task->state) {
case State_None:
case State::None:
break;
case State_Manual:
case State::Manual:
task->song.InitArtManual();
if (task->song.art_manual_is_valid()) task->art_updated = true;
break;
case State_Automatic:
case State::Automatic:
if (task->song.url().isLocalFile()) {
task->song.InitArtAutomatic();
if (task->song.art_automatic_is_valid()) task->art_updated = true;
@ -400,12 +249,12 @@ AlbumCoverLoader::TryLoadResult AlbumCoverLoader::TryLoadImage(Task *task) {
AlbumCoverLoaderResult::Type type(AlbumCoverLoaderResult::Type_None);
QUrl cover_url;
switch (task->state) {
case State_None:
case State_Automatic:
case State::None:
case State::Automatic:
type = AlbumCoverLoaderResult::Type_Automatic;
cover_url = task->song.art_automatic();
break;
case State_Manual:
case State::Manual:
type = AlbumCoverLoaderResult::Type_Manual;
cover_url = task->song.art_manual();
break;
@ -593,7 +442,7 @@ quint64 AlbumCoverLoader::SaveEmbeddedCoverAsync(const QList<QUrl> &urls, const
void AlbumCoverLoader::SaveEmbeddedCover(const quint64 id, const QString &song_filename, const QByteArray &image_data) {
TagReaderReply *reply = TagReaderClient::Instance()->SaveEmbeddedArt(song_filename, image_data);
TagReaderReply *reply = TagReaderClient::Instance()->SaveEmbeddedArt(song_filename, TagReaderClient::SaveCoverOptions(image_data));
tagreader_save_embedded_art_requests_.insert(id, reply);
const bool clear = image_data.isEmpty();
QObject::connect(reply, &TagReaderReply::Finished, this, [this, id, reply, clear]() { SaveEmbeddedArtFinished(id, reply, clear); }, Qt::QueuedConnection);
@ -702,6 +551,6 @@ void AlbumCoverLoader::SaveEmbeddedArtFinished(const quint64 id, TagReaderReply
emit SaveEmbeddedCoverAsyncFinished(id, reply->is_successful(), cleared);
}
QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection);
reply->deleteLater();
}

View File

@ -39,7 +39,6 @@
#include "core/song.h"
#include "core/tagreaderclient.h"
#include "settings/collectionsettingspage.h"
#include "albumcoverloaderoptions.h"
#include "albumcoverloaderresult.h"
#include "albumcoverimageresult.h"
@ -54,25 +53,15 @@ class AlbumCoverLoader : public QObject {
public:
explicit AlbumCoverLoader(QObject *parent = nullptr);
enum State {
State_None,
State_Manual,
State_Automatic,
enum class State {
None,
Manual,
Automatic
};
void ReloadSettings();
void ExitAsync();
void Stop() { stop_requested_ = true; }
static QString AlbumCoverFilename(QString artist, QString album, const QString &extension);
static QString CoverFilenameFromSource(const Song::Source source, const QUrl &cover_url, const QString &artist, const QString &album, const QString &album_id, const QString &extension);
QString CoverFilenameFromVariable(const QString &artist, QString album, const QString &extension = QString());
QString CoverFilePath(const Song &song, const QString &album_dir, const QUrl &cover_url, const QString &extension = QString());
QString CoverFilePath(const Song::Source source, const QString &artist, const QString &album, const QString &album_id, const QString &album_dir, const QUrl &cover_url, const QString &extension = QString());
quint64 LoadImageAsync(const AlbumCoverLoaderOptions &options, const Song &song);
quint64 LoadImageAsync(const AlbumCoverLoaderOptions &options, const QUrl &art_automatic, const QUrl &art_manual, const QUrl &song_url = QUrl(), const Song::Source song_source = Song::Source::Unknown);
quint64 LoadImageAsync(const AlbumCoverLoaderOptions &options, const AlbumCoverImageResult &album_cover);
@ -110,7 +99,7 @@ class AlbumCoverLoader : public QObject {
protected:
struct Task {
explicit Task() : id(0), state(State_None), type(AlbumCoverLoaderResult::Type_None), art_updated(false), redirects(0) {}
explicit Task() : id(0), state(State::None), type(AlbumCoverLoaderResult::Type_None), art_updated(false), redirects(0) {}
AlbumCoverLoaderOptions options;
@ -158,13 +147,6 @@ class AlbumCoverLoader : public QObject {
static const int kMaxRedirects = 3;
CollectionSettingsPage::SaveCoverType save_cover_type_;
CollectionSettingsPage::SaveCoverFilename save_cover_filename_;
QString cover_pattern_;
bool cover_overwrite_;
bool cover_lowercase_;
bool cover_replace_spaces_;
QThread *original_thread_;
QMultiMap<quint64, TagReaderReply*> tagreader_save_embedded_art_requests_;

View File

@ -781,13 +781,13 @@ void AlbumCoverManager::SaveImageToAlbums(Song *song, const AlbumCoverImageResul
QUrl cover_url = result.cover_url;
switch (album_cover_choice_controller_->get_save_album_cover_type()) {
case CollectionSettingsPage::SaveCoverType::Cache:
case CollectionSettingsPage::SaveCoverType::Album:
case CoverOptions::CoverType::Cache:
case CoverOptions::CoverType::Album:
if (cover_url.isEmpty() || !cover_url.isValid() || !cover_url.isLocalFile()) {
cover_url = album_cover_choice_controller_->SaveCoverToFileAutomatic(song, result);
}
break;
case CollectionSettingsPage::SaveCoverType::Embedded:
case CoverOptions::CoverType::Embedded:
cover_url = QUrl::fromLocalFile(Song::kEmbeddedCover);
break;
}
@ -798,14 +798,14 @@ void AlbumCoverManager::SaveImageToAlbums(Song *song, const AlbumCoverImageResul
for (QListWidgetItem *item : context_menu_items_) {
AlbumItem *album_item = static_cast<AlbumItem*>(item);
switch (album_cover_choice_controller_->get_save_album_cover_type()) {
case CollectionSettingsPage::SaveCoverType::Cache:
case CollectionSettingsPage::SaveCoverType::Album:{
case CoverOptions::CoverType::Cache:
case CoverOptions::CoverType::Album:{
Song current_song = ItemAsSong(album_item);
album_cover_choice_controller_->SaveArtManualToSong(&current_song, cover_url);
UpdateCoverInList(album_item, cover_url);
break;
}
case CollectionSettingsPage::SaveCoverType::Embedded:{
case CoverOptions::CoverType::Embedded:{
urls << album_item->urls;
album_items << album_item;
break;
@ -813,7 +813,7 @@ void AlbumCoverManager::SaveImageToAlbums(Song *song, const AlbumCoverImageResul
}
}
if (album_cover_choice_controller_->get_save_album_cover_type() == CollectionSettingsPage::SaveCoverType::Embedded && !urls.isEmpty()) {
if (album_cover_choice_controller_->get_save_album_cover_type() == CoverOptions::CoverType::Embedded && !urls.isEmpty()) {
quint64 id = -1;
if (result.is_jpeg()) {
id = app_->album_cover_loader()->SaveEmbeddedCoverAsync(urls, result.image_data);
@ -971,7 +971,7 @@ void AlbumCoverManager::SaveAndSetCover(AlbumItem *item, const AlbumCoverImageRe
const Song::FileType filetype = static_cast<Song::FileType>(item->data(Role_Filetype).toInt());
const bool has_cue = !item->data(Role_CuePath).toString().isEmpty();
if (album_cover_choice_controller_->get_save_album_cover_type() == CollectionSettingsPage::SaveCoverType::Embedded && Song::save_embedded_cover_supported(filetype) && !has_cue) {
if (album_cover_choice_controller_->get_save_album_cover_type() == CoverOptions::CoverType::Embedded && Song::save_embedded_cover_supported(filetype) && !has_cue) {
if (result.is_jpeg()) {
quint64 id = app_->album_cover_loader()->SaveEmbeddedCoverAsync(urls, result.image_data);
cover_save_tasks_.insert(id, item);

View File

@ -76,7 +76,8 @@
#include "utilities/strutils.h"
#include "utilities/timeutils.h"
#include "utilities/imageutils.h"
#include "utilities/cryptutils.h"
#include "utilities/coverutils.h"
#include "utilities/coveroptions.h"
#include "widgets/busyindicator.h"
#include "widgets/lineedit.h"
#include "collection/collectionbackend.h"
@ -116,8 +117,7 @@ EditTagDialog::EditTagDialog(Application *app, QWidget *parent)
summary_cover_art_id_(-1),
tags_cover_art_id_(-1),
cover_art_is_set_(false),
save_tag_pending_(0),
save_art_pending_(0) {
save_tag_pending_(0) {
QObject::connect(app_->album_cover_loader(), &AlbumCoverLoader::AlbumCoverLoaded, this, &EditTagDialog::AlbumCoverLoaded);
@ -187,7 +187,7 @@ EditTagDialog::EditTagDialog(Application *app, QWidget *parent)
QObject::connect(ui_->song_list->selectionModel(), &QItemSelectionModel::selectionChanged, this, &EditTagDialog::SelectionChanged);
QObject::connect(ui_->button_box, &QDialogButtonBox::clicked, this, &EditTagDialog::ButtonClicked);
QObject::connect(ui_->playcount_reset, &QPushButton::clicked, this, &EditTagDialog::ResetStatistics);
QObject::connect(ui_->playcount_reset, &QPushButton::clicked, this, &EditTagDialog::ResetPlayStatistics);
QObject::connect(ui_->rating, &RatingWidget::RatingChanged, this, &EditTagDialog::SongRated);
#ifdef HAVE_MUSICBRAINZ
QObject::connect(ui_->fetch_tag, &QPushButton::clicked, this, &EditTagDialog::FetchTag);
@ -728,7 +728,7 @@ void EditTagDialog::SelectionChanged() {
ui_->tags_summary->setText(summary);
const bool embedded_cover = (first_song.save_embedded_cover_supported() && (first_song.has_embedded_cover() || album_cover_choice_controller_->get_collection_save_album_cover_type() == CollectionSettingsPage::SaveCoverType::Embedded));
const bool embedded_cover = (first_song.save_embedded_cover_supported() && (first_song.has_embedded_cover() || album_cover_choice_controller_->get_collection_save_album_cover_type() == CoverOptions::CoverType::Embedded));
ui_->checkbox_embedded_cover->setChecked(embedded_cover);
album_cover_choice_controller_->set_save_embedded_cover_override(embedded_cover);
@ -1139,33 +1139,6 @@ void EditTagDialog::SaveData() {
for (int i = 0; i < data_.count(); ++i) {
Data &ref = data_[i];
if (!ref.current_.IsMetadataEqual(ref.original_)) {
++save_tag_pending_;
TagReaderReply *reply = TagReaderClient::Instance()->SaveFile(ref.current_.url().toLocalFile(), ref.current_);
QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, ref]() { SongSaveTagsComplete(reply, ref.current_.url().toLocalFile(), ref.current_); }, Qt::QueuedConnection);
}
if (ref.current_.playcount() == 0 &&
ref.current_.skipcount() == 0 &&
ref.current_.lastplayed() == -1 &&
!ref.current_.IsStatisticsEqual(ref.original_)) {
if (ref.current_.is_collection_song()) {
app_->collection_backend()->ResetStatisticsAsync(ref.current_.id());
}
else {
app_->tag_reader_client()->UpdateSongsPlaycount(SongList() << ref.current_);
}
}
if (!ref.current_.IsRatingEqual(ref.original_)) {
if (ref.current_.is_collection_song()) {
app_->collection_backend()->UpdateSongRatingAsync(ref.current_.id(), ref.current_.rating());
}
else {
app_->tag_reader_client()->UpdateSongsRating(SongList() << ref.current_);
}
}
QString embedded_cover_from_file;
// If embedded album cover is selected, and it isn't saved to the tags, then save it even if no action was done.
if (ui_->checkbox_embedded_cover->isChecked() && ref.cover_action_ == UpdateCoverAction::None && !ref.original_.has_embedded_cover() && ref.original_.save_embedded_cover_supported()) {
@ -1179,6 +1152,11 @@ void EditTagDialog::SaveData() {
}
}
const bool save_tags = !ref.current_.IsMetadataEqual(ref.original_);
const bool save_rating = !ref.current_.IsRatingEqual(ref.original_);
const bool save_playcount = ref.current_.playcount() == 0 && ref.current_.skipcount() == 0 && ref.current_.lastplayed() == -1 && !ref.current_.IsPlayStatisticsEqual(ref.original_);
const bool save_embedded_cover = ref.cover_action_ != UpdateCoverAction::None && ui_->checkbox_embedded_cover->isChecked() && ref.original_.save_embedded_cover_supported();
if (ref.cover_action_ != UpdateCoverAction::None) {
switch (ref.cover_action_) {
case UpdateCoverAction::None:
@ -1191,7 +1169,7 @@ void EditTagDialog::SaveData() {
cover_url = ref.cover_result_.cover_url;
}
else {
QString cover_hash = Utilities::Sha1CoverHash(ref.current_.effective_albumartist(), ref.current_.album()).toHex();
QString cover_hash = CoverUtils::Sha1CoverHash(ref.current_.effective_albumartist(), ref.current_.album()).toHex();
if (cover_urls.contains(cover_hash)) {
cover_url = cover_urls[cover_hash];
}
@ -1232,66 +1210,41 @@ void EditTagDialog::SaveData() {
break;
}
}
if (ui_->checkbox_embedded_cover->isChecked() && ref.original_.save_embedded_cover_supported()) {
if (ref.cover_action_ == UpdateCoverAction::New) {
if (ref.cover_result_.is_jpeg()) { // Save JPEG data directly.
++save_art_pending_;
TagReaderReply *reply = TagReaderClient::Instance()->SaveEmbeddedArt(ref.current_.url().toLocalFile(), ref.cover_result_.image_data);
QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, ref]() {
SongSaveArtComplete(reply, ref.current_.url().toLocalFile(), ref.current_, ref.cover_action_);
}, Qt::QueuedConnection);
}
else if (!ref.cover_result_.image.isNull()) { // Convert image data to JPEG.
++save_art_pending_;
QFuture<QByteArray> future = QtConcurrent::run(&ImageUtils::SaveImageToJpegData, ref.cover_result_.image);
QFutureWatcher<QByteArray> *watcher = new QFutureWatcher<QByteArray>();
QObject::connect(watcher, &QFutureWatcher<QByteArray>::finished, this, [this, watcher, ref]() {
TagReaderReply *reply = TagReaderClient::Instance()->SaveEmbeddedArt(ref.current_.url().toLocalFile(), watcher->result());
QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, ref]() {
SongSaveArtComplete(reply, ref.current_.url().toLocalFile(), ref.current_, ref.cover_action_);
}, Qt::QueuedConnection);
watcher->deleteLater();
});
watcher->setFuture(future);
}
else if (!embedded_cover_from_file.isEmpty()) { // Save existing file on disk as embedded cover.
++save_art_pending_;
QFuture<QByteArray> future = QtConcurrent::run(&ImageUtils::FileToJpegData, embedded_cover_from_file);
QFutureWatcher<QByteArray> *watcher = new QFutureWatcher<QByteArray>();
QObject::connect(watcher, &QFutureWatcher<QByteArray>::finished, this, [this, watcher, ref]() {
TagReaderReply *reply = TagReaderClient::Instance()->SaveEmbeddedArt(ref.current_.url().toLocalFile(), watcher->result());
QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, ref]() {
SongSaveArtComplete(reply, ref.current_.url().toLocalFile(), ref.current_, ref.cover_action_);
}, Qt::QueuedConnection);
watcher->deleteLater();
});
watcher->setFuture(future);
}
}
if (save_tags || save_playcount || save_rating || save_embedded_cover) {
++save_tag_pending_;
TagReaderClient::SaveCoverOptions savecover_options;
savecover_options.enabled = save_embedded_cover;
if (save_embedded_cover && ref.cover_action_ == UpdateCoverAction::New) {
if (!ref.cover_result_.image.isNull()) {
savecover_options.is_jpeg = ref.cover_result_.is_jpeg();
savecover_options.cover_data = ref.cover_result_.image_data;
}
else if (ref.cover_action_ == UpdateCoverAction::Delete) {
++save_art_pending_;
TagReaderReply *reply = TagReaderClient::Instance()->SaveEmbeddedArt(ref.current_.url().toLocalFile(), QByteArray());
QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, ref]() {
SongSaveArtComplete(reply, ref.current_.url().toLocalFile(), ref.current_, ref.cover_action_);
}, Qt::QueuedConnection);
else if (!embedded_cover_from_file.isEmpty()) {
savecover_options.cover_filename = embedded_cover_from_file;
}
}
else if (!ref.current_.effective_albumartist().isEmpty() && !ref.current_.album().isEmpty()) {
if (ref.current_.is_collection_song()) {
collection_songs_.insert(ref.current_.id(), ref.current_);
}
if (ref.current_ == app_->current_albumcover_loader()->last_song()) {
app_->current_albumcover_loader()->LoadAlbumCover(ref.current_);
}
TagReaderReply *reply = TagReaderClient::Instance()->SaveFile(ref.current_.url().toLocalFile(), ref.current_, save_tags ? TagReaderClient::SaveTags::On : TagReaderClient::SaveTags::Off, save_playcount ? TagReaderClient::SavePlaycount::On : TagReaderClient::SavePlaycount::Off, save_rating ? TagReaderClient::SaveRating::On : TagReaderClient::SaveRating::Off, savecover_options);
QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, ref]() { SongSaveTagsComplete(reply, ref.current_.url().toLocalFile(), ref.current_, ref.cover_action_); }, Qt::QueuedConnection);
}
// If the cover was changed, but no tags written, make sure to update the collection.
else if (ref.cover_action_ != UpdateCoverAction::None && !ref.current_.effective_albumartist().isEmpty() && !ref.current_.album().isEmpty()) {
if (ref.current_.is_collection_song()) {
collection_songs_.insert(ref.current_.id(), ref.current_);
}
if (ref.current_ == app_->current_albumcover_loader()->last_song()) {
app_->current_albumcover_loader()->LoadAlbumCover(ref.current_);
}
}
}
if (save_tag_pending_ <= 0 && save_art_pending_ <= 0) AcceptFinished();
if (save_tag_pending_ <= 0) SaveDataFinished();
}
void EditTagDialog::AcceptFinished() {
void EditTagDialog::SaveDataFinished() {
if (!collection_songs_.isEmpty()) {
app_->collection_backend()->AddOrUpdateSongsAsync(collection_songs_.values());
@ -1304,7 +1257,7 @@ void EditTagDialog::AcceptFinished() {
}
void EditTagDialog::ResetStatistics() {
void EditTagDialog::ResetPlayStatistics() {
const QModelIndexList idx_list = ui_->song_list->selectionModel()->selectedIndexes();
if (idx_list.isEmpty()) return;
@ -1312,7 +1265,7 @@ void EditTagDialog::ResetStatistics() {
Song *song = &data_[idx_list.first().row()].current_;
if (!song->is_valid()) return;
if (QMessageBox::question(this, tr("Reset song statistics"), tr("Are you sure you want to reset this song's statistics?"), QMessageBox::Reset, QMessageBox::Cancel) != QMessageBox::Reset) {
if (QMessageBox::question(this, tr("Reset song play statistics"), tr("Are you sure you want to reset this song's play statistics?"), QMessageBox::Reset, QMessageBox::Cancel) != QMessageBox::Reset) {
return;
}
@ -1399,67 +1352,46 @@ void EditTagDialog::FetchTagSongChosen(const Song &original_song, const Song &ne
}
void EditTagDialog::SongSaveTagsComplete(TagReaderReply *reply, const QString &filename, Song song) {
void EditTagDialog::SongSaveTagsComplete(TagReaderReply *reply, const QString &filename, Song song, const UpdateCoverAction cover_action) {
--save_tag_pending_;
const bool success = reply->message().save_file_response().success();
reply->deleteLater();
if (!reply->message().save_file_response().success()) {
QString message = tr("An error occurred writing metadata to '%1'").arg(filename);
emit Error(message);
}
else if (song.is_collection_song()) {
if (collection_songs_.contains(song.id())) {
Song old_song = collection_songs_.take(song.id());
song.set_art_automatic(old_song.art_automatic());
song.set_art_manual(old_song.art_manual());
if (success) {
if (song.is_collection_song()) {
if (collection_songs_.contains(song.id())) {
Song old_song = collection_songs_.take(song.id());
song.set_art_automatic(old_song.art_automatic());
song.set_art_manual(old_song.art_manual());
}
switch (cover_action) {
case UpdateCoverAction::None:
break;
case UpdateCoverAction::New:
song.clear_art_manual();
song.set_embedded_cover();
break;
case UpdateCoverAction::Clear:
case UpdateCoverAction::Delete:
song.clear_art_automatic();
song.clear_art_manual();
break;
case UpdateCoverAction::Unset:
song.clear_art_automatic();
song.set_manually_unset_cover();
break;
}
collection_songs_.insert(song.id(), song);
}
collection_songs_.insert(song.id(), song);
if (cover_action != UpdateCoverAction::None && song == app_->current_albumcover_loader()->last_song()) {
app_->current_albumcover_loader()->LoadAlbumCover(song);
}
}
else {
emit Error(tr("An error occurred writing metadata to '%1'").arg(filename));
}
QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection);
if (save_tag_pending_ <= 0 && save_art_pending_ <= 0) AcceptFinished();
}
void EditTagDialog::SongSaveArtComplete(TagReaderReply *reply, const QString &filename, Song song, const UpdateCoverAction cover_action) {
--save_art_pending_;
if (!reply->message().save_embedded_art_response().success()) {
QString message = tr("An error occurred writing cover art to '%1'").arg(filename);
emit Error(message);
}
else if (song.is_collection_song()) {
if (collection_songs_.contains(song.id())) {
song = collection_songs_.take(song.id());
}
switch (cover_action) {
case UpdateCoverAction::None:
break;
case UpdateCoverAction::New:
song.clear_art_manual();
song.set_embedded_cover();
break;
case UpdateCoverAction::Clear:
case UpdateCoverAction::Delete:
song.clear_art_automatic();
song.clear_art_manual();
break;
case UpdateCoverAction::Unset:
song.clear_art_automatic();
song.set_manually_unset_cover();
break;
}
collection_songs_.insert(song.id(), song);
}
if (song == app_->current_albumcover_loader()->last_song()) {
app_->current_albumcover_loader()->LoadAlbumCover(song);
}
QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection);
if (save_tag_pending_ <= 0 && save_art_pending_ <= 0) AcceptFinished();
if (save_tag_pending_ <= 0) SaveDataFinished();
}

View File

@ -109,13 +109,13 @@ class EditTagDialog : public QDialog {
private slots:
void SetSongsFinished();
void AcceptFinished();
void SaveDataFinished();
void SelectionChanged();
void FieldValueEdited();
void ResetField();
void ButtonClicked(QAbstractButton *button);
void ResetStatistics();
void ResetPlayStatistics();
void SongRated(const float rating);
void FetchTag();
void FetchTagSongChosen(const Song &original_song, const Song &new_metadata);
@ -134,8 +134,7 @@ class EditTagDialog : public QDialog {
void PreviousSong();
void NextSong();
void SongSaveTagsComplete(TagReaderReply *reply, const QString &filename, Song song);
void SongSaveArtComplete(TagReaderReply *reply, const QString &filename, Song song, const EditTagDialog::UpdateCoverAction cover_action);
void SongSaveTagsComplete(TagReaderReply *reply, const QString &filename, Song song, const UpdateCoverAction cover_action);
private:
struct FieldData {
@ -206,7 +205,6 @@ class EditTagDialog : public QDialog {
QPushButton *next_button_;
int save_tag_pending_;
int save_art_pending_;
QMap<int, Song> collection_songs_;
};

View File

@ -36,20 +36,13 @@
#include <QTextDocument>
#include <QTextFormat>
#include "core/arraysize.h"
#include "core/song.h"
#include "utilities/transliterate.h"
#include "utilities/filenameconstants.h"
#include "utilities/timeconstants.h"
#include "utilities/transliterate.h"
#include "core/song.h"
#include "organizeformat.h"
const QRegularExpression OrganizeFormat::kProblematicCharacters("[:?*\"<>|]");
// From http://en.wikipedia.org/wiki/8.3_filename#Directory_table
const QRegularExpression OrganizeFormat::kInvalidFatCharacters("[^a-zA-Z0-9!#\\$%&'()\\-@\\^_`{}~/. ]", QRegularExpression::CaseInsensitiveOption);
const QRegularExpression OrganizeFormat::kInvalidDirCharacters("[/\\\\]");
constexpr char OrganizeFormat::kInvalidPrefixCharacters[] = ".";
constexpr int OrganizeFormat::kInvalidPrefixCharactersCount = arraysize(OrganizeFormat::kInvalidPrefixCharacters) - 1;
constexpr char OrganizeFormat::kBlockPattern[] = "\\{([^{}]+)\\}";
constexpr char OrganizeFormat::kTagPattern[] = "\\%([a-zA-Z]*)";
@ -138,9 +131,9 @@ OrganizeFormat::GetFilenameForSongResult OrganizeFormat::GetFilenameForSong(cons
return GetFilenameForSongResult();
}
if (remove_problematic_) filepath = filepath.remove(kProblematicCharacters);
if (remove_problematic_) filepath = filepath.remove(QRegularExpression(QString(kProblematicCharactersRegex), QRegularExpression::PatternOption::CaseInsensitiveOption));
if (remove_non_fat_ || (remove_non_ascii_ && !allow_ascii_ext_)) filepath = Utilities::Transliterate(filepath);
if (remove_non_fat_) filepath = filepath.remove(kInvalidFatCharacters);
if (remove_non_fat_) filepath = filepath.remove(QRegularExpression(QString(kInvalidFatCharactersRegex), QRegularExpression::PatternOption::CaseInsensitiveOption));
if (remove_non_ascii_) {
int ascii = 128;
@ -327,7 +320,7 @@ QString OrganizeFormat::TagValue(const QString &tag, const Song &song) const {
if (tag == "track" && value.length() == 1) value.prepend('0');
// Replace characters that really shouldn't be in paths
value = value.remove(kInvalidDirCharacters);
value = value.remove(QRegularExpression(QString(kInvalidDirCharactersRegex), QRegularExpression::PatternOption::CaseInsensitiveOption));
if (remove_problematic_) value = value.remove('.');
value = value.trimmed();

View File

@ -27,7 +27,6 @@
#include <QObject>
#include <QString>
#include <QStringList>
#include <QRegularExpression>
#include <QRgb>
#include <QSyntaxHighlighter>
#include <QValidator>
@ -41,9 +40,6 @@ class OrganizeFormat {
public:
explicit OrganizeFormat(const QString &format = QString());
static const QRegularExpression kProblematicCharacters;
static const QRegularExpression kInvalidFatCharacters;
QString format() const { return format_; }
bool remove_problematic() const { return remove_problematic_; }
bool remove_non_fat() const { return remove_non_fat_; }
@ -93,9 +89,6 @@ class OrganizeFormat {
static const char kTagPattern[];
static const QStringList kKnownTags;
static const QStringList kUniqueTags;
static const QRegularExpression kInvalidDirCharacters;
static const char kInvalidPrefixCharacters[];
static const int kInvalidPrefixCharactersCount;
QString ParseBlock(QString block, const Song &song, bool *have_tagdata = nullptr, bool *any_empty = nullptr) const;
QString TagValue(const QString &tag, const Song &song) const;

View File

@ -420,7 +420,7 @@ void Playlist::SongSaveComplete(TagReaderReply *reply, const QPersistentModelInd
}
}
QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection);
reply->deleteLater();
}

View File

@ -36,6 +36,7 @@
#include <QList>
#include <QSet>
#include <QString>
#include <QRegularExpression>
#include <QUrl>
#include <QAbstractItemModel>
#include <QScrollBar>
@ -44,10 +45,10 @@
#include "core/application.h"
#include "core/player.h"
#include "utilities/filenameconstants.h"
#include "utilities/timeutils.h"
#include "collection/collectionbackend.h"
#include "covermanager/currentalbumcoverloader.h"
#include "organize/organizeformat.h"
#include "settings/playlistsettingspage.h"
#include "playlist.h"
#include "playlistbackend.h"
@ -248,7 +249,7 @@ void PlaylistManager::SaveWithUI(const int id, const QString &playlist_name) {
s.endGroup();
QString suggested_filename = playlist_name;
QString filename = last_save_path + "/" + suggested_filename.remove(OrganizeFormat::kProblematicCharacters) + "." + last_save_extension;
QString filename = last_save_path + "/" + suggested_filename.remove(QRegularExpression(QString(kProblematicCharactersRegex), QRegularExpression::CaseInsensitiveOption)) + "." + last_save_extension;
QFileInfo fileinfo;
forever {

View File

@ -38,8 +38,9 @@
#include "core/networkaccessmanager.h"
#include "core/song.h"
#include "core/application.h"
#include "utilities/imageutils.h"
#include "utilities/timeconstants.h"
#include "utilities/imageutils.h"
#include "utilities/coverutils.h"
#include "qobuzservice.h"
#include "qobuzurlhandler.h"
#include "qobuzbaserequest.h"
@ -1266,7 +1267,7 @@ void QobuzRequest::AddAlbumCoverRequest(const Song &song) {
AlbumCoverRequest request;
request.url = cover_url;
request.filename = app_->album_cover_loader()->CoverFilePath(song.source(), song.effective_albumartist(), song.effective_album(), song.album_id(), QString(), cover_url);
request.filename = CoverUtils::CoverFilePath(CoverOptions(), song.source(), song.effective_albumartist(), song.effective_album(), song.album_id(), QString(), cover_url);
if (request.filename.isEmpty()) return;
album_covers_requests_sent_.insert(cover_url, song.song_id());

View File

@ -46,6 +46,7 @@
#include "core/iconloader.h"
#include "utilities/strutils.h"
#include "utilities/timeutils.h"
#include "utilities/coveroptions.h"
#include "collection/collection.h"
#include "collection/collectionmodel.h"
#include "collection/collectiondirectorymodel.h"
@ -204,25 +205,25 @@ void CollectionSettingsPage::Load() {
QStringList filters = s.value("cover_art_patterns", QStringList() << "front" << "cover").toStringList();
ui_->cover_art_patterns->setText(filters.join(","));
const SaveCoverType save_cover_type = static_cast<SaveCoverType>(s.value("save_cover_type", static_cast<int>(SaveCoverType::Cache)).toInt());
const CoverOptions::CoverType save_cover_type = static_cast<CoverOptions::CoverType>(s.value("save_cover_type", static_cast<int>(CoverOptions::CoverType::Cache)).toInt());
switch (save_cover_type) {
case SaveCoverType::Cache:
case CoverOptions::CoverType::Cache:
ui_->radiobutton_save_albumcover_cache->setChecked(true);
break;
case SaveCoverType::Album:
case CoverOptions::CoverType::Album:
ui_->radiobutton_save_albumcover_albumdir->setChecked(true);
break;
case SaveCoverType::Embedded:
case CoverOptions::CoverType::Embedded:
ui_->radiobutton_save_albumcover_embedded->setChecked(true);
break;
}
const SaveCoverFilename save_cover_filename = static_cast<SaveCoverFilename>(s.value("save_cover_filename", static_cast<int>(SaveCoverFilename::Pattern)).toInt());
const CoverOptions::CoverFilename save_cover_filename = static_cast<CoverOptions::CoverFilename>(s.value("save_cover_filename", static_cast<int>(CoverOptions::CoverFilename::Pattern)).toInt());
switch (save_cover_filename) {
case SaveCoverFilename::Hash:
case CoverOptions::CoverFilename::Hash:
ui_->radiobutton_cover_hash->setChecked(true);
break;
case SaveCoverFilename::Pattern:
case CoverOptions::CoverFilename::Pattern:
ui_->radiobutton_cover_pattern->setChecked(true);
break;
}
@ -297,15 +298,15 @@ void CollectionSettingsPage::Save() {
s.setValue("cover_art_patterns", filters);
SaveCoverType save_cover_type = SaveCoverType::Cache;
if (ui_->radiobutton_save_albumcover_cache->isChecked()) save_cover_type = SaveCoverType::Cache;
else if (ui_->radiobutton_save_albumcover_albumdir->isChecked()) save_cover_type = SaveCoverType::Album;
else if (ui_->radiobutton_save_albumcover_embedded->isChecked()) save_cover_type = SaveCoverType::Embedded;
CoverOptions::CoverType save_cover_type = CoverOptions::CoverType::Cache;
if (ui_->radiobutton_save_albumcover_cache->isChecked()) save_cover_type = CoverOptions::CoverType::Cache;
else if (ui_->radiobutton_save_albumcover_albumdir->isChecked()) save_cover_type = CoverOptions::CoverType::Album;
else if (ui_->radiobutton_save_albumcover_embedded->isChecked()) save_cover_type = CoverOptions::CoverType::Embedded;
s.setValue("save_cover_type", static_cast<int>(save_cover_type));
SaveCoverFilename save_cover_filename = SaveCoverFilename::Hash;
if (ui_->radiobutton_cover_hash->isChecked()) save_cover_filename = SaveCoverFilename::Hash;
else if (ui_->radiobutton_cover_pattern->isChecked()) save_cover_filename = SaveCoverFilename::Pattern;
CoverOptions::CoverFilename save_cover_filename = CoverOptions::CoverFilename::Hash;
if (ui_->radiobutton_cover_hash->isChecked()) save_cover_filename = CoverOptions::CoverFilename::Hash;
else if (ui_->radiobutton_cover_pattern->isChecked()) save_cover_filename = CoverOptions::CoverFilename::Pattern;
s.setValue("save_cover_filename", static_cast<int>(save_cover_filename));
s.setValue("cover_pattern", ui_->lineedit_cover_pattern->text());

View File

@ -50,17 +50,6 @@ class CollectionSettingsPage : public SettingsPage {
static const int kSettingsCacheSizeDefault;
static const int kSettingsDiskCacheSizeDefault;
enum class SaveCoverType {
Cache = 1,
Album = 2,
Embedded = 3
};
enum class SaveCoverFilename {
Hash = 1,
Pattern = 2
};
enum class CacheSizeUnit {
KB,
MB,

View File

@ -40,6 +40,7 @@
#include "core/application.h"
#include "utilities/timeconstants.h"
#include "utilities/imageutils.h"
#include "utilities/coverutils.h"
#include "tidalservice.h"
#include "tidalurlhandler.h"
#include "tidalbaserequest.h"
@ -1208,7 +1209,7 @@ void TidalRequest::AddAlbumCoverRequest(const Song &song) {
AlbumCoverRequest request;
request.album_id = song.album_id();
request.url = song.art_automatic();
request.filename = app_->album_cover_loader()->CoverFilePath(song.source(), song.effective_albumartist(), song.effective_album(), song.album_id(), QString(), request.url);
request.filename = CoverUtils::CoverFilePath(CoverOptions(), song.source(), song.effective_albumartist(), song.effective_album(), song.album_id(), QString(), request.url);
if (request.filename.isEmpty()) return;
album_covers_requests_sent_.insert(song.album_id(), song.song_id());

View File

@ -0,0 +1,46 @@
/*
* Strawberry Music Player
* Copyright 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/>.
*
*/
#ifndef COVEROPTIONS_H
#define COVEROPTIONS_H
class CoverOptions {
public:
enum class CoverType {
Cache = 1,
Album = 2,
Embedded = 3
};
enum class CoverFilename {
Hash = 1,
Pattern = 2
};
explicit CoverOptions() : cover_type(CoverType::Cache), cover_filename(CoverFilename::Hash), cover_overwrite(false), cover_lowercase(true), cover_replace_spaces(true) {}
CoverType cover_type;
CoverFilename cover_filename;
QString cover_pattern;
bool cover_overwrite;
bool cover_lowercase;
bool cover_replace_spaces;
};
#endif // COVEROPTIONS_H

View File

@ -0,0 +1,166 @@
/*
* Strawberry Music Player
* Copyright 2019-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 <QByteArray>
#include <QString>
#include <QRegularExpression>
#include <QUrl>
#include <QDir>
#include <QStandardPaths>
#include <QCryptographicHash>
#include "filenameconstants.h"
#include "transliterate.h"
#include "coverutils.h"
#include "core/logging.h"
QByteArray CoverUtils::Sha1CoverHash(const QString &artist, const QString &album) {
QCryptographicHash hash(QCryptographicHash::Sha1);
hash.addData(artist.toLower().toUtf8());
hash.addData(album.toLower().toUtf8());
return hash.result();
}
QString CoverUtils::AlbumCoverFilename(QString artist, QString album, const QString &extension) {
artist.remove('/').remove('\\');
album.remove('/').remove('\\');
QString filename = artist + "-" + album;
filename = Utilities::Transliterate(filename.toLower());
filename = filename.replace(' ', '-')
.replace("--", "-")
.remove(QRegularExpression(QString(kInvalidFatCharactersRegex), QRegularExpression::CaseInsensitiveOption))
.simplified();
if (!extension.isEmpty()) {
filename.append('.');
filename.append(extension);
}
return filename;
}
QString CoverUtils::CoverFilePath(const CoverOptions &options, const Song &song, const QString &album_dir, const QUrl &cover_url, const QString &extension) {
return CoverFilePath(options, song.source(), song.effective_albumartist(), song.album(), song.album_id(), album_dir, cover_url, extension);
}
QString CoverUtils::CoverFilePath(const CoverOptions &options, const Song::Source source, const QString &artist, const QString &album, const QString &album_id, const QString &album_dir, const QUrl &cover_url, const QString &extension) {
QString path;
if (source == Song::Source::Collection && options.cover_type == CoverOptions::CoverType::Album && !album_dir.isEmpty()) {
path = album_dir;
}
else {
path = Song::ImageCacheDir(source);
}
if (path.right(1) == QDir::separator() || path.right(1) == "/") {
path.chop(1);
}
QDir dir;
if (!dir.mkpath(path)) {
qLog(Error) << "Unable to create directory" << path;
path = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
}
QString filename;
if (source == Song::Source::Collection &&
options.cover_type == CoverOptions::CoverType::Album &&
options.cover_filename == CoverOptions::CoverFilename::Pattern &&
!options.cover_pattern.isEmpty()) {
filename = CoverFilenameFromVariable(options, artist, album);
filename.remove(QRegularExpression(QString(kInvalidFatCharactersRegex), QRegularExpression::CaseInsensitiveOption)).remove('/').remove('\\');
if (options.cover_lowercase) filename = filename.toLower();
if (options.cover_replace_spaces) filename.replace(QRegularExpression("\\s"), "-");
if (!extension.isEmpty()) {
filename.append('.');
filename.append(extension);
}
}
if (filename.isEmpty()) {
filename = CoverFilenameFromSource(source, cover_url, artist, album, album_id, extension);
}
QString filepath(path + "/" + filename);
return filepath;
}
QString CoverUtils::CoverFilenameFromSource(const Song::Source source, const QUrl &cover_url, const QString &artist, const QString &album, const QString &album_id, const QString &extension) {
QString filename;
switch (source) {
case Song::Source::Tidal:
if (!album_id.isEmpty()) {
filename = album_id + "-" + cover_url.fileName();
break;
}
[[fallthrough]];
case Song::Source::Subsonic:
case Song::Source::Qobuz:
if (!album_id.isEmpty()) {
filename = album_id;
break;
}
[[fallthrough]];
case Song::Source::Collection:
case Song::Source::LocalFile:
case Song::Source::CDDA:
case Song::Source::Device:
case Song::Source::Stream:
case Song::Source::SomaFM:
case Song::Source::RadioParadise:
case Song::Source::Unknown:
filename = Sha1CoverHash(artist, album).toHex();
break;
}
if (!extension.isEmpty()) {
filename.append('.');
filename.append(extension);
}
return filename;
}
QString CoverUtils::CoverFilenameFromVariable(const CoverOptions &options, const QString &artist, QString album, const QString &extension) {
album = album.remove(Song::kAlbumRemoveDisc);
QString filename(options.cover_pattern);
filename.replace("%albumartist", artist);
filename.replace("%artist", artist);
filename.replace("%album", album);
if (!extension.isEmpty()) {
filename.append('.');
filename.append(extension);
}
return filename;
}

View File

@ -0,0 +1,42 @@
/*
* Strawberry Music Player
* Copyright 2019-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/>.
*
*/
#ifndef COVERUTILS_H
#define COVERUTILS_H
#include <QByteArray>
#include <QString>
#include <QUrl>
#include "core/song.h"
#include "coveroptions.h"
class CoverUtils {
public:
static QByteArray Sha1CoverHash(const QString &artist, const QString &album);
static QString AlbumCoverFilename(QString artist, QString album, const QString &extension);
static QString CoverFilenameFromSource(const Song::Source source, const QUrl &cover_url, const QString &artist, const QString &album, const QString &album_id, const QString &extension);
static QString CoverFilenameFromVariable(const CoverOptions &options, const QString &artist, QString album, const QString &extension = QString());
static QString CoverFilePath(const CoverOptions &options, const Song &song, const QString &album_dir, const QUrl &cover_url, const QString &extension = QString());
static QString CoverFilePath(const CoverOptions &options, const Song::Source source, const QString &artist, const QString &album, const QString &album_id, const QString &album_dir, const QUrl &cover_url, const QString &extension = QString());
};
#endif // COVERUTILS_H

View File

@ -62,14 +62,4 @@ QByteArray HmacSha1(const QByteArray &key, const QByteArray &data) {
return Hmac(key, data, QCryptographicHash::Sha1);
}
QByteArray Sha1CoverHash(const QString &artist, const QString &album) {
QCryptographicHash hash(QCryptographicHash::Sha1);
hash.addData(artist.toLower().toUtf8());
hash.addData(album.toLower().toUtf8());
return hash.result();
}
} // namespace Utilities

View File

@ -30,7 +30,6 @@ QByteArray Hmac(const QByteArray &key, const QByteArray &data, const QCryptograp
QByteArray HmacMd5(const QByteArray &key, const QByteArray &data);
QByteArray HmacSha256(const QByteArray &key, const QByteArray &data);
QByteArray HmacSha1(const QByteArray &key, const QByteArray &data);
QByteArray Sha1CoverHash(const QString &artist, const QString &album);
} // namespace Utilities

View File

@ -0,0 +1,31 @@
/*
* Strawberry Music Player
* Copyright 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/>.
*
*/
#ifndef FILENAMECONSTANTS_H
#define FILENAMECONSTANTS_H
#include "core/arraysize.h"
constexpr char kProblematicCharactersRegex[] = "[:?*\"<>|]";
constexpr char kInvalidFatCharactersRegex[] = "[^a-zA-Z0-9!#\\$%&'()\\-@\\^_`{}~/. ]";
constexpr char kInvalidDirCharactersRegex[] = "[/\\\\]";
constexpr char kInvalidPrefixCharacters[] = ".";
constexpr int kInvalidPrefixCharactersCount = arraysize(kInvalidPrefixCharacters) - 1;
#endif // FILENAMECONSTANTS_H

View File

@ -66,15 +66,18 @@ class TagReaderTest : public ::testing::Test {
return song;
}
static void WriteSongToFile(const Song& song, const QString& filename) {
static void WriteSongToFile(const Song &song, const QString &filename) {
#if defined(USE_TAGLIB)
TagReaderTagLib tag_reader;
#elif defined(USE_TAGPARSER)
TagReaderTagParser tag_reader;
#endif
::spb::tagreader::SongMetadata pb_song;
song.ToProtobuf(&pb_song);
tag_reader.SaveFile(filename, pb_song);
::spb::tagreader::SaveFileRequest request;
const QByteArray filename_data = filename.toUtf8();
request.set_filename(filename_data.constData(), filename_data.length());
request.set_save_tags(true);
song.ToProtobuf(request.mutable_metadata());
tag_reader.SaveFile(request);
}
static QString SHA256SUM(const QString &filename) {