Compare commits

..

4 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
d3409fef8f Complete GVFS tag editing fix implementation
Co-authored-by: jonaski <10343810+jonaski@users.noreply.github.com>
2025-12-17 23:22:31 +00:00
copilot-swe-agent[bot]
034bb175e6 Fix code review issues
- Apply modifications in SaveFileWithFallback before attempting direct save
- Remove duplicate return statement in SaveSongRating

Co-authored-by: jonaski <10343810+jonaski@users.noreply.github.com>
2025-12-17 23:20:56 +00:00
copilot-swe-agent[bot]
d706d3ed34 Implement atomic write workaround for GVFS tag editing
Add SaveFileWithFallback helper function that uses atomic write pattern
when direct TagLib save fails. Refactor WriteFile, SaveEmbeddedCover,
SaveSongPlaycount, and SaveSongRating to use this helper.

Co-authored-by: jonaski <10343810+jonaski@users.noreply.github.com>
2025-12-17 23:18:48 +00:00
copilot-swe-agent[bot]
72eb65e412 Initial plan 2025-12-17 22:58:18 +00:00
39 changed files with 471 additions and 1202 deletions

1
.gitignore vendored
View File

@@ -13,4 +13,3 @@
/CMakeSettings.json
/dist/scripts/maketarball.sh
/debian/changelog
_codeql_detected_source_root

View File

@@ -1463,7 +1463,6 @@ optional_source(HAVE_QOBUZ
src/qobuz/qobuzrequest.cpp
src/qobuz/qobuzstreamurlrequest.cpp
src/qobuz/qobuzfavoriterequest.cpp
src/qobuz/qobuzcredentialfetcher.cpp
src/settings/qobuzsettingspage.cpp
src/covermanager/qobuzcoverprovider.cpp
HEADERS
@@ -1473,7 +1472,6 @@ optional_source(HAVE_QOBUZ
src/qobuz/qobuzrequest.h
src/qobuz/qobuzstreamurlrequest.h
src/qobuz/qobuzfavoriterequest.h
src/qobuz/qobuzcredentialfetcher.h
src/settings/qobuzsettingspage.h
src/covermanager/qobuzcoverprovider.h
UI

View File

@@ -0,0 +1 @@
.

View File

@@ -3318,7 +3318,7 @@ void MainWindow::PlaylistDelete() {
if (DeleteConfirmationDialog::warning(files) != QDialogButtonBox::Yes) return;
if (app_->player()->GetState() == EngineBase::State::Playing && app_->playlist_manager()->current() == app_->playlist_manager()->active() && app_->playlist_manager()->current()->rowCount() == selected_songs.count()) {
if (app_->player()->GetState() == EngineBase::State::Playing && app_->playlist_manager()->current()->rowCount() == selected_songs.count()) {
app_->player()->Stop();
}

View File

@@ -29,7 +29,6 @@
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QNetworkInformation>
#include "networkaccessmanager.h"
#include "threadsafenetworkdiskcache.h"
@@ -42,22 +41,6 @@ NetworkAccessManager::NetworkAccessManager(QObject *parent)
setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
setCache(new ThreadSafeNetworkDiskCache(this));
// Handle network state changes after system suspend/resume
// QNetworkInformation provides cross-platform network reachability monitoring in Qt 6
if (QNetworkInformation::loadDefaultBackend()) {
QNetworkInformation *network_info = QNetworkInformation::instance();
if (network_info) {
QObject::connect(network_info, &QNetworkInformation::reachabilityChanged, this, [this](QNetworkInformation::Reachability reachability) {
if (reachability == QNetworkInformation::Reachability::Online) {
// Clear connection cache to force reconnection after network becomes available
// This fixes issues after system suspend/resume
clearConnectionCache();
clearAccessCache();
}
});
}
}
}
QNetworkReply *NetworkAccessManager::createRequest(Operation op, const QNetworkRequest &network_request, QIODevice *outgoing_data) {

View File

@@ -353,8 +353,6 @@ 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.
@@ -402,8 +400,6 @@ Song::Private::Private(const Source source)
rating_(-1),
bpm_(-1),
id3v2_version_(0),
init_from_file_(false),
suspicious_tags_(false)
@@ -514,8 +510,6 @@ 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_; }
@@ -630,8 +624,6 @@ 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; }
@@ -841,10 +833,6 @@ 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,8 +234,6 @@ 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();
@@ -351,8 +349,6 @@ 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);
@@ -443,8 +439,6 @@ 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,7 +81,6 @@
#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"
@@ -105,29 +104,14 @@
using std::make_shared;
using namespace Qt::Literals::StringLiterals;
#ifdef __clang__
# pragma clang diagnostic push
# pragma clang diagnostic ignored "-Wunused-const-variable"
#endif
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)");
const char EditTagDialog::kArtDifferentHintText[] = QT_TR_NOOP("Different art across multiple songs.");
#ifdef __clang_
# pragma clang diagnostic pop
#endif
EditTagDialog::EditTagDialog(const SharedPtr<NetworkAccessManager> network,
const SharedPtr<TagReaderClient> tagreader_client,
const SharedPtr<CollectionBackend> collection_backend,
@@ -724,9 +708,6 @@ 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();
@@ -788,15 +769,6 @@ 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;
@@ -868,23 +840,6 @@ 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) {
@@ -1416,13 +1371,6 @@ 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;
@@ -1436,7 +1384,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, tag_id3v2_version);
TagReaderReplyPtr reply = tagreader_client_->WriteFileAsync(ref.current_.url().toLocalFile(), ref.current_, save_tags_options, save_tag_cover_data);
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,47 +650,6 @@
</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

