Add option to select ID3v2 version

Fixes #1861
This commit is contained in:
Jonas Kvinge
2025-12-18 22:18:26 +01:00
parent d1ee27fff9
commit 2cd9498469
13 changed files with 193 additions and 13 deletions

View File

@@ -353,6 +353,8 @@ struct Song::Private : public QSharedData {
std::optional<double> ebur128_integrated_loudness_lufs_;
std::optional<double> ebur128_loudness_range_lu_;
int id3v2_version_; // ID3v2 tag version (3 or 4), 0 if not applicable or unknown
bool init_from_file_; // Whether this song was loaded from a file using taglib.
bool suspicious_tags_; // Whether our encoding guesser thinks these tags might be incorrectly encoded.
@@ -400,6 +402,8 @@ Song::Private::Private(const Source source)
rating_(-1),
bpm_(-1),
id3v2_version_(0),
init_from_file_(false),
suspicious_tags_(false)
@@ -510,6 +514,8 @@ const QString &Song::musicbrainz_work_id() const { return d->musicbrainz_work_id
std::optional<double> Song::ebur128_integrated_loudness_lufs() const { return d->ebur128_integrated_loudness_lufs_; }
std::optional<double> Song::ebur128_loudness_range_lu() const { return d->ebur128_loudness_range_lu_; }
int Song::id3v2_version() const { return d->id3v2_version_; }
QString *Song::mutable_title() { return &d->title_; }
QString *Song::mutable_album() { return &d->album_; }
QString *Song::mutable_artist() { return &d->artist_; }
@@ -624,6 +630,8 @@ void Song::set_musicbrainz_work_id(const QString &v) { d->musicbrainz_work_id_ =
void Song::set_ebur128_integrated_loudness_lufs(const std::optional<double> v) { d->ebur128_integrated_loudness_lufs_ = v; }
void Song::set_ebur128_loudness_range_lu(const std::optional<double> v) { d->ebur128_loudness_range_lu_ = v; }
void Song::set_id3v2_version(const int v) { d->id3v2_version_ = v; }
void Song::set_init_from_file(const bool v) { d->init_from_file_ = v; }
void Song::set_stream_url(const QUrl &v) { d->stream_url_ = v; }
@@ -833,6 +841,10 @@ bool Song::save_embedded_cover_supported(const FileType filetype) {
}
bool Song::id3v2_tags_supported() const {
return d->filetype_ == FileType::MPEG || d->filetype_ == FileType::WAV || d->filetype_ == FileType::AIFF;
}
int Song::ColumnIndex(const QString &field) {
return static_cast<int>(kRowIdColumns.indexOf(field));

View File

@@ -234,6 +234,8 @@ class Song {
std::optional<double> ebur128_integrated_loudness_lufs() const;
std::optional<double> ebur128_loudness_range_lu() const;
int id3v2_version() const;
QString *mutable_title();
QString *mutable_album();
QString *mutable_artist();
@@ -349,6 +351,8 @@ class Song {
void set_ebur128_integrated_loudness_lufs(const std::optional<double> v);
void set_ebur128_loudness_range_lu(const std::optional<double> v);
void set_id3v2_version(const int v);
void set_init_from_file(const bool v);
void set_stream_url(const QUrl &v);
@@ -439,6 +443,8 @@ class Song {
static bool save_embedded_cover_supported(const FileType filetype);
bool save_embedded_cover_supported() const { return url().isLocalFile() && save_embedded_cover_supported(filetype()) && !has_cue(); };
bool id3v2_tags_supported() const;
static int ColumnIndex(const QString &field);
static QString JoinSpec(const QString &table);

View File

@@ -81,6 +81,7 @@
#include "utilities/coverutils.h"
#include "utilities/coveroptions.h"
#include "tagreader/tagreaderclient.h"
#include "tagreader/tagid3v2version.h"
#include "widgets/busyindicator.h"
#include "widgets/lineedit.h"
#include "collection/collectionbackend.h"
@@ -107,6 +108,12 @@ using namespace Qt::Literals::StringLiterals;
namespace {
constexpr char kSettingsGroup[] = "EditTagDialog";
constexpr int kSmallImageSize = 128;
// ID3v2 version constants
constexpr int kID3v2_Version_3 = 3;
constexpr int kID3v2_Version_4 = 4;
constexpr int kComboBoxIndex_ID3v2_3 = 0;
constexpr int kComboBoxIndex_ID3v2_4 = 1;
} // namespace
const char EditTagDialog::kTagsDifferentHintText[] = QT_TR_NOOP("(different across multiple songs)");
@@ -708,6 +715,9 @@ void EditTagDialog::SelectionChanged() {
bool titlesort_enabled = false;
bool artistsort_enabled = false;
bool albumsort_enabled = false;
bool has_id3v2_support = false;
int id3v2_version = 0;
bool id3v2_version_different = false;
for (const QModelIndex &idx : indexes) {
if (data_.value(idx.row()).cover_action_ == UpdateCoverAction::None) {
data_[idx.row()].cover_result_ = AlbumCoverImageResult();
@@ -769,6 +779,15 @@ void EditTagDialog::SelectionChanged() {
if (song.albumsort_supported()) {
albumsort_enabled = true;
}
if (song.id3v2_tags_supported()) {
has_id3v2_support = true;
if (id3v2_version == 0) {
id3v2_version = song.id3v2_version();
}
else if (id3v2_version != song.id3v2_version()) {
id3v2_version_different = true;
}
}
}
QString summary;
@@ -840,6 +859,23 @@ void EditTagDialog::SelectionChanged() {
ui_->artistsort->setEnabled(artistsort_enabled);
ui_->albumsort->setEnabled(albumsort_enabled);
ui_->label_id3v2_version->setVisible(has_id3v2_support);
ui_->combobox_id3v2_version->setVisible(has_id3v2_support);
if (has_id3v2_support) {
// Set default based on existing version(s)
if (id3v2_version_different || id3v2_version == 0) {
// Mixed versions or unknown - default to ID3v2.4
ui_->combobox_id3v2_version->setCurrentIndex(kComboBoxIndex_ID3v2_4);
}
else if (id3v2_version == kID3v2_Version_3) {
ui_->combobox_id3v2_version->setCurrentIndex(kComboBoxIndex_ID3v2_3);
}
else {
ui_->combobox_id3v2_version->setCurrentIndex(kComboBoxIndex_ID3v2_4);
}
}
}
void EditTagDialog::UpdateUI(const QModelIndexList &indexes) {
@@ -1371,6 +1407,13 @@ void EditTagDialog::SaveData() {
}
save_tag_cover_data.cover_data = ref.cover_result_.image_data;
}
// Determine ID3v2 version based on user selection
TagID3v2Version tag_id3v2_version = TagID3v2Version::Default;
if (ref.current_.filetype() == Song::FileType::MPEG || ref.current_.filetype() == Song::FileType::WAV || ref.current_.filetype() == Song::FileType::AIFF) {
tag_id3v2_version = ui_->combobox_id3v2_version->currentIndex() == kComboBoxIndex_ID3v2_3 ? TagID3v2Version::V3 : TagID3v2Version::V4;
}
TagReaderClient::SaveOptions save_tags_options;
if (save_tags) {
save_tags_options |= TagReaderClient::SaveOption::Tags;
@@ -1384,7 +1427,7 @@ void EditTagDialog::SaveData() {
if (save_embedded_cover) {
save_tags_options |= TagReaderClient::SaveOption::Cover;
}
TagReaderReplyPtr reply = tagreader_client_->WriteFileAsync(ref.current_.url().toLocalFile(), ref.current_, save_tags_options, save_tag_cover_data);
TagReaderReplyPtr reply = tagreader_client_->WriteFileAsync(ref.current_.url().toLocalFile(), ref.current_, save_tags_options, save_tag_cover_data, tag_id3v2_version);
SharedPtr<QMetaObject::Connection> connection = make_shared<QMetaObject::Connection>();
*connection = QObject::connect(&*reply, &TagReaderReply::Finished, this, [this, reply, ref, connection]() {
SongSaveTagsComplete(reply, ref.current_.url().toLocalFile(), ref.current_, ref.cover_action_);

View File

@@ -650,6 +650,47 @@
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="layout_id3v2_version">
<item>
<widget class="QLabel" name="label_id3v2_version">
<property name="text">
<string>ID3v2 version:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="combobox_id3v2_version">
<property name="enabled">
<bool>true</bool>
</property>
<item>
<property name="text">
<string>2.3</string>
</property>
</item>
<item>
<property name="text">
<string>2.4</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="spacer_id3v2_version">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<spacer name="spacer_albumart_bottom">
<property name="orientation">

View File

@@ -0,0 +1,29 @@
/*
* Strawberry Music Player
* Copyright 2025, 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 TAGID3V2VERSION_H
#define TAGID3V2VERSION_H
enum class TagID3v2Version {
Default = 0, // Use existing version or library default
V3 = 3,
V4 = 4
};
#endif // TAGID3V2VERSION_H

View File

@@ -32,6 +32,7 @@
#include "savetagsoptions.h"
#include "savetagcoverdata.h"
#include "albumcovertagdata.h"
#include "tagid3v2version.h"
class TagReaderBase {
public:
@@ -45,7 +46,7 @@ class TagReaderBase {
virtual TagReaderResult ReadStream(const QUrl &url, const QString &filename, const quint64 size, const quint64 mtime, const QString &token_type, const QString &access_token, Song *song) const = 0;
#endif
virtual TagReaderResult WriteFile(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options, const SaveTagCoverData &save_tag_cover_data) const = 0;
virtual TagReaderResult WriteFile(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options, const SaveTagCoverData &save_tag_cover_data, const TagID3v2Version id3v2_version) const = 0;
virtual TagReaderResult LoadEmbeddedCover(const QString &filename, QByteArray &data) const = 0;
virtual TagReaderResult SaveEmbeddedCover(const QString &filename, const SaveTagCoverData &save_tag_cover_data) const = 0;

View File

@@ -50,6 +50,7 @@
#include "tagreaderreadstreamreply.h"
#include "tagreaderloadcoverdatareply.h"
#include "tagreaderloadcoverimagereply.h"
#include "tagid3v2version.h"
using std::dynamic_pointer_cast;
using namespace Qt::Literals::StringLiterals;
@@ -189,7 +190,7 @@ void TagReaderClient::ProcessRequest(TagReaderRequestPtr request) {
}
#endif // HAVE_STREAMTAGREADER
else if (TagReaderWriteFileRequestPtr write_file_request = dynamic_pointer_cast<TagReaderWriteFileRequest>(request)) {
result = WriteFileBlocking(write_file_request->filename, write_file_request->song, write_file_request->save_tags_options, write_file_request->save_tag_cover_data);
result = WriteFileBlocking(write_file_request->filename, write_file_request->song, write_file_request->save_tags_options, write_file_request->save_tag_cover_data, write_file_request->tag_id3v2_version);
}
else if (TagReaderLoadCoverDataRequestPtr load_cover_data_request = dynamic_pointer_cast<TagReaderLoadCoverDataRequest>(request)) {
QByteArray cover_data;
@@ -303,13 +304,13 @@ TagReaderReadStreamReplyPtr TagReaderClient::ReadStreamAsync(const QUrl &url, co
}
#endif // HAVE_STREAMTAGREADER
TagReaderResult TagReaderClient::WriteFileBlocking(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options, const SaveTagCoverData &save_tag_cover_data) {
TagReaderResult TagReaderClient::WriteFileBlocking(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options, const SaveTagCoverData &save_tag_cover_data, const TagID3v2Version tag_id3v2_version) {
return tagreader_.WriteFile(filename, song, save_tags_options, save_tag_cover_data);
return tagreader_.WriteFile(filename, song, save_tags_options, save_tag_cover_data, tag_id3v2_version);
}
TagReaderReplyPtr TagReaderClient::WriteFileAsync(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options, const SaveTagCoverData &save_tag_cover_data) {
TagReaderReplyPtr TagReaderClient::WriteFileAsync(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options, const SaveTagCoverData &save_tag_cover_data, const TagID3v2Version tag_id3v2_version) {
Q_ASSERT(QThread::currentThread() != thread());
@@ -321,6 +322,7 @@ TagReaderReplyPtr TagReaderClient::WriteFileAsync(const QString &filename, const
request->song = song;
request->save_tags_options = save_tags_options;
request->save_tag_cover_data = save_tag_cover_data;
request->tag_id3v2_version = tag_id3v2_version;
EnqueueRequest(request);

View File

@@ -43,6 +43,7 @@
#include "tagreaderloadcoverimagereply.h"
#include "savetagsoptions.h"
#include "savetagcoverdata.h"
#include "tagid3v2version.h"
class QThread;
class Song;
@@ -72,8 +73,8 @@ class TagReaderClient : public QObject {
[[nodiscard]] TagReaderReadStreamReplyPtr ReadStreamAsync(const QUrl &url, const QString &filename, const quint64 size, const quint64 mtime, const QString &token_type, const QString &access_token);
#endif
TagReaderResult WriteFileBlocking(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options = SaveTagsOption::Tags, const SaveTagCoverData &save_tag_cover_data = SaveTagCoverData());
[[nodiscard]] TagReaderReplyPtr WriteFileAsync(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options = SaveTagsOption::Tags, const SaveTagCoverData &save_tag_cover_data = SaveTagCoverData());
TagReaderResult WriteFileBlocking(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options = SaveTagsOption::Tags, const SaveTagCoverData &save_tag_cover_data = SaveTagCoverData(), const TagID3v2Version tag_id3v2_version = TagID3v2Version::Default);
[[nodiscard]] TagReaderReplyPtr WriteFileAsync(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options = SaveTagsOption::Tags, const SaveTagCoverData &save_tag_cover_data = SaveTagCoverData(), const TagID3v2Version tag_id3v2_version = TagID3v2Version::Default);
TagReaderResult LoadCoverDataBlocking(const QString &filename, QByteArray &data);
TagReaderResult LoadCoverImageBlocking(const QString &filename, QImage &image);

View File

@@ -32,6 +32,7 @@
#include "core/logging.h"
#include "tagreaderbase.h"
#include "tagreadertaglib.h"
#include "tagid3v2version.h"
using namespace Qt::Literals::StringLiterals;
@@ -317,12 +318,13 @@ TagReaderResult TagReaderGME::ReadStream(const QUrl &url, const QString &filenam
}
#endif
TagReaderResult TagReaderGME::WriteFile(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options, const SaveTagCoverData &save_tag_cover_data) const {
TagReaderResult TagReaderGME::WriteFile(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options, const SaveTagCoverData &save_tag_cover_data, const TagID3v2Version id3v2_version) const {
Q_UNUSED(filename);
Q_UNUSED(song);
Q_UNUSED(save_tags_options);
Q_UNUSED(save_tag_cover_data);
Q_UNUSED(id3v2_version);
return TagReaderResult::ErrorCode::Unsupported;

View File

@@ -25,6 +25,7 @@
#include <QFileInfo>
#include "tagreaderbase.h"
#include "tagid3v2version.h"
namespace GME {
bool IsSupportedFormat(const QFileInfo &fileinfo);
@@ -107,7 +108,7 @@ class TagReaderGME : public TagReaderBase {
TagReaderResult ReadStream(const QUrl &url, const QString &filename, const quint64 size, const quint64 mtime, const QString &token_type, const QString &access_token, Song *song) const override;
#endif
TagReaderResult WriteFile(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options, const SaveTagCoverData &save_tag_cover_data) const override;
TagReaderResult WriteFile(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options, const SaveTagCoverData &save_tag_cover_data, const TagID3v2Version id3v2_version) const override;
TagReaderResult LoadEmbeddedCover(const QString &filename, QByteArray &data) const override;
TagReaderResult SaveEmbeddedCover(const QString &filename, const SaveTagCoverData &save_tag_cover_data) const override;

View File

@@ -46,6 +46,7 @@
#include <taglib/apeproperties.h>
#include <taglib/id3v2tag.h>
#include <taglib/id3v2frame.h>
#include <taglib/id3v2header.h>
#include <taglib/attachedpictureframe.h>
#include <taglib/textidentificationframe.h>
#include <taglib/unsynchronizedlyricsframe.h>
@@ -104,6 +105,7 @@
#include "constants/timeconstants.h"
#include "albumcovertagdata.h"
#include "tagid3v2version.h"
using std::make_unique;
using namespace Qt::Literals::StringLiterals;
@@ -149,6 +151,10 @@ constexpr char kID3v2_MusicBrainz_DiscId[] = "MusicBrainz Disc Id";
constexpr char kID3v2_MusicBrainz_ReleaseGroupId[] = "MusicBrainz Release Group Id";
constexpr char kID3v2_MusicBrainz_WorkId[] = "MusicBrainz Work Id";
// ID3v2 version constants
constexpr int kID3v2_Version_3 = 3;
constexpr int kID3v2_Version_4 = 4;
constexpr char kVorbisComment_AlbumArtist1[] = "ALBUMARTIST";
constexpr char kVorbisComment_AlbumArtist2[] = "ALBUM ARTIST";
constexpr char kVorbisComment_AlbumArtistSort[] = "ALBUMARTISTSORT";
@@ -604,8 +610,14 @@ TagReaderResult TagReaderTagLib::ReadStream(const QUrl &url,
void TagReaderTagLib::ParseID3v2Tags(TagLib::ID3v2::Tag *tag, QString *disc, QString *compilation, Song *song) const {
if (!tag) return;
TagLib::ID3v2::FrameListMap map = tag->frameListMap();
if (tag->header()) {
song->set_id3v2_version(tag->header()->majorVersion());
}
if (map.contains(kID3v2_Disc)) *disc = TagLibStringToQString(map[kID3v2_Disc].front()->toString()).trimmed();
if (map.contains(kID3v2_Composer)) song->set_composer(map[kID3v2_Composer].front()->toString());
if (map.contains(kID3v2_ComposerSort)) song->set_composersort(map[kID3v2_ComposerSort].front()->toString());
@@ -1042,7 +1054,7 @@ void TagReaderTagLib::ParseASFAttribute(const TagLib::ASF::AttributeListMap &att
}
TagReaderResult TagReaderTagLib::WriteFile(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options, const SaveTagCoverData &save_tag_cover_data) const {
TagReaderResult TagReaderTagLib::WriteFile(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options, const SaveTagCoverData &save_tag_cover_data, const TagID3v2Version tag_id3v2_version) const {
if (filename.isEmpty()) {
return TagReaderResult::ErrorCode::FilenameMissing;
@@ -1265,7 +1277,34 @@ TagReaderResult TagReaderTagLib::WriteFile(const QString &filename, const Song &
}
}
const bool success = fileref->save();
// Determine ID3v2 version to use and convert to TagLib type
TagLib::ID3v2::Version taglib_id3v2_version = TagLib::ID3v2::v4;
if (tag_id3v2_version == TagID3v2Version::V3) {
taglib_id3v2_version = TagLib::ID3v2::v3;
}
else if (tag_id3v2_version == TagID3v2Version::V4) {
taglib_id3v2_version = TagLib::ID3v2::v4;
}
bool success = false;
// For MPEG files, use save with ID3v2 version parameter
if (TagLib::MPEG::File *file_mpeg = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
success = file_mpeg->save(TagLib::MPEG::File::AllTags, TagLib::File::StripOthers, taglib_id3v2_version);
}
// For WAV files with ID3v2 tags
else if (TagLib::RIFF::WAV::File *file_wav = dynamic_cast<TagLib::RIFF::WAV::File*>(fileref->file())) {
success = file_wav->save(TagLib::RIFF::WAV::File::AllTags, TagLib::File::StripOthers, taglib_id3v2_version);
}
// For AIFF files with ID3v2 tags
else if (TagLib::RIFF::AIFF::File *file_aiff = dynamic_cast<TagLib::RIFF::AIFF::File*>(fileref->file())) {
success = file_aiff->save(taglib_id3v2_version);
}
// For all other file types, use default save
else {
success = fileref->save();
}
#ifdef Q_OS_LINUX
if (success) {
// Linux: inotify doesn't seem to notice the change to the file unless we change the timestamps as well. (this is what touch does)

View File

@@ -46,6 +46,7 @@
#include "tagreaderbase.h"
#include "savetagcoverdata.h"
#include "tagid3v2version.h"
#undef TStringToQString
#undef QStringToTString
@@ -72,7 +73,7 @@ class TagReaderTagLib : public TagReaderBase {
TagReaderResult ReadStream(const QUrl &url, const QString &filename, const quint64 size, const quint64 mtime, const QString &token_type, const QString &access_token, Song *song) const override;
#endif
TagReaderResult WriteFile(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options, const SaveTagCoverData &save_tag_cover_data) const override;
TagReaderResult WriteFile(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options, const SaveTagCoverData &save_tag_cover_data, const TagID3v2Version tag_id3v2_version) const override;
TagReaderResult LoadEmbeddedCover(const QString &filename, QByteArray &data) const override;
TagReaderResult SaveEmbeddedCover(const QString &filename, const SaveTagCoverData &save_tag_cover_data) const override;

View File

@@ -27,6 +27,7 @@
#include "tagreaderrequest.h"
#include "savetagsoptions.h"
#include "savetagcoverdata.h"
#include "tagid3v2version.h"
using std::make_shared;
@@ -39,6 +40,7 @@ class TagReaderWriteFileRequest : public TagReaderRequest {
SaveTagsOptions save_tags_options;
Song song;
SaveTagCoverData save_tag_cover_data;
TagID3v2Version tag_id3v2_version;
};
using TagReaderWriteFileRequestPtr = SharedPtr<TagReaderWriteFileRequest>;