Hello community, here is the log from the commit of package clementine for openSUSE:Factory checked in at 2019-03-28 22:48:39 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/clementine (Old) and /work/SRC/openSUSE:Factory/.clementine.new.25356 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "clementine" Thu Mar 28 22:48:39 2019 rev:48 rq:689094 version:1.3.1+git20190213 Changes: -------- --- /work/SRC/openSUSE:Factory/clementine/clementine.changes 2019-03-26 22:34:25.113676880 +0100 +++ /work/SRC/openSUSE:Factory/.clementine.new.25356/clementine.changes 2019-03-28 22:48:41.135054753 +0100 @@ -1,0 +2,15 @@ +Wed Mar 27 11:54:32 UTC 2019 - plater <davejpla...@gmail.com> + +- Added use_system_qxtglobalshortcut.patch to improve kde + integration. +_ Added cherrypicked patches to bring qt5 branch to master's state: + 0001-Improved-support-for-APEv2-tags.-6280.patch, + 0001-Prevent-UI-hang-during-device-scan.-6291.patch, + 0001-Fix-thread-safety-issues-when-initially-loading-devi.patch, + 0001-Handle-case-where-a-lister-adds-a-device-before-load.patch, + 0001-Fix-MoodbarPipeline-crash-on-gstreamer-error.patch, + 0001-Fix-potential-use-of-streamer-element-after-deletion.patch, + 0001-Free-decoder-bin-if-error-occurs-during-setup.patch and + 0001-Fix-several-gstreamer-object-leaks.patch. + +------------------------------------------------------------------- New: ---- 0001-Fix-MoodbarPipeline-crash-on-gstreamer-error.patch 0001-Fix-potential-use-of-streamer-element-after-deletion.patch 0001-Fix-several-gstreamer-object-leaks.patch 0001-Fix-thread-safety-issues-when-initially-loading-devi.patch 0001-Free-decoder-bin-if-error-occurs-during-setup.patch 0001-Handle-case-where-a-lister-adds-a-device-before-load.patch 0001-Improved-support-for-APEv2-tags.-6280.patch 0001-Prevent-UI-hang-during-device-scan.-6291.patch use_system_qxtglobalshortcut.patch ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ clementine.spec ++++++ --- /var/tmp/diff_new_pack.Io6t6p/_old 2019-03-28 22:48:43.339054354 +0100 +++ /var/tmp/diff_new_pack.Io6t6p/_new 2019-03-28 22:48:43.379054347 +0100 @@ -45,7 +45,17 @@ Patch2: clementine-moodbar-fpic.patch # PATCH-FIX-OPENSUSE clementine-hidden-systray-icon.patch davejpla...@gmail.com -- sys tray icon is hidden on some plasma5 systems. Patch4: clementine-hidden-systray-icon.patch -# Fix global shortcuts using Gnome (GSD) D-Bus backend +# PATCH-FEATURE-OPENSUSE +Patch6: use_system_qxtglobalshortcut.patch +#PATCH-FIX-GIT to 2019-03-26 +Patch7: 0001-Improved-support-for-APEv2-tags.-6280.patch +Patch8: 0001-Prevent-UI-hang-during-device-scan.-6291.patch +Patch9: 0001-Fix-thread-safety-issues-when-initially-loading-devi.patch +Patch10: 0001-Handle-case-where-a-lister-adds-a-device-before-load.patch +Patch11: 0001-Fix-MoodbarPipeline-crash-on-gstreamer-error.patch +Patch12: 0001-Fix-potential-use-of-streamer-element-after-deletion.patch +Patch13: 0001-Free-decoder-bin-if-error-occurs-during-setup.patch +Patch14: 0001-Fix-several-gstreamer-object-leaks.patch %if 0%{?suse_version} > 1325 BuildRequires: libboost_headers-devel @@ -77,6 +87,7 @@ BuildRequires: pkgconfig(Qt5X11Extras) BuildRequires: pkgconfig(Qt5Xml) BuildRequires: pkgconfig(libmygpo-qt5) +BuildRequires: pkgconfig(qxtglobalshortcut) #BuildRequires: pkgconfig(QxtCore-qt5) %else BuildRequires: liblastfm-devel @@ -145,9 +156,8 @@ %else %setup -q -n Clementine-%{rev} %endif -%patch1 -%patch2 -%patch4 +%autopatch -p1 + # NOTE: Build using system versions of libraries. rm -rvf 3rdparty/taglib rm -rvf 3rdparty/SPMediaKeyTap @@ -162,7 +172,7 @@ -DUSE_SYSTEM_PROJECTM=ON \ -DBUNDLE_PROJECTM_PRESETS=OFF \ %if %{with qt5} - -DUSE_SYSTEM_QXT=OFF \ + -DUSE_SYSTEM_QXT=ON \ %else -DUSE_SYSTEM_QXT=ON \ %endif ++++++ 0001-Fix-MoodbarPipeline-crash-on-gstreamer-error.patch ++++++ >From 55edcf5321051e44281f067a7e3ee44871982c12 Mon Sep 17 00:00:00 2001 From: Jim Broadus <jbroa...@gmail.com> Date: Sun, 10 Mar 2019 23:34:11 -0700 Subject: [PATCH] Fix MoodbarPipeline crash on gstreamer error. As reported in issue 6302, playing a stream that causes gstreamer to error at start can cause a crash. The problem occurs when the MoodbarPipeline receives a pad-added signal after it has handled an error callback. In the error callback, the builder_ is freed. In the pad-added handler (NewPadCallback), this object is accessed. This change adds a running_ flag that is set when the pipeline is started and cleared on an error, end of stream, or object destruction. We check this flag at the beginning of NewPadCallback. For sanity sake, we also check the builder_ pointer before dereferencing. Note that checking the state of the pipeline wasn't an option since the pipeline is in the process of changing states during the pad-added callback and gst_element_get_state wants to block during a state change. This solution is not complete as there are still some syncronization issues. With this specific situation, the error and new pad callbacks appear to always occur on the same thread, but that's probably not true for all error conditions. The object is also destroyed by a different thread, so it may be possible that a callback can occur at the wrong time during or after the deletion of the object. See https://github.com/clementine-player/Clementine/issues/6302 --- src/moodbar/moodbarpipeline.cpp | 17 +++++++++++++++-- src/moodbar/moodbarpipeline.h | 1 + 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/moodbar/moodbarpipeline.cpp b/src/moodbar/moodbarpipeline.cpp index 7163efda1..bddbf678c 100644 --- a/src/moodbar/moodbarpipeline.cpp +++ b/src/moodbar/moodbarpipeline.cpp @@ -37,7 +37,8 @@ MoodbarPipeline::MoodbarPipeline(const QUrl& local_filename) local_filename_(local_filename), pipeline_(nullptr), convert_element_(nullptr), - success_(false) {} + success_(false), + running_(false) {} MoodbarPipeline::~MoodbarPipeline() { Cleanup(); } @@ -117,6 +118,7 @@ void MoodbarPipeline::Start() { gst_object_unref(bus); // Start playing + running_ = true; gst_element_set_state(pipeline_, GST_STATE_PLAYING); } @@ -135,6 +137,12 @@ void MoodbarPipeline::ReportError(GstMessage* msg) { void MoodbarPipeline::NewPadCallback(GstElement*, GstPad* pad, gpointer data) { MoodbarPipeline* self = reinterpret_cast<MoodbarPipeline*>(data); + + if (!self->running_) { + qLog(Warning) << "Received gstreamer callback after pipeline has stopped."; + return; + } + GstPad* const audiopad = gst_element_get_static_pad(self->convert_element_, "sink"); @@ -152,7 +160,10 @@ void MoodbarPipeline::NewPadCallback(GstElement*, GstPad* pad, gpointer data) { gst_structure_get_int(structure, "rate", &rate); gst_caps_unref(caps); - self->builder_->Init(kBands, rate); + if (self->builder_ != nullptr) + self->builder_->Init(kBands, rate); + else + qLog(Error) << "Builder does not exist"; } GstBusSyncReply MoodbarPipeline::BusCallbackSync(GstBus*, GstMessage* msg, @@ -177,6 +188,7 @@ GstBusSyncReply MoodbarPipeline::BusCallbackSync(GstBus*, GstMessage* msg, void MoodbarPipeline::Stop(bool success) { success_ = success; + running_ = false; if (builder_ != nullptr) { data_ = builder_->Finish(1000); builder_.reset(); @@ -189,6 +201,7 @@ void MoodbarPipeline::Cleanup() { Q_ASSERT(QThread::currentThread() == thread()); Q_ASSERT(QThread::currentThread() != qApp->thread()); + running_ = false; if (pipeline_) { GstBus* bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline_)); gst_bus_set_sync_handler(bus, nullptr, nullptr, nullptr); diff --git a/src/moodbar/moodbarpipeline.h b/src/moodbar/moodbarpipeline.h index c7acad8e5..629781d64 100644 --- a/src/moodbar/moodbarpipeline.h +++ b/src/moodbar/moodbarpipeline.h @@ -71,6 +71,7 @@ class MoodbarPipeline : public QObject { std::unique_ptr<MoodbarBuilder> builder_; bool success_; + bool running_; QByteArray data_; }; -- 2.16.4 ++++++ 0001-Fix-potential-use-of-streamer-element-after-deletion.patch ++++++ >From 102c529f80c057d90d45c13e10771fcf2742e337 Mon Sep 17 00:00:00 2001 From: Jim Broadus <jbroa...@gmail.com> Date: Tue, 19 Mar 2019 18:47:19 -0700 Subject: [PATCH] Fix potential use of streamer element after deletion. If ReplaceDecodeBin fails from TransitionToNext, uridecodebin_ will not be replaced with a new element. Since TransitionToNext does not check the return value, it unknowingly deletes uridecodebin_. --- src/engines/gstenginepipeline.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) Index: Clementine-36cc5b82f4daf5c2d4e93dc8072665e5a3ca622b/src/engines/gstenginepipeline.cpp =================================================================== --- Clementine-36cc5b82f4daf5c2d4e93dc8072665e5a3ca622b.orig/src/engines/gstenginepipeline.cpp 2019-02-13 06:02:56.000000000 +0200 +++ Clementine-36cc5b82f4daf5c2d4e93dc8072665e5a3ca622b/src/engines/gstenginepipeline.cpp 2019-03-27 12:24:45.226455370 +0200 @@ -1011,7 +1011,10 @@ void GstEnginePipeline::TransitionToNext ignore_tags_ = true; - ReplaceDecodeBin(next_url_); + if (!ReplaceDecodeBin(next_url_)) { + qLog(Error) << "ReplaceDecodeBin failed with " << next_url_; + return; + } gst_element_set_state(uridecodebin_, GST_STATE_PLAYING); MaybeLinkDecodeToAudio(); ++++++ 0001-Fix-several-gstreamer-object-leaks.patch ++++++ >From 5c2ceb349019e98108c0c8a451646742bb76848a Mon Sep 17 00:00:00 2001 From: Jim Broadus <jbroa...@gmail.com> Date: Mon, 25 Mar 2019 22:37:15 -0700 Subject: [PATCH] Fix several gstreamer object leaks. There are a number of cases where gst_pipeline_get_bus, gst_element_get_static_pad, and g_object_get are called without releasing references. In addition to memory usage, some of these elements hold file descriptors. In normal operation, two file descriptors are leaked for each played track. The default fd ulimit for many linux distros is 1024. This is likely the cause of the crash reported in issue 6309. This change fixes the obvious and consistent leaks, but it's probably not a complete solution. There are many error and corner conditions that need to be examined. --- src/engines/gstenginepipeline.cpp | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/engines/gstenginepipeline.cpp b/src/engines/gstenginepipeline.cpp index 7f12c2628..17ddd6089 100644 --- a/src/engines/gstenginepipeline.cpp +++ b/src/engines/gstenginepipeline.cpp @@ -417,10 +417,13 @@ bool GstEnginePipeline::Init() { gst_element_link(probe_converter, probe_sink); // Link the outputs of tee to the queues on each path. - gst_pad_link(gst_element_get_request_pad(tee, "src_%u"), - gst_element_get_static_pad(probe_queue, "sink")); - gst_pad_link(gst_element_get_request_pad(tee, "src_%u"), - gst_element_get_static_pad(audio_queue, "sink")); + pad = gst_element_get_static_pad(probe_queue, "sink"); + gst_pad_link(gst_element_get_request_pad(tee, "src_%u"), pad); + gst_object_unref(pad); + + pad = gst_element_get_static_pad(audio_queue, "sink"); + gst_pad_link(gst_element_get_request_pad(tee, "src_%u"), pad); + gst_object_unref(pad); // Link replaygain elements if enabled. if (rg_enabled_) { @@ -454,12 +457,14 @@ bool GstEnginePipeline::Init() { gst_caps_unref(caps); // Add probes and handlers. - gst_pad_add_probe(gst_element_get_static_pad(probe_converter, "src"), - GST_PAD_PROBE_TYPE_BUFFER, HandoffCallback, this, nullptr); - gst_bus_set_sync_handler(gst_pipeline_get_bus(GST_PIPELINE(pipeline_)), - BusCallbackSync, this, nullptr); - bus_cb_id_ = gst_bus_add_watch(gst_pipeline_get_bus(GST_PIPELINE(pipeline_)), - BusCallback, this); + pad = gst_element_get_static_pad(probe_converter, "src"); + gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_BUFFER, HandoffCallback, this, + nullptr); + gst_object_unref(pad); + GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline_)); + gst_bus_set_sync_handler(bus, BusCallbackSync, this, nullptr); + bus_cb_id_ = gst_bus_add_watch(bus, BusCallback, this); + gst_object_unref(bus); MaybeLinkDecodeToAudio(); @@ -519,8 +524,10 @@ bool GstEnginePipeline::InitFromUrl(const QUrl& url, qint64 end_nanosec) { GstEnginePipeline::~GstEnginePipeline() { if (pipeline_) { - gst_bus_set_sync_handler(gst_pipeline_get_bus(GST_PIPELINE(pipeline_)), - nullptr, nullptr, nullptr); + GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline_)); + gst_bus_set_sync_handler(bus, nullptr, nullptr, nullptr); + gst_object_unref(bus); + g_source_remove(bus_cb_id_); gst_element_set_state(pipeline_, GST_STATE_NULL); gst_object_unref(GST_OBJECT(pipeline_)); @@ -1011,6 +1018,7 @@ void GstEnginePipeline::SourceSetupCallback(GstURIDecodeBin* bin, g_object_set(element, "ssl-strict", TRUE, nullptr); #endif } + g_object_unref(element); } void GstEnginePipeline::TransitionToNext() { -- 2.16.4 ++++++ 0001-Fix-thread-safety-issues-when-initially-loading-devi.patch ++++++ >From a62062127e0d17ff10e961355ca3807fbd13811a Mon Sep 17 00:00:00 2001 From: Jim Broadus <jbroa...@gmail.com> Date: Sat, 23 Feb 2019 22:12:07 -0800 Subject: [PATCH] Fix thread-safety issues when initially loading devices from the database. When DeviceManager initializes, it creates a thread to load device information from the database. Part of this process includes use of QPixMap for icons which produced a warning message: 22:32:53.763 WARN unknown QPixmap: It is not safe to use pixmaps outside the GUI thread In addition, the device is added to the view using beginInsertRows and endInsertRows. This could contend with a device added by a lister signaling PhysicalDeviceAdded. To solve these problems, this change moves the icon loading and insertion to the main thread. LoadAllDevices reads the data from the database and creates the DeviceInfo object, then sends a signal to the main thread. In the signal handler, the icon is loaded and the device is added to the master list and view. --- src/devices/deviceinfo.cpp | 11 +++-------- src/devices/devicemanager.cpp | 22 +++++++++++++++++++--- src/devices/devicemanager.h | 2 ++ 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/devices/deviceinfo.cpp b/src/devices/deviceinfo.cpp index 9c1d83dac..85b148b43 100644 --- a/src/devices/deviceinfo.cpp +++ b/src/devices/deviceinfo.cpp @@ -58,14 +58,9 @@ void DeviceInfo::InitFromDb(const DeviceDatabaseBackend::Device& dev) { size_ = dev.size_; transcode_mode_ = dev.transcode_mode_; transcode_format_ = dev.transcode_format_; - - QStringList icon_names = dev.icon_name_.split(','); - QVariantList icons; - for (const QString& icon_name : icon_names) { - icons << icon_name; - } - - LoadIcon(icons, friendly_name_); + // Store the raw value for now. If it's a comma delimited list, it will be + // sorted out later. + icon_name_ = dev.icon_name_; QStringList unique_ids = dev.unique_id_.split(','); for (const QString& id : unique_ids) { diff --git a/src/devices/devicemanager.cpp b/src/devices/devicemanager.cpp index 6e911d23e..940907509 100644 --- a/src/devices/devicemanager.cpp +++ b/src/devices/devicemanager.cpp @@ -83,6 +83,8 @@ DeviceManager::DeviceManager(Application* app, QObject* parent) backend_->moveToThread(app_->database()->thread()); backend_->Init(app_->database()); + connect(this, SIGNAL(DeviceCreatedFromDb(DeviceInfo*)), + SLOT(AddDeviceFromDb(DeviceInfo*))); // This reads from the database and contends on the database mutex, which can // be very slow on startup. ConcurrentRun::Run<void>(&thread_pool_, @@ -140,11 +142,25 @@ void DeviceManager::LoadAllDevices() { for (const DeviceDatabaseBackend::Device& device : devices) { DeviceInfo* info = new DeviceInfo(DeviceInfo::Type_Device, root_); info->InitFromDb(device); + // Use of QPixMap and device insertion should only be done on the main + // thread. Send a signal to finish the device addition. + emit DeviceCreatedFromDb(info); + } +} - beginInsertRows(ItemToIndex(root_), devices_.count(), devices_.count()); - devices_ << info; - endInsertRows(); +void DeviceManager::AddDeviceFromDb(DeviceInfo* info) { + // At this point, icon_name_ contains the value from the database where the + // value is allowed to be a comma delimited list. + QStringList icon_names = info->icon_name_.split(','); + QVariantList icons; + for (const QString& icon_name : icon_names) { + icons << icon_name; } + info->LoadIcon(icons, info->friendly_name_); + + beginInsertRows(ItemToIndex(root_), devices_.count(), devices_.count()); + devices_ << info; + endInsertRows(); } QVariant DeviceManager::data(const QModelIndex& idx, int role) const { diff --git a/src/devices/devicemanager.h b/src/devices/devicemanager.h index 595a5d5c7..80044256b 100644 --- a/src/devices/devicemanager.h +++ b/src/devices/devicemanager.h @@ -101,6 +101,7 @@ class DeviceManager : public SimpleTreeModel<DeviceInfo> { signals: void DeviceConnected(QModelIndex idx); void DeviceDisconnected(QModelIndex idx); + void DeviceCreatedFromDb(DeviceInfo* info); private slots: void PhysicalDeviceAdded(const QString& id); @@ -111,6 +112,7 @@ class DeviceManager : public SimpleTreeModel<DeviceInfo> { void DeviceSongCountUpdated(int count); void LoadAllDevices(); void DeviceConnectFinished(const QString& id, bool success); + void AddDeviceFromDb(DeviceInfo* info); protected: void LazyPopulate(DeviceInfo* item) { LazyPopulate(item, true); } -- 2.16.4 ++++++ 0001-Free-decoder-bin-if-error-occurs-during-setup.patch ++++++ >From ca8db288d57dec6dcfaadd94329af4668edc1bdb Mon Sep 17 00:00:00 2001 From: Jim Broadus <jbroa...@gmail.com> Date: Fri, 22 Mar 2019 19:07:06 -0700 Subject: [PATCH] Free decoder bin if error occurs during setup. In the case that an error occurs in ReplaceDecodeBin before the bin is added to the pipeline, unreference the object to allow cleanup. This change also separates CreateDecodeBinFromUrl from ReplaceDecodeBin, following the pattern of CreateDecodeBinFromString. --- src/engines/gstenginepipeline.cpp | 26 ++++++++++++++++++++------ src/engines/gstenginepipeline.h | 1 + 2 files changed, 21 insertions(+), 6 deletions(-) Index: Clementine-36cc5b82f4daf5c2d4e93dc8072665e5a3ca622b/src/engines/gstenginepipeline.cpp =================================================================== --- Clementine-36cc5b82f4daf5c2d4e93dc8072665e5a3ca622b.orig/src/engines/gstenginepipeline.cpp 2019-03-27 12:30:08.162577265 +0200 +++ Clementine-36cc5b82f4daf5c2d4e93dc8072665e5a3ca622b/src/engines/gstenginepipeline.cpp 2019-03-27 12:33:21.501836558 +0200 @@ -153,17 +153,28 @@ bool GstEnginePipeline::ReplaceDecodeBin } bool GstEnginePipeline::ReplaceDecodeBin(const QUrl& url) { - GstElement* new_bin = nullptr; + GstElement* new_bin = CreateDecodeBinFromUrl(url); + return ReplaceDecodeBin(new_bin); +} +GstElement* GstEnginePipeline::CreateDecodeBinFromUrl(const QUrl& url) { + GstElement* new_bin = nullptr; #ifdef HAVE_SPOTIFY if (url.scheme() == "spotify") { new_bin = gst_bin_new("spotify_bin"); + if (!new_bin) return nullptr; // Create elements GstElement* src = engine_->CreateElement("tcpserversrc", new_bin); - if (!src) return false; + if (!src) { + gst_object_unref(GST_OBJECT(new_bin)); + return nullptr; + } GstElement* gdp = engine_->CreateElement("gdpdepay", new_bin); - if (!gdp) return false; + if (!gdp) { + gst_object_unref(GST_OBJECT(new_bin)); + return nullptr; + } // Pick a port number const int port = Utilities::PickUnusedPort(); @@ -196,7 +207,7 @@ bool GstEnginePipeline::ReplaceDecodeBin uri = url.toEncoded(); } new_bin = engine_->CreateElement("uridecodebin"); - if (!new_bin) return false; + if (!new_bin) return nullptr; g_object_set(G_OBJECT(new_bin), "uri", uri.constData(), nullptr); CHECKED_GCONNECT(G_OBJECT(new_bin), "drained", &SourceDrainedCallback, this); @@ -207,7 +218,7 @@ bool GstEnginePipeline::ReplaceDecodeBin } #endif - return ReplaceDecodeBin(new_bin); + return new_bin; } GstElement* GstEnginePipeline::CreateDecodeBinFromString(const char* pipeline) { @@ -481,7 +492,10 @@ bool GstEnginePipeline::InitFromString(c return false; } - if (!ReplaceDecodeBin(new_bin)) return false; + if (!ReplaceDecodeBin(new_bin)) { + gst_object_unref(GST_OBJECT(new_bin)); + return false; + } if (!Init()) return false; return gst_element_link(new_bin, audiobin_); Index: Clementine-36cc5b82f4daf5c2d4e93dc8072665e5a3ca622b/src/engines/gstenginepipeline.h =================================================================== --- Clementine-36cc5b82f4daf5c2d4e93dc8072665e5a3ca622b.orig/src/engines/gstenginepipeline.h 2019-03-27 12:30:08.162577265 +0200 +++ Clementine-36cc5b82f4daf5c2d4e93dc8072665e5a3ca622b/src/engines/gstenginepipeline.h 2019-03-27 12:31:00.916557865 +0200 @@ -151,6 +151,7 @@ signals: bool Init(); GstElement* CreateDecodeBinFromString(const char* pipeline); + GstElement* CreateDecodeBinFromUrl(const QUrl& url); void UpdateVolume(); void UpdateEqualizer(); ++++++ 0001-Handle-case-where-a-lister-adds-a-device-before-load.patch ++++++ >From d041da18cc2e73ef2a6f1982546702b4844b9893 Mon Sep 17 00:00:00 2001 From: Jim Broadus <jbroa...@gmail.com> Date: Sat, 23 Feb 2019 23:42:31 -0800 Subject: [PATCH] Handle case where a lister adds a device before loaded from database. There is a small chance that a device lister is able to discover and add a previously known device before it is added by the database loader thread. In this case, copy the data that is user-settable to the existing DeviceInfo object and destroy the object created from the database query. This adds and utilizes a new FindEquivalentDevice method that compares the device unique IDs. This could probably be made more robust as the unique IDs for some listers may change. However, this is a problem with the database storage implementation in general. --- src/devices/devicemanager.cpp | 27 ++++++++++++++++++++++++--- src/devices/devicemanager.h | 1 + 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/devices/devicemanager.cpp b/src/devices/devicemanager.cpp index 940907509..11123229d 100644 --- a/src/devices/devicemanager.cpp +++ b/src/devices/devicemanager.cpp @@ -158,9 +158,22 @@ void DeviceManager::AddDeviceFromDb(DeviceInfo* info) { } info->LoadIcon(icons, info->friendly_name_); - beginInsertRows(ItemToIndex(root_), devices_.count(), devices_.count()); - devices_ << info; - endInsertRows(); + DeviceInfo* existing = FindEquivalentDevice(info); + if (existing) { + qLog(Info) << "Found existing device: " << info->friendly_name_; + // Update user configuration from the database. + existing->icon_name_ = info->icon_name_; + existing->icon_ = info->icon_; + QModelIndex idx = ItemToIndex(existing); + if (idx.isValid()) emit dataChanged(idx, idx); + // Discard the info loaded from the database. + delete info; + } else { + qLog(Info) << "Device added from database: " << info->friendly_name_; + beginInsertRows(ItemToIndex(root_), devices_.count(), devices_.count()); + devices_ << info; + endInsertRows(); + } } QVariant DeviceManager::data(const QModelIndex& idx, int role) const { @@ -328,6 +341,14 @@ DeviceInfo* DeviceManager::FindDeviceByUrl(const QList<QUrl>& urls) const { return nullptr; } +DeviceInfo* DeviceManager::FindEquivalentDevice(DeviceInfo* info) const { + for (const DeviceInfo::Backend& backend : info->backends_) { + DeviceInfo* match = FindDeviceById(backend.unique_id_); + if (match) return match; + } + return nullptr; +} + void DeviceManager::PhysicalDeviceAdded(const QString& id) { DeviceLister* lister = qobject_cast<DeviceLister*>(sender()); diff --git a/src/devices/devicemanager.h b/src/devices/devicemanager.h index 80044256b..2caccb6f5 100644 --- a/src/devices/devicemanager.h +++ b/src/devices/devicemanager.h @@ -79,6 +79,7 @@ class DeviceManager : public SimpleTreeModel<DeviceInfo> { DeviceInfo* FindDeviceById(const QString& id) const; DeviceInfo* FindDeviceByUrl(const QList<QUrl>& url) const; + DeviceInfo* FindEquivalentDevice(DeviceInfo* info) const; // Actions on devices std::shared_ptr<ConnectedDevice> Connect(DeviceInfo* info); -- 2.16.4 ++++++ 0001-Improved-support-for-APEv2-tags.-6280.patch ++++++ >From 8dd5750efa7565f341e8ed2cabfdc43588e3d441 Mon Sep 17 00:00:00 2001 From: smithjd15 <46389639+smithj...@users.noreply.github.com> Date: Wed, 13 Feb 2019 23:37:44 -0700 Subject: [PATCH] Improved support for APEv2 tags. (#6280) --- ext/libclementine-tagreader/tagreader.cpp | 215 ++++++++++++++++++++- .../tagreadermessages.proto | 1 + src/core/song.h | 1 + 3 files changed, 208 insertions(+), 9 deletions(-) diff --git a/ext/libclementine-tagreader/tagreader.cpp b/ext/libclementine-tagreader/tagreader.cpp index d3a89099f..900f042b4 100644 --- a/ext/libclementine-tagreader/tagreader.cpp +++ b/ext/libclementine-tagreader/tagreader.cpp @@ -28,6 +28,7 @@ #include <QVector> #include <aifffile.h> +#include <apefile.h> #include <asffile.h> #include <attachedpictureframe.h> #include <commentsframe.h> @@ -161,6 +162,83 @@ void TagReader::ReadFile(const QString& filename, QString compilation; QString lyrics; + auto parseApeTag = [&](TagLib::APE::Tag* tag) { + const TagLib::APE::ItemListMap& items = tag->itemListMap(); + + // Find album artists + TagLib::APE::ItemListMap::ConstIterator it = items.find("ALBUM ARTIST"); + if (it != items.end()) { + TagLib::StringList album_artists = it->second.toStringList(); + if (!album_artists.isEmpty()) { + Decode(album_artists.front(), nullptr, song->mutable_albumartist()); + } + } + + // Find album cover art + if (items.find("COVER ART (FRONT)") != items.end()) { + song->set_art_automatic(kEmbeddedCover); + } + + if (items.contains("COMPILATION")) { + compilation = TStringToQString( + TagLib::String::number(items["COMPILATION"].toString().toInt())); + } + + if (items.contains("DISC")) { + disc = TStringToQString( + TagLib::String::number(items["DISC"].toString().toInt())); + } + + if (items.contains("FMPS_RATING")) { + float rating = + TStringToQString(items["FMPS_RATING"].toString()).toFloat(); + if (song->rating() <= 0 && rating > 0) { + song->set_rating(rating); + } + } + if (items.contains("FMPS_PLAYCOUNT")) { + int playcount = + TStringToQString(items["FMPS_PLAYCOUNT"].toString()).toFloat(); + if (song->playcount() <= 0 && playcount > 0) { + song->set_playcount(playcount); + } + } + if (items.contains("FMPS_RATING_AMAROK_SCORE")) { + int score = TStringToQString(items["FMPS_RATING_AMAROK_SCORE"].toString()) + .toFloat() * + 100; + if (song->score() <= 0 && score > 0) { + song->set_score(score); + } + } + + if (items.contains("BPM")) { + Decode(items["BPM"].toStringList().toString(", "), nullptr, + song->mutable_performer()); + } + + if (items.contains("PERFORMER")) { + Decode(items["PERFORMER"].toStringList().toString(", "), nullptr, + song->mutable_performer()); + } + + if (items.contains("COMPOSER")) { + Decode(items["COMPOSER"].toStringList().toString(", "), nullptr, + song->mutable_composer()); + } + + if (items.contains("GROUPING")) { + Decode(items["GROUPING"].toStringList().toString(" "), nullptr, + song->mutable_grouping()); + } + + if (items.contains("LYRICS")) { + Decode(items["LYRICS"].toString(), nullptr, song->mutable_lyrics()); + } + + Decode(tag->comment(), nullptr, song->mutable_comment()); + }; + // 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 @@ -351,6 +429,21 @@ void TagReader::ReadFile(const QString& filename, Decode(mp4_tag->comment(), nullptr, song->mutable_comment()); } + } else if (TagLib::APE::File* file = + dynamic_cast<TagLib::APE::File*>(fileref->file())) { + if (file->tag()) { + parseApeTag(file->APETag()); + } + } else if (TagLib::MPC::File* file = + dynamic_cast<TagLib::MPC::File*>(fileref->file())) { + if (file->tag()) { + parseApeTag(file->APETag()); + } + } else if (TagLib::WavPack::File* file = + dynamic_cast<TagLib::WavPack::File*>(fileref->file())) { + if (file->tag()) { + parseApeTag(file->APETag()); + } } #ifdef TAGLIB_WITH_ASF else if (TagLib::ASF::File* file = @@ -668,6 +761,8 @@ pb::tagreader::SongMetadata_Type TagReader::GuessFileType( return pb::tagreader::SongMetadata_Type_TRUEAUDIO; if (dynamic_cast<TagLib::WavPack::File*>(fileref->file())) return pb::tagreader::SongMetadata_Type_WAVPACK; + if (dynamic_cast<TagLib::APE::File*>(fileref->file())) + return pb::tagreader::SongMetadata_Type_APE; return pb::tagreader::SongMetadata_Type_UNKNOWN; } @@ -691,6 +786,38 @@ bool TagReader::SaveFile(const QString& filename, fileref->tag()->setYear(song.year()); fileref->tag()->setTrack(song.track()); + auto saveApeTag = [&](TagLib::APE::Tag* tag) { + tag->setItem( + "disc", + TagLib::APE::Item("disc", TagLib::String::number( + song.disc() <= 0 - 1 ? 0 : song.disc()))); + tag->setItem("bpm", + TagLib::APE::Item( + "bpm", TagLib::StringList( + song.bpm() <= 0 - 1 + ? "0" + : TagLib::String::number(song.bpm())))); + tag->setItem("composer", + TagLib::APE::Item( + "composer", TagLib::StringList(song.composer().c_str()))); + tag->setItem("grouping", + TagLib::APE::Item( + "grouping", TagLib::StringList(song.grouping().c_str()))); + tag->setItem("performer", + TagLib::APE::Item("performer", TagLib::StringList( + song.performer().c_str()))); + tag->setItem( + "album artist", + TagLib::APE::Item("album artist", + TagLib::StringList(song.albumartist().c_str()))); + tag->setItem("lyrics", + TagLib::APE::Item("lyrics", TagLib::String(song.lyrics()))); + tag->setItem( + "compilation", + TagLib::APE::Item("compilation", + TagLib::StringList(song.compilation() ? "1" : "0"))); + }; + if (TagLib::MPEG::File* file = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) { TagLib::ID3v2::Tag* tag = file->ID3v2Tag(true); @@ -723,17 +850,15 @@ bool TagReader::SaveFile(const QString& filename, tag->itemListMap()["aART"] = TagLib::StringList(song.albumartist().c_str()); tag->itemListMap()["cpil"] = TagLib::StringList(song.compilation() ? "1" : "0"); + } else if (TagLib::APE::File* file = + dynamic_cast<TagLib::APE::File*>(fileref->file())) { + saveApeTag(file->APETag(true)); + } else if (TagLib::MPC::File* file = + dynamic_cast<TagLib::MPC::File*>(fileref->file())) { + saveApeTag(file->APETag(true)); } else if (TagLib::WavPack::File* file = dynamic_cast<TagLib::WavPack::File*>(fileref->file())) { - TagLib::APE::Tag* tag = file->APETag(true); - if (!tag) return false; - tag->setArtist(StdStringToTaglibString(song.artist())); - tag->setAlbum(StdStringToTaglibString(song.album())); - tag->setTitle(StdStringToTaglibString(song.title())); - tag->setGenre(StdStringToTaglibString(song.genre())); - tag->setComment(StdStringToTaglibString(song.comment())); - tag->setYear(song.year()); - tag->setTrack(song.track()); + saveApeTag(file->APETag(true)); } // Handle all the files which have VorbisComments (Ogg, OPUS, ...) in the same @@ -768,6 +893,19 @@ bool TagReader::SaveSongStatisticsToFile( if (!fileref || fileref->isNull()) // The file probably doesn't exist return false; + auto saveApeSongStats = [&](TagLib::APE::Tag* tag) { + tag->setItem( + "FMPS_Rating_Amarok_Score", + TagLib::APE::Item("FMPS_Rating_Amarok_Score", + TagLib::StringList(QStringToTaglibString( + QString::number(song.score() / 100.0))))); + tag->setItem( + "FMPS_PlayCount", + TagLib::APE::Item( + "FMPS_PlayCount", + TagLib::StringList(TagLib::String::number(song.playcount())))); + }; + if (TagLib::MPEG::File* file = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) { TagLib::ID3v2::Tag* tag = file->ID3v2Tag(true); @@ -806,6 +944,15 @@ bool TagReader::SaveSongStatisticsToFile( QStringToTaglibString(QString::number(song.score() / 100.0))); tag->itemListMap()[kMP4_FMPS_Playcount_ID] = TagLib::StringList(TagLib::String::number(song.playcount())); + } else if (TagLib::APE::File* file = + dynamic_cast<TagLib::APE::File*>(fileref->file())) { + saveApeSongStats(file->APETag(true)); + } else if (TagLib::MPC::File* file = + dynamic_cast<TagLib::MPC::File*>(fileref->file())) { + saveApeSongStats(file->APETag(true)); + } else if (TagLib::WavPack::File* file = + dynamic_cast<TagLib::WavPack::File*>(fileref->file())) { + saveApeSongStats(file->APETag(true)); } else { // Nothing to save: stop now return true; @@ -841,6 +988,13 @@ bool TagReader::SaveSongRatingToFile( if (!fileref || fileref->isNull()) // The file probably doesn't exist return false; + auto saveApeSongRating = [&](TagLib::APE::Tag* tag) { + tag->setItem("FMPS_Rating", + TagLib::APE::Item("FMPS_Rating", + TagLib::StringList(QStringToTaglibString( + QString::number(song.rating()))))); + }; + if (TagLib::MPEG::File* file = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) { TagLib::ID3v2::Tag* tag = file->ID3v2Tag(true); @@ -873,6 +1027,15 @@ bool TagReader::SaveSongRatingToFile( TagLib::MP4::Tag* tag = file->tag(); tag->itemListMap()[kMP4_FMPS_Rating_ID] = TagLib::StringList( QStringToTaglibString(QString::number(song.rating()))); + } else if (TagLib::APE::File* file = + dynamic_cast<TagLib::APE::File*>(fileref->file())) { + saveApeSongRating(file->APETag(true)); + } else if (TagLib::MPC::File* file = + dynamic_cast<TagLib::MPC::File*>(fileref->file())) { + saveApeSongRating(file->APETag(true)); + } else if (TagLib::WavPack::File* file = + dynamic_cast<TagLib::WavPack::File*>(fileref->file())) { + saveApeSongRating(file->APETag(true)); } else { // Nothing to save: stop now return true; @@ -1110,6 +1273,40 @@ QByteArray TagReader::LoadEmbeddedArt(const QString& filename) const { } } + // APE formats + auto apeTagCover = [&](TagLib::APE::Tag* tag) { + QByteArray cover; + const TagLib::APE::ItemListMap& items = tag->itemListMap(); + TagLib::APE::ItemListMap::ConstIterator it = + items.find("COVER ART (FRONT)"); + if (it != items.end()) { + TagLib::ByteVector data = it->second.binaryData(); + + int pos = data.find('\0') + 1; + if ((pos > 0) && (pos < data.size())) { + cover = QByteArray(data.data() + pos, data.size() - pos); + } + } + + return cover; + }; + + TagLib::APE::File* ape_file = dynamic_cast<TagLib::APE::File*>(ref.file()); + if (ape_file) { + return apeTagCover(ape_file->APETag()); + } + + TagLib::MPC::File* mpc_file = dynamic_cast<TagLib::MPC::File*>(ref.file()); + if (mpc_file) { + return apeTagCover(mpc_file->APETag()); + } + + TagLib::WavPack::File* wavPack_file = + dynamic_cast<TagLib::WavPack::File*>(ref.file()); + if (wavPack_file) { + return apeTagCover(wavPack_file->APETag()); + } + return QByteArray(); } diff --git a/ext/libclementine-tagreader/tagreadermessages.proto b/ext/libclementine-tagreader/tagreadermessages.proto index b1b8e3ea7..531efb717 100644 --- a/ext/libclementine-tagreader/tagreadermessages.proto +++ b/ext/libclementine-tagreader/tagreadermessages.proto @@ -21,6 +21,7 @@ message SongMetadata { WAVPACK = 14; SPC = 15; VGM = 16; + APE = 17; STREAM = 99; } diff --git a/src/core/song.h b/src/core/song.h index bc109a622..e2fd9e8c2 100644 --- a/src/core/song.h +++ b/src/core/song.h @@ -108,6 +108,7 @@ class Song { Type_WavPack = 14, Type_Spc = 15, Type_VGM = 16, + Type_APE = 17, Type_Stream = 99, }; static QString TextForFiletype(FileType type); -- 2.16.4 ++++++ 0001-Prevent-UI-hang-during-device-scan.-6291.patch ++++++ >From 248f1d8596b69d58a885130bcada4799f116341c Mon Sep 17 00:00:00 2001 From: Jim Broadus <jbroa...@gmail.com> Date: Wed, 20 Feb 2019 00:03:44 -0800 Subject: [PATCH] Prevent UI hang during device scan. (#6291) When unmounting a device, the ConnectedDevice object is destroyed. The FileSystemDevice destructor waits on its worker thread. If a scan is in progress, this will block until completion. There is an existing Stop method in the LibraryWatcher class that is intended to stop long running operations. To fix, or at least significantly shorten this hang, we'll call this before waiting for the thread to exit. Also add a stop_requested check in the cover art scan. In addition, add a call to Stop in the Library destructor, which has a similar usage. --- src/devices/filesystemdevice.cpp | 1 + src/library/library.cpp | 1 + src/library/librarywatcher.cpp | 2 ++ 3 files changed, 4 insertions(+) diff --git a/src/devices/filesystemdevice.cpp b/src/devices/filesystemdevice.cpp index 3b1bd3ec0..342384718 100644 --- a/src/devices/filesystemdevice.cpp +++ b/src/devices/filesystemdevice.cpp @@ -71,6 +71,7 @@ void FilesystemDevice::Init() { } FilesystemDevice::~FilesystemDevice() { + watcher_->Stop(); watcher_->deleteLater(); watcher_thread_->exit(); watcher_thread_->wait(); diff --git a/src/library/library.cpp b/src/library/library.cpp index 6603e6043..60aafabda 100644 --- a/src/library/library.cpp +++ b/src/library/library.cpp @@ -121,6 +121,7 @@ Library::Library(Application* app, QObject* parent) } Library::~Library() { + watcher_->Stop(); watcher_->deleteLater(); watcher_thread_->exit(); watcher_thread_->wait(5000 /* five seconds */); diff --git a/src/library/librarywatcher.cpp b/src/library/librarywatcher.cpp index 4cb7d934b..b77843c32 100644 --- a/src/library/librarywatcher.cpp +++ b/src/library/librarywatcher.cpp @@ -678,6 +678,8 @@ QString LibraryWatcher::PickBestImage(const QStringList& images) { QString biggest_path; for (const QString& path : filtered) { + if (stop_requested_) return ""; + QImage image(path); if (image.isNull()) continue; -- 2.16.4 ++++++ clementine-hidden-systray-icon.patch ++++++ --- /var/tmp/diff_new_pack.Io6t6p/_old 2019-03-28 22:48:44.955054061 +0100 +++ /var/tmp/diff_new_pack.Io6t6p/_new 2019-03-28 22:48:44.991054054 +0100 @@ -1,7 +1,7 @@ -Index: dist/clementine.desktop +Index: Clementine-36cc5b82f4daf5c2d4e93dc8072665e5a3ca622b/dist/clementine.desktop =================================================================== ---- dist/clementine.desktop.orig 2019-01-15 09:06:34.053172793 +0200 -+++ dist/clementine.desktop 2019-01-15 09:06:42.977475930 +0200 +--- Clementine-36cc5b82f4daf5c2d4e93dc8072665e5a3ca622b.orig/dist/clementine.desktop 2019-02-13 06:02:56.000000000 +0200 ++++ Clementine-36cc5b82f4daf5c2d4e93dc8072665e5a3ca622b/dist/clementine.desktop 2019-03-27 09:08:14.514962063 +0200 @@ -36,7 +36,7 @@ TryExec=clementine Icon=clementine Terminal=false @@ -11,10 +11,10 @@ MimeType=application/ogg;application/x-ogg;application/x-ogm-audio;audio/aac;audio/mp4;audio/mpeg;audio/mpegurl;audio/ogg;audio/vnd.rn-realaudio;audio/vorbis;audio/x-flac;audio/x-mp3;audio/x-mpeg;audio/x-mpegurl;audio/x-ms-wma;audio/x-musepack;audio/x-oggflac;audio/x-pn-realaudio;audio/x-scpls;audio/x-speex;audio/x-vorbis;audio/x-vorbis+ogg;audio/x-wav;video/x-ms-asf;x-content/audio-player;x-scheme-handler/zune;x-scheme-handler/itpc;x-scheme-handler/itms;x-scheme-handler/feed; Actions=Play;Pause;Stop;StopAfterCurrent;Previous;Next; -Index: src/ui/mainwindow.cpp +Index: Clementine-36cc5b82f4daf5c2d4e93dc8072665e5a3ca622b/src/ui/mainwindow.cpp =================================================================== ---- src/ui/mainwindow.cpp.orig 2018-12-03 02:45:20.000000000 +0200 -+++ src/ui/mainwindow.cpp 2019-01-15 09:41:32.575111320 +0200 +--- Clementine-36cc5b82f4daf5c2d4e93dc8072665e5a3ca622b.orig/src/ui/mainwindow.cpp 2019-02-13 06:02:56.000000000 +0200 ++++ Clementine-36cc5b82f4daf5c2d4e93dc8072665e5a3ca622b/src/ui/mainwindow.cpp 2019-03-27 09:08:14.514962063 +0200 @@ -1041,6 +1041,7 @@ MainWindow::MainWindow(Application* app, if (hidden && (!QSystemTrayIcon::isSystemTrayAvailable() || !tray_icon_ || !tray_icon_->IsVisible())) { ++++++ clementine-moodbar-fpic.patch ++++++ --- /var/tmp/diff_new_pack.Io6t6p/_old 2019-03-28 22:48:45.135054028 +0100 +++ /var/tmp/diff_new_pack.Io6t6p/_new 2019-03-28 22:48:45.203054016 +0100 @@ -1,7 +1,7 @@ -Index: gst/moodbar/CMakeLists.txt +Index: Clementine-36cc5b82f4daf5c2d4e93dc8072665e5a3ca622b/gst/moodbar/CMakeLists.txt =================================================================== ---- gst/moodbar/CMakeLists.txt.orig 2018-10-05 11:56:49.000000000 +0200 -+++ gst/moodbar/CMakeLists.txt 2018-11-02 13:33:13.223890411 +0200 +--- Clementine-36cc5b82f4daf5c2d4e93dc8072665e5a3ca622b.orig/gst/moodbar/CMakeLists.txt 2019-02-13 06:02:56.000000000 +0200 ++++ Clementine-36cc5b82f4daf5c2d4e93dc8072665e5a3ca622b/gst/moodbar/CMakeLists.txt 2019-03-27 09:08:06.882659408 +0200 @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 2.8.11) ++++++ clementine-udisks-headers.patch ++++++ --- /var/tmp/diff_new_pack.Io6t6p/_old 2019-03-28 22:48:45.359053988 +0100 +++ /var/tmp/diff_new_pack.Io6t6p/_new 2019-03-28 22:48:45.391053981 +0100 @@ -1,8 +1,8 @@ -Index: src/CMakeLists.txt +Index: Clementine-36cc5b82f4daf5c2d4e93dc8072665e5a3ca622b/src/CMakeLists.txt =================================================================== ---- src/CMakeLists.txt.orig 2018-11-02 14:30:43.839117342 +0200 -+++ src/CMakeLists.txt 2018-11-02 14:31:29.000773095 +0200 -@@ -946,6 +946,8 @@ if(HAVE_DBUS) +--- Clementine-36cc5b82f4daf5c2d4e93dc8072665e5a3ca622b.orig/src/CMakeLists.txt 2019-02-13 06:02:56.000000000 +0200 ++++ Clementine-36cc5b82f4daf5c2d4e93dc8072665e5a3ca622b/src/CMakeLists.txt 2019-03-27 09:07:45.077792580 +0200 +@@ -965,6 +965,8 @@ if(UNIX AND HAVE_DBUS) list(APPEND HEADERS ${CMAKE_CURRENT_BINARY_DIR}/dbus/avahientrygroup.h) list(APPEND SOURCES ${CMAKE_CURRENT_BINARY_DIR}/dbus/avahientrygroup.cpp) ++++++ use_system_qxtglobalshortcut.patch ++++++ Index: Clementine-36cc5b82f4daf5c2d4e93dc8072665e5a3ca622b/CMakeLists.txt =================================================================== --- Clementine-36cc5b82f4daf5c2d4e93dc8072665e5a3ca622b.orig/CMakeLists.txt 2019-02-13 06:02:56.000000000 +0200 +++ Clementine-36cc5b82f4daf5c2d4e93dc8072665e5a3ca622b/CMakeLists.txt 2019-03-27 09:11:18.298146436 +0200 @@ -399,11 +399,10 @@ endif(NOT QTIOCOMPRESSOR_INCLUDE_DIRS OR # used to link to system installed qxt instead. option(USE_SYSTEM_QXT "Don't set this option unless your system Qxt library has been compiled with the Clementine patches in 3rdparty" OFF) if (USE_SYSTEM_QXT) - find_path(QXTCORE_INCLUDE_DIRS qxtglobal.h PATH_SUFFIXES qt5/QxtCore) - find_path(QXTGUI_INCLUDE_DIRS qxtglobalshortcut.h PATH_SUFFIXES qt5/QxtWidgets) - set(QXT_INCLUDE_DIRS ${QXTCORE_INCLUDE_DIRS} ${QXTGUI_INCLUDE_DIRS}) + find_path(QXTGLOBALSHORTCUT_INCLUDE_DIRS qxtglobalshortcut.h PATH_SUFFIXES qxtglobalshortcut) + set(QXT_INCLUDE_DIRS ${QXTGLOBALSHORTCUT_INCLUDE_DIRS}) # We only need its header. We don't need to link to QxtCore. - find_library(QXT_LIBRARIES QxtWidgets-qt5) + find_library(QXT_LIBRARIES qxtglobalshortcut) else (USE_SYSTEM_QXT) add_definitions(-DQXT_STATIC -DBUILD_QXT_GUI -DBUILD_QXT_CORE) set(QXT_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/qxt) Index: Clementine-36cc5b82f4daf5c2d4e93dc8072665e5a3ca622b/src/core/qxtglobalshortcutbackend.cpp =================================================================== --- Clementine-36cc5b82f4daf5c2d4e93dc8072665e5a3ca622b.orig/src/core/qxtglobalshortcutbackend.cpp 2019-02-13 06:02:56.000000000 +0200 +++ Clementine-36cc5b82f4daf5c2d4e93dc8072665e5a3ca622b/src/core/qxtglobalshortcutbackend.cpp 2019-03-27 09:11:18.298146436 +0200 @@ -41,7 +41,7 @@ bool QxtGlobalShortcutBackend::DoRegiste void QxtGlobalShortcutBackend::AddShortcut(QAction* action) { if (action->shortcut().isEmpty()) return; QxtGlobalShortcut* shortcut = new QxtGlobalShortcut(action->shortcut(), this); - connect(shortcut, SIGNAL(activated()), action, SLOT(trigger())); + connect(shortcut, &QxtGlobalShortcut::activated, action, &QAction::trigger); shortcuts_ << shortcut; }