@@ -57,7 +57,6 @@
#include "core/enginemetadata.h"
#include "constants/timeconstants.h"
#include "enginebase.h"
#include "gsturl.h"
#include "gstengine.h"
#include "gstenginepipeline.h"
#include "gstbufferconsumer.h"
@@ -180,18 +179,15 @@ EngineBase::State GstEngine::state() const {
void GstEngine::StartPreloading(const QUrl &media_url, const QUrl &stream_url, const bool force_stop_at_end, const qint64 beginning_offset_nanosec, const qint64 end_offset_nanosec) {
const GstUrl gst_url = FixupUrl(stream_url);
const QByteArray gst_url = FixupUrl(stream_url);
// No crossfading, so we can just queue the new URL in the existing pipeline and get gapless playback (hopefully)
if (current_pipeline_) {
if (!gst_url.source_device.isEmpty()) {
current_pipeline_->SetSourceDevice(gst_url.source_device);
}
current_pipeline_->PrepareNextUrl(media_url, stream_url, gst_url.url, beginning_offset_nanosec, force_stop_at_end ? end_offset_nanosec : 0);
current_pipeline_->PrepareNextUrl(media_url, stream_url, gst_url, beginning_offset_nanosec, force_stop_at_end ? end_offset_nanosec : 0);
// Add request to discover the stream
if (discoverer_ && media_url.scheme() != u"spotify"_s) {
if (!gst_discoverer_discover_uri_async(discoverer_, gst_url.url.constData())) {
qLog(Error) << "Failed to start stream discovery for" << gst_url.url;
if (!gst_discoverer_discover_uri_async(discoverer_, gst_url.constData())) {
qLog(Error) << "Failed to start stream discovery for" << gst_url;
}
}
}
@@ -202,7 +198,7 @@ bool GstEngine::Load(const QUrl &media_url, const QUrl &stream_url, const Engine
EngineBase::Load(media_url, stream_url, change, force_stop_at_end, beginning_offset_nanosec, end_offset_nanosec, ebur128_integrated_loudness_lufs);
const GstUrl gst_url = FixupUrl(stream_url);
const QByteArray gst_url = FixupUrl(stream_url);
bool crossfade = current_pipeline_ && ((crossfade_enabled_ && change & EngineBase::TrackChangeType::Manual) || (autocrossfade_enabled_ && change & EngineBase::TrackChangeType::Auto) || ((crossfade_enabled_ || autocrossfade_enabled_) && change & EngineBase::TrackChangeType::Intro));
@@ -219,14 +215,9 @@ bool GstEngine::Load(const QUrl &media_url, const QUrl &stream_url, const Engine
}
}
GstEnginePipelinePtr pipeline = CreatePipeline(media_url, stream_url, gst_url.url, static_cast<qint64>(beginning_offset_nanosec), force_stop_at_end ? end_offset_nanosec : 0, ebur128_loudness_normalizing_gain_db_);
GstEnginePipelinePtr pipeline = CreatePipeline(media_url, stream_url, gst_url, static_cast<qint64>(beginning_offset_nanosec), force_stop_at_end ? end_offset_nanosec : 0, ebur128_loudness_normalizing_gain_db_);
if (!pipeline) return false;
// Set the source device if one was extracted from the URL
if (!gst_url.source_device.isEmpty()) {
pipeline->SetSourceDevice(gst_url.source_device);
}
GstEnginePipelinePtr old_pipeline = current_pipeline_;
current_pipeline_ = pipeline;
@@ -262,8 +253,8 @@ bool GstEngine::Load(const QUrl &media_url, const QUrl &stream_url, const Engine
// Add request to discover the stream
if (discoverer_ && media_url.scheme() != u"spotify"_s) {
if (!gst_discoverer_discover_uri_async(discoverer_, gst_url.url.constData())) {
qLog(Error) << "Failed to start stream discovery for" << gst_url.url;
if (!gst_discoverer_discover_uri_async(discoverer_, gst_url.constData())) {
qLog(Error) << "Failed to start stream discovery for" << gst_url;
}
}
@@ -823,16 +814,16 @@ void GstEngine::BufferingFinished() {
}
GstUrl GstEngine::FixupUrl(const QUrl &url) {
QByteArray GstEngine::FixupUrl(const QUrl &url) {
GstUrl gst_url;
QByteArray uri;
// It's a file:// url with a hostname set.
// QUrl::fromLocalFile does this when given a \\host\share\file path on Windows.
// Munge it back into a path that gstreamer will recognise.
if (url.isLocalFile() && !url.host().isEmpty()) {
QString str = "file:////"_L1 + url.host() + url.path();
gst_url.url = str.toUtf8();
uri = str.toUtf8();
}
else if (url.scheme() == "cdda"_L1) {
QString str;
@@ -846,15 +837,16 @@ GstUrl GstEngine::FixupUrl(const QUrl &url) {
// We keep the device in mind, and we will set it later using SourceSetupCallback
QStringList path = url.path().split(u'/');
str = QStringLiteral("cdda://%1").arg(path.takeLast());
gst_url.source_device = path.join(u'/');
QString device = path.join(u'/');
if (current_pipeline_) current_pipeline_->SetSourceDevice(device);
}
gst_url.url = str.toUtf8();
uri = str.toUtf8();
}
else {
gst_url.url = url.toEncoded();
uri = url.toEncoded();
}
return gst_url;
return uri;
}

View File

@@ -41,7 +41,6 @@
#include "includes/shared_ptr.h"
#include "enginebase.h"
#include "gsturl.h"
#include "gstenginepipeline.h"
#include "gstbufferconsumer.h"
@@ -124,7 +123,7 @@ class GstEngine : public EngineBase, public GstBufferConsumer {
void PipelineFinished(const int pipeline_id);
private:
GstUrl FixupUrl(const QUrl &url);
QByteArray FixupUrl(const QUrl &url);
void StartFadeout(GstEnginePipelinePtr pipeline);
void StartFadeoutPause();

View File

@@ -26,7 +26,6 @@
#include <cstdint>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <glib.h>
#include <glib-object.h>
@@ -43,7 +42,6 @@
#include <QObject>
#include <QCoreApplication>
#include <QtConcurrentRun>
#include <QThreadPool>
#include <QFuture>
#include <QFutureWatcher>
#include <QMutex>
@@ -92,9 +90,6 @@ constexpr std::chrono::milliseconds kFaderTimeoutMsec = 3000ms;
constexpr int kEqBandCount = 10;
constexpr int kEqBandFrequencies[] = { 60, 170, 310, 600, 1000, 3000, 6000, 12000, 14000, 16000 };
// When within this many seconds of track end during gapless playback, ignore buffering messages
constexpr int kIgnoreBufferingNearEndSeconds = 5;
} // namespace
#ifdef __clang_
@@ -103,23 +98,6 @@ constexpr int kIgnoreBufferingNearEndSeconds = 5;
int GstEnginePipeline::sId = 1;
QThreadPool *GstEnginePipeline::shared_state_threadpool() {
// C++11 guarantees thread-safe initialization of static local variables
static QThreadPool pool;
static const auto init = []() {
// Limit the number of threads to prevent resource exhaustion
// Use 2 threads max since state changes are typically sequential per pipeline
pool.setMaxThreadCount(2);
return true;
}();
Q_UNUSED(init);
return &pool;
}
GstEnginePipeline::GstEnginePipeline(QObject *parent)
: QObject(parent),
id_(sId++),
@@ -219,23 +197,6 @@ GstEnginePipeline::~GstEnginePipeline() {
if (pipeline_) {
// Wait for any ongoing state changes for this pipeline to complete before setting to NULL.
// This prevents race conditions with async state transitions.
{
// Copy futures to local list to avoid holding mutex during waitForFinished()
QList<QFuture<GstStateChangeReturn>> futures_to_wait;
{
QMutexLocker locker(&mutex_pending_state_changes_);
futures_to_wait = pending_state_changes_;
pending_state_changes_.clear();
}
// Wait for all pending futures to complete
for (QFuture<GstStateChangeReturn> &future : futures_to_wait) {
future.waitForFinished();
}
}
gst_element_set_state(pipeline_, GST_STATE_NULL);
GstElement *audiobin = nullptr;
@@ -1403,13 +1364,6 @@ void GstEnginePipeline::AboutToFinishCallback(GstPlayBin *playbin, gpointer self
GstEnginePipeline *instance = reinterpret_cast<GstEnginePipeline*>(self);
// Ignore about-to-finish if we're in the process of tearing down the pipeline
// This prevents race conditions in GStreamer's decodebin3 when rapidly switching tracks
// See: https://gitlab.freedesktop.org/gstreamer/gstreamer/-/issues/4626
if (instance->finish_requested_.value()) {
return;
}
{
QMutexLocker l(&instance->mutex_url_);
qLog(Debug) << "Stream from URL" << instance->gst_url_ << "about to finish.";
@@ -1786,18 +1740,6 @@ void GstEnginePipeline::BufferingMessageReceived(GstMessage *msg) {
const GstState current_state = state();
if (percent < 100 && !buffering_.value()) {
// If we're near the end of the track and about-to-finish has been signaled, ignore buffering messages to prevent getting stuck in buffering state.
// This can happen with local files where spurious buffering messages appear near the end while the next track is being prepared for gapless playback.
if (about_to_finish_.value()) {
const qint64 current_position = position();
const qint64 track_length = length();
// Ignore buffering if we're within kIgnoreBufferingNearEndSeconds of the end
if (track_length > 0 && current_position > 0 && (track_length - current_position) < kIgnoreBufferingNearEndSeconds * kNsecPerSec) {
qLog(Debug) << "Ignoring buffering message near end of track (position:" << current_position << "length:" << track_length << ")";
return;
}
}
qLog(Debug) << "Buffering started";
buffering_ = true;
Q_EMIT BufferingStarted();
@@ -1899,15 +1841,9 @@ QFuture<GstStateChangeReturn> GstEnginePipeline::SetState(const GstState state)
watcher->deleteLater();
SetStateFinishedSlot(state, state_change_return);
});
QFuture<GstStateChangeReturn> future = QtConcurrent::run(shared_state_threadpool(), &gst_element_set_state, pipeline_, state);
QFuture<GstStateChangeReturn> future = QtConcurrent::run(&set_state_threadpool_, &gst_element_set_state, pipeline_, state);
watcher->setFuture(future);
// Track this future so destructor can wait for it
{
QMutexLocker locker(&mutex_pending_state_changes_);
pending_state_changes_.append(future);
}
return future;
}
@@ -1917,12 +1853,6 @@ void GstEnginePipeline::SetStateFinishedSlot(const GstState state, const GstStat
last_set_state_in_progress_ = GST_STATE_VOID_PENDING;
--set_state_in_progress_;
// Remove finished futures from tracking list to prevent unbounded growth
{
QMutexLocker locker(&mutex_pending_state_changes_);
pending_state_changes_.erase(std::remove_if(pending_state_changes_.begin(), pending_state_changes_.end(), [](const QFuture<GstStateChangeReturn> &f) { return f.isFinished(); }), pending_state_changes_.end());
}
switch (state_change_return) {
case GST_STATE_CHANGE_SUCCESS:
case GST_STATE_CHANGE_ASYNC:

View File

@@ -215,8 +215,7 @@ class GstEnginePipeline : public QObject {
static int sId;
mutex_protected<int> id_;
// Shared thread pool for all pipeline state changes to prevent thread/FD exhaustion
static QThreadPool *shared_state_threadpool();
QThreadPool set_state_threadpool_;
bool playbin3_support_;
bool volume_full_range_support_;
@@ -385,10 +384,6 @@ class GstEnginePipeline : public QObject {
mutex_protected<GstState> last_set_state_in_progress_;
mutex_protected<GstState> last_set_state_async_in_progress_;
// Track futures for this pipeline's state changes to allow waiting for them in destructor
QList<QFuture<GstStateChangeReturn>> pending_state_changes_;
QMutex mutex_pending_state_changes_;
};
using GstEnginePipelinePtr = QSharedPointer<GstEnginePipeline>;

View File

@@ -1,32 +0,0 @@
/*
* 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 GSTURL_H
#define GSTURL_H
#include <QByteArray>
#include <QString>
class GstUrl {
public:
QByteArray url;
QString source_device;
};
#endif // GSTURL_H

View File

@@ -1205,7 +1205,7 @@ void Playlist::InsertItemsWithoutUndo(const PlaylistItemPtrList &items, const in
queue_->InsertFirst(indexes);
}
if (auto_sort_ && !is_loading_) {
if (auto_sort_) {
sort(static_cast<int>(sort_column_), sort_order_);
}

View File

@@ -22,7 +22,6 @@
#include "config.h"
#include <utility>
#include <algorithm>
#include <QWidget>
#include <QVariant>
@@ -34,12 +33,9 @@
#include <QAction>
#include <QActionGroup>
#include <QToolButton>
#include <QScreen>
#include <QGuiApplication>
#include "core/iconloader.h"
#include "core/settingsprovider.h"
#include "utilities/screenutils.h"
#include "playlistsequence.h"
#include "ui_playlistsequence.h"
@@ -47,14 +43,7 @@ using namespace Qt::Literals::StringLiterals;
namespace {
constexpr char kSettingsGroup[] = "PlaylistSequence";
// Base icon size for reference resolution (1920x1080 at 1.0 scale)
constexpr int kBaseIconSize = 20;
constexpr int kReferenceWidth = 1920;
constexpr int kReferenceHeight = 1080;
constexpr qreal kReferenceDevicePixelRatio = 1.0;
} // namespace
}
PlaylistSequence::PlaylistSequence(QWidget *parent, SettingsProvider *settings)
: QWidget(parent),
@@ -71,11 +60,9 @@ PlaylistSequence::PlaylistSequence(QWidget *parent, SettingsProvider *settings)
// Icons
ui_->repeat->setIcon(AddDesaturatedIcon(IconLoader::Load(u"media-playlist-repeat"_s)));
ui_->shuffle->setIcon(AddDesaturatedIcon(IconLoader::Load(u"media-playlist-shuffle"_s)));
// Calculate icon size dynamically based on screen resolution, aspect ratio, and scaling
const int icon_size = CalculateIconSize();
ui_->repeat->setIconSize(QSize(icon_size, icon_size));
ui_->shuffle->setIconSize(QSize(icon_size, icon_size));
const int base_icon_size = static_cast<int>(fontMetrics().height() * 1.2);
ui_->repeat->setIconSize(QSize(base_icon_size, base_icon_size));
ui_->shuffle->setIconSize(QSize(base_icon_size, base_icon_size));
// Remove arrow indicators
ui_->repeat->setStyleSheet(u"QToolButton::menu-indicator { image: none; }"_s);
@@ -112,48 +99,6 @@ PlaylistSequence::~PlaylistSequence() {
delete ui_;
}
int PlaylistSequence::CalculateIconSize() {
// Get screen information for the widget
QScreen *screen = Utilities::GetScreen(this);
if (!screen) {
screen = QGuiApplication::primaryScreen();
}
if (!screen) {
// Fallback to a reasonable default if no screen is available
return kBaseIconSize;
}
// Get screen properties
const QSize screen_size = screen->size();
const qreal device_pixel_ratio = screen->devicePixelRatio();
const int screen_width = screen_size.width();
const int screen_height = screen_size.height();
// Calculate scaling factors based on resolution
// Use the smaller dimension to handle both landscape and portrait orientations
const int min_dimension = std::min(screen_width, screen_height);
const int ref_min_dimension = std::min(kReferenceWidth, kReferenceHeight);
const qreal resolution_factor = static_cast<qreal>(min_dimension) / static_cast<qreal>(ref_min_dimension);
// Apply device pixel ratio (for high-DPI displays)
const qreal dpi_factor = device_pixel_ratio / kReferenceDevicePixelRatio;
// Calculate final icon size with combined scaling
// Formula: 50% from resolution scaling + 50% from DPI scaling + 50% base multiplier
// The 0.5 base ensures icons scale up appropriately across different displays
// Without it, icons would be too small on average displays
const qreal combined_factor = (resolution_factor * 0.5) + (dpi_factor * 0.5) + 0.5;
int calculated_size = static_cast<int>(kBaseIconSize * combined_factor);
// Clamp to reasonable bounds (minimum 16px, maximum 48px)
calculated_size = std::max(16, std::min(48, calculated_size));
return calculated_size;
}
void PlaylistSequence::Load() {
loading_ = true; // Stops these setter functions calling Save()

View File

@@ -83,7 +83,6 @@ class PlaylistSequence : public QWidget {
private:
void Load();
void Save();
int CalculateIconSize();
static QIcon AddDesaturatedIcon(const QIcon &icon);
static QPixmap DesaturatedPixmap(const QPixmap &pixmap);

View File

@@ -1,277 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2019-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/>.
*
*/
#include "config.h"
#include <QByteArray>
#include <QMap>
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QRegularExpression>
#include <QRegularExpressionMatch>
#include <QRegularExpressionMatchIterator>
#include "includes/shared_ptr.h"
#include "core/logging.h"
#include "core/networkaccessmanager.h"
#include "qobuzcredentialfetcher.h"
using namespace Qt::Literals::StringLiterals;
namespace {
constexpr char kLoginPageUrl[] = "https://play.qobuz.com/login";
constexpr char kPlayQobuzUrl[] = "https://play.qobuz.com";
} // namespace
QobuzCredentialFetcher::QobuzCredentialFetcher(const SharedPtr<NetworkAccessManager> network, QObject *parent)
: QObject(parent),
network_(network),
login_page_reply_(nullptr),
bundle_reply_(nullptr) {}
void QobuzCredentialFetcher::FetchCredentials() {
qLog(Debug) << "Qobuz: Fetching credentials from web player";
QNetworkRequest request(QUrl(QString::fromLatin1(kLoginPageUrl)));
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
request.setHeader(QNetworkRequest::UserAgentHeader, u"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"_s);
login_page_reply_ = network_->get(request);
QObject::connect(login_page_reply_, &QNetworkReply::finished, this, &QobuzCredentialFetcher::LoginPageReceived);
}
void QobuzCredentialFetcher::LoginPageReceived() {
if (!login_page_reply_) return;
QNetworkReply *reply = login_page_reply_;
login_page_reply_ = nullptr;
if (reply->error() != QNetworkReply::NoError) {
QString error = QStringLiteral("Failed to fetch login page: %1").arg(reply->errorString());
qLog(Error) << "Qobuz:" << error;
reply->deleteLater();
Q_EMIT CredentialsFetchError(error);
return;
}
const QString login_page = QString::fromUtf8(reply->readAll());
reply->deleteLater();
// Extract bundle.js URL from the login page
// Pattern: <script src="(/resources/\d+\.\d+\.\d+-[a-z]\d{3}/bundle\.js)"></script>
static const QRegularExpression bundle_url_regex(u"<script src=\"(/resources/[\\d.]+-[a-z]\\d+/bundle\\.js)\"></script>"_s);
const QRegularExpressionMatch bundle_match = bundle_url_regex.match(login_page);
if (!bundle_match.hasMatch()) {
QString error = u"Failed to find bundle.js URL in login page"_s;
qLog(Error) << "Qobuz:" << error;
Q_EMIT CredentialsFetchError(error);
return;
}
bundle_url_ = bundle_match.captured(1);
qLog(Debug) << "Qobuz: Found bundle URL:" << bundle_url_;
// Fetch the bundle.js
QNetworkRequest request(QUrl(QString::fromLatin1(kPlayQobuzUrl) + bundle_url_));
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
request.setHeader(QNetworkRequest::UserAgentHeader, u"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"_s);
bundle_reply_ = network_->get(request);
QObject::connect(bundle_reply_, &QNetworkReply::finished, this, &QobuzCredentialFetcher::BundleReceived);
}
void QobuzCredentialFetcher::BundleReceived() {
if (!bundle_reply_) return;
QNetworkReply *reply = bundle_reply_;
bundle_reply_ = nullptr;
if (reply->error() != QNetworkReply::NoError) {
QString error = QStringLiteral("Failed to fetch bundle.js: %1").arg(reply->errorString());
qLog(Error) << "Qobuz:" << error;
reply->deleteLater();
Q_EMIT CredentialsFetchError(error);
return;
}
const QString bundle = QString::fromUtf8(reply->readAll());
reply->deleteLater();
qLog(Debug) << "Qobuz: Bundle size:" << bundle.length();
const QString app_id = ExtractAppId(bundle);
if (app_id.isEmpty()) {
QString error = u"Failed to extract app_id from bundle"_s;
qLog(Error) << "Qobuz:" << error;
Q_EMIT CredentialsFetchError(error);
return;
}
const QString app_secret = ExtractAppSecret(bundle);
if (app_secret.isEmpty()) {
QString error = u"Failed to extract app_secret from bundle"_s;
qLog(Error) << "Qobuz:" << error;
Q_EMIT CredentialsFetchError(error);
return;
}
qLog(Debug) << "Qobuz: Successfully extracted credentials - app_id:" << app_id;
Q_EMIT CredentialsFetched(app_id, app_secret);
}
QString QobuzCredentialFetcher::ExtractAppId(const QString &bundle) {
// Pattern: production:{api:{appId:"(\d+)"
static const QRegularExpression app_id_regex(u"production:\\{api:\\{appId:\"(\\d+)\""_s);
const QRegularExpressionMatch app_id_match = app_id_regex.match(bundle);
if (app_id_match.hasMatch()) {
return app_id_match.captured(1);
}
return QString();
}
QString QobuzCredentialFetcher::ExtractAppSecret(const QString &bundle) {
// The plain-text appSecret in the bundle doesn't work for API requests.
// We need to use the Spoofbuz method to extract the real secrets:
// 1. Find seed/timezone pairs
// 2. Find info/extras for each timezone
// 3. Combine seed + info + extras, remove last 44 chars, base64 decode
// Pattern to find seed and timezone: [a-z].initialSeed("seed",window.utimezone.timezone)
static const QRegularExpression seed_regex(u"[a-z]\\.initialSeed\\(\"([\\w=]+)\",window\\.utimezone\\.([a-z]+)\\)"_s);
QMap<QString, QString> seeds; // timezone -> seed
QRegularExpressionMatchIterator seed_iter = seed_regex.globalMatch(bundle);
while (seed_iter.hasNext()) {
const QRegularExpressionMatch seed_match = seed_iter.next();
const QString seed = seed_match.captured(1);
const QString tz = seed_match.captured(2);
seeds[tz] = seed;
qLog(Debug) << "Qobuz: Found seed for timezone" << tz;
}
if (seeds.isEmpty()) {
qLog(Error) << "Qobuz: No seed/timezone pairs found in bundle";
return QString();
}
// Try each timezone - Berlin was confirmed working
const QStringList preferred_order = {u"berlin"_s, u"london"_s, u"abidjan"_s};
for (const QString &tz : preferred_order) {
if (!seeds.contains(tz)) {
continue;
}
// Pattern to find info and extras for this timezone
// name:"xxx/Berlin",info:"...",extras:"..."
const QString capitalized_tz = tz.at(0).toUpper() + tz.mid(1);
const QString info_pattern = QStringLiteral("name:\"\\w+/%1\",info:\"([\\w=]+)\",extras:\"([\\w=]+)\"").arg(capitalized_tz);
const QRegularExpression info_regex(info_pattern);
const QRegularExpressionMatch info_match = info_regex.match(bundle);
if (!info_match.hasMatch()) {
qLog(Debug) << "Qobuz: No info/extras found for timezone" << tz;
continue;
}
const QString seed = seeds[tz];
const QString info = info_match.captured(1);
const QString extras = info_match.captured(2);
qLog(Debug) << "Qobuz: Decoding secret for timezone" << tz;
// Combine seed + info + extras
const QString combined = seed + info + extras;
// Remove last 44 characters
if (combined.length() <= 44) {
qLog(Debug) << "Qobuz: Combined string too short for timezone" << tz;
continue;
}
const QString trimmed = combined.left(combined.length() - 44);
// Base64 decode
const QByteArray decoded = QByteArray::fromBase64(trimmed.toLatin1());
const QString secret = QString::fromLatin1(decoded);
// Validate: should be 32 hex characters
static const QRegularExpression hex_regex(u"^[a-f0-9]{32}$"_s);
if (hex_regex.match(secret).hasMatch()) {
qLog(Debug) << "Qobuz: Successfully decoded secret from timezone" << tz;
return secret;
}
qLog(Debug) << "Qobuz: Decoded secret invalid for timezone" << tz;
}
// Try any remaining timezones not in preferred order
for (auto it = seeds.constBegin(); it != seeds.constEnd(); ++it) {
const QString &tz = it.key();
if (preferred_order.contains(tz)) {
continue; // Already tried
}
const QString capitalized_tz = tz.at(0).toUpper() + tz.mid(1);
const QString info_pattern = QStringLiteral("name:\"\\w+/%1\",info:\"([\\w=]+)\",extras:\"([\\w=]+)\"").arg(capitalized_tz);
const QRegularExpression info_regex(info_pattern);
const QRegularExpressionMatch info_match = info_regex.match(bundle);
if (!info_match.hasMatch()) {
continue;
}
const QString seed = it.value();
const QString info = info_match.captured(1);
const QString extras = info_match.captured(2);
const QString combined = seed + info + extras;
if (combined.length() <= 44) {
continue;
}
const QString trimmed = combined.left(combined.length() - 44);
const QByteArray decoded = QByteArray::fromBase64(trimmed.toLatin1());
const QString secret = QString::fromLatin1(decoded);
static const QRegularExpression hex_regex(u"^[a-f0-9]{32}$"_s);
if (hex_regex.match(secret).hasMatch()) {
qLog(Debug) << "Qobuz: Successfully decoded secret from timezone" << tz;
return secret;
}
}
qLog(Error) << "Qobuz: Failed to decode any valid app_secret from bundle";
return QString();
}

View File

@@ -1,59 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2019-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 QOBUZCREDENTIALFETCHER_H
#define QOBUZCREDENTIALFETCHER_H
#include "config.h"
#include <QObject>
#include <QString>
#include "includes/shared_ptr.h"
class QNetworkReply;
class NetworkAccessManager;
class QobuzCredentialFetcher : public QObject {
Q_OBJECT
public:
explicit QobuzCredentialFetcher(const SharedPtr<NetworkAccessManager> network, QObject *parent = nullptr);
void FetchCredentials();
Q_SIGNALS:
void CredentialsFetched(const QString &app_id, const QString &app_secret);
void CredentialsFetchError(const QString &error);
private Q_SLOTS:
void LoginPageReceived();
void BundleReceived();
private:
QString ExtractAppId(const QString &bundle);
QString ExtractAppSecret(const QString &bundle);
const SharedPtr<NetworkAccessManager> network_;
QNetworkReply *login_page_reply_;
QNetworkReply *bundle_reply_;
QString bundle_url_;
};
#endif // QOBUZCREDENTIALFETCHER_H

View File

@@ -310,10 +310,6 @@ void QobuzService::HandleLoginSSLErrors(const QList<QSslError> &ssl_errors) {
void QobuzService::HandleAuthReply(QNetworkReply *reply) {
if (replies_.contains(reply)) {
replies_.removeAll(reply);
}
QObject::disconnect(reply, nullptr, this, nullptr);
reply->deleteLater();
login_sent_ = false;

View File

@@ -106,8 +106,6 @@ class QobuzService : public StreamingService {
bool login_sent() const { return login_sent_; }
bool login_attempts() const { return login_attempts_; }
SharedPtr<NetworkAccessManager> network() const { return network_; }
uint GetStreamURL(const QUrl &url, QString &error);
SharedPtr<CollectionBackend> artists_collection_backend() override { return artists_collection_backend_; }

View File

@@ -115,7 +115,6 @@ void QobuzStreamURLRequest::GetStreamURL() {
const quint64 timestamp = static_cast<quint64>(QDateTime::currentSecsSinceEpoch());
ParamList params_to_sign = ParamList() << Param(u"format_id"_s, QString::number(service_->format()))
<< Param(u"intent"_s, u"stream"_s)
<< Param(u"track_id"_s, QString::number(song_id_));
std::sort(params_to_sign.begin(), params_to_sign.end());

View File

@@ -38,7 +38,6 @@
#include "core/settings.h"
#include "widgets/loginstatewidget.h"
#include "qobuz/qobuzservice.h"
#include "qobuz/qobuzcredentialfetcher.h"
#include "constants/qobuzsettings.h"
using namespace Qt::Literals::StringLiterals;
@@ -47,15 +46,13 @@ using namespace QobuzSettings;
QobuzSettingsPage::QobuzSettingsPage(SettingsDialog *dialog, const SharedPtr<QobuzService> service, QWidget *parent)
: SettingsPage(dialog, parent),
ui_(new Ui::QobuzSettingsPage),
service_(service),
credential_fetcher_(nullptr) {
service_(service) {
ui_->setupUi(this);
setWindowIcon(IconLoader::Load(u"qobuz"_s, true, 0, 32));
QObject::connect(ui_->button_login, &QPushButton::clicked, this, &QobuzSettingsPage::LoginClicked);
QObject::connect(ui_->login_state, &LoginStateWidget::LogoutClicked, this, &QobuzSettingsPage::LogoutClicked);
QObject::connect(ui_->button_fetch_credentials, &QPushButton::clicked, this, &QobuzSettingsPage::FetchCredentialsClicked);
QObject::connect(this, &QobuzSettingsPage::Login, &*service_, &StreamingService::LoginWithCredentials);
@@ -189,40 +186,3 @@ void QobuzSettingsPage::LoginFailure(const QString &failure_reason) {
QMessageBox::warning(this, tr("Authentication failed"), failure_reason);
}
void QobuzSettingsPage::FetchCredentialsClicked() {
ui_->button_fetch_credentials->setEnabled(false);
ui_->button_fetch_credentials->setText(tr("Fetching..."));
if (!credential_fetcher_) {
credential_fetcher_ = new QobuzCredentialFetcher(service_->network(), this);
QObject::connect(credential_fetcher_, &QobuzCredentialFetcher::CredentialsFetched, this, &QobuzSettingsPage::CredentialsFetched);
QObject::connect(credential_fetcher_, &QobuzCredentialFetcher::CredentialsFetchError, this, &QobuzSettingsPage::CredentialsFetchError);
}
credential_fetcher_->FetchCredentials();
}
void QobuzSettingsPage::CredentialsFetched(const QString &app_id, const QString &app_secret) {
ui_->app_id->setText(app_id);
ui_->app_secret->setText(app_secret);
ui_->checkbox_base64_secret->setChecked(false);
ui_->button_fetch_credentials->setEnabled(true);
ui_->button_fetch_credentials->setText(tr("Fetch Credentials"));
QMessageBox::information(this, tr("Credentials fetched"), tr("App ID and secret have been successfully fetched from the Qobuz web player."));
}
void QobuzSettingsPage::CredentialsFetchError(const QString &error) {
ui_->button_fetch_credentials->setEnabled(true);
ui_->button_fetch_credentials->setText(tr("Fetch Credentials"));
QMessageBox::warning(this, tr("Credential fetch failed"), error);
}

View File

@@ -30,7 +30,6 @@ class QShowEvent;
class QEvent;
class SettingsDialog;
class QobuzService;
class QobuzCredentialFetcher;
class Ui_QobuzSettingsPage;
class QobuzSettingsPage : public SettingsPage {
@@ -56,14 +55,10 @@ class QobuzSettingsPage : public SettingsPage {
void LogoutClicked();
void LoginSuccess();
void LoginFailure(const QString &failure_reason);
void FetchCredentialsClicked();
void CredentialsFetched(const QString &app_id, const QString &app_secret);
void CredentialsFetchError(const QString &error);
private:
Ui_QobuzSettingsPage *ui_;
const SharedPtr<QobuzService> service_;
QobuzCredentialFetcher *credential_fetcher_;
};
#endif // QOBUZSETTINGSPAGE_H

View File

@@ -115,16 +115,6 @@
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QPushButton" name="button_fetch_credentials">
<property name="text">
<string>Fetch Credentials</string>
</property>
<property name="toolTip">
<string>Automatically fetch app ID and secret from Qobuz web player</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View File

@@ -134,10 +134,13 @@ void SmartPlaylistsModel::Init() {
// Append the new ones
s.beginWriteArray(collection_backend_->songs_table(), playlist_index + unwritten_defaults);
WriteDefaultsToSettings(&s, version, playlist_index);
for (; version < default_smart_playlists_.count(); ++version) {
const GeneratorList generators = default_smart_playlists_.value(version);
for (PlaylistGeneratorPtr gen : generators) {
SaveGenerator(&s, playlist_index++, gen);
}
}
s.endArray();
version = default_smart_playlists_.count();
}
s.setValue(collection_backend_->songs_table() + u"_version"_s, version);
@@ -266,46 +269,6 @@ PlaylistGeneratorPtr SmartPlaylistsModel::CreateGenerator(const QModelIndex &idx
}
void SmartPlaylistsModel::WriteDefaultsToSettings(Settings *s, const int start_version, const int start_index) {
int playlist_index = start_index;
for (int version = start_version; version < default_smart_playlists_.count(); ++version) {
const GeneratorList generators = default_smart_playlists_.value(version);
for (PlaylistGeneratorPtr gen : generators) {
SaveGenerator(s, playlist_index++, gen);
}
}
}
void SmartPlaylistsModel::RestoreDefaults() {
root_->ClearNotify();
Settings s;
s.beginGroup(kSettingsGroup);
int total_defaults = 0;
for (const GeneratorList &generators : default_smart_playlists_) {
total_defaults += static_cast<int>(generators.count());
}
s.beginWriteArray(collection_backend_->songs_table(), total_defaults);
WriteDefaultsToSettings(&s, 0, 0);
s.endArray();
s.setValue(collection_backend_->songs_table() + u"_version"_s, default_smart_playlists_.count());
const int count = s.beginReadArray(collection_backend_->songs_table());
for (int i = 0; i < count; ++i) {
s.setArrayIndex(i);
ItemFromSmartPlaylist(s, true);
}
s.endArray();
s.endGroup();
}
QVariant SmartPlaylistsModel::data(const QModelIndex &idx, const int role) const {
if (!idx.isValid()) return QVariant();

View File

@@ -66,7 +66,6 @@ class SmartPlaylistsModel : public SimpleTreeModel<SmartPlaylistsItem> {
void AddGenerator(PlaylistGeneratorPtr gen);
void UpdateGenerator(const QModelIndex &idx, PlaylistGeneratorPtr gen);
void DeleteGenerator(const QModelIndex &idx);
void RestoreDefaults();
private:
QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
@@ -80,7 +79,6 @@ class SmartPlaylistsModel : public SimpleTreeModel<SmartPlaylistsItem> {
static void SaveGenerator(Settings *s, const int i, PlaylistGeneratorPtr generator);
void ItemFromSmartPlaylist(const Settings &s, const bool notify);
void WriteDefaultsToSettings(Settings *s, const int start_version, const int start_index);
private:
SharedPtr<CollectionBackend> collection_backend_;

View File

@@ -23,7 +23,6 @@
#include <QMenu>
#include <QSettings>
#include <QShowEvent>
#include <QMessageBox>
#include "core/iconloader.h"
#include "core/mimedata.h"
@@ -61,7 +60,6 @@ SmartPlaylistsViewContainer::SmartPlaylistsViewContainer(const SharedPtr<Player>
action_new_smart_playlist_(nullptr),
action_edit_smart_playlist_(nullptr),
action_delete_smart_playlist_(nullptr),
action_restore_defaults_(nullptr),
action_append_to_playlist_(nullptr),
action_replace_current_playlist_(nullptr),
action_open_in_new_playlist_(nullptr),
@@ -76,7 +74,6 @@ SmartPlaylistsViewContainer::SmartPlaylistsViewContainer(const SharedPtr<Player>
model_->Init();
action_new_smart_playlist_ = context_menu_->addAction(IconLoader::Load(u"document-new"_s), tr("New smart playlist..."), this, &SmartPlaylistsViewContainer::NewSmartPlaylist);
action_restore_defaults_ = context_menu_->addAction(IconLoader::Load(u"view-refresh"_s), tr("Restore defaults"), this, &SmartPlaylistsViewContainer::RestoreDefaultsFromContext);
action_append_to_playlist_ = context_menu_selected_->addAction(IconLoader::Load(u"media-playback-start"_s), tr("Append to current playlist"), this, &SmartPlaylistsViewContainer::AppendToPlaylist);
action_replace_current_playlist_ = context_menu_selected_->addAction(IconLoader::Load(u"media-playback-start"_s), tr("Replace current playlist"), this, &SmartPlaylistsViewContainer::ReplaceCurrentPlaylist);
@@ -93,16 +90,13 @@ SmartPlaylistsViewContainer::SmartPlaylistsViewContainer(const SharedPtr<Player>
action_delete_smart_playlist_ = context_menu_selected_->addAction(IconLoader::Load(u"edit-delete"_s), tr("Delete smart playlist"), this, &SmartPlaylistsViewContainer::DeleteSmartPlaylistFromContext);
context_menu_selected_->addSeparator();
context_menu_selected_->addAction(action_restore_defaults_);
ui_->new_->setDefaultAction(action_new_smart_playlist_);
ui_->edit_->setIcon(IconLoader::Load(u"edit-rename"_s));
ui_->delete_->setIcon(IconLoader::Load(u"edit-delete"_s));
ui_->restore_->setIcon(IconLoader::Load(u"view-refresh"_s));
QObject::connect(ui_->edit_, &QToolButton::clicked, this, &SmartPlaylistsViewContainer::EditSmartPlaylistFromButton);
QObject::connect(ui_->delete_, &QToolButton::clicked, this, &SmartPlaylistsViewContainer::DeleteSmartPlaylistFromButton);
QObject::connect(ui_->restore_, &QToolButton::clicked, this, &SmartPlaylistsViewContainer::RestoreDefaults);
QObject::connect(ui_->view, &SmartPlaylistsView::ItemsSelectedChanged, this, &SmartPlaylistsViewContainer::ItemsSelectedChanged);
QObject::connect(ui_->view, &SmartPlaylistsView::doubleClicked, this, &SmartPlaylistsViewContainer::ItemDoubleClicked);
@@ -136,7 +130,6 @@ void SmartPlaylistsViewContainer::ReloadSettings() {
ui_->new_->setIconSize(QSize(iconsize, iconsize));
ui_->delete_->setIconSize(QSize(iconsize, iconsize));
ui_->edit_->setIconSize(QSize(iconsize, iconsize));
ui_->restore_->setIconSize(QSize(iconsize, iconsize));
}
@@ -311,18 +304,3 @@ void SmartPlaylistsViewContainer::ItemDoubleClicked(const QModelIndex &idx) {
Q_EMIT AddToPlaylist(q_mimedata);
}
void SmartPlaylistsViewContainer::RestoreDefaultsFromContext() {
RestoreDefaults();
}
void SmartPlaylistsViewContainer::RestoreDefaults() {
const QMessageBox::StandardButton messagebox_answer = QMessageBox::question(this, tr("Restore defaults"), tr("Are you sure you want to restore the default smart playlists? This will remove all custom smart playlists"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
if (messagebox_answer == QMessageBox::Yes) {
model_->RestoreDefaults();
}
}

View File

@@ -83,13 +83,11 @@ class SmartPlaylistsViewContainer : public QWidget {
void EditSmartPlaylist(const QModelIndex &idx);
void DeleteSmartPlaylist(const QModelIndex &idx);
void RestoreDefaults();
void EditSmartPlaylistFromButton();
void DeleteSmartPlaylistFromButton();
void EditSmartPlaylistFromContext();
void DeleteSmartPlaylistFromContext();
void RestoreDefaultsFromContext();
void NewSmartPlaylistFinished();
void EditSmartPlaylistFinished();
@@ -115,7 +113,6 @@ class SmartPlaylistsViewContainer : public QWidget {
QAction *action_new_smart_playlist_;
QAction *action_edit_smart_playlist_;
QAction *action_delete_smart_playlist_;
QAction *action_restore_defaults_;
QAction *action_append_to_playlist_;
QAction *action_replace_current_playlist_;
QAction *action_open_in_new_playlist_;

View File

@@ -95,19 +95,6 @@
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="restore_">
<property name="toolTip">
<string>Restore defaults</string>
</property>
<property name="iconSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="spacer_buttons">
<property name="orientation">

View File

@@ -1,29 +0,0 @@
/*
* 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,7 +32,6 @@
#include "savetagsoptions.h"
#include "savetagcoverdata.h"
#include "albumcovertagdata.h"
#include "tagid3v2version.h"
class TagReaderBase {
public:
@@ -46,7 +45,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 TagID3v2Version id3v2_version) const = 0;
virtual TagReaderResult WriteFile(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options, const SaveTagCoverData &save_tag_cover_data) 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,7 +50,6 @@
#include "tagreaderreadstreamreply.h"
#include "tagreaderloadcoverdatareply.h"
#include "tagreaderloadcoverimagereply.h"
#include "tagid3v2version.h"
using std::dynamic_pointer_cast;
using namespace Qt::Literals::StringLiterals;
@@ -190,7 +189,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, write_file_request->tag_id3v2_version);
result = WriteFileBlocking(write_file_request->filename, write_file_request->song, write_file_request->save_tags_options, write_file_request->save_tag_cover_data);
}
else if (TagReaderLoadCoverDataRequestPtr load_cover_data_request = dynamic_pointer_cast<TagReaderLoadCoverDataRequest>(request)) {
QByteArray cover_data;
@@ -304,13 +303,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, const TagID3v2Version tag_id3v2_version) {
TagReaderResult TagReaderClient::WriteFileBlocking(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options, const SaveTagCoverData &save_tag_cover_data) {
return tagreader_.WriteFile(filename, song, save_tags_options, save_tag_cover_data, tag_id3v2_version);
return tagreader_.WriteFile(filename, song, save_tags_options, 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) {
TagReaderReplyPtr TagReaderClient::WriteFileAsync(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options, const SaveTagCoverData &save_tag_cover_data) {
Q_ASSERT(QThread::currentThread() != thread());
@@ -322,7 +321,6 @@ 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,7 +43,6 @@
#include "tagreaderloadcoverimagereply.h"
#include "savetagsoptions.h"
#include "savetagcoverdata.h"
#include "tagid3v2version.h"
class QThread;
class Song;
@@ -73,8 +72,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(), 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 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 LoadCoverDataBlocking(const QString &filename, QByteArray &data);
TagReaderResult LoadCoverImageBlocking(const QString &filename, QImage &image);

View File

@@ -32,7 +32,6 @@
#include "core/logging.h"
#include "tagreaderbase.h"
#include "tagreadertaglib.h"
#include "tagid3v2version.h"
using namespace Qt::Literals::StringLiterals;
@@ -318,13 +317,12 @@ 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 TagID3v2Version id3v2_version) const {
TagReaderResult TagReaderGME::WriteFile(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options, const SaveTagCoverData &save_tag_cover_data) 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,7 +25,6 @@
#include <QFileInfo>
#include "tagreaderbase.h"
#include "tagid3v2version.h"
namespace GME {
bool IsSupportedFormat(const QFileInfo &fileinfo);
@@ -108,7 +107,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 TagID3v2Version id3v2_version) const override;
TagReaderResult WriteFile(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options, const SaveTagCoverData &save_tag_cover_data) 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,7 +46,6 @@
#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>
@@ -95,6 +94,8 @@
#include <QStringList>
#include <QFile>
#include <QFileInfo>
#include <QDir>
#include <QTemporaryFile>
#include <QUrl>
#include <QDateTime>
#include <QtDebug>
@@ -105,7 +106,6 @@
#include "constants/timeconstants.h"
#include "albumcovertagdata.h"
#include "tagid3v2version.h"
using std::make_unique;
using namespace Qt::Literals::StringLiterals;
@@ -296,6 +296,100 @@ TagReaderTagLib::~TagReaderTagLib() {
delete factory_;
}
bool TagReaderTagLib::SaveFileWithFallback(const QString &filename, const std::function<bool(TagLib::FileRef*)> &save_function) const {
// First, try the normal save operation directly on the file
{
ScopedPtr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
if (!fileref || fileref->isNull()) {
qLog(Error) << "TagLib could not open file for saving" << filename;
return false;
}
// Apply modifications using the callback
if (!save_function(fileref.get())) {
qLog(Error) << "Failed to apply modifications to file" << filename;
return false;
}
// Try direct save first
if (fileref->save()) {
qLog(Debug) << "Successfully saved file directly" << filename;
return true;
}
}
qLog(Warning) << "Direct save failed, trying atomic write workaround for" << filename;
// If direct save fails (common on GVFS mounts), use atomic write workaround:
// 1. Copy file to temporary location in the same directory
// 2. Re-apply modifications and save to the temporary file
// 3. Replace original file with the temporary file
const QFileInfo file_info(filename);
const QString temp_pattern = file_info.dir().absoluteFilePath(file_info.fileName() + u".XXXXXX"_s);
QTemporaryFile temp_file(temp_pattern);
temp_file.setAutoRemove(false); // We'll handle removal manually
if (!temp_file.open()) {
qLog(Error) << "Could not create temporary file for atomic write:" << temp_file.fileName();
return false;
}
const QString temp_filename = temp_file.fileName();
temp_file.close();
// Copy original file to temporary location
if (!QFile::copy(filename, temp_filename)) {
qLog(Error) << "Could not copy file to temporary location:" << filename << "->" << temp_filename;
QFile::remove(temp_filename);
return false;
}
// Try to modify and save the temporary file
{
ScopedPtr<TagLib::FileRef> temp_fileref(factory_->GetFileRef(temp_filename));
if (!temp_fileref || temp_fileref->isNull()) {
qLog(Error) << "TagLib could not open temporary file" << temp_filename;
QFile::remove(temp_filename);
return false;
}
// Apply modifications using the callback
if (!save_function(temp_fileref.get())) {
qLog(Error) << "Failed to apply modifications to temporary file" << temp_filename;
QFile::remove(temp_filename);
return false;
}
// Save the temporary file
if (!temp_fileref->save()) {
qLog(Error) << "Failed to save temporary file" << temp_filename;
QFile::remove(temp_filename);
return false;
}
}
// Replace the original file with the temporary file
// First remove the original, then rename temp to original
if (!QFile::remove(filename)) {
qLog(Error) << "Could not remove original file for replacement:" << filename;
QFile::remove(temp_filename);
return false;
}
if (!QFile::rename(temp_filename, filename)) {
qLog(Error) << "Could not rename temporary file to original:" << temp_filename << "->" << filename;
// This is a critical error - original file was removed but rename failed
return false;
}
qLog(Debug) << "Successfully saved file using atomic write workaround" << filename;
return true;
}
TagReaderResult TagReaderTagLib::IsMediaFile(const QString &filename) const {
qLog(Debug) << "Checking for valid file" << filename;
@@ -606,14 +700,8 @@ 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());
@@ -1050,7 +1138,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 TagID3v2Version tag_id3v2_version) const {
TagReaderResult TagReaderTagLib::WriteFile(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options, const SaveTagCoverData &save_tag_cover_data) const {
if (filename.isEmpty()) {
return TagReaderResult::ErrorCode::FilenameMissing;
@@ -1087,175 +1175,26 @@ TagReaderResult TagReaderTagLib::WriteFile(const QString &filename, const Song &
cover = LoadAlbumCoverTagData(filename, save_tag_cover_data);
}
ScopedPtr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
if (!fileref || fileref->isNull()) {
qLog(Error) << "TagLib could not open file" << filename;
return TagReaderResult::ErrorCode::FileOpenError;
}
if (save_tags) {
fileref->tag()->setTitle(song.title().isEmpty() ? TagLib::String() : QStringToTagLibString(song.title()));
fileref->tag()->setArtist(song.artist().isEmpty() ? TagLib::String() : QStringToTagLibString(song.artist()));
fileref->tag()->setAlbum(song.album().isEmpty() ? TagLib::String() : QStringToTagLibString(song.album()));
fileref->tag()->setGenre(song.genre().isEmpty() ? TagLib::String() : QStringToTagLibString(song.genre()));
fileref->tag()->setComment(song.comment().isEmpty() ? TagLib::String() : QStringToTagLibString(song.comment()));
fileref->tag()->setYear(song.year() <= 0 ? 0 : static_cast<uint>(song.year()));
fileref->tag()->setTrack(song.track() <= 0 ? 0 : static_cast<uint>(song.track()));
}
bool is_flac = false;
if (TagLib::FLAC::File *file_flac = dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
is_flac = true;
TagLib::Ogg::XiphComment *vorbis_comment = file_flac->xiphComment(true);
if (vorbis_comment) {
if (save_tags) {
SetVorbisComments(vorbis_comment, song);
}
if (save_playcount) {
SetPlaycount(vorbis_comment, song.playcount());
}
if (save_rating) {
SetRating(vorbis_comment, song.rating());
}
if (save_cover) {
SetEmbeddedCover(file_flac, vorbis_comment, cover.data, cover.mimetype);
}
// Lambda function that applies all tag modifications to a FileRef
auto apply_modifications = [&](TagLib::FileRef *fileref) -> bool {
if (!fileref || fileref->isNull()) {
return false;
}
}
else if (TagLib::WavPack::File *file_wavpack = dynamic_cast<TagLib::WavPack::File*>(fileref->file())) {
TagLib::APE::Tag *tag = file_wavpack->APETag(true);
if (tag) {
if (save_tags) {
SetAPETag(tag, song);
}
if (save_playcount) {
SetPlaycount(tag, song.playcount());
}
if (save_rating) {
SetRating(tag, song.rating());
}
if (save_tags) {
fileref->tag()->setTitle(song.title().isEmpty() ? TagLib::String() : QStringToTagLibString(song.title()));
fileref->tag()->setArtist(song.artist().isEmpty() ? TagLib::String() : QStringToTagLibString(song.artist()));
fileref->tag()->setAlbum(song.album().isEmpty() ? TagLib::String() : QStringToTagLibString(song.album()));
fileref->tag()->setGenre(song.genre().isEmpty() ? TagLib::String() : QStringToTagLibString(song.genre()));
fileref->tag()->setComment(song.comment().isEmpty() ? TagLib::String() : QStringToTagLibString(song.comment()));
fileref->tag()->setYear(song.year() <= 0 ? 0 : static_cast<uint>(song.year()));
fileref->tag()->setTrack(song.track() <= 0 ? 0 : static_cast<uint>(song.track()));
}
}
else if (TagLib::APE::File *file_ape = dynamic_cast<TagLib::APE::File*>(fileref->file())) {
TagLib::APE::Tag *tag = file_ape->APETag(true);
if (tag) {
if (save_tags) {
SetAPETag(tag, song);
}
if (save_playcount) {
SetPlaycount(tag, song.playcount());
}
if (save_rating) {
SetRating(tag, song.rating());
}
}
}
else if (TagLib::MPC::File *file_mpc = dynamic_cast<TagLib::MPC::File*>(fileref->file())) {
TagLib::APE::Tag *tag = file_mpc->APETag(true);
if (tag) {
if (save_tags) {
SetAPETag(tag, song);
}
if (save_playcount) {
SetPlaycount(tag, song.playcount());
}
if (save_rating) {
SetRating(tag, song.rating());
}
}
}
else if (TagLib::MPEG::File *file_mpeg = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
TagLib::ID3v2::Tag *tag = file_mpeg->ID3v2Tag(true);
if (tag) {
if (save_tags) {
SetID3v2Tag(tag, song);
}
if (save_playcount) {
SetPlaycount(tag, song.playcount());
}
if (save_rating) {
SetRating(tag, song.rating());
}
if (save_cover) {
SetEmbeddedCover(tag, cover.data, cover.mimetype);
}
}
}
else if (TagLib::MP4::File *file_mp4 = dynamic_cast<TagLib::MP4::File*>(fileref->file())) {
TagLib::MP4::Tag *tag = file_mp4->tag();
if (tag) {
if (save_tags) {
tag->setItem(kMP4_Disc, TagLib::MP4::Item(song.disc() <= 0 - 1 ? 0 : song.disc(), 0));
tag->setItem(kMP4_Composer, TagLib::StringList(QStringToTagLibString(song.composer())));
tag->setItem(kMP4_Grouping, TagLib::StringList(QStringToTagLibString(song.grouping())));
tag->setItem(kMP4_Lyrics, TagLib::StringList(QStringToTagLibString(song.lyrics())));
tag->setItem(kMP4_AlbumArtist, TagLib::StringList(QStringToTagLibString(song.albumartist())));
tag->setItem(kMP4_Compilation, TagLib::MP4::Item(song.compilation()));
}
if (save_playcount) {
SetPlaycount(tag, song.playcount());
}
if (save_rating) {
SetRating(tag, song.rating());
}
if (save_cover) {
SetEmbeddedCover(file_mp4, tag, cover.data, cover.mimetype);
}
}
}
else if (TagLib::RIFF::WAV::File *file_wav = dynamic_cast<TagLib::RIFF::WAV::File*>(fileref->file())) {
TagLib::ID3v2::Tag *tag = file_wav->ID3v2Tag();
if (tag) {
if (save_tags) {
SetID3v2Tag(tag, song);
}
if (save_playcount) {
SetPlaycount(tag, song.playcount());
}
if (save_rating) {
SetRating(tag, song.rating());
}
if (save_cover) {
SetEmbeddedCover(tag, cover.data, cover.mimetype);
}
}
}
else if (TagLib::RIFF::AIFF::File *file_aiff = dynamic_cast<TagLib::RIFF::AIFF::File*>(fileref->file())) {
TagLib::ID3v2::Tag *tag = file_aiff->tag();
if (tag) {
if (save_tags) {
SetID3v2Tag(tag, song);
}
if (save_playcount) {
SetPlaycount(tag, song.playcount());
}
if (save_rating) {
SetRating(tag, song.rating());
}
if (save_cover) {
SetEmbeddedCover(tag, cover.data, cover.mimetype);
}
}
}
else if (TagLib::ASF::File *file_asf = dynamic_cast<TagLib::ASF::File*>(fileref->file())) {
TagLib::ASF::Tag *tag = file_asf->tag();
if (tag) {
SetASFTag(tag, song);
}
}
// Handle all the files which have VorbisComments (Ogg, OPUS, ...) in the same way;
// apart, so we keep specific behavior for some formats by adding another "else if" block above.
if (!is_flac) {
if (TagLib::Ogg::XiphComment *vorbis_comment = dynamic_cast<TagLib::Ogg::XiphComment*>(fileref->file()->tag())) {
bool is_flac = false;
if (TagLib::FLAC::File *file_flac = dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
is_flac = true;
TagLib::Ogg::XiphComment *vorbis_comment = file_flac->xiphComment(true);
if (vorbis_comment) {
if (save_tags) {
SetVorbisComments(vorbis_comment, song);
@@ -1267,39 +1206,166 @@ TagReaderResult TagReaderTagLib::WriteFile(const QString &filename, const Song &
SetRating(vorbis_comment, song.rating());
}
if (save_cover) {
SetEmbeddedCover(vorbis_comment, cover.data, cover.mimetype);
SetEmbeddedCover(file_flac, vorbis_comment, cover.data, cover.mimetype);
}
}
}
}
// 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;
}
else if (TagLib::WavPack::File *file_wavpack = dynamic_cast<TagLib::WavPack::File*>(fileref->file())) {
TagLib::APE::Tag *tag = file_wavpack->APETag(true);
if (tag) {
if (save_tags) {
SetAPETag(tag, song);
}
if (save_playcount) {
SetPlaycount(tag, song.playcount());
}
if (save_rating) {
SetRating(tag, song.rating());
}
}
}
bool success = false;
else if (TagLib::APE::File *file_ape = dynamic_cast<TagLib::APE::File*>(fileref->file())) {
TagLib::APE::Tag *tag = file_ape->APETag(true);
if (tag) {
if (save_tags) {
SetAPETag(tag, song);
}
if (save_playcount) {
SetPlaycount(tag, song.playcount());
}
if (save_rating) {
SetRating(tag, song.rating());
}
}
}
// 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();
}
else if (TagLib::MPC::File *file_mpc = dynamic_cast<TagLib::MPC::File*>(fileref->file())) {
TagLib::APE::Tag *tag = file_mpc->APETag(true);
if (tag) {
if (save_tags) {
SetAPETag(tag, song);
}
if (save_playcount) {
SetPlaycount(tag, song.playcount());
}
if (save_rating) {
SetRating(tag, song.rating());
}
}
}
else if (TagLib::MPEG::File *file_mpeg = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
TagLib::ID3v2::Tag *tag = file_mpeg->ID3v2Tag(true);
if (tag) {
if (save_tags) {
SetID3v2Tag(tag, song);
}
if (save_playcount) {
SetPlaycount(tag, song.playcount());
}
if (save_rating) {
SetRating(tag, song.rating());
}
if (save_cover) {
SetEmbeddedCover(tag, cover.data, cover.mimetype);
}
}
}
else if (TagLib::MP4::File *file_mp4 = dynamic_cast<TagLib::MP4::File*>(fileref->file())) {
TagLib::MP4::Tag *tag = file_mp4->tag();
if (tag) {
if (save_tags) {
tag->setItem(kMP4_Disc, TagLib::MP4::Item(song.disc() <= 0 - 1 ? 0 : song.disc(), 0));
tag->setItem(kMP4_Composer, TagLib::StringList(QStringToTagLibString(song.composer())));
tag->setItem(kMP4_Grouping, TagLib::StringList(QStringToTagLibString(song.grouping())));
tag->setItem(kMP4_Lyrics, TagLib::StringList(QStringToTagLibString(song.lyrics())));
tag->setItem(kMP4_AlbumArtist, TagLib::StringList(QStringToTagLibString(song.albumartist())));
tag->setItem(kMP4_Compilation, TagLib::MP4::Item(song.compilation()));
}
if (save_playcount) {
SetPlaycount(tag, song.playcount());
}
if (save_rating) {
SetRating(tag, song.rating());
}
if (save_cover) {
SetEmbeddedCover(file_mp4, tag, cover.data, cover.mimetype);
}
}
}
else if (TagLib::RIFF::WAV::File *file_wav = dynamic_cast<TagLib::RIFF::WAV::File*>(fileref->file())) {
TagLib::ID3v2::Tag *tag = file_wav->ID3v2Tag();
if (tag) {
if (save_tags) {
SetID3v2Tag(tag, song);
}
if (save_playcount) {
SetPlaycount(tag, song.playcount());
}
if (save_rating) {
SetRating(tag, song.rating());
}
if (save_cover) {
SetEmbeddedCover(tag, cover.data, cover.mimetype);
}
}
}
else if (TagLib::RIFF::AIFF::File *file_aiff = dynamic_cast<TagLib::RIFF::AIFF::File*>(fileref->file())) {
TagLib::ID3v2::Tag *tag = file_aiff->tag();
if (tag) {
if (save_tags) {
SetID3v2Tag(tag, song);
}
if (save_playcount) {
SetPlaycount(tag, song.playcount());
}
if (save_rating) {
SetRating(tag, song.rating());
}
if (save_cover) {
SetEmbeddedCover(tag, cover.data, cover.mimetype);
}
}
}
else if (TagLib::ASF::File *file_asf = dynamic_cast<TagLib::ASF::File*>(fileref->file())) {
TagLib::ASF::Tag *tag = file_asf->tag();
if (tag) {
SetASFTag(tag, song);
}
}
// Handle all the files which have VorbisComments (Ogg, OPUS, ...) in the same way;
// apart, so we keep specific behavior for some formats by adding another "else if" block above.
if (!is_flac) {
if (TagLib::Ogg::XiphComment *vorbis_comment = dynamic_cast<TagLib::Ogg::XiphComment*>(fileref->file()->tag())) {
if (vorbis_comment) {
if (save_tags) {
SetVorbisComments(vorbis_comment, song);
}
if (save_playcount) {
SetPlaycount(vorbis_comment, song.playcount());
}
if (save_rating) {
SetRating(vorbis_comment, song.rating());
}
if (save_cover) {
SetEmbeddedCover(vorbis_comment, cover.data, cover.mimetype);
}
}
}
}
return true;
};
// Use SaveFileWithFallback which tries direct save first, then atomic write workaround
const bool success = SaveFileWithFallback(filename, apply_modifications);
#ifdef Q_OS_LINUX
if (success) {
@@ -1753,64 +1819,69 @@ TagReaderResult TagReaderTagLib::SaveEmbeddedCover(const QString &filename, cons
return TagReaderResult::ErrorCode::FileDoesNotExist;
}
ScopedPtr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
if (!fileref || fileref->isNull()) {
qLog(Error) << "TagLib could not open file" << filename;
return TagReaderResult::ErrorCode::FileOpenError;
}
const AlbumCoverTagData cover = LoadAlbumCoverTagData(filename, save_tag_cover_data);
// FLAC
if (TagLib::FLAC::File *file_flac = dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
TagLib::Ogg::XiphComment *vorbis_comment = file_flac->xiphComment(true);
if (vorbis_comment) {
SetEmbeddedCover(file_flac, vorbis_comment, cover.data, cover.mimetype);
// Lambda function that applies cover modifications to a FileRef
auto apply_modifications = [&](TagLib::FileRef *fileref) -> bool {
if (!fileref || fileref->isNull()) {
return false;
}
}
// Ogg Vorbis / Opus / Speex
else if (TagLib::Ogg::XiphComment *vorbis_comment = dynamic_cast<TagLib::Ogg::XiphComment*>(fileref->file()->tag())) {
SetEmbeddedCover(vorbis_comment, cover.data, cover.mimetype);
}
// MP3
else if (TagLib::MPEG::File *file_mp3 = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
TagLib::ID3v2::Tag *tag = file_mp3->ID3v2Tag();
if (tag) {
SetEmbeddedCover(tag, cover.data, cover.mimetype);
// FLAC
if (TagLib::FLAC::File *file_flac = dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
TagLib::Ogg::XiphComment *vorbis_comment = file_flac->xiphComment(true);
if (vorbis_comment) {
SetEmbeddedCover(file_flac, vorbis_comment, cover.data, cover.mimetype);
}
}
}
// MP4/AAC
else if (TagLib::MP4::File *file_aac = dynamic_cast<TagLib::MP4::File*>(fileref->file())) {
TagLib::MP4::Tag *tag = file_aac->tag();
if (tag) {
SetEmbeddedCover(file_aac, tag, cover.data, cover.mimetype);
// Ogg Vorbis / Opus / Speex
else if (TagLib::Ogg::XiphComment *vorbis_comment = dynamic_cast<TagLib::Ogg::XiphComment*>(fileref->file()->tag())) {
SetEmbeddedCover(vorbis_comment, cover.data, cover.mimetype);
}
}
// WAV
else if (TagLib::RIFF::WAV::File *file_wav = dynamic_cast<TagLib::RIFF::WAV::File*>(fileref->file())) {
if (file_wav->ID3v2Tag()) {
SetEmbeddedCover(file_wav->ID3v2Tag(), cover.data, cover.mimetype);
// MP3
else if (TagLib::MPEG::File *file_mp3 = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
TagLib::ID3v2::Tag *tag = file_mp3->ID3v2Tag();
if (tag) {
SetEmbeddedCover(tag, cover.data, cover.mimetype);
}
}
}
// AIFF
else if (TagLib::RIFF::AIFF::File *file_aiff = dynamic_cast<TagLib::RIFF::AIFF::File*>(fileref->file())) {
if (file_aiff->tag()) {
SetEmbeddedCover(file_aiff->tag(), cover.data, cover.mimetype);
// MP4/AAC
else if (TagLib::MP4::File *file_aac = dynamic_cast<TagLib::MP4::File*>(fileref->file())) {
TagLib::MP4::Tag *tag = file_aac->tag();
if (tag) {
SetEmbeddedCover(file_aac, tag, cover.data, cover.mimetype);
}
}
}
// Not supported.
else {
qLog(Error) << "Saving embedded art is not supported for %1" << filename;
return TagReaderResult::ErrorCode::Unsupported;
}
// WAV
else if (TagLib::RIFF::WAV::File *file_wav = dynamic_cast<TagLib::RIFF::WAV::File*>(fileref->file())) {
if (file_wav->ID3v2Tag()) {
SetEmbeddedCover(file_wav->ID3v2Tag(), cover.data, cover.mimetype);
}
}
// AIFF
else if (TagLib::RIFF::AIFF::File *file_aiff = dynamic_cast<TagLib::RIFF::AIFF::File*>(fileref->file())) {
if (file_aiff->tag()) {
SetEmbeddedCover(file_aiff->tag(), cover.data, cover.mimetype);
}
}
// Not supported.
else {
qLog(Error) << "Saving embedded art is not supported for %1" << filename;
return false;
}
return true;
};
// Use SaveFileWithFallback which tries direct save first, then atomic write workaround
const bool success = SaveFileWithFallback(filename, apply_modifications);
const bool success = fileref->file()->save();
#ifdef Q_OS_LINUX
if (success) {
// Linux: inotify doesn't seem to notice the change to the file unless we change the timestamps as well. (this is what touch does)
@@ -1907,64 +1978,69 @@ TagReaderResult TagReaderTagLib::SaveSongPlaycount(const QString &filename, cons
return TagReaderResult::ErrorCode::FileDoesNotExist;
}
ScopedPtr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
if (!fileref || fileref->isNull()) {
qLog(Error) << "TagLib could not open file" << filename;
return TagReaderResult::ErrorCode::FileOpenError;
}
// Lambda function that applies playcount modifications to a FileRef
auto apply_modifications = [&](TagLib::FileRef *fileref) -> bool {
if (!fileref || fileref->isNull()) {
return false;
}
if (TagLib::FLAC::File *flac_file = dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
TagLib::Ogg::XiphComment *vorbis_comment = flac_file->xiphComment(true);
if (vorbis_comment) {
SetPlaycount(vorbis_comment, playcount);
if (TagLib::FLAC::File *flac_file = dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
TagLib::Ogg::XiphComment *vorbis_comment = flac_file->xiphComment(true);
if (vorbis_comment) {
SetPlaycount(vorbis_comment, playcount);
}
}
}
else if (TagLib::WavPack::File *wavpack_file = dynamic_cast<TagLib::WavPack::File*>(fileref->file())) {
TagLib::APE::Tag *tag = wavpack_file->APETag(true);
if (tag) {
SetPlaycount(tag, playcount);
else if (TagLib::WavPack::File *wavpack_file = dynamic_cast<TagLib::WavPack::File*>(fileref->file())) {
TagLib::APE::Tag *tag = wavpack_file->APETag(true);
if (tag) {
SetPlaycount(tag, playcount);
}
}
}
else if (TagLib::APE::File *ape_file = dynamic_cast<TagLib::APE::File*>(fileref->file())) {
TagLib::APE::Tag *tag = ape_file->APETag(true);
if (tag) {
SetPlaycount(tag, playcount);
else if (TagLib::APE::File *ape_file = dynamic_cast<TagLib::APE::File*>(fileref->file())) {
TagLib::APE::Tag *tag = ape_file->APETag(true);
if (tag) {
SetPlaycount(tag, playcount);
}
}
}
else if (TagLib::Ogg::XiphComment *vorbis_comment = dynamic_cast<TagLib::Ogg::XiphComment*>(fileref->file()->tag())) {
if (vorbis_comment) {
SetPlaycount(vorbis_comment, playcount);
else if (TagLib::Ogg::XiphComment *vorbis_comment = dynamic_cast<TagLib::Ogg::XiphComment*>(fileref->file()->tag())) {
if (vorbis_comment) {
SetPlaycount(vorbis_comment, playcount);
}
}
}
else if (TagLib::MPEG::File *mpeg_file = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
TagLib::ID3v2::Tag *tag = mpeg_file->ID3v2Tag(true);
if (tag) {
SetPlaycount(tag, playcount);
else if (TagLib::MPEG::File *mpeg_file = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
TagLib::ID3v2::Tag *tag = mpeg_file->ID3v2Tag(true);
if (tag) {
SetPlaycount(tag, playcount);
}
}
}
else if (TagLib::MP4::File *mp4_file = dynamic_cast<TagLib::MP4::File*>(fileref->file())) {
TagLib::MP4::Tag *tag = mp4_file->tag();
if (tag) {
SetPlaycount(tag, playcount);
else if (TagLib::MP4::File *mp4_file = dynamic_cast<TagLib::MP4::File*>(fileref->file())) {
TagLib::MP4::Tag *tag = mp4_file->tag();
if (tag) {
SetPlaycount(tag, playcount);
}
}
}
else if (TagLib::MPC::File *mpc_file = dynamic_cast<TagLib::MPC::File*>(fileref->file())) {
TagLib::APE::Tag *tag = mpc_file->APETag(true);
if (tag) {
SetPlaycount(tag, playcount);
else if (TagLib::MPC::File *mpc_file = dynamic_cast<TagLib::MPC::File*>(fileref->file())) {
TagLib::APE::Tag *tag = mpc_file->APETag(true);
if (tag) {
SetPlaycount(tag, playcount);
}
}
}
else if (TagLib::ASF::File *asf_file = dynamic_cast<TagLib::ASF::File*>(fileref->file())) {
TagLib::ASF::Tag *tag = asf_file->tag();
if (tag && playcount > 0) {
tag->addAttribute(kASF_FMPS_Playcount, TagLib::ASF::Attribute(QStringToTagLibString(QString::number(playcount))));
else if (TagLib::ASF::File *asf_file = dynamic_cast<TagLib::ASF::File*>(fileref->file())) {
TagLib::ASF::Tag *tag = asf_file->tag();
if (tag && playcount > 0) {
tag->addAttribute(kASF_FMPS_Playcount, TagLib::ASF::Attribute(QStringToTagLibString(QString::number(playcount))));
}
}
else {
return false;
}
}
else {
return TagReaderResult::ErrorCode::Unsupported;
}
const bool success = fileref->save();
return true;
};
// Use SaveFileWithFallback which tries direct save first, then atomic write workaround
const bool success = SaveFileWithFallback(filename, apply_modifications);
#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)
@@ -2037,63 +2113,68 @@ TagReaderResult TagReaderTagLib::SaveSongRating(const QString &filename, const f
return TagReaderResult::ErrorCode::Success;
}
ScopedPtr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
if (!fileref || fileref->isNull()) {
qLog(Error) << "TagLib could not open file" << filename;
return TagReaderResult::ErrorCode::FileOpenError;
}
// Lambda function that applies rating modifications to a FileRef
auto apply_modifications = [&](TagLib::FileRef *fileref) -> bool {
if (!fileref || fileref->isNull()) {
return false;
}
if (TagLib::FLAC::File *flac_file = dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
TagLib::Ogg::XiphComment *vorbis_comment = flac_file->xiphComment(true);
if (vorbis_comment) {
if (TagLib::FLAC::File *flac_file = dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
TagLib::Ogg::XiphComment *vorbis_comment = flac_file->xiphComment(true);
if (vorbis_comment) {
SetRating(vorbis_comment, rating);
}
}
else if (TagLib::WavPack::File *wavpack_file = dynamic_cast<TagLib::WavPack::File*>(fileref->file())) {
TagLib::APE::Tag *tag = wavpack_file->APETag(true);
if (tag) {
SetRating(tag, rating);
}
}
else if (TagLib::APE::File *ape_file = dynamic_cast<TagLib::APE::File*>(fileref->file())) {
TagLib::APE::Tag *tag = ape_file->APETag(true);
if (tag) {
SetRating(tag, rating);
}
}
else if (TagLib::Ogg::XiphComment *vorbis_comment = dynamic_cast<TagLib::Ogg::XiphComment*>(fileref->file()->tag())) {
SetRating(vorbis_comment, rating);
}
}
else if (TagLib::WavPack::File *wavpack_file = dynamic_cast<TagLib::WavPack::File*>(fileref->file())) {
TagLib::APE::Tag *tag = wavpack_file->APETag(true);
if (tag) {
SetRating(tag, rating);
else if (TagLib::MPEG::File *mpeg_file = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
TagLib::ID3v2::Tag *tag = mpeg_file->ID3v2Tag(true);
if (tag) {
SetRating(tag, rating);
}
}
}
else if (TagLib::APE::File *ape_file = dynamic_cast<TagLib::APE::File*>(fileref->file())) {
TagLib::APE::Tag *tag = ape_file->APETag(true);
if (tag) {
SetRating(tag, rating);
else if (TagLib::MP4::File *mp4_file = dynamic_cast<TagLib::MP4::File*>(fileref->file())) {
TagLib::MP4::Tag *tag = mp4_file->tag();
if (tag) {
SetRating(tag, rating);
}
}
}
else if (TagLib::Ogg::XiphComment *vorbis_comment = dynamic_cast<TagLib::Ogg::XiphComment*>(fileref->file()->tag())) {
SetRating(vorbis_comment, rating);
}
else if (TagLib::MPEG::File *mpeg_file = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
TagLib::ID3v2::Tag *tag = mpeg_file->ID3v2Tag(true);
if (tag) {
SetRating(tag, rating);
else if (TagLib::ASF::File *asf_file = dynamic_cast<TagLib::ASF::File*>(fileref->file())) {
TagLib::ASF::Tag *tag = asf_file->tag();
if (tag) {
SetRating(tag, rating);
}
}
}
else if (TagLib::MP4::File *mp4_file = dynamic_cast<TagLib::MP4::File*>(fileref->file())) {
TagLib::MP4::Tag *tag = mp4_file->tag();
if (tag) {
SetRating(tag, rating);
else if (TagLib::MPC::File *mpc_file = dynamic_cast<TagLib::MPC::File*>(fileref->file())) {
TagLib::APE::Tag *tag = mpc_file->APETag(true);
if (tag) {
SetRating(tag, rating);
}
}
}
else if (TagLib::ASF::File *asf_file = dynamic_cast<TagLib::ASF::File*>(fileref->file())) {
TagLib::ASF::Tag *tag = asf_file->tag();
if (tag) {
SetRating(tag, rating);
else {
qLog(Error) << "Unsupported file for saving rating for" << filename;
return false;
}
}
else if (TagLib::MPC::File *mpc_file = dynamic_cast<TagLib::MPC::File*>(fileref->file())) {
TagLib::APE::Tag *tag = mpc_file->APETag(true);
if (tag) {
SetRating(tag, rating);
}
}
else {
qLog(Error) << "Unsupported file for saving rating for" << filename;
return TagReaderResult::ErrorCode::Unsupported;
}
const bool success = fileref->save();
return true;
};
// Use SaveFileWithFallback which tries direct save first, then atomic write workaround
const bool success = SaveFileWithFallback(filename, apply_modifications);
#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

@@ -23,6 +23,8 @@
#include "config.h"
#include <functional>
#include <QByteArray>
#include <QString>
@@ -46,7 +48,6 @@
#include "tagreaderbase.h"
#include "savetagcoverdata.h"
#include "tagid3v2version.h"
#undef TStringToQString
#undef QStringToTString
@@ -73,7 +74,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 TagID3v2Version tag_id3v2_version) const override;
TagReaderResult WriteFile(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options, const SaveTagCoverData &save_tag_cover_data) const override;
TagReaderResult LoadEmbeddedCover(const QString &filename, QByteArray &data) const override;
TagReaderResult SaveEmbeddedCover(const QString &filename, const SaveTagCoverData &save_tag_cover_data) const override;
@@ -127,6 +128,8 @@ class TagReaderTagLib : public TagReaderBase {
static TagLib::String TagLibStringListToSlashSeparatedString(const TagLib::StringList &taglib_string_list, const uint begin_index = 0);
bool SaveFileWithFallback(const QString &filename, const std::function<bool(TagLib::FileRef*)> &save_function) const;
private:
FileRefFactory *factory_;

View File

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