Compare commits

..

5 Commits

Author SHA1 Message Date
Jonas Kvinge
8f1bda0546 Add artist biography 2025-12-29 00:48:24 +01:00
Strawberry Bot
da2f28811a New translations 2025-12-29 00:02:45 +01:00
Jonas Kvinge
0bfa736081 GstEnginePipeline: Add audioresample elements 2025-12-28 22:01:42 +01:00
Jonas Kvinge
1392bcbbe1 FilesystemMusicStorage: Fallback to delete if moving to trash fails
Fixes #1679
2025-12-28 21:28:49 +01:00
Jonas Kvinge
11705889f1 Show playlist load errors
Fixes #1470
2025-12-28 20:54:36 +01:00
52 changed files with 2953 additions and 420 deletions

View File

@@ -295,12 +295,6 @@ if(UNIX AND NOT APPLE)
)
endif()
if(MSVC)
optional_component(WINDOWS_MEDIA_CONTROLS ON "Windows Media Transport Controls"
DEPENDS "MSVC compiler" MSVC
)
endif()
optional_component(SONGFINGERPRINTING ON "Song fingerprinting and tracking"
DEPENDS "chromaprint" CHROMAPRINT_FOUND
)
@@ -840,6 +834,12 @@ set(SOURCES
src/device/devicestatefiltermodel.cpp
src/device/deviceviewcontainer.cpp
src/device/deviceview.cpp
src/artistbio/artistbioview.cpp
src/artistbio/artistbiofetcher.cpp
src/artistbio/artistbioprovider.cpp
src/artistbio/lastfmartistbio.cpp
src/artistbio/wikipediaartistbio.cpp
)
set(HEADERS
@@ -1128,6 +1128,12 @@ set(HEADERS
src/device/devicestatefiltermodel.h
src/device/deviceviewcontainer.h
src/device/deviceview.h
src/artistbio/artistbiofetcher.h
src/artistbio/artistbioprovider.h
src/artistbio/artistbioview.h
src/artistbio/lastfmartistbio.h
src/artistbio/wikipediaartistbio.h
)
set(UI
@@ -1300,7 +1306,6 @@ endif()
optional_source(HAVE_ALSA SOURCES src/engine/alsadevicefinder.cpp src/engine/alsapcmdevicefinder.cpp)
optional_source(HAVE_PULSE SOURCES src/engine/pulsedevicefinder.cpp)
optional_source(MSVC SOURCES src/engine/uwpdevicefinder.cpp src/engine/asiodevicefinder.cpp)
optional_source(HAVE_WINDOWS_MEDIA_CONTROLS SOURCES src/core/windowsmediacontroller.cpp HEADERS src/core/windowsmediacontroller.h)
optional_source(HAVE_CHROMAPRINT SOURCES src/engine/chromaprinter.cpp)
optional_source(HAVE_MUSICBRAINZ

View File

@@ -15,6 +15,7 @@
<file>schema/schema-21.sql</file>
<file>schema/device-schema.sql</file>
<file>style/strawberry.css</file>
<file>style/artistbio.css</file>
<file>style/smartplaylistsearchterm.css</file>
<file>html/oauthsuccess.html</file>
<file>pictures/strawberry.png</file>

View File

@@ -98,6 +98,7 @@
<file>icons/128x128/somafm.png</file>
<file>icons/128x128/radioparadise.png</file>
<file>icons/128x128/musicbrainz.png</file>
<file>icons/128x128/guitar.png</file>
<file>icons/64x64/albums.png</file>
<file>icons/64x64/alsa.png</file>
<file>icons/64x64/application-exit.png</file>
@@ -197,6 +198,7 @@
<file>icons/64x64/somafm.png</file>
<file>icons/64x64/radioparadise.png</file>
<file>icons/64x64/musicbrainz.png</file>
<file>icons/64x64/guitar.png</file>
<file>icons/48x48/albums.png</file>
<file>icons/48x48/alsa.png</file>
<file>icons/48x48/application-exit.png</file>
@@ -300,6 +302,7 @@
<file>icons/48x48/somafm.png</file>
<file>icons/48x48/radioparadise.png</file>
<file>icons/48x48/musicbrainz.png</file>
<file>icons/48x48/guitar.png</file>
<file>icons/32x32/albums.png</file>
<file>icons/32x32/alsa.png</file>
<file>icons/32x32/application-exit.png</file>
@@ -403,6 +406,7 @@
<file>icons/32x32/somafm.png</file>
<file>icons/32x32/radioparadise.png</file>
<file>icons/32x32/musicbrainz.png</file>
<file>icons/32x32/guitar.png</file>
<file>icons/22x22/albums.png</file>
<file>icons/22x22/alsa.png</file>
<file>icons/22x22/application-exit.png</file>
@@ -506,5 +510,6 @@
<file>icons/22x22/somafm.png</file>
<file>icons/22x22/radioparadise.png</file>
<file>icons/22x22/musicbrainz.png</file>
<file>icons/22x22/guitar.png</file>
</qresource>
</RCC>

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
data/icons/22x22/guitar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
data/icons/32x32/guitar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
data/icons/48x48/guitar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
data/icons/64x64/guitar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
data/icons/full/guitar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

7
data/style/artistbio.css Normal file
View File

@@ -0,0 +1,7 @@
QScrollArea {
background: qpalette(base);
}
QTextEdit {
border: 0px;
}

View File

@@ -0,0 +1,126 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 <QObject>
#include <QTimer>
#include <QUrl>
#include "artistbiofetcher.h"
#include "artistbioprovider.h"
#include "core/logging.h"
ArtistBioFetcher::ArtistBioFetcher(QObject *parent)
: QObject(parent),
timeout_duration_(kDefaultTimeoutDuration),
next_id_(1) {}
void ArtistBioFetcher::AddProvider(ArtistBioProvider *provider) {
providers_ << provider;
connect(provider, SIGNAL(ImageReady(int, QUrl)), SLOT(ImageReady(int, QUrl)), Qt::QueuedConnection);
connect(provider, SIGNAL(InfoReady(int, CollapsibleInfoPane::Data)), SLOT(InfoReady(int, CollapsibleInfoPane::Data)), Qt::QueuedConnection);
connect(provider, SIGNAL(Finished(int)), SLOT(ProviderFinished(int)), Qt::QueuedConnection);
}
ArtistBioFetcher::~ArtistBioFetcher() {
while (!providers_.isEmpty()) {
ArtistBioProvider *provider = providers_.takeFirst();
provider->deleteLater();
}
}
int ArtistBioFetcher::FetchInfo(const Song &metadata) {
const int id = next_id_++;
results_[id] = Result();
timeout_timers_[id] = new QTimer(this);
timeout_timers_[id]->setSingleShot(true);
timeout_timers_[id]->setInterval(timeout_duration_);
timeout_timers_[id]->start();
connect(timeout_timers_[id], &QTimer::timeout, [this, id]() { Timeout(id); });
for (ArtistBioProvider *provider : providers_) {
if (provider->is_enabled()) {
waiting_for_[id].append(provider);
provider->Start(id, metadata);
}
}
return id;
}
void ArtistBioFetcher::ImageReady(const int id, const QUrl &url) {
if (!results_.contains(id)) return;
results_[id].images_ << url;
}
void ArtistBioFetcher::InfoReady(const int id, const CollapsibleInfoPane::Data &data) {
if (!results_.contains(id)) return;
results_[id].info_ << data;
if (!waiting_for_.contains(id)) return;
Q_EMIT InfoResultReady(id, data);
}
void ArtistBioFetcher::ProviderFinished(const int id) {
if (!results_.contains(id)) return;
if (!waiting_for_.contains(id)) return;
ArtistBioProvider *provider = qobject_cast<ArtistBioProvider*>(sender());
if (!waiting_for_[id].contains(provider)) return;
waiting_for_[id].removeAll(provider);
if (waiting_for_[id].isEmpty()) {
Result result = results_.take(id);
Q_EMIT ResultReady(id, result);
waiting_for_.remove(id);
delete timeout_timers_.take(id);
}
}
void ArtistBioFetcher::Timeout(const int id) {
if (!results_.contains(id)) return;
if (!waiting_for_.contains(id)) return;
// Emit the results that we have already
Q_EMIT ResultReady(id, results_.take(id));
// Cancel any providers that we're still waiting for
for (ArtistBioProvider *provider : waiting_for_[id]) {
qLog(Info) << "Request timed out from info provider" << provider->name();
provider->Cancel(id);
}
waiting_for_.remove(id);
// Remove the timer
delete timeout_timers_.take(id);
}

View File

@@ -0,0 +1,75 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 ARTISTBIOFETCHER_H
#define ARTISTBIOFETCHER_H
#include <QObject>
#include <QList>
#include <QMap>
#include <QUrl>
#include "widgets/collapsibleinfopane.h"
#include "core/song.h"
class QTimer;
class ArtistBioProvider;
class ArtistBioFetcher : public QObject {
Q_OBJECT
public:
explicit ArtistBioFetcher(QObject *parent = nullptr);
~ArtistBioFetcher() override;
struct Result {
QList<QUrl> images_;
QList<CollapsibleInfoPane::Data> info_;
};
static const int kDefaultTimeoutDuration = 25000;
void AddProvider(ArtistBioProvider *provider);
int FetchInfo(const Song &metadata);
QList<ArtistBioProvider*> providers() const { return providers_; }
Q_SIGNALS:
void InfoResultReady(int, CollapsibleInfoPane::Data);
void ResultReady(int, ArtistBioFetcher::Result);
private Q_SLOTS:
void ImageReady(const int id, const QUrl &url);
void InfoReady(const int id, const CollapsibleInfoPane::Data &data);
void ProviderFinished(const int id);
void Timeout(const int id);
private:
QList<ArtistBioProvider*> providers_;
QMap<int, Result> results_;
QMap<int, QList<ArtistBioProvider*>> waiting_for_;
QMap<int, QTimer*> timeout_timers_;
int timeout_duration_;
int next_id_;
};
#endif // ARTISTBIOFETCHER_H

View File

@@ -0,0 +1,25 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 "artistbioprovider.h"
ArtistBioProvider::ArtistBioProvider() : enabled_(true) {}
QString ArtistBioProvider::name() const { return QString::fromLatin1(metaObject()->className()); }

View File

@@ -0,0 +1,53 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 ARTISTBIOPROVIDER_H
#define ARTISTBIOPROVIDER_H
#include <QObject>
#include <QUrl>
#include "widgets/collapsibleinfopane.h"
#include "core/song.h"
class ArtistBioProvider : public QObject {
Q_OBJECT
public:
explicit ArtistBioProvider();
virtual void Start(const int id, const Song &song) = 0;
virtual void Cancel(const int) {}
virtual QString name() const;
bool is_enabled() const { return enabled_; }
void set_enabled(bool enabled) { enabled_ = enabled; }
Q_SIGNALS:
void ImageReady(int, QUrl);
void InfoReady(int, CollapsibleInfoPane::Data);
void Finished(int);
private:
bool enabled_;
};
#endif // ARTISTBIOPROVIDER_H

View File

@@ -0,0 +1,287 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 <QWidget>
#include <QFile>
#include <QScrollArea>
#include <QSettings>
#include <QSpacerItem>
#include <QTimer>
#include <QVBoxLayout>
#include <QShowEvent>
#include "core/song.h"
#include "core/networkaccessmanager.h"
#include "widgets/prettyimageview.h"
#include "widgets/widgetfadehelper.h"
#include "artistbiofetcher.h"
#include "lastfmartistbio.h"
#include "wikipediaartistbio.h"
#include "artistbioview.h"
const char *ArtistBioView::kSettingsGroup = "ArtistBio";
ArtistBioView::ArtistBioView(QWidget *parent)
: QWidget(parent),
network_(new NetworkAccessManager(this)),
fetcher_(new ArtistBioFetcher(this)),
current_request_id_(-1),
container_(new QVBoxLayout),
section_container_(nullptr),
fader_(new WidgetFadeHelper(this, 1000)),
dirty_(false) {
// Add the top-level scroll area
QScrollArea *scrollarea = new QScrollArea(this);
setLayout(new QVBoxLayout);
layout()->setContentsMargins(0, 0, 0, 0);
layout()->addWidget(scrollarea);
// Add a container widget to the scroll area
QWidget *container_widget = new QWidget;
container_widget->setLayout(container_);
container_widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
container_widget->setBackgroundRole(QPalette::Base);
container_->setSizeConstraint(QLayout::SetMinAndMaxSize);
container_->setContentsMargins(0, 0, 0, 0);
container_->setSpacing(6);
scrollarea->setWidget(container_widget);
scrollarea->setWidgetResizable(true);
// Add a spacer to the bottom of the container
container_->addStretch();
// Set stylesheet
QFile stylesheet(QStringLiteral(":/style/artistbio.css"));
if (stylesheet.open(QIODevice::ReadOnly)) {
setStyleSheet(QString::fromLatin1(stylesheet.readAll()));
stylesheet.close();
}
fetcher_->AddProvider(new LastFMArtistBio);
fetcher_->AddProvider(new WikipediaArtistBio);
connect(fetcher_, SIGNAL(ResultReady(int, ArtistBioFetcher::Result)), SLOT(ResultReady(int, ArtistBioFetcher::Result)));
connect(fetcher_, SIGNAL(InfoResultReady(int, CollapsibleInfoPane::Data)), SLOT(InfoResultReady(int, CollapsibleInfoPane::Data)));
}
ArtistBioView::~ArtistBioView() {}
void ArtistBioView::showEvent(QShowEvent *e) {
if (dirty_) {
MaybeUpdate(queued_metadata_);
dirty_ = false;
}
QWidget::showEvent(e);
}
void ArtistBioView::ReloadSettings() {
for (CollapsibleInfoPane *pane : sections_) {
QWidget *contents = pane->data().contents_;
if (!contents) continue;
QMetaObject::invokeMethod(contents, "ReloadSettings");
}
}
bool ArtistBioView::NeedsUpdate(const Song &old_metadata, const Song &new_metadata) const {
if (new_metadata.artist().isEmpty()) return false;
return old_metadata.artist() != new_metadata.artist();
}
void ArtistBioView::InfoResultReady(const int id, const CollapsibleInfoPane::Data &_data) {
if (id != current_request_id_) return;
AddSection(new CollapsibleInfoPane(_data, this));
CollapseSections();
}
void ArtistBioView::ResultReady(const int id, const ArtistBioFetcher::Result &result) {
if (id != current_request_id_) return;
if (!result.images_.isEmpty()) {
// Image view goes at the top
PrettyImageView *image_view = new PrettyImageView(network_, this);
AddWidget(image_view);
for (const QUrl& url : result.images_) {
image_view->AddImage(url);
}
}
CollapseSections();
}
void ArtistBioView::Clear() {
fader_->StartFade();
qDeleteAll(widgets_);
widgets_.clear();
if (section_container_) {
container_->removeWidget(section_container_);
delete section_container_;
}
sections_.clear();
// Container for collapsible sections goes below
section_container_ = new QWidget;
section_container_->setLayout(new QVBoxLayout);
section_container_->layout()->setContentsMargins(0, 0, 0, 0);
section_container_->layout()->setSpacing(1);
section_container_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
container_->insertWidget(0, section_container_);
}
void ArtistBioView::AddSection(CollapsibleInfoPane *section) {
int i = 0;
for (; i < sections_.count(); ++i) {
if (section->data() < sections_[i]->data()) break;
}
ConnectWidget(section->data().contents_);
sections_.insert(i, section);
qobject_cast<QVBoxLayout*>(section_container_->layout())->insertWidget(i, section);
section->show();
}
void ArtistBioView::AddWidget(QWidget *widget) {
ConnectWidget(widget);
container_->insertWidget(container_->count() - 2, widget);
widgets_ << widget;
}
void ArtistBioView::SongChanged(const Song &metadata) {
if (isVisible()) {
MaybeUpdate(metadata);
dirty_ = false;
}
else {
queued_metadata_ = metadata;
dirty_ = true;
}
}
void ArtistBioView::SongFinished() { dirty_ = false; }
void ArtistBioView::MaybeUpdate(const Song &metadata) {
if (old_metadata_.is_valid()) {
if (!NeedsUpdate(old_metadata_, metadata)) {
return;
}
}
Update(metadata);
old_metadata_ = metadata;
}
void ArtistBioView::Update(const Song &metadata) {
current_request_id_ = fetcher_->FetchInfo(metadata);
// Do this after the new pane has been shown otherwise it'll just grab a black rectangle.
Clear();
QTimer::singleShot(0, fader_, SLOT(StartBlur()));
}
void ArtistBioView::CollapseSections() {
QSettings s;
s.beginGroup(kSettingsGroup);
// Sections are already sorted by type and relevance, so the algorithm we use to determine which ones to show by default is:
// * In the absence of any user preference, show the first (highest relevance section of each type and hide the rest)
// * If one or more sections in a type have been explicitly hidden/shown by the user before then hide all sections in that type and show only the ones that are explicitly shown.
QMultiMap<CollapsibleInfoPane::Data::Type, CollapsibleInfoPane*> types_;
QSet<CollapsibleInfoPane::Data::Type> has_user_preference_;
for (CollapsibleInfoPane *pane : sections_) {
const CollapsibleInfoPane::Data::Type type = pane->data().type_;
types_.insert(type, pane);
QVariant preference = s.value(pane->data().id_);
if (preference.isValid()) {
has_user_preference_.insert(type);
if (preference.toBool()) {
pane->Expand();
}
}
}
for (CollapsibleInfoPane::Data::Type type : types_.keys()) {
if (!has_user_preference_.contains(type)) {
// Expand the first one
types_.values(type).last()->Expand();
}
}
for (CollapsibleInfoPane *pane : sections_) {
connect(pane, SIGNAL(Toggled(bool)), SLOT(SectionToggled(bool)));
}
}
void ArtistBioView::SectionToggled(const bool value) {
CollapsibleInfoPane *pane = qobject_cast<CollapsibleInfoPane*>(sender());
if (!pane || !sections_.contains(pane)) return;
QSettings s;
s.beginGroup(kSettingsGroup);
s.setValue(pane->data().id_, value);
s.endGroup();
}
void ArtistBioView::ConnectWidget(QWidget *widget) {
const QMetaObject *m = widget->metaObject();
if (m->indexOfSignal("ShowSettingsDialog()") != -1) {
connect(widget, SIGNAL(ShowSettingsDialog()), SIGNAL(ShowSettingsDialog()));
}
}

View File

@@ -0,0 +1,104 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 ARTISTBIOVIEW_H
#define ARTISTBIOVIEW_H
#include <QObject>
#include <QWidget>
#include <QList>
#include "core/song.h"
#include "widgets/collapsibleinfopane.h"
#include "widgets/widgetfadehelper.h"
#include "widgets/collapsibleinfopane.h"
#include "playlist/playlistitem.h"
#include "smartplaylists/playlistgenerator_fwd.h"
#include "artistbiofetcher.h"
class QNetworkAccessManager;
class QTimeLine;
class QVBoxLayout;
class QScrollArea;
class QShowEvent;
class PrettyImageView;
class CollapsibleInfoPane;
class WidgetFadeHelper;
class ArtistBioView : public QWidget {
Q_OBJECT
public:
explicit ArtistBioView(QWidget *parent = nullptr);
~ArtistBioView() override;
static const char *kSettingsGroup;
public Q_SLOTS:
void SongChanged(const Song& metadata);
void SongFinished();
virtual void ReloadSettings();
Q_SIGNALS:
void ShowSettingsDialog();
protected:
void showEvent(QShowEvent *e) override;
void Update(const Song &metadata);
void AddWidget(QWidget *widget);
void AddSection(CollapsibleInfoPane *section);
void Clear();
void CollapseSections();
bool NeedsUpdate(const Song& old_metadata, const Song &new_metadata) const;
protected Q_SLOTS:
void ResultReady(const int id, const ArtistBioFetcher::Result &result);
void InfoResultReady(const int id, const CollapsibleInfoPane::Data &data);
protected:
QNetworkAccessManager *network_;
ArtistBioFetcher *fetcher_;
int current_request_id_;
private:
void MaybeUpdate(const Song &metadata);
void ConnectWidget(QWidget *widget);
private Q_SLOTS:
void SectionToggled(const bool value);
private:
QVBoxLayout *container_;
QList<QWidget*> widgets_;
QWidget *section_container_;
QList<CollapsibleInfoPane*> sections_;
WidgetFadeHelper *fader_;
Song queued_metadata_;
Song old_metadata_;
bool dirty_;
};
#endif // ARTISTBIOVIEW_H

View File

@@ -0,0 +1,209 @@
/*
* Strawberry Music Player
* Copyright 2020, 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 <algorithm>
#include <QtGlobal>
#include <QLocale>
#include <QVariant>
#include <QByteArray>
#include <QString>
#include <QUrl>
#include <QUrlQuery>
#include <QDateTime>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
#include "core/networkaccessmanager.h"
#include "core/song.h"
#include "core/logging.h"
#include "core/iconloader.h"
#include "lastfmartistbio.h"
#include "widgets/infotextview.h"
#include "scrobbler/scrobblingapi20.h"
#include "scrobbler/lastfmscrobbler.h"
LastFMArtistBio::LastFMArtistBio() : ArtistBioProvider(), network_(new NetworkAccessManager(this)) {}
LastFMArtistBio::~LastFMArtistBio() {
while (!replies_.isEmpty()) {
QNetworkReply *reply = replies_.takeFirst();
disconnect(reply, nullptr, this, nullptr);
reply->abort();
reply->deleteLater();
}
}
void LastFMArtistBio::Start(const int id, const Song &song) {
ParamList params = ParamList()
<< Param(QStringLiteral("api_key"), QString::fromLatin1(ScrobblingAPI20::kApiKey))
<< Param(QStringLiteral("lang"), QLocale().name().left(2).toLower())
<< Param(QStringLiteral("format"), QStringLiteral("json"))
<< Param(QStringLiteral("method"), QStringLiteral("artist.getinfo"))
<< Param(QStringLiteral("artist"), song.artist());
std::sort(params.begin(), params.end());
QUrlQuery url_query;
for (const Param &param : params) {
url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first)), QString::fromLatin1(QUrl::toPercentEncoding(param.second)));
}
QUrl url(QString::fromLatin1(LastFMScrobbler::kApiUrl));
url.setQuery(url_query);
QNetworkRequest req(url);
#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
#else
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
#endif
req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded"));
QNetworkReply *reply = network_->get(req);
replies_ << reply;
connect(reply, &QNetworkReply::finished, [=] { RequestFinished(reply, id); });
qLog(Debug) << "Sending request" << url_query.toString(QUrl::FullyDecoded);
}
QByteArray LastFMArtistBio::GetReplyData(QNetworkReply *reply) {
QByteArray data;
if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
data = reply->readAll();
}
else {
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
// This is a network error, there is nothing more to do.
Error(QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
}
else {
QString error;
// See if there is Json data containing "error" and "message" - then use that instead.
data = reply->readAll();
QJsonParseError json_error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
int error_code = -1;
if (json_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) {
QJsonObject json_obj = json_doc.object();
if (json_obj.contains(QLatin1String("error")) && json_obj.contains(QLatin1String("message"))) {
error_code = json_obj[QLatin1String("error")].toInt();
QString error_message = json_obj[QLatin1String("message")].toString();
error = QStringLiteral("%1 (%2)").arg(error_message).arg(error_code);
}
}
if (error.isEmpty()) {
if (reply->error() != QNetworkReply::NoError) {
error = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
else {
error = QStringLiteral("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
}
}
Error(error);
}
return QByteArray();
}
return data;
}
QJsonObject LastFMArtistBio::ExtractJsonObj(const QByteArray &data) {
if (data.isEmpty()) return QJsonObject();
QJsonParseError error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) {
Error(QStringLiteral("Reply from server missing Json data."), data);
return QJsonObject();
}
if (json_doc.isEmpty()) {
Error(QStringLiteral("Received empty Json document."), json_doc);
return QJsonObject();
}
if (!json_doc.isObject()) {
Error(QStringLiteral("Json document is not an object."), json_doc);
return QJsonObject();
}
QJsonObject json_obj = json_doc.object();
if (json_obj.isEmpty()) {
Error(QStringLiteral("Received empty Json object."), json_doc);
return QJsonObject();
}
return json_obj;
}
void LastFMArtistBio::RequestFinished(QNetworkReply *reply, const int id) {
if (!replies_.contains(reply)) return;
replies_.removeAll(reply);
disconnect(reply, nullptr, this, nullptr);
reply->deleteLater();
QJsonObject json_obj = ExtractJsonObj(GetReplyData(reply));
QString title;
QString text;
if (!json_obj.isEmpty() && json_obj.contains(QLatin1String("artist")) && json_obj[QLatin1String("artist")].isObject()) {
json_obj = json_obj[QLatin1String("artist")].toObject();
if (json_obj.contains(QLatin1String("bio")) && json_obj[QLatin1String("bio")].isObject()) {
title = json_obj[QLatin1String("name")].toString();
QJsonObject obj_bio = json_obj[QLatin1String("bio")].toObject();
if (obj_bio.contains(QLatin1String("content"))) {
text = obj_bio[QLatin1String("content")].toString();
}
}
}
CollapsibleInfoPane::Data info_data;
info_data.id_ = title;
info_data.title_ = tr("Biography");
info_data.type_ = CollapsibleInfoPane::Data::Type_Biography;
info_data.icon_ = IconLoader::Load(QStringLiteral("scrobble"));
InfoTextView *editor = new InfoTextView;
editor->SetHtml(text);
info_data.contents_ = editor;
Q_EMIT InfoReady(id, info_data);
Q_EMIT Finished(id);
}
void LastFMArtistBio::Error(const QString &error, const QVariant &debug) {
qLog(Error) << error;
if (debug.isValid()) qLog(Debug) << debug;
}

View File

@@ -0,0 +1,66 @@
/*
* Strawberry Music Player
* Copyright 2020, 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 LASTFMARTISTBIO_H
#define LASTFMARTISTBIO_H
#include "config.h"
#include <QtGlobal>
#include <QObject>
#include <QList>
#include <QVariant>
#include <QByteArray>
#include <QString>
#include "core/song.h"
#include "artistbioprovider.h"
class NetworkAccessManager;
class QNetworkReply;
class LastFMArtistBio : public ArtistBioProvider {
Q_OBJECT
public:
explicit LastFMArtistBio();
~LastFMArtistBio();
void Start(const int id, const Song &song) override;
private:
using Param = QPair<QString, QString>;
using ParamList = QList<Param>;
QNetworkReply *CreateRequest(const ParamList &request_params);
QByteArray GetReplyData(QNetworkReply *reply);
QJsonObject ExtractJsonObj(const QByteArray &data);
void Error(const QString &error, const QVariant &debug = QVariant());
private Q_SLOTS:
void RequestFinished(QNetworkReply *reply, const int id);
private:
NetworkAccessManager *network_;
QList<QNetworkReply*> replies_;
};
#endif // LASTFMARTISTBIO_H

View File

@@ -0,0 +1,319 @@
/*
* Strawberry Music Player
* Copyright 2020, 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 <QObject>
#include <QList>
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QUrlQuery>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonValue>
#include <QNetworkReply>
#include "core/logging.h"
#include "core/networkaccessmanager.h"
#include "core/iconloader.h"
#include "core/latch.h"
#include "widgets/infotextview.h"
#include "wikipediaartistbio.h"
namespace {
constexpr char kApiUrl[] = "https://en.wikipedia.org/w/api.php";
constexpr int kMinimumImageSize = 400;
}
WikipediaArtistBio::WikipediaArtistBio() : ArtistBioProvider(), network_(new NetworkAccessManager(this)) {}
WikipediaArtistBio::~WikipediaArtistBio() {
while (!replies_.isEmpty()) {
QNetworkReply *reply = replies_.takeFirst();
disconnect(reply, nullptr, this, nullptr);
if (reply->isRunning()) reply->abort();
reply->deleteLater();
}
}
QNetworkReply *WikipediaArtistBio::CreateRequest(QList<Param> &params) {
params << Param(QLatin1String("format"), QLatin1String("json"));
params << Param(QLatin1String("action"), QLatin1String("query"));
QUrlQuery url_query;
for (const Param &param : params) {
url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first)), QString::fromLatin1(QUrl::toPercentEncoding(param.second)));
}
QUrl url(QString::fromLatin1(kApiUrl));
url.setQuery(url_query);
QNetworkRequest req(url);
#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
#else
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
#endif
req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded"));
QNetworkReply *reply = network_->get(req);
connect(reply, &QNetworkReply::sslErrors, this, &WikipediaArtistBio::HandleSSLErrors);
replies_ << reply;
return reply;
}
QByteArray WikipediaArtistBio::GetReplyData(QNetworkReply *reply) {
QByteArray data;
if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
data = reply->readAll();
}
else {
if (reply->error() == QNetworkReply::NoError) {
qLog(Error) << "Wikipedia artist biography error: Received HTTP code" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
}
else {
qLog(Error) << "Wikipedia artist biography error:" << reply->error() << reply->errorString();
}
}
return data;
}
QJsonObject WikipediaArtistBio::ExtractJsonObj(const QByteArray &data) {
if (data.isEmpty()) return QJsonObject();
QJsonParseError json_error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
if (json_error.error != QJsonParseError::NoError) {
qLog(Error) << "Wikipedia artist biography error: Failed to parse json data:" << json_error.errorString();
return QJsonObject();
}
if (json_doc.isEmpty()) {
qLog(Error) << "Wikipedia artist biography error: Received empty Json document.";
return QJsonObject();
}
if (!json_doc.isObject()) {
qLog(Error) << "Wikipedia artist biography error: Json document is not an object.";
return QJsonObject();
}
QJsonObject json_obj = json_doc.object();
if (json_obj.isEmpty()) {
qLog(Error) << "Wikipedia artist biography error: Received empty Json object.";
return QJsonObject();
}
return json_obj;
}
void WikipediaArtistBio::HandleSSLErrors(QList<QSslError>) {}
void WikipediaArtistBio::Start(const int id, const Song &metadata) {
if (metadata.artist().isEmpty()) {
Q_EMIT Finished(id);
return;
}
CountdownLatch *latch = new CountdownLatch;
connect(latch, &CountdownLatch::Done, [this, id, latch](){
latch->deleteLater();
Q_EMIT Finished(id);
});
GetImageTitles(id, metadata.artist(), latch);
//GetArticle(id, metadata.artist(), latch);
}
void WikipediaArtistBio::GetArticle(const int id, const QString &artist, CountdownLatch *latch) {
latch->Wait();
ParamList params = ParamList() << Param(QStringLiteral("titles"), artist)
<< Param(QStringLiteral("prop"), QStringLiteral("extracts"));
QNetworkReply *reply = CreateRequest(params);
connect(reply, &QNetworkReply::finished, [this, reply, id, latch]() { GetArticleReply(reply, id, latch); });
}
void WikipediaArtistBio::GetArticleReply(QNetworkReply *reply, const int id, CountdownLatch *latch) {
reply->deleteLater();
replies_.removeAll(reply);
QJsonObject json_obj = ExtractJsonObj(GetReplyData(reply));
QString title;
QString text;
if (!json_obj.isEmpty() && json_obj.contains(QLatin1String("query")) && json_obj[QLatin1String("query")].isObject()) {
json_obj = json_obj[QLatin1String("query")].toObject();
if (json_obj.contains(QLatin1String("pages")) && json_obj[QLatin1String("pages")].isObject()) {
QJsonObject value_pages = json_obj[QLatin1String("pages")].toObject();
for (const QJsonValue value_page : value_pages) {
if (!value_page.isObject()) continue;
QJsonObject obj_page = value_page.toObject();
if (!obj_page.contains(QLatin1String("title")) || !obj_page.contains(QLatin1String("extract"))) continue;
title = obj_page[QLatin1String("title")].toString();
text = obj_page[QLatin1String("extract")].toString();
}
}
}
CollapsibleInfoPane::Data info_data;
info_data.id_ = title;
info_data.title_ = tr("Biography");
info_data.type_ = CollapsibleInfoPane::Data::Type_Biography;
info_data.icon_ = IconLoader::Load(QStringLiteral("wikipedia"));
InfoTextView *editor = new InfoTextView;
editor->SetHtml(text);
info_data.contents_ = editor;
Q_EMIT InfoReady(id, info_data);
latch->CountDown();
}
void WikipediaArtistBio::GetImageTitles(const int id, const QString &artist, CountdownLatch *latch) {
latch->Wait();
ParamList params = ParamList() << Param(QStringLiteral("titles"), artist)
<< Param(QStringLiteral("prop"), QStringLiteral("images"))
<< Param(QStringLiteral("imlimit"), QString::number(25));
QNetworkReply *reply = CreateRequest(params);
connect(reply, &QNetworkReply::finished, [this, reply, id, latch]() { GetImageTitlesFinished(reply, id, latch); });
}
void WikipediaArtistBio::GetImageTitlesFinished(QNetworkReply *reply, const int id, CountdownLatch *latch) {
reply->deleteLater();
replies_.removeAll(reply);
QJsonObject json_obj = ExtractJsonObj(GetReplyData(reply));
QString title;
QStringList titles;
if (!json_obj.isEmpty() && json_obj.contains(QLatin1String("query")) && json_obj[QLatin1String("query")].isObject()) {
json_obj = json_obj[QLatin1String("query")].toObject();
if (json_obj.contains(QLatin1String("pages")) && json_obj[QLatin1String("pages")].isObject()) {
QJsonObject value_pages = json_obj[QLatin1String("pages")].toObject();
for (const QJsonValue value_page : value_pages) {
if (!value_page.isObject()) continue;
QJsonObject obj_page = value_page.toObject();
if (!obj_page.contains(QLatin1String("title")) || !obj_page.contains(QLatin1String("images")) || !obj_page[QLatin1String("images")].isArray()) continue;
title = obj_page[QLatin1String("title")].toString();
QJsonArray array_images = obj_page[QLatin1String("images")].toArray();
for (const QJsonValue value_image : array_images) {
if (!value_image.isObject()) continue;
QJsonObject obj_image = value_image.toObject();
if (!obj_image.contains(QLatin1String("title"))) continue;
QString filename = obj_image[QLatin1String("title")].toString();
if (filename.endsWith(QLatin1String(".jpg"), Qt::CaseInsensitive) || filename.endsWith(QLatin1String(".png"), Qt::CaseInsensitive)) {
titles << filename;
}
}
}
}
}
for (const QString &image_title : titles) {
GetImage(id, image_title, latch);
}
latch->CountDown();
}
void WikipediaArtistBio::GetImage(const int id, const QString &title, CountdownLatch *latch) {
latch->Wait();
ParamList params2 = ParamList() << Param(QStringLiteral("titles"), title)
<< Param(QStringLiteral("prop"), QStringLiteral("imageinfo"))
<< Param(QStringLiteral("iiprop"), QStringLiteral("url|size"));
QNetworkReply *reply = CreateRequest(params2);
connect(reply, &QNetworkReply::finished, [this, reply, id, latch]() { GetImageFinished(reply, id, latch); });
}
void WikipediaArtistBio::GetImageFinished(QNetworkReply *reply, const int id, CountdownLatch *latch) {
reply->deleteLater();
replies_.removeAll(reply);
QJsonObject json_obj = ExtractJsonObj(GetReplyData(reply));
if (!json_obj.isEmpty()) {
QList<QUrl> urls = ExtractImageUrls(json_obj);
for (const QUrl &url : urls) {
Q_EMIT ImageReady(id, url);
}
}
latch->CountDown();
}
QList<QUrl> WikipediaArtistBio::ExtractImageUrls(QJsonObject json_obj) {
QList<QUrl> urls;
if (json_obj.contains(QLatin1String("query")) && json_obj[QLatin1String("query")].isObject()) {
json_obj = json_obj[QLatin1String("query")].toObject();
if (json_obj.contains(QLatin1String("pages")) && json_obj[QLatin1String("pages")].isObject()) {
QJsonObject value_pages = json_obj[QLatin1String("pages")].toObject();
for (const QJsonValue value_page : value_pages) {
if (!value_page.isObject()) continue;
QJsonObject obj_page = value_page.toObject();
if (!obj_page.contains(QLatin1String("title")) || !obj_page.contains(QLatin1String("imageinfo")) || !obj_page[QLatin1String("imageinfo")].isArray()) continue;
QJsonArray array_images = obj_page[QLatin1String("imageinfo")].toArray();
for (const QJsonValue value_image : array_images) {
if (!value_image.isObject()) continue;
QJsonObject obj_image = value_image.toObject();
if (!obj_image.contains(QLatin1String("url")) || !obj_image.contains(QLatin1String("width")) || !obj_image.contains(QLatin1String("height"))) continue;
QUrl url(obj_image[QLatin1String("url")].toString());
const int width = obj_image[QLatin1String("width")].toInt();
const int height = obj_image[QLatin1String("height")].toInt();
if (!url.isValid() || width < kMinimumImageSize || height < kMinimumImageSize) continue;
urls << url;
}
}
}
}
return urls;
}

View File

@@ -0,0 +1,69 @@
/*
* Strawberry Music Player
* Copyright 2020, 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 WIKIPEDIAARTISTBIO_H
#define WIKIPEDIAARTISTBIO_H
#include <QList>
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QSslError>
#include <QJsonObject>
#include "artistbioprovider.h"
class QNetworkReply;
class CountdownLatch;
class NetworkAccessManager;
class WikipediaArtistBio : public ArtistBioProvider {
Q_OBJECT
public:
explicit WikipediaArtistBio();
~WikipediaArtistBio();
void Start(const int id, const Song &song) override;
private:
typedef QPair<QString, QString> Param;
typedef QList<Param> ParamList;
QNetworkReply *CreateRequest(QList<Param> &params);
QByteArray GetReplyData(QNetworkReply *reply);
QJsonObject ExtractJsonObj(const QByteArray &data);
void GetArticle(const int id, const QString &artist, CountdownLatch* latch);
void GetImageTitles(const int id, const QString &artist, CountdownLatch* latch);
void GetImage(const int id, const QString &title, CountdownLatch *latch);
QList<QUrl> ExtractImageUrls(QJsonObject json_obj);
private Q_SLOTS:
void HandleSSLErrors(QList<QSslError> ssl_errors);
void GetArticleReply(QNetworkReply *reply, const int id, CountdownLatch *latch);
void GetImageTitlesFinished(QNetworkReply *reply, const int id, CountdownLatch *latch);
void GetImageFinished(QNetworkReply *reply, const int id, CountdownLatch *latch);
private:
NetworkAccessManager *network_;
QList<QNetworkReply*> replies_;
};
#endif // WIKIPEDIAARTISTBIO_H

View File

@@ -14,7 +14,6 @@
#cmakedefine HAVE_GIO_UNIX
#cmakedefine HAVE_DBUS
#cmakedefine HAVE_MPRIS2
#cmakedefine HAVE_WINDOWS_MEDIA_CONTROLS
#cmakedefine HAVE_UDISKS2
#cmakedefine HAVE_AUDIOCD
#cmakedefine HAVE_MTP

View File

@@ -110,21 +110,32 @@ bool FilesystemMusicStorage::CopyToStorage(const CopyJob &job, QString &error_te
bool FilesystemMusicStorage::DeleteFromStorage(const DeleteJob &job) {
QString path = job.metadata_.url().toLocalFile();
QFileInfo fileInfo(path);
const QString path = job.metadata_.url().toLocalFile();
const QFileInfo fileInfo(path);
#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
if (job.use_trash_ && QFile::supportsMoveToTrash()) {
#else
if (job.use_trash_) {
#endif
return QFile::moveToTrash(path);
if (QFile::moveToTrash(path)) {
return true;
}
qLog(Warning) << "Moving file to trash failed for" << path << ", falling back to direct deletion";
}
bool success = false;
if (fileInfo.isDir()) {
return Utilities::RemoveRecursive(path);
success = Utilities::RemoveRecursive(path);
}
else {
success = QFile::remove(path);
}
return QFile::remove(path);
if (!success) {
qLog(Error) << "Failed to delete file" << path;
}
return success;
}

39
src/core/latch.cpp Normal file
View File

@@ -0,0 +1,39 @@
/* This file is part of Clementine.
Copyright 2016, John Maguire <john.maguire@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include <QMutexLocker>
#include "latch.h"
CountdownLatch::CountdownLatch() : count_(0) {}
void CountdownLatch::Wait() {
QMutexLocker l(&mutex_);
++count_;
}
void CountdownLatch::CountDown() {
QMutexLocker l(&mutex_);
Q_ASSERT(count_ > 0);
--count_;
if (count_ == 0) {
emit Done();
}
}

39
src/core/latch.h Normal file
View File

@@ -0,0 +1,39 @@
/* This file is part of Clementine.
Copyright 2016, John Maguire <john.maguire@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef LATCH_H
#define LATCH_H
#include <QObject>
#include <QMutex>
class CountdownLatch : public QObject {
Q_OBJECT
public:
explicit CountdownLatch();
void Wait();
void CountDown();
Q_SIGNALS:
void Done();
private:
QMutex mutex_;
int count_;
};
#endif // LATCH_H

View File

@@ -205,6 +205,7 @@
#include "smartplaylists/smartplaylistsviewcontainer.h"
#include "organize/organizeerrordialog.h"
#include "artistbio/artistbioview.h"
#ifdef Q_OS_WIN32
# include "core/windows7thumbbar.h"
@@ -232,6 +233,7 @@
using std::make_unique;
using std::make_shared;
using namespace std::chrono_literals;
using namespace Qt::Literals::StringLiterals;
@@ -357,6 +359,7 @@ MainWindow::MainWindow(Application *app,
qobuz_view_(new StreamingTabsView(app->streaming_services()->ServiceBySource(Song::Source::Qobuz), app->albumcover_loader(), QLatin1String(QobuzSettings::kSettingsGroup), this)),
#endif
radio_view_(new RadioViewContainer(this)),
artistbio_view_(new ArtistBioView(this)),
lastfm_import_dialog_(new LastFMImportDialog(app_->lastfm_import(), this)),
collection_show_all_(nullptr),
collection_show_duplicates_(nullptr),
@@ -441,6 +444,7 @@ MainWindow::MainWindow(Application *app,
#ifdef HAVE_QOBUZ
ui_->tabs->AddTab(qobuz_view_, u"qobuz"_s, IconLoader::Load(u"qobuz"_s, true, 0, 32), tr("Qobuz"));
#endif
ui_->tabs->AddTab(artistbio_view_, QStringLiteral("artistbio"), IconLoader::Load(QStringLiteral("guitar")), tr("Artist biography"));
// Add the playing widget to the fancy tab widget
ui_->tabs->AddBottomWidget(ui_->widget_playing);
@@ -977,6 +981,10 @@ MainWindow::MainWindow(Application *app,
ui_->action_open_cd->setVisible(false);
#endif
connect(&*app_->playlist_manager(), &PlaylistManager::CurrentSongChanged, artistbio_view_, &ArtistBioView::SongChanged);
connect(&*app_->player(), &Player::PlaylistFinished, artistbio_view_, &ArtistBioView::SongFinished);
connect(&*app_->player(), &Player::Stopped, artistbio_view_, &ArtistBioView::SongFinished);
// Load settings
qLog(Debug) << "Loading settings";
Settings settings;
@@ -1232,6 +1240,16 @@ void MainWindow::ReloadSettings() {
album_cover_choice_controller_->search_cover_auto_action()->setChecked(s.value(MainWindowSettings::kSearchForCoverAuto, true).toBool());
s.endGroup();
s.beginGroup(BehaviourSettings::kSettingsGroup);
bool artistbio = s.value("artistbio", false).toBool();
s.endGroup();
if (artistbio) {
ui_->tabs->EnableTab(artistbio_view_);
}
else {
ui_->tabs->DisableTab(artistbio_view_);
}
#ifdef HAVE_SUBSONIC
s.beginGroup(SubsonicSettings::kSettingsGroup);
bool enable_subsonic = s.value(SubsonicSettings::kEnabled, false).toBool();

View File

@@ -97,6 +97,7 @@ class Windows7ThumbBar;
class AddStreamDialog;
class LastFMImportDialog;
class RadioViewContainer;
class ArtistBioView;
#ifdef HAVE_DISCORD_RPC
namespace discord {
@@ -358,6 +359,8 @@ class MainWindow : public QMainWindow, public PlatformInterface {
RadioViewContainer *radio_view_;
ArtistBioView *artistbio_view_;
LastFMImportDialog *lastfm_import_dialog_;
QAction *collection_show_all_;

View File

@@ -68,6 +68,7 @@
#include "smartplaylists/playlistgenerator_fwd.h"
#include "radios/radiochannel.h"
#include "widgets/collapsibleinfopane.h"
#ifdef HAVE_MTP
# include "device/mtpconnection.h"
@@ -147,6 +148,8 @@ void RegisterMetaTypes() {
qRegisterMetaType<RadioChannel>("RadioChannel");
qRegisterMetaType<RadioChannelList>("RadioChannelList");
qRegisterMetaType<CollapsibleInfoPane::Data>("CollapsibleInfoPane::Data");
#ifdef HAVE_MTP
qRegisterMetaType<MtpConnection*>("MtpConnection*");
#endif

View File

@@ -98,6 +98,9 @@ SongLoader::SongLoader(const SharedPtr<UrlHandlers> url_handlers,
QObject::connect(timeout_timer_, &QTimer::timeout, this, &SongLoader::Timeout);
QObject::connect(playlist_parser_, &PlaylistParser::Error, this, &SongLoader::ParserError);
QObject::connect(cue_parser_, &CueParser::Error, this, &SongLoader::ParserError);
}
SongLoader::~SongLoader() {
@@ -106,6 +109,10 @@ SongLoader::~SongLoader() {
}
void SongLoader::ParserError(const QString &error) {
errors_ << error;
}
SongLoader::Result SongLoader::Load(const QUrl &url) {
if (url.isEmpty()) return Result::Error;
@@ -287,6 +294,7 @@ SongLoader::Result SongLoader::LoadLocalAsync(const QString &filename) {
}
if (parser) { // It's a playlist!
QObject::connect(parser, &ParserBase::Error, this, &SongLoader::ParserError, static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::UniqueConnection));
qLog(Debug) << "Parsing using" << parser->name();
LoadPlaylist(parser, filename);
return Result::Success;
@@ -706,6 +714,10 @@ void SongLoader::MagicReady() {
StopTypefindAsync(true);
}
if (parser_) {
QObject::connect(parser_, &ParserBase::Error, this, &SongLoader::ParserError, static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::UniqueConnection));
}
state_ = State::WaitingForData;
if (!IsPipelinePlaying()) {

View File

@@ -99,6 +99,7 @@ class SongLoader : public QObject {
void ScheduleTimeout();
void Timeout();
void StopTypefind();
void ParserError(const QString &error);
#ifdef HAVE_AUDIOCD
void AudioCDTracksLoadErrorSlot(const QString &error);

View File

@@ -1,303 +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/>.
*
*/
#include "config.h"
#include <windows.h>
#include <QObject>
#include <QString>
#include <QUrl>
// Undefine 'interface' macro from windows.h before including WinRT headers
#pragma push_macro("interface")
#undef interface
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Media.h>
#include <winrt/Windows.Storage.h>
#include <winrt/Windows.Storage.Streams.h>
#pragma pop_macro("interface")
// Include the interop header for ISystemMediaTransportControlsInterop
#include <systemmediatransportcontrolsinterop.h>
#include "core/logging.h"
#include "windowsmediacontroller.h"
#include "core/song.h"
#include "core/player.h"
#include "engine/enginebase.h"
#include "playlist/playlistmanager.h"
#include "covermanager/currentalbumcoverloader.h"
#include "covermanager/albumcoverloaderresult.h"
using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Media;
using namespace Windows::Storage;
using namespace Windows::Storage::Streams;
// Helper struct to hold the WinRT object
struct WindowsMediaControllerPrivate {
SystemMediaTransportControls smtc{nullptr};
};
WindowsMediaController::WindowsMediaController(HWND hwnd,
const SharedPtr<Player> player,
const SharedPtr<PlaylistManager> playlist_manager,
const SharedPtr<CurrentAlbumCoverLoader> current_albumcover_loader,
QObject *parent)
: QObject(parent),
player_(player),
playlist_manager_(playlist_manager),
current_albumcover_loader_(current_albumcover_loader),
smtc_(nullptr),
apartment_initialized_(false) {
try {
// Initialize WinRT apartment if not already initialized
// Qt or other components may have already initialized it
try {
winrt::init_apartment(winrt::apartment_type::single_threaded);
apartment_initialized_ = true;
}
catch (const hresult_error &e) {
// Apartment already initialized - this is fine, continue
if (e.code() != RPC_E_CHANGED_MODE) {
throw;
}
}
// Create private implementation
auto *priv = new WindowsMediaControllerPrivate();
smtc_ = priv;
// Get the SystemMediaTransportControls instance for this window
// Use the interop interface
auto interop = winrt::get_activation_factory<SystemMediaTransportControls, ISystemMediaTransportControlsInterop>();
if (!interop) {
qLog(Warning) << "Failed to get ISystemMediaTransportControlsInterop";
delete priv;
smtc_ = nullptr;
return;
}
// Get SMTC for the window
winrt::com_ptr<IInspectable> inspectable;
HRESULT hr = interop->GetForWindow(hwnd, winrt::guid_of<SystemMediaTransportControls>(), inspectable.put_void());
if (FAILED(hr) || !inspectable) {
qLog(Warning) << "Failed to get SystemMediaTransportControls for window, HRESULT:" << Qt::hex << static_cast<unsigned int>(hr);
delete priv;
smtc_ = nullptr;
return;
}
// Convert to SystemMediaTransportControls
priv->smtc = inspectable.as<SystemMediaTransportControls>();
if (!priv->smtc) {
qLog(Warning) << "Failed to cast to SystemMediaTransportControls";
delete priv;
smtc_ = nullptr;
return;
}
// Enable the controls
priv->smtc.IsEnabled(true);
priv->smtc.IsPlayEnabled(true);
priv->smtc.IsPauseEnabled(true);
priv->smtc.IsStopEnabled(true);
priv->smtc.IsNextEnabled(true);
priv->smtc.IsPreviousEnabled(true);
// Setup button handlers
SetupButtonHandlers();
// Connect signals from Player
QObject::connect(&*player_->engine(), &EngineBase::StateChanged, this, &WindowsMediaController::EngineStateChanged);
QObject::connect(&*playlist_manager_, &PlaylistManager::CurrentSongChanged, this, &WindowsMediaController::CurrentSongChanged);
QObject::connect(&*current_albumcover_loader_, &CurrentAlbumCoverLoader::AlbumCoverLoaded, this, &WindowsMediaController::AlbumCoverLoaded);
qLog(Info) << "Windows Media Transport Controls initialized successfully";
}
catch (const hresult_error &e) {
qLog(Warning) << "Failed to initialize Windows Media Transport Controls:" << QString::fromWCharArray(e.message().c_str());
if (smtc_) {
delete static_cast<WindowsMediaControllerPrivate*>(smtc_);
smtc_ = nullptr;
}
}
catch (...) {
qLog(Warning) << "Failed to initialize Windows Media Transport Controls: unknown error";
if (smtc_) {
delete static_cast<WindowsMediaControllerPrivate*>(smtc_);
smtc_ = nullptr;
}
}
}
WindowsMediaController::~WindowsMediaController() {
if (smtc_) {
auto *priv = static_cast<WindowsMediaControllerPrivate*>(smtc_);
if (priv->smtc) {
priv->smtc.IsEnabled(false);
}
delete priv;
smtc_ = nullptr;
}
// Only uninit if we initialized the apartment
if (apartment_initialized_) {
winrt::uninit_apartment();
}
}
void WindowsMediaController::SetupButtonHandlers() {
if (!smtc_) return;
auto *priv = static_cast<WindowsMediaControllerPrivate*>(smtc_);
if (!priv->smtc) return;
// Handle button pressed events
priv->smtc.ButtonPressed([this](const SystemMediaTransportControls &, const SystemMediaTransportControlsButtonPressedEventArgs &args) {
switch (args.Button()) {
case SystemMediaTransportControlsButton::Play:
player_->Play();
break;
case SystemMediaTransportControlsButton::Pause:
player_->Pause();
break;
case SystemMediaTransportControlsButton::Stop:
player_->Stop();
break;
case SystemMediaTransportControlsButton::Next:
player_->Next();
break;
case SystemMediaTransportControlsButton::Previous:
player_->Previous();
break;
default:
break;
}
});
}
void WindowsMediaController::EngineStateChanged(EngineBase::State newState) {
UpdatePlaybackStatus(newState);
}
void WindowsMediaController::UpdatePlaybackStatus(EngineBase::State state) {
if (!smtc_) return;
auto *priv = static_cast<WindowsMediaControllerPrivate*>(smtc_);
if (!priv->smtc) return;
try {
switch (state) {
case EngineBase::State::Playing:
priv->smtc.PlaybackStatus(MediaPlaybackStatus::Playing);
break;
case EngineBase::State::Paused:
priv->smtc.PlaybackStatus(MediaPlaybackStatus::Paused);
break;
case EngineBase::State::Empty:
case EngineBase::State::Idle:
priv->smtc.PlaybackStatus(MediaPlaybackStatus::Stopped);
break;
}
}
catch (const hresult_error &e) {
qLog(Warning) << "Failed to update playback status:" << QString::fromWCharArray(e.message().c_str());
}
}
void WindowsMediaController::CurrentSongChanged(const Song &song) {
if (!song.is_valid()) {
return;
}
// Update metadata immediately with what we have
UpdateMetadata(song, QUrl());
// Album cover will be updated via AlbumCoverLoaded signal
}
void WindowsMediaController::AlbumCoverLoaded(const Song &song, const AlbumCoverLoaderResult &result) {
if (!song.is_valid()) {
return;
}
// Update metadata with album cover
UpdateMetadata(song, result.temp_cover_url.isEmpty() ? result.album_cover.cover_url : result.temp_cover_url);
}
void WindowsMediaController::UpdateMetadata(const Song &song, const QUrl &art_url) {
if (!smtc_) return;
auto *priv = static_cast<WindowsMediaControllerPrivate*>(smtc_);
if (!priv->smtc) return;
try {
// Get the updater
SystemMediaTransportControlsDisplayUpdater updater = priv->smtc.DisplayUpdater();
updater.Type(MediaPlaybackType::Music);
// Get the music properties
auto musicProperties = updater.MusicProperties();
// Set basic metadata
if (!song.title().isEmpty()) {
musicProperties.Title(winrt::hstring(song.title().toStdWString()));
}
if (!song.artist().isEmpty()) {
musicProperties.Artist(winrt::hstring(song.artist().toStdWString()));
}
if (!song.album().isEmpty()) {
musicProperties.AlbumTitle(winrt::hstring(song.album().toStdWString()));
}
// Set album art if available
if (art_url.isValid() && art_url.isLocalFile()) {
QString artPath = art_url.toLocalFile();
if (!artPath.isEmpty()) {
try {
// Use file:// URI to avoid async blocking in STA thread
QString fileUri = QUrl::fromLocalFile(artPath).toString();
auto thumbnailStream = RandomAccessStreamReference::CreateFromUri(
winrt::Windows::Foundation::Uri(winrt::hstring(fileUri.toStdWString()))
);
updater.Thumbnail(thumbnailStream);
current_song_art_url_ = artPath;
}
catch (const hresult_error &e) {
qLog(Debug) << "Failed to set album art:" << QString::fromWCharArray(e.message().c_str());
}
}
}
// Update the display
updater.Update();
}
catch (const hresult_error &e) {
qLog(Warning) << "Failed to update metadata:" << QString::fromWCharArray(e.message().c_str());
}
}

View File

@@ -1,69 +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 WINDOWSMEDIACONTROLLER_H
#define WINDOWSMEDIACONTROLLER_H
#include "config.h"
#include <windows.h>
#include <QObject>
#include <QString>
#include "includes/shared_ptr.h"
#include "engine/enginebase.h"
#include "covermanager/albumcoverloaderresult.h"
class Player;
class PlaylistManager;
class CurrentAlbumCoverLoader;
class Song;
class WindowsMediaController : public QObject {
Q_OBJECT
public:
explicit WindowsMediaController(HWND hwnd,
const SharedPtr<Player> player,
const SharedPtr<PlaylistManager> playlist_manager,
const SharedPtr<CurrentAlbumCoverLoader> current_albumcover_loader,
QObject *parent = nullptr);
~WindowsMediaController() override;
private Q_SLOTS:
void AlbumCoverLoaded(const Song &song, const AlbumCoverLoaderResult &result = AlbumCoverLoaderResult());
void EngineStateChanged(EngineBase::State newState);
void CurrentSongChanged(const Song &song);
private:
void UpdatePlaybackStatus(EngineBase::State state);
void UpdateMetadata(const Song &song, const QUrl &art_url);
void SetupButtonHandlers();
private:
const SharedPtr<Player> player_;
const SharedPtr<PlaylistManager> playlist_manager_;
const SharedPtr<CurrentAlbumCoverLoader> current_albumcover_loader_;
void *smtc_; // Pointer to SystemMediaTransportControls (opaque to avoid WinRT headers in public header)
QString current_song_art_url_;
bool apartment_initialized_; // Track if we initialized the WinRT apartment
};
#endif // WINDOWSMEDIACONTROLLER_H

View File

@@ -178,7 +178,6 @@ GstEnginePipeline::GstEnginePipeline(QObject *parent)
audiobin_(nullptr),
audiosink_(nullptr),
audioqueue_(nullptr),
audioqueueconverter_(nullptr),
volume_(nullptr),
volume_sw_(nullptr),
volume_fading_(nullptr),
@@ -187,6 +186,7 @@ GstEnginePipeline::GstEnginePipeline(QObject *parent)
equalizer_(nullptr),
equalizer_preamp_(nullptr),
eventprobe_(nullptr),
bufferprobe_(nullptr),
logged_unsupported_analyzer_format_(false),
about_to_finish_(false),
finish_requested_(false),
@@ -436,7 +436,7 @@ void GstEnginePipeline::Disconnect() {
}
if (buffer_probe_cb_id_.has_value()) {
GstPad *pad = gst_element_get_static_pad(audioqueueconverter_, "src");
GstPad *pad = gst_element_get_static_pad(bufferprobe_, "src");
if (pad) {
gst_pad_remove_probe(pad, buffer_probe_cb_id_.value());
gst_object_unref(pad);
@@ -674,8 +674,13 @@ bool GstEnginePipeline::InitAudioBin(QString &error) {
return false;
}
audioqueueconverter_ = CreateElement(u"audioconvert"_s, u"audioqueueconverter"_s, audiobin_, error);
if (!audioqueueconverter_) {
GstElement *audioqueueconverter = CreateElement(u"audioconvert"_s, u"audioqueueconverter"_s, audiobin_, error);
if (!audioqueueconverter) {
return false;
}
GstElement *audioqueueresampler = CreateElement(u"audioresample"_s, u"audioqueueresampler"_s, audiobin_, error);
if (!audioqueueresampler) {
return false;
}
@@ -684,6 +689,11 @@ bool GstEnginePipeline::InitAudioBin(QString &error) {
return false;
}
GstElement *audiosinkresampler = CreateElement(u"audioresample"_s, u"audiosinkresampler"_s, audiobin_, error);
if (!audiosinkresampler) {
return false;
}
// Create the volume element if it's enabled.
if (volume_enabled_ && !volume_) {
volume_sw_ = CreateElement(u"volume"_s, u"volume_sw"_s, audiobin_, error);
@@ -761,7 +771,8 @@ bool GstEnginePipeline::InitAudioBin(QString &error) {
}
eventprobe_ = audioqueueconverter_;
eventprobe_ = audioqueueconverter;
bufferprobe_ = audioqueueconverter;
// Create the replaygain elements if it's enabled.
GstElement *rgvolume = nullptr;
@@ -847,12 +858,17 @@ bool GstEnginePipeline::InitAudioBin(QString &error) {
// Link all elements
if (!gst_element_link(audioqueue_, audioqueueconverter_)) {
if (!gst_element_link(audioqueue_, audioqueueconverter)) {
error = u"Failed to link audio queue to audio queue converter."_s;
return false;
}
GstElement *element_link = audioqueueconverter_; // The next element to link from.
if (!gst_element_link(audioqueueconverter, audioqueueresampler)) {
error = u"Failed to link audio queue converter to audio queue resampler."_s;
return false;
}
GstElement *element_link = audioqueueresampler; // The next element to link from.
// Link replaygain elements if enabled.
if (rg_enabled_ && rgvolume && rglimiter && rgconverter) {
@@ -928,6 +944,11 @@ bool GstEnginePipeline::InitAudioBin(QString &error) {
return false;
}
if (!gst_element_link(audiosinkconverter, audiosinkresampler)) {
error = "Failed to link audio sink converter to audio sink resampler."_L1;
return false;
}
{
GstCaps *caps = gst_caps_new_empty_simple("audio/x-raw");
if (!caps) {
@@ -938,16 +959,16 @@ bool GstEnginePipeline::InitAudioBin(QString &error) {
qLog(Debug) << "Setting channels to" << channels_;
gst_caps_set_simple(caps, "channels", G_TYPE_INT, channels_, nullptr);
}
const bool link_filtered_result = gst_element_link_filtered(audiosinkconverter, audiosink_, caps);
const bool link_filtered_result = gst_element_link_filtered(audiosinkresampler, audiosink_, caps);
gst_caps_unref(caps);
if (!link_filtered_result) {
error = "Failed to link audio sink converter to audio sink with filter for "_L1 + output_;
error = "Failed to link audio sink resampler to audio sink with filter for "_L1 + output_;
return false;
}
}
{ // Add probes and handlers.
GstPad *pad = gst_element_get_static_pad(audioqueueconverter_, "src");
GstPad *pad = gst_element_get_static_pad(bufferprobe_, "src");
if (pad) {
buffer_probe_cb_id_ = gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_BUFFER, BufferProbeCallback, this, nullptr);
gst_object_unref(pad);

View File

@@ -355,7 +355,6 @@ class GstEnginePipeline : public QObject {
GstElement *audiobin_;
GstElement *audiosink_;
GstElement *audioqueue_;
GstElement *audioqueueconverter_;
GstElement *volume_;
GstElement *volume_sw_;
GstElement *volume_fading_;
@@ -364,6 +363,7 @@ class GstEnginePipeline : public QObject {
GstElement *equalizer_;
GstElement *equalizer_preamp_;
GstElement *eventprobe_;
GstElement *bufferprobe_;
std::optional<gulong> upstream_events_probe_cb_id_;
std::optional<gulong> buffer_probe_cb_id_;

View File

@@ -93,10 +93,6 @@
# include "discord/richpresence.h"
#endif
#ifdef HAVE_WINDOWS_MEDIA_CONTROLS
# include "core/windowsmediacontroller.h"
#endif
#include "core/iconloader.h"
#include "core/commandlineoptions.h"
#include "core/networkproxyfactory.h"
@@ -369,11 +365,6 @@ int main(int argc, char *argv[]) {
#endif
options);
#ifdef HAVE_WINDOWS_MEDIA_CONTROLS
// Initialize Windows Media Transport Controls
WindowsMediaController windows_media_controller(reinterpret_cast<HWND>(w.winId()), app.player(), app.playlist_manager(), app.current_albumcover_loader());
#endif
#ifdef Q_OS_MACOS
mac::EnableFullScreen(w);
#endif // Q_OS_MACOS

View File

@@ -80,12 +80,13 @@ void SongLoaderInserter::Load(Playlist *destination, const int row, const bool p
songs_ << loader->songs();
playlist_name_ = loader->playlist_name();
}
else {
const QStringList errors = loader->errors();
for (const QString &error : errors) {
Q_EMIT Error(error);
}
// Always check for errors, even on success (e.g., playlist parsed but some songs failed to load)
const QStringList errors = loader->errors();
for (const QString &error : errors) {
Q_EMIT Error(error);
}
delete loader;
}
@@ -192,11 +193,13 @@ void SongLoaderInserter::AsyncLoad() {
const SongLoader::Result result = loader->LoadFilenamesBlocking();
task_manager_->SetTaskProgress(async_load_id, static_cast<quint64>(++async_progress));
// Always check for errors, even on success (e.g., playlist parsed but some songs failed to load)
const QStringList errors = loader->errors();
for (const QString &error : errors) {
Q_EMIT Error(error);
}
if (result == SongLoader::Result::Error) {
const QStringList errors = loader->errors();
for (const QString &error : errors) {
Q_EMIT Error(error);
}
continue;
}

View File

@@ -112,10 +112,18 @@ void ParserBase::LoadSong(const QString &filename_or_url, const qint64 beginning
}
}
// Check if the file exists before trying to read it
if (!QFile::exists(filename)) {
qLog(Error) << "File does not exist:" << filename;
Q_EMIT Error(tr("File %1 does not exist").arg(filename));
return;
}
if (tagreader_client_) {
const TagReaderResult result = tagreader_client_->ReadFileBlocking(filename, song);
if (!result.success()) {
qLog(Error) << "Could not read file" << filename << result.error_string();
Q_EMIT Error(tr("Could not read file %1: %2").arg(filename, result.error_string()));
}
}

View File

@@ -165,6 +165,7 @@ void BehaviourSettingsPage::Load() {
ui_->checkbox_resumeplayback->setChecked(s.value(kResumePlayback, false).toBool());
ui_->checkbox_playingwidget->setChecked(s.value(kPlayingWidget, true).toBool());
ui_->checkbox_artistbio->setChecked(s.value("artistbio", false).toBool());
#ifndef Q_OS_MACOS
const StartupBehaviour startup_behaviour = static_cast<StartupBehaviour>(s.value(kStartupBehaviour, static_cast<int>(StartupBehaviour::Remember)).toInt());
@@ -232,9 +233,12 @@ void BehaviourSettingsPage::Save() {
#if defined(HAVE_DBUS) && !defined(Q_OS_MACOS)
s.setValue(kTaskbarProgress, ui_->checkbox_taskbar_progress->isChecked());
#endif
s.setValue(kResumePlayback, ui_->checkbox_resumeplayback->isChecked());
s.setValue(kPlayingWidget, ui_->checkbox_playingwidget->isChecked());
s.setValue("artistbio", ui_->checkbox_artistbio->isChecked());
StartupBehaviour startup_behaviour = StartupBehaviour::Remember;
if (ui_->radiobutton_remember->isChecked()) startup_behaviour = StartupBehaviour::Remember;
if (ui_->radiobutton_show->isChecked()) startup_behaviour = StartupBehaviour::Show;

View File

@@ -65,6 +65,13 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkbox_artistbio">
<property name="text">
<string>Artist biography</string>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupbox_startup">
<property name="title">

View File

@@ -5526,7 +5526,7 @@ Are you sure you want to continue?</source>
<name>RadioParadiseService</name>
<message>
<source>Getting %1 channels</source>
<translation>Получение %1 каналов</translation>
<translation>Получение каналов %1</translation>
</message>
</context>
<context>
@@ -6191,7 +6191,7 @@ Are you sure you want to continue?</source>
<name>SomaFMService</name>
<message>
<source>Getting %1 channels</source>
<translation>Получение %1 каналов</translation>
<translation>Получение каналов %1</translation>
</message>
</context>
<context>

View File

@@ -1187,7 +1187,7 @@ If there are no matches then it will use the largest image in the directory.</tr
</message>
<message>
<source>Queue to play next</source>
<translation>Sıradaki Yap</translation>
<translation>Sıradaki yap</translation>
</message>
<message>
<source>Search for this</source>
@@ -3496,7 +3496,7 @@ If there are no matches then it will use the largest image in the directory.</tr
</message>
<message>
<source>Queue to play next</source>
<translation>Sıradaki Yap</translation>
<translation>Sıradaki yap</translation>
</message>
<message>
<source>Unskip track</source>
@@ -4431,7 +4431,7 @@ If there are no matches then it will use the largest image in the directory.</tr
</message>
<message>
<source>&amp;Hide %1</source>
<translation>&amp;%1&apos;i sakla</translation>
<translation>&amp;%1 ögesini sakla</translation>
</message>
</context>
<context>
@@ -6407,7 +6407,7 @@ Devam etmek istediğinizden emin misiniz?</translation>
</message>
<message>
<source>Queue to play next</source>
<translation>Sıradaki Yap</translation>
<translation>Sıradaki yap</translation>
</message>
<message>
<source>Remove from favorites</source>

View File

@@ -0,0 +1,174 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 <QWidget>
#include <QApplication>
#include <QString>
#include <QIcon>
#include <QPainter>
#include <QPalette>
#include <QColor>
#include <QStyle>
#include <QFont>
#include <QPropertyAnimation>
#include <QStyleOption>
#include <QEnterEvent>
#include <QPaintEvent>
#include <QMouseEvent>
#include <QEvent>
#include "collapsibleinfoheader.h"
const int CollapsibleInfoHeader::kHeight = 20;
const int CollapsibleInfoHeader::kIconSize = 16;
CollapsibleInfoHeader::CollapsibleInfoHeader(QWidget* parent)
: QWidget(parent),
expanded_(false),
hovering_(false),
animation_(new QPropertyAnimation(this, "opacity", this)),
opacity_(0.0) {
setMinimumHeight(kHeight);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
setCursor(QCursor(Qt::PointingHandCursor));
}
void CollapsibleInfoHeader::SetTitle(const QString &title) {
title_ = title;
update();
}
void CollapsibleInfoHeader::SetIcon(const QIcon &icon) {
icon_ = icon;
update();
}
void CollapsibleInfoHeader::SetExpanded(const bool expanded) {
expanded_ = expanded;
emit ExpandedToggled(expanded);
if (expanded)
emit Expanded();
else
emit Collapsed();
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
void CollapsibleInfoHeader::enterEvent(QEnterEvent*) {
#else
void CollapsibleInfoHeader::enterEvent(QEvent*) {
#endif
hovering_ = true;
if (!expanded_) {
animation_->stop();
animation_->setEndValue(1.0);
animation_->setDuration(80);
animation_->start();
}
}
void CollapsibleInfoHeader::leaveEvent(QEvent*) {
hovering_ = false;
if (!expanded_) {
animation_->stop();
animation_->setEndValue(0.0);
animation_->setDuration(160);
animation_->start();
}
}
void CollapsibleInfoHeader::set_opacity(const float opacity) {
opacity_ = opacity;
update();
}
void CollapsibleInfoHeader::paintEvent(QPaintEvent*) {
QPainter p(this);
QColor active_text_color(palette().color(QPalette::Active, QPalette::HighlightedText));
QColor inactive_text_color(palette().color(QPalette::Active, QPalette::Text));
QColor text_color;
if (expanded_) {
text_color = active_text_color;
}
else {
p.setOpacity(0.4 + opacity_ * 0.6);
text_color = QColor(active_text_color.red() * opacity_ + inactive_text_color.red() * (1.0 - opacity_), active_text_color.green() * opacity_ + inactive_text_color.green() * (1.0 - opacity_), active_text_color.blue() * opacity_ + inactive_text_color.blue() * (1.0 - opacity_));
}
QRect indicator_rect(0, 0, height(), height());
QRect icon_rect(height() + 2, (kHeight - kIconSize) / 2, kIconSize, kIconSize);
QRect text_rect(rect());
text_rect.setLeft(icon_rect.right() + 4);
// Draw the background
QColor highlight(palette().color(QPalette::Active, QPalette::Highlight));
const QColor bg_color_1(highlight.lighter(120));
const QColor bg_color_2(highlight.darker(120));
const QColor bg_border(palette().color(QPalette::Dark));
QLinearGradient bg_brush(rect().topLeft(), rect().bottomLeft());
bg_brush.setColorAt(0.0, bg_color_1);
bg_brush.setColorAt(0.5, bg_color_1);
bg_brush.setColorAt(0.5, bg_color_2);
bg_brush.setColorAt(1.0, bg_color_2);
p.setPen(Qt::NoPen);
p.fillRect(rect(), bg_brush);
p.setPen(bg_border);
p.drawLine(rect().topLeft(), rect().topRight());
p.drawLine(rect().bottomLeft(), rect().bottomRight());
// Draw the expand/collapse indicator
QStyleOption opt;
opt.initFrom(this);
opt.rect = indicator_rect;
opt.state |= QStyle::State_Children;
if (expanded_) opt.state |= QStyle::State_Open;
if (hovering_) opt.state |= QStyle::State_Active;
// Have to use the application's style here because using the widget's style
// will trigger QStyleSheetStyle's recursion guard (I don't know why).
QApplication::style()->drawPrimitive(QStyle::PE_IndicatorBranch, &opt, &p, this);
// Draw the icon
p.drawPixmap(icon_rect, icon_.pixmap(kIconSize));
// Draw the title text
QFont bold_font(font());
bold_font.setBold(true);
p.setFont(bold_font);
p.setPen(text_color);
p.drawText(text_rect, Qt::AlignLeft | Qt::AlignVCenter, title_);
}
void CollapsibleInfoHeader::mouseReleaseEvent(QMouseEvent*) {
SetExpanded(!expanded_);
}

View File

@@ -0,0 +1,82 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 COLLAPSIBLEINFOHEADER_H
#define COLLAPSIBLEINFOHEADER_H
#include <QWidget>
#include <QString>
#include <QIcon>
class QPropertyAnimation;
class QEnterEvent;
class QEvent;
class QPaintEvent;
class QMouseEvent;
class CollapsibleInfoHeader : public QWidget {
Q_OBJECT
Q_PROPERTY(float opacity READ opacity WRITE set_opacity)
public:
CollapsibleInfoHeader(QWidget *parent = nullptr);
static const int kHeight;
static const int kIconSize;
bool expanded() const { return expanded_; }
bool hovering() const { return hovering_; }
const QString &title() const { return title_; }
const QIcon &icon() const { return icon_; }
float opacity() const { return opacity_; }
void set_opacity(const float opacity);
public Q_SLOTS:
void SetExpanded(const bool expanded);
void SetTitle(const QString &title);
void SetIcon(const QIcon &icon);
Q_SIGNALS:
void Expanded();
void Collapsed();
void ExpandedToggled(const bool expanded);
protected:
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
void enterEvent(QEnterEvent*) override;
#else
void enterEvent(QEvent*) override;
#endif
void leaveEvent(QEvent*) override;
void paintEvent(QPaintEvent*) override;
void mouseReleaseEvent(QMouseEvent*) override;
private:
bool expanded_;
bool hovering_;
QString title_;
QIcon icon_;
QPropertyAnimation *animation_;
float opacity_;
};
#endif // COLLAPSIBLEINFOHEADER_H

View File

@@ -0,0 +1,64 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 "collapsibleinfoheader.h"
#include "collapsibleinfopane.h"
#include <QVBoxLayout>
CollapsibleInfoPane::CollapsibleInfoPane(const Data &data, QWidget *parent)
: QWidget(parent), data_(data), header_(new CollapsibleInfoHeader(this)) {
QVBoxLayout* layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(3);
layout->setSizeConstraint(QLayout::SetMinAndMaxSize);
setLayout(layout);
layout->addWidget(header_);
layout->addWidget(data.contents_);
data.contents_->hide();
header_->SetTitle(data.title_);
header_->SetIcon(data.icon_);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
connect(header_, SIGNAL(ExpandedToggled(bool)), SLOT(ExpandedToggled(bool)));
connect(header_, SIGNAL(ExpandedToggled(bool)), SIGNAL(Toggled(bool)));
}
void CollapsibleInfoPane::Collapse() { header_->SetExpanded(false); }
void CollapsibleInfoPane::Expand() { header_->SetExpanded(true); }
void CollapsibleInfoPane::ExpandedToggled(bool expanded) {
data_.contents_->setVisible(expanded);
}
bool CollapsibleInfoPane::Data::operator<(const CollapsibleInfoPane::Data &other) const {
const int my_score = (TypeCount - type_) * 1000 + relevance_;
const int other_score = (TypeCount - other.type_) * 1000 + other.relevance_;
return my_score > other_score;
}

View File

@@ -0,0 +1,72 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 COLLAPSIBLEINFOPANE_H
#define COLLAPSIBLEINFOPANE_H
#include <QIcon>
#include <QWidget>
class CollapsibleInfoHeader;
class CollapsibleInfoPane : public QWidget {
Q_OBJECT
public:
struct Data {
explicit Data() : type_(Type_Biography), relevance_(0) {}
bool operator<(const Data& other) const;
enum Type {
Type_Biography,
TypeCount
};
QString id_;
QString title_;
QIcon icon_;
Type type_;
int relevance_;
QWidget *contents_;
QObject *content_object_;
};
CollapsibleInfoPane(const Data &data, QWidget* parent = nullptr);
const Data &data() const { return data_; }
public Q_SLOTS:
void Expand();
void Collapse();
Q_SIGNALS:
void Toggled(const bool expanded);
private Q_SLOTS:
void ExpandedToggled(const bool expanded);
private:
Data data_;
CollapsibleInfoHeader *header_;
};
#endif // COLLAPSIBLEINFOPANE_H

View File

@@ -0,0 +1,89 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 <QWidget>
#include <QApplication>
#include <QMenu>
#include <QWheelEvent>
#include <QRegularExpression>
#include <QtDebug>
#include "core/logging.h"
#include "infotextview.h"
InfoTextView::InfoTextView(QWidget *parent) : QTextBrowser(parent), last_width_(-1), recursion_filter_(false) {
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setOpenExternalLinks(true);
}
void InfoTextView::resizeEvent(QResizeEvent *e) {
const int w = qMax(100, width());
if (w == last_width_) return;
last_width_ = w;
document()->setTextWidth(w);
setMinimumHeight(document()->size().height());
QTextBrowser::resizeEvent(e);
}
QSize InfoTextView::sizeHint() const { return minimumSize(); }
void InfoTextView::wheelEvent(QWheelEvent *e) { e->ignore(); }
void InfoTextView::SetHtml(const QString &html) {
QString copy(html.trimmed());
// Simplify newlines
copy.replace(QRegularExpression(QStringLiteral("\\r\\n?")), QStringLiteral("\n"));
// Convert two or more newlines to <p>, convert single newlines to <br>
copy.replace(QRegularExpression(QStringLiteral("([^>])([\\t ]*\\n){2,}")), QStringLiteral("\\1<p>"));
copy.replace(QRegularExpression(QStringLiteral("([^>])[\\t ]*\\n")), QStringLiteral("\\1<br>"));
// Strip any newlines from the end
copy.replace(QRegularExpression(QStringLiteral("((<\\s*br\\s*/?\\s*>)|(<\\s*/?\\s*p\\s*/?\\s*>))+$")), QLatin1String(""));
setHtml(copy);
}
// Prevents QTextDocument from trying to load remote images before they are ready.
QVariant InfoTextView::loadResource(int type, const QUrl &name) {
if (recursion_filter_) {
recursion_filter_ = false;
return QVariant();
}
recursion_filter_ = true;
if (type == QTextDocument::ImageResource && name.scheme() == QLatin1String("http")) {
if (document()->resource(type, name).isNull()) {
return QVariant();
}
}
return QTextBrowser::loadResource(type, name);
}

View File

@@ -0,0 +1,52 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 INFOTEXTVIEW_H
#define INFOTEXTVIEW_H
#include <QTextBrowser>
#include <QString>
#include <QUrl>
class QResizeEvent;
class QWheelEvent;
class InfoTextView : public QTextBrowser {
Q_OBJECT
public:
explicit InfoTextView(QWidget *parent = nullptr);
QSize sizeHint() const override;
public Q_SLOTS:
void SetHtml(const QString &html);
protected:
void resizeEvent(QResizeEvent *e) override;
void wheelEvent(QWheelEvent *e) override;
QVariant loadResource(int type, const QUrl &name) override;
private:
int last_width_;
bool recursion_filter_;
};
#endif // INFOTEXTVIEW_H

257
src/widgets/prettyimage.cpp Normal file
View File

@@ -0,0 +1,257 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 <QApplication>
#include <QWindow>
#include <QScreen>
#include <QtConcurrentRun>
#include <QFuture>
#include <QString>
#include <QImage>
#include <QPixmap>
#include <QPainter>
#include <QFileInfo>
#include <QDir>
#include <QFileDialog>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QSettings>
#include <QLabel>
#include <QMenu>
#include <QScrollArea>
#include <QContextMenuEvent>
#include "core/logging.h"
#include "core/networkaccessmanager.h"
#include "core/iconloader.h"
#include "prettyimage.h"
const int PrettyImage::kTotalHeight = 200;
const int PrettyImage::kReflectionHeight = 40;
const int PrettyImage::kImageHeight = PrettyImage::kTotalHeight - PrettyImage::kReflectionHeight;
const int PrettyImage::kMaxImageWidth = 300;
const char *PrettyImage::kSettingsGroup = "PrettyImageView";
PrettyImage::PrettyImage(const QUrl &url, QNetworkAccessManager *network, QWidget *parent)
: QWidget(parent),
network_(network),
state_(State_WaitingForLazyLoad),
url_(url),
menu_(nullptr) {
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
LazyLoad();
}
void PrettyImage::LazyLoad() {
if (state_ != State_WaitingForLazyLoad) return;
// Start fetching the image
QNetworkReply *reply = network_->get(QNetworkRequest(url_));
state_ = State_Fetching;
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { ImageFetched(reply); });
}
QSize PrettyImage::image_size() const {
if (state_ != State_Finished) return QSize(kImageHeight * 1.6, kImageHeight);
QSize ret = image_.size();
ret.scale(kMaxImageWidth, kImageHeight, Qt::KeepAspectRatio);
return ret;
}
QSize PrettyImage::sizeHint() const {
return QSize(image_size().width(), kTotalHeight);
}
void PrettyImage::ImageFetched(QNetworkReply *reply) {
reply->deleteLater();
QImage image = QImage::fromData(reply->readAll());
if (image.isNull()) {
qLog(Debug) << "Image failed to load" << reply->request().url() << reply->error();
deleteLater();
}
else {
state_ = State_CreatingThumbnail;
image_ = image;
(void)QtConcurrent::run([=]{ ImageScaled(image_.scaled(image_size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); });
}
}
void PrettyImage::ImageScaled(QImage image) {
thumbnail_ = QPixmap::fromImage(image);
state_ = State_Finished;
updateGeometry();
update();
emit Loaded();
}
void PrettyImage::paintEvent(QPaintEvent*) {
// Draw at the bottom of our area
QRect image_rect(QPoint(0, 0), image_size());
image_rect.moveBottom(kImageHeight);
QPainter p(this);
// Draw the main image
DrawThumbnail(&p, image_rect);
// Draw the reflection
// Figure out where to draw it
QRect reflection_rect(image_rect);
reflection_rect.moveTop(image_rect.bottom());
// Create the reflected pixmap
QImage reflection(reflection_rect.size(), QImage::Format_ARGB32_Premultiplied);
reflection.fill(palette().color(QPalette::Base).rgba());
QPainter reflection_painter(&reflection);
// Set up the transformation
QTransform transform;
transform.scale(1.0, -1.0);
transform.translate(0.0, -reflection_rect.height());
reflection_painter.setTransform(transform);
QRect fade_rect(reflection.rect().bottomLeft() - QPoint(0, kReflectionHeight), reflection.rect().bottomRight());
// Draw the reflection into the buffer
DrawThumbnail(&reflection_painter, reflection.rect());
// Make it fade out towards the bottom
QLinearGradient fade_gradient(fade_rect.topLeft(), fade_rect.bottomLeft());
fade_gradient.setColorAt(0.0, QColor(0, 0, 0, 0));
fade_gradient.setColorAt(1.0, QColor(0, 0, 0, 128));
reflection_painter.setCompositionMode(QPainter::CompositionMode_DestinationIn);
reflection_painter.fillRect(fade_rect, fade_gradient);
reflection_painter.end();
// Draw the reflection on the image
p.drawImage(reflection_rect, reflection);
}
void PrettyImage::DrawThumbnail(QPainter *p, const QRect &rect) {
switch (state_) {
case State_WaitingForLazyLoad:
case State_Fetching:
case State_CreatingThumbnail:
p->setPen(palette().color(QPalette::Disabled, QPalette::Text));
p->drawText(rect, Qt::AlignHCenter | Qt::AlignBottom, tr("Loading..."));
break;
case State_Finished:
p->drawPixmap(rect, thumbnail_);
break;
}
}
void PrettyImage::contextMenuEvent(QContextMenuEvent *e) {
if (e->pos().y() >= kImageHeight) return;
if (!menu_) {
menu_ = new QMenu(this);
menu_->addAction(IconLoader::Load(QStringLiteral("zoom-in")), tr("Show fullsize..."), this, &PrettyImage::ShowFullsize);
menu_->addAction(IconLoader::Load(QStringLiteral("document-save")), tr("Save image") + QLatin1String("..."), this, &PrettyImage::SaveAs);
}
menu_->popup(e->globalPos());
}
void PrettyImage::ShowFullsize() {
// Create the window
QScrollArea *pwindow = new QScrollArea;
pwindow->setAttribute(Qt::WA_DeleteOnClose, true);
pwindow->setWindowTitle(tr("%1 image viewer").arg(QLatin1String("Strawberry")));
// Work out how large to make the window, based on the size of the screen
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
QScreen *screen = QWidget::screen();
#else
QScreen *screen = (window() && window()->windowHandle() ? window()->windowHandle()->screen() : QGuiApplication::primaryScreen());
#endif
if (screen) {
QRect desktop_rect(screen->availableGeometry());
QSize window_size(qMin(desktop_rect.width() - 20, image_.width()), qMin(desktop_rect.height() - 20, image_.height()));
pwindow->resize(window_size);
}
// Create the label that displays the image
QLabel *label = new QLabel(pwindow);
label->setPixmap(QPixmap::fromImage(image_));
// Show the label in the window
pwindow->setWidget(label);
pwindow->setFrameShape(QFrame::NoFrame);
pwindow->show();
}
void PrettyImage::SaveAs() {
QString filename = QFileInfo(url_.path()).fileName();
if (filename.isEmpty()) filename = QLatin1String("artwork.jpg");
QSettings s;
s.beginGroup(kSettingsGroup);
QString last_save_dir = s.value("last_save_dir", QDir::homePath()).toString();
QString path = last_save_dir.isEmpty() ? QDir::homePath() : last_save_dir;
QFileInfo path_info(path);
if (path_info.isDir()) {
path += QLatin1Char('/') + filename;
}
else {
path = path_info.path() + QLatin1Char('/') + filename;
}
filename = QFileDialog::getSaveFileName(this, tr("Save image"), path);
if (filename.isEmpty()) return;
image_.save(filename);
s.setValue("last_save_dir", last_save_dir);
s.endGroup();
}

92
src/widgets/prettyimage.h Normal file
View File

@@ -0,0 +1,92 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 PRETTYIMAGE_H
#define PRETTYIMAGE_H
#include <QWidget>
#include <QString>
#include <QUrl>
#include <QImage>
#include <QPixmap>
#include <QPainter>
class QMenu;
class QNetworkAccessManager;
class QNetworkReply;
class QContextMenuEvent;
class QPaintEvent;
class PrettyImage : public QWidget {
Q_OBJECT
public:
PrettyImage(const QUrl &url, QNetworkAccessManager *network, QWidget *parent = nullptr);
static const int kTotalHeight;
static const int kReflectionHeight;
static const int kImageHeight;
static const int kMaxImageWidth;
static const char *kSettingsGroup;
QSize sizeHint() const override;
QSize image_size() const;
signals:
void Loaded();
public slots:
void LazyLoad();
void SaveAs();
void ShowFullsize();
protected:
void contextMenuEvent(QContextMenuEvent*) override;
void paintEvent(QPaintEvent*) override;
private slots:
void ImageFetched(QNetworkReply *reply);
void ImageScaled(QImage image);
private:
enum State {
State_WaitingForLazyLoad,
State_Fetching,
State_CreatingThumbnail,
State_Finished,
};
void DrawThumbnail(QPainter *p, const QRect &rect);
private:
QNetworkAccessManager *network_;
State state_;
QUrl url_;
QImage image_;
QPixmap thumbnail_;
QMenu *menu_;
QString last_save_dir_;
};
#endif // PRETTYIMAGE_H

View File

@@ -0,0 +1,189 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 <QObject>
#include <QUrl>
#include <QNetworkAccessManager>
#include <QPropertyAnimation>
#include <QAbstractSlider>
#include <QHBoxLayout>
#include <QScrollArea>
#include <QScrollBar>
#include <QTimer>
#include <QMouseEvent>
#include <QResizeEvent>
#include <QWheelEvent>
#include "core/networkaccessmanager.h"
#include "prettyimage.h"
#include "prettyimageview.h"
PrettyImageView::PrettyImageView(QNetworkAccessManager *network, QWidget* parent)
: QScrollArea(parent),
network_(network),
container_(new QWidget(this)),
layout_(new QHBoxLayout(container_)),
current_index_(-1),
scroll_animation_(new QPropertyAnimation(horizontalScrollBar(), "value", this)),
recursion_filter_(false) {
setWidget(container_);
setWidgetResizable(true);
setMinimumHeight(PrettyImage::kTotalHeight + 10);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
setFrameShape(QFrame::NoFrame);
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scroll_animation_->setDuration(250);
scroll_animation_->setEasingCurve(QEasingCurve::InOutCubic);
connect(horizontalScrollBar(), SIGNAL(sliderReleased()), SLOT(ScrollBarReleased()));
connect(horizontalScrollBar(), SIGNAL(actionTriggered(int)), SLOT(ScrollBarAction(int)));
layout_->setSizeConstraint(QLayout::SetMinAndMaxSize);
layout_->setContentsMargins(6, 6, 6, 6);
layout_->setSpacing(6);
layout_->addSpacing(200);
layout_->addSpacing(200);
container_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
}
bool PrettyImageView::eventFilter(QObject *obj, QEvent *event) {
// Work around infinite recursion in QScrollArea resizes.
if (recursion_filter_) {
return false;
}
recursion_filter_ = true;
bool ret = QScrollArea::eventFilter(obj, event);
recursion_filter_ = false;
return ret;
}
void PrettyImageView::AddImage(const QUrl &url) {
PrettyImage *image = new PrettyImage(url, network_, container_);
connect(image, SIGNAL(destroyed()), SLOT(ScrollToCurrent()));
connect(image, SIGNAL(Loaded()), SLOT(ScrollToCurrent()));
layout_->insertWidget(layout_->count() - 1, image);
if (current_index_ == -1) ScrollTo(0);
}
void PrettyImageView::mouseReleaseEvent(QMouseEvent *e) {
// Find the image that was clicked on
QWidget *widget = container_->childAt(container_->mapFrom(this, e->pos()));
if (!widget) return;
// Get the index of that image
const int index = layout_->indexOf(widget) - 1;
if (index == -1) return;
if (index == current_index_) {
// Show the image fullsize
PrettyImage* pretty_image = qobject_cast<PrettyImage*>(widget);
if (pretty_image) {
pretty_image->ShowFullsize();
}
}
else {
// Scroll to the image
ScrollTo(index);
}
}
void PrettyImageView::ScrollTo(const int index, const bool smooth) {
current_index_ = qBound(0, index, layout_->count() - 3);
const int layout_index = current_index_ + 1;
const QWidget *target_widget = layout_->itemAt(layout_index)->widget();
if (!target_widget) return;
const int current_x = horizontalScrollBar()->value();
const int target_x = target_widget->geometry().center().x() - width() / 2;
if (current_x == target_x) return;
if (smooth) {
scroll_animation_->setStartValue(current_x);
scroll_animation_->setEndValue(target_x);
scroll_animation_->start();
}
else {
scroll_animation_->stop();
horizontalScrollBar()->setValue(target_x);
}
}
void PrettyImageView::ScrollToCurrent() { ScrollTo(current_index_); }
void PrettyImageView::ScrollBarReleased() {
// Find the nearest widget to where the scroll bar was released
const int current_x = horizontalScrollBar()->value() + width() / 2;
int layout_index = 1;
for (; layout_index < layout_->count() - 1; ++layout_index) {
const QWidget *widget = layout_->itemAt(layout_index)->widget();
if (widget && widget->geometry().right() > current_x) {
break;
}
}
ScrollTo(layout_index - 1);
}
void PrettyImageView::ScrollBarAction(const int action) {
switch (action) {
case QAbstractSlider::SliderSingleStepAdd:
case QAbstractSlider::SliderPageStepAdd:
ScrollTo(current_index_ + 1);
break;
case QAbstractSlider::SliderSingleStepSub:
case QAbstractSlider::SliderPageStepSub:
ScrollTo(current_index_ - 1);
break;
}
}
void PrettyImageView::resizeEvent(QResizeEvent *e) {
QScrollArea::resizeEvent(e);
ScrollTo(current_index_, false);
}
void PrettyImageView::wheelEvent(QWheelEvent *e) {
const int d = e->angleDelta().x() > 0 ? -1 : 1;
ScrollTo(current_index_ + d, true);
}

View File

@@ -0,0 +1,74 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 PRETTYIMAGEVIEW_H
#define PRETTYIMAGEVIEW_H
#include <QScrollArea>
#include <QMap>
#include <QUrl>
class QNetworkAccessManager;
class QNetworkReply;
class QMenu;
class QHBoxLayout;
class QPropertyAnimation;
class QTimeLine;
class QMouseEvent;
class QResizeEvent;
class QWheelEvent;
class PrettyImageView : public QScrollArea {
Q_OBJECT
public:
PrettyImageView(QNetworkAccessManager *network, QWidget *parent = nullptr);
static const char* kSettingsGroup;
public Q_SLOTS:
void AddImage(const QUrl& url);
protected:
void mouseReleaseEvent(QMouseEvent*) override;
void resizeEvent(QResizeEvent *e) override;
void wheelEvent(QWheelEvent *e) override;
private Q_SLOTS:
void ScrollBarReleased();
void ScrollBarAction(const int action);
void ScrollTo(const int index, const bool smooth = true);
void ScrollToCurrent();
private:
bool eventFilter(QObject*, QEvent*) override;
QNetworkAccessManager *network_;
QWidget *container_;
QHBoxLayout *layout_;
int current_index_;
QPropertyAnimation *scroll_animation_;
bool recursion_filter_;
};
#endif // PRETTYIMAGEVIEW_H

View File

@@ -0,0 +1,187 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 <QObject>
#include <QWidget>
#include <QImage>
#include <QPixmap>
#include <QPainter>
#include <QTimeLine>
#include <QResizeEvent>
#include <QEvent>
#include "widgetfadehelper.h"
#include "core/qt_blurimage.h"
const int WidgetFadeHelper::kLoadingPadding = 9;
const int WidgetFadeHelper::kLoadingBorderRadius = 10;
WidgetFadeHelper::WidgetFadeHelper(QWidget *parent, const int msec)
: QWidget(parent),
parent_(parent),
blur_timeline_(new QTimeLine(msec, this)),
fade_timeline_(new QTimeLine(msec, this)) {
parent->installEventFilter(this);
connect(blur_timeline_, SIGNAL(valueChanged(qreal)), SLOT(update()));
connect(fade_timeline_, SIGNAL(valueChanged(qreal)), SLOT(update()));
connect(fade_timeline_, SIGNAL(finished()), SLOT(FadeFinished()));
hide();
}
bool WidgetFadeHelper::eventFilter(QObject *obj, QEvent *event) {
// We're only interested in our parent's resize events
if (obj != parent_ || event->type() != QEvent::Resize) return false;
// Don't care if we're hidden
if (!isVisible()) return false;
QResizeEvent *re = static_cast<QResizeEvent*>(event);
if (re->oldSize() == re->size()) {
// Ignore phoney resize events
return false;
}
// Get a new capture of the parent
hide();
CaptureParent();
show();
return false;
}
void WidgetFadeHelper::StartBlur() {
CaptureParent();
// Cover the parent
raise();
show();
// Start the timeline
blur_timeline_->stop();
blur_timeline_->start();
setAttribute(Qt::WA_TransparentForMouseEvents, false);
}
void WidgetFadeHelper::CaptureParent() {
// Take a "screenshot" of the window
original_pixmap_ = parent_->grab();
QImage original_image = original_pixmap_.toImage();
// Blur it
QImage blurred(original_image.size(), QImage::Format_ARGB32_Premultiplied);
blurred.fill(Qt::transparent);
QPainter blur_painter(&blurred);
blur_painter.save();
qt_blurImage(&blur_painter, original_image, 10.0, true, false);
blur_painter.restore();
// Draw some loading text over the top
QFont loading_font(font());
loading_font.setBold(true);
QFontMetrics loading_font_metrics(loading_font);
const QString loading_text = tr("Loading...");
#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
const QSize loading_size(kLoadingPadding * 2 + loading_font_metrics.horizontalAdvance(loading_text), kLoadingPadding * 2 + loading_font_metrics.height());
#else
const QSize loading_size(kLoadingPadding * 2 + loading_font_metrics.width(loading_text), kLoadingPadding * 2 + loading_font_metrics.height());
#endif
const QRect loading_rect((blurred.width() - loading_size.width()) / 2, 100, loading_size.width(), loading_size.height());
blur_painter.setRenderHint(QPainter::Antialiasing);
blur_painter.translate(0.5, 0.5);
blur_painter.setPen(QColor(200, 200, 200, 255));
blur_painter.setBrush(QColor(200, 200, 200, 192));
blur_painter.drawRoundedRect(loading_rect, kLoadingBorderRadius, kLoadingBorderRadius);
blur_painter.setPen(palette().brush(QPalette::Text).color());
blur_painter.setFont(loading_font);
blur_painter.drawText(loading_rect.translated(-1, -1), Qt::AlignCenter, loading_text);
blur_painter.translate(-0.5, -0.5);
blur_painter.end();
blurred_pixmap_ = QPixmap::fromImage(blurred);
resize(parent_->size());
}
void WidgetFadeHelper::StartFade() {
if (blur_timeline_->state() == QTimeLine::Running) {
// Blur timeline is still running, so we need render the current state
// into a new pixmap.
QPixmap pixmap(original_pixmap_);
QPainter painter(&pixmap);
painter.setOpacity(blur_timeline_->currentValue());
painter.drawPixmap(0, 0, blurred_pixmap_);
painter.end();
blurred_pixmap_ = pixmap;
}
blur_timeline_->stop();
original_pixmap_ = QPixmap();
// Start the timeline
fade_timeline_->stop();
fade_timeline_->start();
setAttribute(Qt::WA_TransparentForMouseEvents, true);
}
void WidgetFadeHelper::paintEvent(QPaintEvent *event) {
Q_UNUSED(event)
QPainter p(this);
if (fade_timeline_->state() != QTimeLine::Running) {
// We're fading in the blur
p.drawPixmap(0, 0, original_pixmap_);
p.setOpacity(blur_timeline_->currentValue());
}
else {
// Fading out the blur into the new image
p.setOpacity(1.0 - fade_timeline_->currentValue());
}
p.drawPixmap(0, 0, blurred_pixmap_);
}
void WidgetFadeHelper::FadeFinished() {
hide();
original_pixmap_ = QPixmap();
blurred_pixmap_ = QPixmap();
}

View File

@@ -0,0 +1,63 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 WIDGETFADEHELPER_H
#define WIDGETFADEHELPER_H
#include <QWidget>
#include <QPixmap>
class QTimeLine;
class QPaintEvent;
class QEvent;
class WidgetFadeHelper : public QWidget {
Q_OBJECT
public:
WidgetFadeHelper(QWidget *parent, const int msec = 500);
public Q_SLOTS:
void StartBlur();
void StartFade();
protected:
void paintEvent(QPaintEvent *event) override;
bool eventFilter(QObject *obj, QEvent *event) override;
private Q_SLOTS:
void FadeFinished();
private:
void CaptureParent();
private:
static const int kLoadingPadding;
static const int kLoadingBorderRadius;
QWidget *parent_;
QTimeLine *blur_timeline_;
QTimeLine *fade_timeline_;
QPixmap original_pixmap_;
QPixmap blurred_pixmap_;
};
#endif // WIDGETFADEHELPER_H