Compare commits

..

1 Commits

Author SHA1 Message Date
Jonas Kvinge
8f1bda0546 Add artist biography 2025-12-29 00:48:24 +01:00
96 changed files with 2621 additions and 8207 deletions

View File

@@ -208,8 +208,6 @@ else()
pkg_check_modules(TAGLIB REQUIRED IMPORTED_TARGET taglib>=1.12)
endif()
pkg_check_modules(LIBMYGPO libmygpo-qt6)
find_package(GTest)
pkg_check_modules(LIBSPARSEHASH IMPORTED_TARGET libsparsehash)
@@ -383,8 +381,6 @@ optional_component(DISCORD_RPC ON "Discord Rich Presence"
DEPENDS "RapidJSON" RapidJSON_FOUND
)
optional_component(PODCASTS ON "Podcasts support" DEPENDS "libmygpo" LIBMYGPO_FOUND)
if(HAVE_SONGFINGERPRINTING OR HAVE_MUSICBRAINZ)
set(HAVE_CHROMAPRINT ON)
endif()
@@ -838,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
@@ -1126,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
@@ -1484,69 +1492,6 @@ optional_source(HAVE_QOBUZ
src/settings/qobuzsettingspage.ui
)
optional_source(HAVE_PODCASTS
SOURCES
podcasts/gpoddersync.cpp
podcasts/gpoddertoptagsmodel.cpp
podcasts/gpoddertoptagspage.cpp
podcasts/itunessearchpage.cpp
podcasts/podcastbackend.cpp
podcasts/podcastservice.cpp
podcasts/podcast.cpp
podcasts/podcastdownloader.cpp
podcasts/podcastupdater.cpp
podcasts/podcastdeleter.cpp
podcasts/podcastdiscoverymodel.cpp
podcasts/podcastepisode.cpp
podcasts/podcastinfodialog.cpp
podcasts/podcastinfowidget.cpp
podcasts/podcastparser.cpp
podcasts/podcastservicemodel.cpp
podcasts/podcasturlloader.cpp
podcasts/gpoddersearchpage.cpp
podcasts/addpodcastbyurl.cpp
podcasts/addpodcastdialog.cpp
podcasts/addpodcastpage.cpp
podcasts/episodeinfowidget.cpp
podcasts/fixedopmlpage.cpp
settings/podcastsettingspage.cpp
HEADERS
podcasts/addpodcastbyurl.h
podcasts/addpodcastdialog.h
podcasts/addpodcastpage.h
podcasts/episodeinfowidget.h
podcasts/fixedopmlpage.h
podcasts/gpoddersync.h
podcasts/gpoddertoptagsmodel.h
podcasts/gpoddertoptagspage.h
podcasts/itunessearchpage.h
podcasts/opmlcontainer.h
podcasts/podcastbackend.h
podcasts/podcastdeleter.h
podcasts/podcastdiscoverymodel.h
podcasts/podcastdownloader.h
podcasts/podcastepisode.h
podcasts/podcast.h
podcasts/podcastinfodialog.h
podcasts/podcastinfowidget.h
podcasts/podcastparser.h
podcasts/podcastservice.h
podcasts/podcastservicemodel.h
podcasts/podcastupdater.h
podcasts/podcasturlloader.h
podcasts/gpoddersearchpage.h
settings/podcastsettingspage.h
UI
podcasts/addpodcastbyurl.ui
podcasts/addpodcastdialog.ui
podcasts/episodeinfowidget.ui
podcasts/itunessearchpage.ui
podcasts/podcastinfodialog.ui
podcasts/podcastinfowidget.ui
podcasts/gpoddersearchpage.ui
settings/podcastsettingspage.ui
)
qt_wrap_cpp(SOURCES ${HEADERS})
qt_wrap_ui(SOURCES ${UI})
qt_add_resources(SOURCES data/data.qrc data/icons.qrc)

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

@@ -1,10 +1,7 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, Jonas Kvinge <jonas@jkvinge.net>
* 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
@@ -21,16 +18,8 @@
*
*/
#include <QWidget>
#include "artistbioprovider.h"
#include "addpodcastpage.h"
ArtistBioProvider::ArtistBioProvider() : enabled_(true) {}
#include "podcastdiscoverymodel.h"
AddPodcastPage::AddPodcastPage(Application *app, QWidget *parent)
: QWidget(parent), model_(new PodcastDiscoveryModel(app, this)) {}
void AddPodcastPage::SetModel(PodcastDiscoveryModel *model) {
delete model_;
model_ = model;
}
QString ArtistBioProvider::name() const { return QString::fromLatin1(metaObject()->className()); }

View File

@@ -1,8 +1,7 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
Copyright 2018, Jim Broadus <jbroadus@gmail.com>
* Copyright 2019-2021, Jonas Kvinge <jonas@jkvinge.net>
* 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
@@ -19,32 +18,36 @@
*
*/
#ifndef EPISODEINFOWIDGET_H
#define EPISODEINFOWIDGET_H
#ifndef ARTISTBIOPROVIDER_H
#define ARTISTBIOPROVIDER_H
#include <QWidget>
#include <QObject>
#include <QUrl>
#include "podcastepisode.h"
#include "widgets/collapsibleinfopane.h"
#include "core/song.h"
class Application;
class Ui_EpisodeInfoWidget;
class EpisodeInfoWidget : public QWidget {
class ArtistBioProvider : public QObject {
Q_OBJECT
public:
explicit EpisodeInfoWidget(QWidget *parent = nullptr);
~EpisodeInfoWidget();
explicit ArtistBioProvider();
void SetApplication(Application *app);
virtual void Start(const int id, const Song &song) = 0;
virtual void Cancel(const int) {}
void SetEpisode(const PodcastEpisode &episode);
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:
Ui_EpisodeInfoWidget *ui_;
Application *app_;
PodcastEpisode episode_;
bool enabled_;
};
#endif // EPISODEINFOWIDGET_H
#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

@@ -33,7 +33,6 @@
#cmakedefine HAVE_SPOTIFY
#cmakedefine HAVE_QOBUZ
#cmakedefine HAVE_DISCORD_RPC
#cmakedefine HAVE_PODCASTS
#cmakedefine HAVE_TAGLIB_DSFFILE
#cmakedefine HAVE_TAGLIB_DSDIFFFILE

View File

@@ -113,16 +113,7 @@
#include "radios/radioservices.h"
#include "radios/radiobackend.h"
#ifdef HAVE_PODCASTS
# include "podcasts/podcastbackend.h"
# include "podcasts/gpoddersync.h"
# include "podcasts/podcastdownloader.h"
# include "podcasts/podcastupdater.h"
# include "podcasts/podcastdeleter.h"
#endif
using std::make_shared;
using namespace std::chrono_literals;
class ApplicationImpl {
@@ -225,21 +216,6 @@ class ApplicationImpl {
#ifdef HAVE_MOODBAR
moodbar_loader_([app]() { return new MoodbarLoader(app); }),
moodbar_controller_([app]() { return new MoodbarController(app->player(), app->moodbar_loader()); }),
#endif
#ifdef HAVE_PODCASTS
podcast_backend_([app]() {
PodcastBackend* backend = new PodcastBackend(app, app);
app->MoveToThread(backend, database_->thread());
return backend;
}),
gpodder_sync_([app]() { return new GPodderSync(app, app); }),
podcast_downloader_([app]() { return new PodcastDownloader(app, app); }),
podcast_updater_([app]() { return new PodcastUpdater(app, app); }),
podcast_deleter_([app]() {
PodcastDeleter* deleter = new PodcastDeleter(app, app);
app->MoveToNewThread(deleter);
return deleter;
}),
#endif
lastfm_import_([app]() { return new LastFMImport(app->network()); })
{}
@@ -265,13 +241,6 @@ class ApplicationImpl {
#ifdef HAVE_MOODBAR
Lazy<MoodbarLoader> moodbar_loader_;
Lazy<MoodbarController> moodbar_controller_;
#endif
#ifdef HAVE_PODCASTS
Lazy<PodcastBackend> podcast_backend_;
Lazy<GPodderSync> gpodder_sync_;
Lazy<PodcastDownloader> podcast_downloader_;
Lazy<PodcastUpdater> podcast_updater_;
Lazy<PodcastDeleter> podcast_deleter_;
#endif
Lazy<LastFMImport> lastfm_import_;
@@ -421,10 +390,3 @@ SharedPtr<LastFMImport> Application::lastfm_import() const { return p_->lastfm_i
SharedPtr<MoodbarController> Application::moodbar_controller() const { return p_->moodbar_controller_.ptr(); }
SharedPtr<MoodbarLoader> Application::moodbar_loader() const { return p_->moodbar_loader_.ptr(); }
#endif
#ifdef HAVE_PODCASTS
PodcastBackend *Application::podcast_backend() const { return p_->podcast_backend_.get(); }
GPodderSync *Application::gpodder_sync() const { return p_->gpodder_sync_.get(); }
PodcastDownloader *Application::podcast_downloader() const { return p_->podcast_downloader_.get(); }
PodcastUpdater *Application::podcast_updater() const { return p_->podcast_updater_.get(); }
PodcastDeleter *Application::podcast_deleter() const { return p_->podcast_deleter_.get(); }
#endif

View File

@@ -63,14 +63,6 @@ class RadioServices;
class MoodbarController;
class MoodbarLoader;
#endif
#ifdef HAVE_PODCASTS
class PodcastBackend;
class GPodderSync;
class PodcastDownloader;
class PodcastUpdater;
class PodcastDeleter;
#endif
class Application : public QObject {
Q_OBJECT
@@ -110,13 +102,6 @@ class Application : public QObject {
SharedPtr<MoodbarController> moodbar_controller() const;
SharedPtr<MoodbarLoader> moodbar_loader() const;
#endif
#ifdef HAVE_PODCASTS
PodcastBackend *podcast_backend() const;
GPodderSync *gpodder_sync() const;
PodcastDownloader *podcast_downloader() const;
PodcastUpdater *podcast_updater() const;
PodcastDeleter *podcast_deleter() const;
#endif
SharedPtr<LastFMImport> lastfm_import() const;

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

@@ -1,116 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 <QString>
#include <QUrl>
#include <QClipboard>
#include <QMessageBox>
#include "core/iconloader.h"
#include "podcastdiscoverymodel.h"
#include "podcasturlloader.h"
#include "addpodcastbyurl.h"
#include "ui_addpodcastbyurl.h"
AddPodcastByUrl::AddPodcastByUrl(Application *app, QWidget *parent)
: AddPodcastPage(app, parent),
ui_(new Ui_AddPodcastByUrl),
loader_(new PodcastUrlLoader(this)) {
ui_->setupUi(this);
QObject::connect(ui_->go, &QPushButton::clicked, this, &AddPodcastByUrl::GoClicked);
setWindowIcon(IconLoader::Load("podcast"));
}
AddPodcastByUrl::~AddPodcastByUrl() { delete ui_; }
void AddPodcastByUrl::SetUrlAndGo(const QUrl &url) {
ui_->url->setText(url.toString());
GoClicked();
}
void AddPodcastByUrl::SetOpml(const OpmlContainer &opml) {
ui_->url->setText(opml.url.toString());
model()->clear();
model()->CreateOpmlContainerItems(opml, model()->invisibleRootItem());
}
void AddPodcastByUrl::GoClicked() {
emit Busy(true);
model()->clear();
PodcastUrlLoaderReply* reply = loader_->Load(ui_->url->text());
ui_->url->setText(reply->url().toString());
QObject::connect(reply, &PodcastUrlLoaderReply::Finished, this, [this, reply]() { RequestFinished(reply); });
}
void AddPodcastByUrl::RequestFinished(PodcastUrlLoaderReply *reply) {
reply->deleteLater();
emit Busy(false);
if (!reply->is_success()) {
QMessageBox::warning(this, tr("Failed to load podcast"), reply->error_text(), QMessageBox::Close);
return;
}
switch (reply->result_type()) {
case PodcastUrlLoaderReply::Type_Podcast:
for (const Podcast& podcast : reply->podcast_results()) {
model()->appendRow(model()->CreatePodcastItem(podcast));
}
break;
case PodcastUrlLoaderReply::Type_Opml:
model()->CreateOpmlContainerItems(reply->opml_results(), model()->invisibleRootItem());
break;
}
}
void AddPodcastByUrl::Show() {
ui_->url->setFocus();
const QClipboard *clipboard = QApplication::clipboard();
QStringList contents;
contents << clipboard->text(QClipboard::Selection) << clipboard->text(QClipboard::Clipboard);
for (const QString &content : contents) {
if (content.contains("://")) {
ui_->url->setText(content);
return;
}
}
}

View File

@@ -1,60 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 ADDPODCASTBYURL_H
#define ADDPODCASTBYURL_H
#include <QObject>
#include <QUrl>
#include "addpodcastpage.h"
#include "opmlcontainer.h"
class Application;
class AddPodcastPage;
class PodcastUrlLoader;
class PodcastUrlLoaderReply;
class Ui_AddPodcastByUrl;
class AddPodcastByUrl : public AddPodcastPage {
Q_OBJECT
public:
explicit AddPodcastByUrl(Application *app, QWidget *parent = nullptr);
~AddPodcastByUrl();
void Show();
void SetOpml(const OpmlContainer &opml);
void SetUrlAndGo(const QUrl &url);
private slots:
void GoClicked();
void RequestFinished(PodcastUrlLoaderReply *reply);
private:
Ui_AddPodcastByUrl *ui_;
PodcastUrlLoader *loader_;
};
#endif // ADDPODCASTBYURL_H

View File

@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AddPodcastByUrl</class>
<widget class="QWidget" name="AddPodcastByUrl">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>431</width>
<height>51</height>
</rect>
</property>
<property name="windowTitle">
<string>Enter a URL</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>If you know the URL of a podcast, enter it below and press Go.</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="url"/>
</item>
<item>
<widget class="QPushButton" name="go">
<property name="text">
<string>Go</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<connections>
<connection>
<sender>url</sender>
<signal>returnPressed()</signal>
<receiver>go</receiver>
<slot>click()</slot>
<hints>
<hint type="sourcelabel">
<x>109</x>
<y>24</y>
</hint>
<hint type="destinationlabel">
<x>429</x>
<y>49</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -1,270 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 <QDialog>
#include <QVariant>
#include <QString>
#include <QUrl>
#include <QDir>
#include <QFileDialog>
#include <QTimer>
#include <QPushButton>
#include <QListWidget>
#include <QItemSelectionModel>
#include "core/application.h"
#include "core/iconloader.h"
#include "widgets/widgetfadehelper.h"
#include "fixedopmlpage.h"
#include "gpoddersearchpage.h"
#include "gpoddertoptagspage.h"
#include "itunessearchpage.h"
#include "podcastbackend.h"
#include "podcastdiscoverymodel.h"
#include "addpodcastbyurl.h"
#include "podcastinfowidget.h"
#include "addpodcastdialog.h"
#include "ui_addpodcastdialog.h"
const char *AddPodcastDialog::kBbcOpmlUrl = "http://www.bbc.co.uk/podcasts.opml";
const char *AddPodcastDialog::kCbcOpmlUrl = "http://cbc.ca/podcasts.opml";
AddPodcastDialog::AddPodcastDialog(Application *app, QWidget *parent)
: QDialog(parent),
app_(app),
ui_(new Ui_AddPodcastDialog),
last_opml_path_(QDir::homePath()) {
ui_->setupUi(this);
ui_->details->SetApplication(app);
ui_->results->SetExpandOnReset(false);
ui_->results->SetAddOnDoubleClick(false);
ui_->results_stack->setCurrentWidget(ui_->results_page);
fader_ = new WidgetFadeHelper(ui_->details_scroll_area);
QObject::connect(ui_->provider_list, &QListWidget::currentRowChanged, this, &AddPodcastDialog::ChangePage);
QObject::connect(ui_->details, &PodcastInfoWidget::LoadingFinished, fader_, &WidgetFadeHelper::StartFade);
QObject::connect(ui_->results, &AutoExpandingTreeView::doubleClicked, this, &AddPodcastDialog::PodcastDoubleClicked);
// Create Add and Remove Podcast buttons
add_button_ = new QPushButton(IconLoader::Load("list-add"), tr("Add Podcast"), this);
add_button_->setEnabled(false);
connect(add_button_, &QPushButton::clicked, this, &AddPodcastDialog::AddPodcast);
ui_->button_box->addButton(add_button_, QDialogButtonBox::ActionRole);
remove_button_ = new QPushButton(IconLoader::Load("list-remove"), tr("Unsubscribe"), this);
remove_button_->setEnabled(false);
connect(remove_button_, &QPushButton::clicked, this, &AddPodcastDialog::RemovePodcast);
ui_->button_box->addButton(remove_button_, QDialogButtonBox::ActionRole);
QPushButton *settings_button = new QPushButton(IconLoader::Load("configure"), tr("Configure podcasts..."), this);
connect(settings_button, &QPushButton::clicked, this, &AddPodcastDialog::OpenSettingsPage);
ui_->button_box->addButton(settings_button, QDialogButtonBox::ResetRole);
// Create an Open OPML file button
QPushButton *open_opml_button = new QPushButton(IconLoader::Load("document-open"), tr("Open OPML file..."), this);
QObject::connect(open_opml_button, &QPushButton::clicked, this, &AddPodcastDialog::OpenOPMLFile);
ui_->button_box->addButton(open_opml_button, QDialogButtonBox::ResetRole);
// Add providers
by_url_page_ = new AddPodcastByUrl(app, this);
AddPage(by_url_page_);
AddPage(new FixedOpmlPage(QUrl(kBbcOpmlUrl), tr("BBC Podcasts"), IconLoader::Load("bbc"), app, this));
AddPage(new FixedOpmlPage(QUrl(kCbcOpmlUrl), tr("CBC Podcasts"), IconLoader::Load("cbc"), app, this));
AddPage(new GPodderTopTagsPage(app, this));
AddPage(new GPodderSearchPage(app, this));
AddPage(new ITunesSearchPage(app, this));
ui_->provider_list->setCurrentRow(0);
}
AddPodcastDialog::~AddPodcastDialog() { delete ui_; }
void AddPodcastDialog::ShowWithUrl(const QUrl& url) {
by_url_page_->SetUrlAndGo(url);
ui_->provider_list->setCurrentRow(0);
show();
}
void AddPodcastDialog::ShowWithOpml(const OpmlContainer& opml) {
by_url_page_->SetOpml(opml);
ui_->provider_list->setCurrentRow(0);
show();
}
void AddPodcastDialog::AddPage(AddPodcastPage *page) {
pages_.append(page);
page_is_busy_.append(false);
ui_->stack->addWidget(page);
new QListWidgetItem(page->windowIcon(), page->windowTitle(), ui_->provider_list);
QObject::connect(page, &AddPodcastPage::Busy, this, &AddPodcastDialog::PageBusyChanged);
}
void AddPodcastDialog::ChangePage(const int index) {
AddPodcastPage *page = pages_[index];
ui_->stack->setCurrentIndex(index);
ui_->stack->setVisible(page->has_visible_widget());
ui_->results->setModel(page->model());
ui_->results_stack->setCurrentWidget(page_is_busy_[index] ? ui_->busy_page : ui_->results_page);
QObject::connect(ui_->results->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &AddPodcastDialog::ChangePodcast);
ChangePodcast(QModelIndex());
CurrentPageBusyChanged(page_is_busy_[index]);
page->Show();
}
void AddPodcastDialog::ChangePodcast(const QModelIndex &current) {
QVariant podcast_variant = current.data(PodcastDiscoveryModel::Role_Podcast);
// If the selected item is invalid or not a podcast, hide the details pane.
if (podcast_variant.isNull()) {
ui_->details_scroll_area->hide();
add_button_->setEnabled(false);
remove_button_->setEnabled(false);
return;
}
current_podcast_ = podcast_variant.value<Podcast>();
// Start the blur+fade if there's already a podcast in the details pane.
if (ui_->details_scroll_area->isVisible()) {
fader_->StartBlur();
}
else {
ui_->details_scroll_area->show();
}
// Update the details pane
ui_->details->SetPodcast(current_podcast_);
// Is the user already subscribed to this podcast?
Podcast subscribed_podcast = app_->podcast_backend()->GetSubscriptionByUrl(current_podcast_.url());
const bool is_subscribed = subscribed_podcast.url().isValid();
if (is_subscribed) {
// Use the one from the database which will contain the ID.
current_podcast_ = subscribed_podcast;
}
add_button_->setEnabled(!is_subscribed);
remove_button_->setEnabled(is_subscribed);
}
void AddPodcastDialog::PageBusyChanged(const bool busy) {
const int index = pages_.indexOf(qobject_cast<AddPodcastPage*>(sender()));
if (index == -1) return;
page_is_busy_[index] = busy;
if (index == ui_->provider_list->currentRow()) {
CurrentPageBusyChanged(busy);
}
}
void AddPodcastDialog::CurrentPageBusyChanged(const bool busy) {
ui_->results_stack->setCurrentWidget(busy ? ui_->busy_page : ui_->results_page);
ui_->stack->setDisabled(busy);
QTimer::singleShot(0, this, &AddPodcastDialog::SelectFirstPodcast);
}
void AddPodcastDialog::SelectFirstPodcast() {
// Select the first item if there was one.
const PodcastDiscoveryModel *model = pages_[ui_->provider_list->currentRow()]->model();
if (model->rowCount() > 0) {
ui_->results->selectionModel()->setCurrentIndex(model->index(0, 0), QItemSelectionModel::ClearAndSelect);
}
}
void AddPodcastDialog::AddPodcast() {
app_->podcast_backend()->Subscribe(&current_podcast_);
add_button_->setEnabled(false);
remove_button_->setEnabled(true);
}
void AddPodcastDialog::PodcastDoubleClicked(const QModelIndex &idx) {
QVariant podcast_variant = idx.data(PodcastDiscoveryModel::Role_Podcast);
if (podcast_variant.isNull()) {
return;
}
current_podcast_ = podcast_variant.value<Podcast>();
app_->podcast_backend()->Subscribe(&current_podcast_);
add_button_->setEnabled(false);
remove_button_->setEnabled(true);
}
void AddPodcastDialog::RemovePodcast() {
app_->podcast_backend()->Unsubscribe(current_podcast_);
current_podcast_.set_database_id(-1);
add_button_->setEnabled(true);
remove_button_->setEnabled(false);
}
void AddPodcastDialog::OpenSettingsPage() {
//app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Podcasts);
}
void AddPodcastDialog::OpenOPMLFile() {
const QString filename = QFileDialog::getOpenFileName(this, tr("Open OPML file"), last_opml_path_, "OPML files (*.opml)");
if (filename.isEmpty()) {
return;
}
last_opml_path_ = filename;
by_url_page_->SetUrlAndGo(QUrl::fromLocalFile(last_opml_path_));
ChangePage(ui_->stack->indexOf(by_url_page_));
}

View File

@@ -1,91 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 ADDPODCASTDIALOG_H
#define ADDPODCASTDIALOG_H
#include <QDialog>
#include <QList>
#include <QString>
#include <QUrl>
#include "podcast.h"
class Application;
class AddPodcastByUrl;
class AddPodcastPage;
class OpmlContainer;
class WidgetFadeHelper;
class Ui_AddPodcastDialog;
class AddPodcastDialog : public QDialog {
Q_OBJECT
public:
explicit AddPodcastDialog(Application *app, QWidget *parent = nullptr);
~AddPodcastDialog();
// Convenience methods that open the dialog at the Add By Url page and fill it with either a URL (which is then fetched), or a pre-fetched OPML container.
void ShowWithUrl(const QUrl &url);
void ShowWithOpml(const OpmlContainer &opml);
private slots:
void OpenSettingsPage();
void AddPodcast();
void PodcastDoubleClicked(const QModelIndex &idx);
void RemovePodcast();
void ChangePage(const int index);
void ChangePodcast(const QModelIndex &current);
void PageBusyChanged(const bool busy);
void CurrentPageBusyChanged(const bool busy);
void SelectFirstPodcast();
void OpenOPMLFile();
private:
void AddPage(AddPodcastPage *page);
private:
static const char *kBbcOpmlUrl;
static const char *kCbcOpmlUrl;
Application *app_;
Ui_AddPodcastDialog *ui_;
QPushButton *add_button_;
QPushButton *remove_button_;
QList<AddPodcastPage*> pages_;
QList<bool> page_is_busy_;
AddPodcastByUrl *by_url_page_;
WidgetFadeHelper *fader_;
Podcast current_podcast_;
QString last_opml_path_;
};
#endif // ADDPODCASTDIALOG_H

View File

@@ -1,276 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AddPodcastDialog</class>
<widget class="QDialog" name="AddPodcastDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>941</width>
<height>473</height>
</rect>
</property>
<property name="windowTitle">
<string>Add podcast</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QListWidget" name="provider_list">
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="uniformItemSizes">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QStackedWidget" name="stack">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QStackedWidget" name="results_stack">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="results_page">
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="AutoExpandingTreeView" name="results">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="busy_page">
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>192</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="BusyIndicator" name="widget" native="true">
<property name="text" stdset="0">
<string>Loading...</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QScrollArea" name="details_scroll_area">
<property name="minimumSize">
<size>
<width>250</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>250</width>
<height>16777215</height>
</size>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="PodcastInfoWidget" name="details">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>248</width>
<height>415</height>
</rect>
</property>
</widget>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="button_box">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>BusyIndicator</class>
<extends>QWidget</extends>
<header>widgets/busyindicator.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>PodcastInfoWidget</class>
<extends>QWidget</extends>
<header>podcasts/podcastinfowidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>AutoExpandingTreeView</class>
<extends>QTreeView</extends>
<header>widgets/autoexpandingtreeview.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>button_box</sender>
<signal>accepted()</signal>
<receiver>AddPodcastDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>836</x>
<y>463</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>button_box</sender>
<signal>rejected()</signal>
<receiver>AddPodcastDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>885</x>
<y>463</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -1,53 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 ADDPODCASTPAGE_H
#define ADDPODCASTPAGE_H
#include <QWidget>
class Application;
class PodcastDiscoveryModel;
class AddPodcastPage : public QWidget {
Q_OBJECT
public:
explicit AddPodcastPage(Application *app, QWidget *parent = nullptr);
PodcastDiscoveryModel *model() const { return model_; }
virtual bool has_visible_widget() const { return true; }
virtual void Show() {}
signals:
void Busy(bool busy);
protected:
void SetModel(PodcastDiscoveryModel *model);
private:
PodcastDiscoveryModel *model_;
};
#endif // ADDPODCASTPAGE_H

View File

@@ -1,49 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
Copyright 2018, Jim Broadus <jbroadus@gmail.com>
* Copyright 2019-2021, 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 <QWidget>
#include "core/utilities.h"
#include "episodeinfowidget.h"
#include "ui_episodeinfowidget.h"
EpisodeInfoWidget::EpisodeInfoWidget(QWidget *parent)
: QWidget(parent), ui_(new Ui_EpisodeInfoWidget), app_(nullptr) {
ui_->setupUi(this);
}
EpisodeInfoWidget::~EpisodeInfoWidget() { delete ui_; }
void EpisodeInfoWidget::SetApplication(Application *app) { app_ = app; }
void EpisodeInfoWidget::SetEpisode(const PodcastEpisode &episode) {
episode_ = episode;
ui_->title->setText(episode.title());
ui_->description->setText(episode.description());
ui_->author->setText(episode.author());
ui_->date->setText(episode.publication_date().toString("d MMMM yyyy"));
ui_->duration->setText(Utilities::PrettyTime(episode.duration_secs()));
}

View File

@@ -1,137 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>EpisodeInfoWidget</class>
<widget class="QWidget" name="EpisodeInfoWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>398</width>
<height>551</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<property name="styleSheet">
<string notr="true">#title {
font-weight: bold;
}
#description {
font-size: smaller;
}
QLineEdit {
background: transparent;
}</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="sizeConstraint">
<enum>QLayout::SetMinAndMaxSize</enum>
</property>
<item>
<widget class="QLabel" name="title">
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="description">
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout">
<property name="sizeConstraint">
<enum>QLayout::SetMinAndMaxSize</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="author_label">
<property name="text">
<string>Author</string>
</property>
<property name="field_label" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="date">
<property name="frame">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="author">
<property name="frame">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="date_label">
<property name="text">
<string>Date</string>
</property>
<property name="field_label" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="duration_label">
<property name="text">
<string>Duration</string>
</property>
<property name="field_label" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="duration">
<property name="frame">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -1,82 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 <QString>
#include <QUrl>
#include <QIcon>
#include <QMessageBox>
#include "podcastdiscoverymodel.h"
#include "podcasturlloader.h"
#include "fixedopmlpage.h"
FixedOpmlPage::FixedOpmlPage(const QUrl &opml_url, const QString &title, const QIcon &icon, Application *app, QWidget *parent)
: AddPodcastPage(app, parent),
loader_(new PodcastUrlLoader(this)),
opml_url_(opml_url),
done_initial_load_(false) {
setWindowTitle(title);
setWindowIcon(icon);
}
void FixedOpmlPage::Show() {
if (!done_initial_load_) {
emit Busy(true);
done_initial_load_ = true;
PodcastUrlLoaderReply *reply = loader_->Load(opml_url_);
QObject::connect(reply, &PodcastUrlLoaderReply::Finished, this, [this, reply]() { LoadFinished(reply); });
}
}
void FixedOpmlPage::LoadFinished(PodcastUrlLoaderReply *reply) {
reply->deleteLater();
emit Busy(false);
if (!reply->is_success()) {
QMessageBox::warning(this, tr("Failed to load podcast"), reply->error_text(), QMessageBox::Close);
return;
}
switch (reply->result_type()) {
case PodcastUrlLoaderReply::Type_Podcast:{
const PodcastList podcasts = reply->podcast_results();
for (const Podcast &podcast : podcasts) {
model()->appendRow(model()->CreatePodcastItem(podcast));
}
break;
}
case PodcastUrlLoaderReply::Type_Opml:
model()->CreateOpmlContainerItems(reply->opml_results(), model()->invisibleRootItem());
break;
}
}

View File

@@ -1,56 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 FIXEDOPMLPAGE_H
#define FIXEDOPMLPAGE_H
#include <QObject>
#include <QUrl>
#include <QIcon>
#include "addpodcastpage.h"
class Application;
class PodcastUrlLoader;
class PodcastUrlLoaderReply;
class FixedOpmlPage : public AddPodcastPage {
Q_OBJECT
public:
FixedOpmlPage(const QUrl &opml_url, const QString &title, const QIcon &icon, Application *app, QWidget *parent = nullptr);
bool has_visible_widget() const { return false; }
void Show();
private slots:
void LoadFinished(PodcastUrlLoaderReply *reply);
private:
PodcastUrlLoader *loader_;
QUrl opml_url_;
bool done_initial_load_;
};
#endif // FIXEDOPMLPAGE_H

View File

@@ -1,100 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 <QMessageBox>
#include <QPushButton>
#include "core/application.h"
#include "core/iconloader.h"
#include "core/networkaccessmanager.h"
#include "podcast.h"
#include "podcastdiscoverymodel.h"
#include "gpoddersearchpage.h"
#include "ui_gpoddersearchpage.h"
GPodderSearchPage::GPodderSearchPage(Application *app, QWidget *parent)
: AddPodcastPage(app, parent),
ui_(new Ui_GPodderSearchPage),
network_(new NetworkAccessManager(this)),
api_(new mygpo::ApiRequest(network_)) {
ui_->setupUi(this);
QObject::connect(ui_->search, &QPushButton::clicked, this, &GPodderSearchPage::SearchClicked);
setWindowIcon(IconLoader::Load("mygpo"));
}
GPodderSearchPage::~GPodderSearchPage() {
delete ui_;
delete api_;
}
void GPodderSearchPage::SearchClicked() {
emit Busy(true);
mygpo::PodcastListPtr list(api_->search(ui_->query->text()));
QObject::connect(list.data(), &mygpo::PodcastList::finished, this, [this, list]() { SearchFinished(list); });
QObject::connect(list.data(), &mygpo::PodcastList::parseError, this, [this, list]() { SearchFailed(list); });
QObject::connect(list.data(), &mygpo::PodcastList::requestError, this, [this, list]() { SearchFailed(list); });
}
void GPodderSearchPage::SearchFinished(mygpo::PodcastListPtr list) {
emit Busy(false);
model()->clear();
for (mygpo::PodcastPtr gpo_podcast : list->list()) {
Podcast podcast;
podcast.InitFromGpo(gpo_podcast.data());
model()->appendRow(model()->CreatePodcastItem(podcast));
}
}
void GPodderSearchPage::SearchFailed(mygpo::PodcastListPtr list) {
emit Busy(false);
model()->clear();
if (QMessageBox::warning(
nullptr, tr("Failed to fetch podcasts"),
tr("There was a problem communicating with gpodder.net"),
QMessageBox::Retry | QMessageBox::Close,
QMessageBox::Retry) != QMessageBox::Retry) {
return;
}
// Try doing the search again.
SearchClicked();
}
void GPodderSearchPage::Show() { ui_->query->setFocus(); }

View File

@@ -1,57 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 GPODDERSEARCHPAGE_H
#define GPODDERSEARCHPAGE_H
#include <ApiRequest.h>
#include "addpodcastpage.h"
class QNetworkAccessManager;
class Application;
class Ui_GPodderSearchPage;
class GPodderSearchPage : public AddPodcastPage {
Q_OBJECT
public:
explicit GPodderSearchPage(Application *app, QWidget *parent = nullptr);
~GPodderSearchPage();
void Show();
private slots:
void SearchClicked();
void SearchFinished(mygpo::PodcastListPtr list);
void SearchFailed(mygpo::PodcastListPtr list);
private:
Ui_GPodderSearchPage *ui_;
QNetworkAccessManager *network_;
mygpo::ApiRequest *api_;
};
#endif // GPODDERSEARCHPAGE_H

View File

@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>GPodderSearchPage</class>
<widget class="QWidget" name="GPodderSearchPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>538</width>
<height>69</height>
</rect>
</property>
<property name="windowTitle">
<string>Search gpodder.net</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Enter search terms below to find podcasts on gpodder.net</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="query"/>
</item>
<item>
<widget class="QPushButton" name="search">
<property name="text">
<string>Search</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<connections>
<connection>
<sender>query</sender>
<signal>returnPressed()</signal>
<receiver>search</receiver>
<slot>click()</slot>
<hints>
<hint type="sourcelabel">
<x>130</x>
<y>45</y>
</hint>
<hint type="destinationlabel">
<x>198</x>
<y>46</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -1,415 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 <QCoreApplication>
#include <QSet>
#include <QList>
#include <QMap>
#include <QString>
#include <QUrl>
#include <QHostInfo>
#include <QNetworkReply>
#include <QNetworkCookieJar>
#include <QSettings>
#include <QTimer>
#include "core/application.h"
#include "core/logging.h"
#include "core/networkaccessmanager.h"
#include "core/timeconstants.h"
#include "core/utilities.h"
#include "podcastbackend.h"
#include "podcasturlloader.h"
#include "gpoddersync.h"
const char *GPodderSync::kSettingsGroup = "Podcasts";
const int GPodderSync::kFlushUpdateQueueDelay = 30 * kMsecPerSec; // 30 seconds
const int GPodderSync::kGetUpdatesInterval = 30 * 60 * kMsecPerSec; // 30 minutes
const int GPodderSync::kRequestTimeout = 30 * kMsecPerSec; // 30 seconds
GPodderSync::GPodderSync(Application *app, QObject *parent)
: QObject(parent),
app_(app),
network_(new NetworkAccessManager(this)),
backend_(app_->podcast_backend()),
loader_(new PodcastUrlLoader(this)),
get_updates_timer_(new QTimer(this)),
flush_queue_timer_(new QTimer(this)),
flushing_queue_(false) {
ReloadSettings();
LoadQueue();
QObject::connect(app_, &Application::SettingsChanged, this, &GPodderSync::ReloadSettings);
QObject::connect(backend_, &PodcastBackend::SubscriptionAdded, this, &GPodderSync::SubscriptionAdded);
QObject::connect(backend_, &PodcastBackend::SubscriptionRemoved, this, &GPodderSync::SubscriptionRemoved);
get_updates_timer_->setInterval(kGetUpdatesInterval);
connect(get_updates_timer_, &QTimer::timeout, this, &GPodderSync::GetUpdatesNow);
flush_queue_timer_->setInterval(kFlushUpdateQueueDelay);
flush_queue_timer_->setSingleShot(true);
QObject::connect(flush_queue_timer_, &QTimer::timeout, this, &GPodderSync::FlushUpdateQueue);
if (is_logged_in()) {
GetUpdatesNow();
flush_queue_timer_->start();
get_updates_timer_->start();
}
}
GPodderSync::~GPodderSync() {}
QString GPodderSync::DeviceId() {
return QString("%1-%2").arg(qApp->applicationName(), QHostInfo::localHostName()).toLower();
}
QString GPodderSync::DefaultDeviceName() {
return tr("%1 on %2").arg(qApp->applicationName(), QHostInfo::localHostName());
}
bool GPodderSync::is_logged_in() const {
return !username_.isEmpty() && !password_.isEmpty() && api_;
}
void GPodderSync::ReloadSettings() {
QSettings s;
s.beginGroup(kSettingsGroup);
username_ = s.value("gpodder_username").toString();
password_ = s.value("gpodder_password").toString();
last_successful_get_ = s.value("gpodder_last_get").toDateTime();
s.endGroup();
if (!username_.isEmpty() && !password_.isEmpty()) {
api_.reset(new mygpo::ApiRequest(username_, password_, network_));
}
}
void GPodderSync::Login(const QString &username, const QString &password, const QString &device_name) {
api_.reset(new mygpo::ApiRequest(username, password, network_));
QNetworkReply *reply = api_->renameDevice(username, DeviceId(), device_name, mygpo::Device::DESKTOP);
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, username, password]() { LoginFinished(reply, username, password); });
}
void GPodderSync::LoginFinished(QNetworkReply *reply, const QString &username, const QString &password) {
reply->deleteLater();
if (reply->error() == QNetworkReply::NoError) {
username_ = username;
password_ = password;
QSettings s;
s.beginGroup(kSettingsGroup);
s.setValue("gpodder_username", username);
s.setValue("gpodder_password", password);
s.endGroup();
DoInitialSync();
emit LoginSuccess();
}
else {
api_.reset();
emit LoginFailure(reply->errorString());
}
}
void GPodderSync::Logout() {
QSettings s;
s.beginGroup(kSettingsGroup);
s.remove("gpodder_username");
s.remove("gpodder_password");
s.remove("gpodder_last_get");
s.endGroup();
api_.reset();
// Remove session cookies. QNetworkAccessManager takes ownership of the new object and frees the previous.
network_->setCookieJar(new QNetworkCookieJar());
}
void GPodderSync::GetUpdatesNow() {
if (!is_logged_in()) return;
qlonglong timestamp = 0;
if (last_successful_get_.isValid()) {
timestamp = last_successful_get_.toSecsSinceEpoch();
}
mygpo::DeviceUpdatesPtr reply(api_->deviceUpdates(username_, DeviceId(), timestamp));
QObject::connect(reply.data(), &mygpo::DeviceUpdates::finished, this, [this, reply]() { DeviceUpdatesFinished(reply); });
QObject::connect(reply.data(), &mygpo::DeviceUpdates::parseError, this, &GPodderSync::DeviceUpdatesParseError);
QObject::connect(reply.data(), &mygpo::DeviceUpdates::requestError, this, &GPodderSync::DeviceUpdatesRequestError);
}
void GPodderSync::DeviceUpdatesParseError() {
qLog(Warning) << "Failed to get gpodder device updates: parse error";
}
void GPodderSync::DeviceUpdatesRequestError(QNetworkReply::NetworkError error) {
qLog(Warning) << "Failed to get gpodder device updates:" << error;
}
void GPodderSync::DeviceUpdatesFinished(mygpo::DeviceUpdatesPtr reply) {
// Remember episode actions for each podcast, so when we add a new podcast
// we can apply the actions immediately.
QMap<QUrl, QList<mygpo::EpisodePtr>> episodes_by_podcast;
for (mygpo::EpisodePtr episode : reply->updateList()) {
episodes_by_podcast[episode->podcastUrl()].append(episode);
}
for (mygpo::PodcastPtr podcast : reply->addList()) {
const QUrl url(podcast->url());
// Are we subscribed to this podcast already?
Podcast existing_podcast = backend_->GetSubscriptionByUrl(url);
if (existing_podcast.is_valid()) {
// Just apply actions to this existing podcast
ApplyActions(episodes_by_podcast[url], existing_podcast.mutable_episodes());
backend_->UpdateEpisodes(existing_podcast.episodes());
continue;
}
// Start loading the podcast. Remember actions and apply them after we have a list of the episodes.
PodcastUrlLoaderReply *loader_reply = loader_->Load(url);
QObject::connect(loader_reply, &PodcastUrlLoaderReply::Finished, this, [this, loader_reply, url, episodes_by_podcast]() { NewPodcastLoaded(loader_reply, url, episodes_by_podcast[url]); });
}
// Unsubscribe from podcasts that were removed.
for (const QUrl &url : reply->removeList()) {
backend_->Unsubscribe(backend_->GetSubscriptionByUrl(url));
}
last_successful_get_ = QDateTime::currentDateTime();
QSettings s;
s.beginGroup(kSettingsGroup);
s.setValue("gpodder_last_get", last_successful_get_);
s.endGroup();
}
void GPodderSync::NewPodcastLoaded(PodcastUrlLoaderReply *reply, const QUrl &url, const QList<mygpo::EpisodePtr> &actions) {
reply->deleteLater();
if (!reply->is_success()) {
qLog(Warning) << "Error fetching podcast at" << url << ":" << reply->error_text();
return;
}
if (reply->result_type() != PodcastUrlLoaderReply::Type_Podcast) {
qLog(Warning) << "The URL" << url << "no longer contains a podcast";
return;
}
// Apply the actions to the episodes in the podcast.
for (Podcast podcast : reply->podcast_results()) {
ApplyActions(actions, podcast.mutable_episodes());
// Add the subscription
backend_->Subscribe(&podcast);
}
}
void GPodderSync::ApplyActions(const QList<QSharedPointer<mygpo::Episode>> &actions, PodcastEpisodeList *episodes) {
for (PodcastEpisodeList::iterator it = episodes->begin(); it != episodes->end(); ++it) {
// Find an action for this episode
for (mygpo::EpisodePtr action : actions) {
if (action->url() != it->url()) continue;
switch (action->status()) {
case mygpo::Episode::PLAY:
case mygpo::Episode::DOWNLOAD:
it->set_listened(true);
break;
default:
break;
}
break;
}
}
}
void GPodderSync::SubscriptionAdded(const Podcast &podcast) {
if (!is_logged_in()) return;
const QUrl &url = podcast.url();
queued_remove_subscriptions_.remove(url);
queued_add_subscriptions_.insert(url);
SaveQueue();
flush_queue_timer_->start();
}
void GPodderSync::SubscriptionRemoved(const Podcast &podcast) {
if (!is_logged_in()) return;
const QUrl &url = podcast.url();
queued_remove_subscriptions_.insert(url);
queued_add_subscriptions_.remove(url);
SaveQueue();
flush_queue_timer_->start();
}
namespace {
template<typename T>
void WriteContainer(const T &container, QSettings *s, const char *array_name, const char *item_name) {
s->beginWriteArray(array_name, container.count());
int index = 0;
for (const auto &item : container) {
s->setArrayIndex(index++);
s->setValue(item_name, item);
}
s->endArray();
}
template<typename T>
void ReadContainer(T *container, QSettings *s, const char *array_name, const char *item_name) {
container->clear();
const int count = s->beginReadArray(array_name);
for (int i = 0; i < count; ++i) {
s->setArrayIndex(i);
*container << s->value(item_name).value<typename T::value_type>();
}
s->endArray();
}
} // namespace
void GPodderSync::SaveQueue() {
QSettings s;
s.beginGroup(kSettingsGroup);
WriteContainer(queued_add_subscriptions_, &s, "gpodder_queued_add_subscriptions", "url");
WriteContainer(queued_remove_subscriptions_, &s, "gpodder_queued_remove_subscriptions", "url");
s.endGroup();
}
void GPodderSync::LoadQueue() {
QSettings s;
s.beginGroup(kSettingsGroup);
ReadContainer(&queued_add_subscriptions_, &s, "gpodder_queued_add_subscriptions", "url");
ReadContainer(&queued_remove_subscriptions_, &s, "gpodder_queued_remove_subscriptions", "url");
s.endGroup();
}
void GPodderSync::FlushUpdateQueue() {
if (!is_logged_in() || flushing_queue_) return;
QSet<QUrl> all_urls = queued_add_subscriptions_ + queued_remove_subscriptions_;
if (all_urls.isEmpty()) return;
flushing_queue_ = true;
mygpo::AddRemoveResultPtr reply(api_->addRemoveSubscriptions(username_, DeviceId(), queued_add_subscriptions_.values(), queued_remove_subscriptions_.values()));
qLog(Info) << "Sending" << all_urls.count() << "changes to gpodder.net";
QObject::connect(reply.data(), &mygpo::AddRemoveResult::finished, this, [this, all_urls]() { AddRemoveFinished(all_urls.values()); });
QObject::connect(reply.data(), &mygpo::AddRemoveResult::parseError, this, &GPodderSync::AddRemoveParseError);
QObject::connect(reply.data(), &mygpo::AddRemoveResult::requestError, this, &GPodderSync::AddRemoveRequestError);
}
void GPodderSync::AddRemoveParseError() {
flushing_queue_ = false;
qLog(Warning) << "Failed to update gpodder subscriptions: parse error";
}
void GPodderSync::AddRemoveRequestError(QNetworkReply::NetworkError err) {
flushing_queue_ = false;
qLog(Warning) << "Failed to update gpodder subscriptions:" << err;
}
void GPodderSync::AddRemoveFinished(const QList<QUrl> &affected_urls) {
flushing_queue_ = false;
// Remove the URLs from the queue.
for (const QUrl &url : affected_urls) {
queued_add_subscriptions_.remove(url);
queued_remove_subscriptions_.remove(url);
}
SaveQueue();
// Did more change in the mean time?
if (!queued_add_subscriptions_.isEmpty() ||
!queued_remove_subscriptions_.isEmpty()) {
flush_queue_timer_->start();
}
}
void GPodderSync::DoInitialSync() {
// Get updates from the server
GetUpdatesNow();
get_updates_timer_->start();
// Send our complete list of subscriptions
queued_remove_subscriptions_.clear();
queued_add_subscriptions_.clear();
for (const Podcast &podcast : backend_->GetAllSubscriptions()) {
queued_add_subscriptions_.insert(podcast.url());
}
SaveQueue();
FlushUpdateQueue();
}

View File

@@ -1,125 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 GPODDERSYNC_H
#define GPODDERSYNC_H
#include <QObject>
#include <QScopedPointer>
#include <QSet>
#include <QList>
#include <QString>
#include <QDateTime>
#include <QUrl>
#include <QNetworkReply>
#include <ApiRequest.h>
#include "podcastepisode.h"
class QTimer;
class Application;
class NetworkAccessManager;
class Podcast;
class PodcastBackend;
class PodcastUrlLoader;
class PodcastUrlLoaderReply;
class GPodderSync : public QObject {
Q_OBJECT
public:
explicit GPodderSync(Application *app, QObject *parent = nullptr);
~GPodderSync();
static const char *kSettingsGroup;
static const int kFlushUpdateQueueDelay;
static const int kGetUpdatesInterval;
static const int kRequestTimeout;
static QString DefaultDeviceName();
static QString DeviceId();
bool is_logged_in() const;
// Tries to login using the given username and password. Also sets the device name and type on gpodder.net.
// If login succeeds the username and password will be saved in QSettings.
void Login(const QString &username, const QString &password, const QString &device_name);
// Clears any saved username and password from QSettings.
void Logout();
signals:
void LoginSuccess();
void LoginFailure(const QString &error);
public slots:
void GetUpdatesNow();
private slots:
void ReloadSettings();
void LoginFinished(QNetworkReply *reply, const QString &username, const QString &password);
void DeviceUpdatesFinished(mygpo::DeviceUpdatesPtr reply);
void DeviceUpdatesParseError();
void DeviceUpdatesRequestError(QNetworkReply::NetworkError error);
void NewPodcastLoaded(PodcastUrlLoaderReply *reply, const QUrl &url, const QList<mygpo::EpisodePtr> &actions);
void ApplyActions(const QList<mygpo::EpisodePtr> &actions, PodcastEpisodeList *episodes);
void SubscriptionAdded(const Podcast &podcast);
void SubscriptionRemoved(const Podcast &podcast);
void FlushUpdateQueue();
void AddRemoveFinished(const QList<QUrl> &affected_urls);
void AddRemoveParseError();
void AddRemoveRequestError(QNetworkReply::NetworkError error);
private:
void LoadQueue();
void SaveQueue();
void DoInitialSync();
private:
Application *app_;
NetworkAccessManager *network_;
QScopedPointer<mygpo::ApiRequest> api_;
PodcastBackend *backend_;
PodcastUrlLoader *loader_;
QString username_;
QString password_;
QDateTime last_successful_get_;
QTimer *get_updates_timer_;
QTimer *flush_queue_timer_;
QSet<QUrl> queued_add_subscriptions_;
QSet<QUrl> queued_remove_subscriptions_;
bool flushing_queue_;
};
#endif // GPODDERSYNC_H

View File

@@ -1,115 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 <QMessageBox>
#include <ApiRequest.h>
#include "core/application.h"
#include "gpoddertoptagsmodel.h"
#include "gpoddertoptagspage.h"
#include "podcast.h"
GPodderTopTagsModel::GPodderTopTagsModel(mygpo::ApiRequest *api, Application *app, QObject *parent)
: PodcastDiscoveryModel(app, parent), api_(api) {}
bool GPodderTopTagsModel::hasChildren(const QModelIndex &parent) const {
if (parent.isValid() && parent.data(Role_Type).toInt() == Type_Folder) {
return true;
}
return PodcastDiscoveryModel::hasChildren(parent);
}
bool GPodderTopTagsModel::canFetchMore(const QModelIndex &parent) const {
if (parent.isValid() && parent.data(Role_Type).toInt() == Type_Folder &&
!parent.data(Role_HasLazyLoaded).toBool()) {
return true;
}
return PodcastDiscoveryModel::canFetchMore(parent);
}
void GPodderTopTagsModel::fetchMore(const QModelIndex &parent) {
if (!parent.isValid() || parent.data(Role_Type).toInt() != Type_Folder ||
parent.data(Role_HasLazyLoaded).toBool()) {
return;
}
setData(parent, true, Role_HasLazyLoaded);
// Create a little Loading... item.
itemFromIndex(parent)->appendRow(CreateLoadingIndicator());
mygpo::PodcastListPtr list(api_->podcastsOfTag(GPodderTopTagsPage::kMaxTagCount, parent.data().toString()));
QObject::connect(list.get(), &mygpo::PodcastList::finished, this, [this, parent, list]() { PodcastsOfTagFinished(parent, list.data()); });
QObject::connect(list.get(), &mygpo::PodcastList::parseError, this, [this, parent, list]() { PodcastsOfTagFailed(parent, list.data()); });
QObject::connect(list.get(), &mygpo::PodcastList::requestError, this, [this, parent, list]() { PodcastsOfTagFailed(parent, list.data()); });
}
void GPodderTopTagsModel::PodcastsOfTagFinished(const QModelIndex &parent, mygpo::PodcastList *list) {
QStandardItem *parent_item = itemFromIndex(parent);
if (!parent_item) return;
// Remove the Loading... item.
while (parent_item->hasChildren()) {
parent_item->removeRow(0);
}
for (mygpo::PodcastPtr gpo_podcast : list->list()) {
Podcast podcast;
podcast.InitFromGpo(gpo_podcast.data());
parent_item->appendRow(CreatePodcastItem(podcast));
}
}
void GPodderTopTagsModel::PodcastsOfTagFailed(const QModelIndex &parent, mygpo::PodcastList*) {
QStandardItem *parent_item = itemFromIndex(parent);
if (!parent_item) return;
// Remove the Loading... item.
while (parent_item->hasChildren()) {
parent_item->removeRow(0);
}
if (QMessageBox::warning(nullptr, tr("Failed to fetch podcasts"), tr("There was a problem communicating with gpodder.net"), QMessageBox::Retry | QMessageBox::Close, QMessageBox::Retry) != QMessageBox::Retry) {
return;
}
// Try fetching the list again.
setData(parent, false, Role_HasLazyLoaded);
fetchMore(parent);
}

View File

@@ -1,61 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 GPODDERTOPTAGSMODEL_H
#define GPODDERTOPTAGSMODEL_H
#include <QObject>
#include "podcastdiscoverymodel.h"
namespace mygpo {
class ApiRequest;
class PodcastList;
} // namespace mygpo
class Application;
class GPodderTopTagsModel : public PodcastDiscoveryModel {
Q_OBJECT
public:
GPodderTopTagsModel(mygpo::ApiRequest *api, Application *app, QObject *parent = nullptr);
enum Role {
Role_HasLazyLoaded = PodcastDiscoveryModel::RoleCount,
RoleCount
};
bool hasChildren(const QModelIndex &parent) const;
bool canFetchMore(const QModelIndex &parent) const;
void fetchMore(const QModelIndex &parent);
private slots:
void PodcastsOfTagFinished(const QModelIndex &parent, mygpo::PodcastList *list);
void PodcastsOfTagFailed(const QModelIndex &parent, mygpo::PodcastList *list);
private:
mygpo::ApiRequest *api_;
};
#endif // GPODDERTOPTAGSMODEL_H

View File

@@ -1,93 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 <QMessageBox>
#include "TagList.h"
#include "core/application.h"
#include "core/networkaccessmanager.h"
#include "core/iconloader.h"
#include "gpoddertoptagsmodel.h"
#include "gpoddertoptagspage.h"
const int GPodderTopTagsPage::kMaxTagCount = 100;
GPodderTopTagsPage::GPodderTopTagsPage(Application *app, QWidget *parent)
: AddPodcastPage(app, parent),
network_(new NetworkAccessManager(this)),
api_(new mygpo::ApiRequest(network_)),
done_initial_load_(false) {
setWindowTitle(tr("gpodder.net directory"));
setWindowIcon(IconLoader::Load("mygpo"));
SetModel(new GPodderTopTagsModel(api_, app, this));
}
GPodderTopTagsPage::~GPodderTopTagsPage() { delete api_; }
void GPodderTopTagsPage::Show() {
if (!done_initial_load_) {
// Start the request for list of top-level tags
emit Busy(true);
done_initial_load_ = true;
mygpo::TagListPtr tag_list(api_->topTags(kMaxTagCount));
QObject::connect(tag_list.get(), &mygpo::TagList::finished, this, [this, tag_list]() { TagListLoaded(tag_list); });
QObject::connect(tag_list.get(), &mygpo::TagList::parseError, this, [this]() { TagListFailed(); });
QObject::connect(tag_list.get(), &mygpo::TagList::requestError, this, [this]() { TagListFailed(); });
}
}
void GPodderTopTagsPage::TagListLoaded(mygpo::TagListPtr tag_list) {
emit Busy(false);
for (mygpo::TagPtr tag : tag_list->list()) {
model()->appendRow(model()->CreateFolder(tag->tag()));
}
}
void GPodderTopTagsPage::TagListFailed() {
emit Busy(false);
done_initial_load_ = false;
if (QMessageBox::warning(
nullptr, tr("Failed to fetch directory"),
tr("There was a problem communicating with gpodder.net"),
QMessageBox::Retry | QMessageBox::Close,
QMessageBox::Retry) != QMessageBox::Retry) {
return;
}
// Try doing the search again.
Show();
}

View File

@@ -1,59 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 GPODDERTOPTAGSPAGE_H
#define GPODDERTOPTAGSPAGE_H
#include <QObject>
#include <ApiRequest.h>
#include "addpodcastpage.h"
class Application;
class NetworkAccessManager;
class GPodderTopTagsPage : public AddPodcastPage {
Q_OBJECT
public:
explicit GPodderTopTagsPage(Application *app, QWidget *parent = nullptr);
~GPodderTopTagsPage();
static const int kMaxTagCount;
virtual bool has_visible_widget() const { return false; }
virtual void Show();
private slots:
void TagListLoaded(mygpo::TagListPtr tag_list);
void TagListFailed();
private:
NetworkAccessManager *network_;
mygpo::ApiRequest *api_;
bool done_initial_load_;
};
#endif // GPODDERTOPTAGSPAGE_H

View File

@@ -1,133 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 <QJsonDocument>
#include <QJsonParseError>
#include <QJsonValue>
#include <QJsonObject>
#include <QJsonArray>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QUrl>
#include <QUrlQuery>
#include <QMessageBox>
#include <QPushButton>
#include "core/networkaccessmanager.h"
#include "core/iconloader.h"
#include "podcast.h"
#include "podcastdiscoverymodel.h"
#include "itunessearchpage.h"
#include "ui_itunessearchpage.h"
const char* ITunesSearchPage::kUrlBase = "http://ax.phobos.apple.com.edgesuite.net/WebObjects/MZStoreServices.woa/wa/wsSearch?country=US&media=podcast";
ITunesSearchPage::ITunesSearchPage(Application* app, QWidget* parent)
: AddPodcastPage(app, parent),
ui_(new Ui_ITunesSearchPage),
network_(new NetworkAccessManager(this)) {
ui_->setupUi(this);
QObject::connect(ui_->search, &QPushButton::clicked, this, &ITunesSearchPage::SearchClicked);
setWindowIcon(IconLoader::Load("itunes"));
}
ITunesSearchPage::~ITunesSearchPage() { delete ui_; }
void ITunesSearchPage::SearchClicked() {
emit Busy(true);
QUrl url(QUrl::fromEncoded(kUrlBase));
QUrlQuery url_query;
url_query.addQueryItem("term", ui_->query->text());
url.setQuery(url_query);
QNetworkReply *reply = network_->get(QNetworkRequest(url));
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { SearchFinished(reply); });
}
void ITunesSearchPage::SearchFinished(QNetworkReply* reply) {
reply->deleteLater();
emit Busy(false);
model()->clear();
// Was there a network error?
if (reply->error() != QNetworkReply::NoError) {
QMessageBox::warning(this, tr("Failed to fetch podcasts"), reply->errorString());
return;
}
QJsonParseError error;
QJsonDocument json_document = QJsonDocument::fromJson(reply->readAll(), &error);
if (error.error != QJsonParseError::NoError) {
QMessageBox::warning(this, tr("Failed to fetch podcasts"), tr("There was a problem parsing the response from the iTunes Store"));
return;
}
QJsonObject json_data = json_document.object();
// Was there an error message in the JSON?
if (json_data.contains("errorMessage")) {
QMessageBox::warning(this, tr("Failed to fetch podcasts"), json_data["errorMessage"].toString());
return;
}
QJsonArray array = json_data["results"].toArray();
for (const QJsonValueRef &result : array) {
if (!result.isObject()) continue;
QJsonObject json_result = result.toObject();
if (json_result["kind"].toString() != "podcast") {
continue;
}
if (!json_result.contains("artistName") ||
!json_result.contains("trackName") ||
!json_result.contains("feedUrl") ||
!json_result.contains("trackViewUrl") ||
!json_result.contains("artworkUrl30") ||
!json_result.contains("artworkUrl100")) {
continue;
}
Podcast podcast;
podcast.set_author(json_result["artistName"].toString());
podcast.set_title(json_result["trackName"].toString());
podcast.set_url(QUrl(json_result["feedUrl"].toString()));
podcast.set_link(QUrl(json_result["trackViewUrl"].toString()));
podcast.set_image_url_small(QUrl(json_result["artworkUrl30"].toString()));
podcast.set_image_url_large(QUrl(json_result["artworkUrl100"].toString()));
model()->appendRow(model()->CreatePodcastItem(podcast));
}
}
void ITunesSearchPage::Show() { ui_->query->setFocus(); }

View File

@@ -1,56 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 ITUNESSEARCHPAGE_H
#define ITUNESSEARCHPAGE_H
#include "addpodcastpage.h"
class Ui_ITunesSearchPage;
class QNetworkReply;
class NetworkAccessManager;
class ITunesSearchPage : public AddPodcastPage {
Q_OBJECT
public:
ITunesSearchPage(Application *app, QWidget *parent);
~ITunesSearchPage();
void Show();
private slots:
void SearchClicked();
void SearchFinished(QNetworkReply *reply);
private:
static const char *kUrlBase;
Ui_ITunesSearchPage *ui_;
NetworkAccessManager *network_;
};
#endif // ITUNESSEARCHPAGE_H

View File

@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ITunesSearchPage</class>
<widget class="QWidget" name="ITunesSearchPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>516</width>
<height>69</height>
</rect>
</property>
<property name="windowTitle">
<string>Search iTunes</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Enter search terms below to find podcasts in the iTunes Store</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="query"/>
</item>
<item>
<widget class="QPushButton" name="search">
<property name="text">
<string>Search</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<connections>
<connection>
<sender>query</sender>
<signal>returnPressed()</signal>
<receiver>search</receiver>
<slot>click()</slot>
<hints>
<hint type="sourcelabel">
<x>237</x>
<y>52</y>
</hint>
<hint type="destinationlabel">
<x>461</x>
<y>55</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -1,194 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 <QSharedData>
#include <QVariant>
#include <QVariantMap>
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QDataStream>
#include <QDateTime>
#include <QSqlQuery>
#include "core/utilities.h"
#include "podcast.h"
#include <Podcast.h>
const QStringList Podcast::kColumns = QStringList() << "url"
<< "title"
<< "description"
<< "copyright"
<< "link"
<< "image_url_large"
<< "image_url_small"
<< "author"
<< "owner_name"
<< "owner_email"
<< "last_updated"
<< "last_update_error"
<< "extra";
const QString Podcast::kColumnSpec = Podcast::kColumns.join(", ");
const QString Podcast::kJoinSpec = Utilities::Prepend("p.", Podcast::kColumns).join(", ");
const QString Podcast::kBindSpec = Utilities::Prepend(":", Podcast::kColumns).join(", ");
const QString Podcast::kUpdateSpec = Utilities::Updateify(Podcast::kColumns).join(", ");
struct Podcast::Private : public QSharedData {
Private();
int database_id_;
QUrl url_;
QString title_;
QString description_;
QString copyright_;
QUrl link_;
QUrl image_url_large_;
QUrl image_url_small_;
// iTunes extensions
QString author_;
QString owner_name_;
QString owner_email_;
QDateTime last_updated_;
QString last_update_error_;
QVariantMap extra_;
// These are stored in a different table
PodcastEpisodeList episodes_;
};
Podcast::Private::Private() : database_id_(-1) {}
Podcast::Podcast() : d(new Private) {}
Podcast::Podcast(const Podcast &other) : d(other.d) {}
Podcast::~Podcast() {}
Podcast &Podcast::operator=(const Podcast &other) {
d = other.d;
return *this;
}
int Podcast::database_id() const { return d->database_id_; }
const QUrl &Podcast::url() const { return d->url_; }
const QString &Podcast::title() const { return d->title_; }
const QString &Podcast::description() const { return d->description_; }
const QString &Podcast::copyright() const { return d->copyright_; }
const QUrl &Podcast::link() const { return d->link_; }
const QUrl &Podcast::image_url_large() const { return d->image_url_large_; }
const QUrl &Podcast::image_url_small() const { return d->image_url_small_; }
const QString &Podcast::author() const { return d->author_; }
const QString &Podcast::owner_name() const { return d->owner_name_; }
const QString &Podcast::owner_email() const { return d->owner_email_; }
const QDateTime &Podcast::last_updated() const { return d->last_updated_; }
const QString &Podcast::last_update_error() const {
return d->last_update_error_;
}
const QVariantMap &Podcast::extra() const { return d->extra_; }
QVariant Podcast::extra(const QString &key) const { return d->extra_[key]; }
void Podcast::set_database_id(const int v) { d->database_id_ = v; }
void Podcast::set_url(const QUrl &v) { d->url_ = v; }
void Podcast::set_title(const QString &v) { d->title_ = v; }
void Podcast::set_description(const QString &v) { d->description_ = v; }
void Podcast::set_copyright(const QString &v) { d->copyright_ = v; }
void Podcast::set_link(const QUrl &v) { d->link_ = v; }
void Podcast::set_image_url_large(const QUrl &v) { d->image_url_large_ = v; }
void Podcast::set_image_url_small(const QUrl &v) { d->image_url_small_ = v; }
void Podcast::set_author(const QString &v) { d->author_ = v; }
void Podcast::set_owner_name(const QString &v) { d->owner_name_ = v; }
void Podcast::set_owner_email(const QString &v) { d->owner_email_ = v; }
void Podcast::set_last_updated(const QDateTime &v) { d->last_updated_ = v; }
void Podcast::set_last_update_error(const QString &v) { d->last_update_error_ = v; }
void Podcast::set_extra(const QVariantMap &v) { d->extra_ = v; }
void Podcast::set_extra(const QString &key, const QVariant &value) { d->extra_[key] = value; }
const PodcastEpisodeList &Podcast::episodes() const { return d->episodes_; }
PodcastEpisodeList* Podcast::mutable_episodes() { return &d->episodes_; }
void Podcast::set_episodes(const PodcastEpisodeList &v) { d->episodes_ = v; }
void Podcast::add_episode(const PodcastEpisode &episode) { d->episodes_.append(episode); }
void Podcast::InitFromQuery(const QSqlQuery &query) {
d->database_id_ = query.value(0).toInt();
d->url_ = QUrl::fromEncoded(query.value(1).toByteArray());
d->title_ = query.value(2).toString();
d->description_ = query.value(3).toString();
d->copyright_ = query.value(4).toString();
d->link_ = QUrl::fromEncoded(query.value(5).toByteArray());
d->image_url_large_ = QUrl::fromEncoded(query.value(6).toByteArray());
d->image_url_small_ = QUrl::fromEncoded(query.value(7).toByteArray());
d->author_ = query.value(8).toString();
d->owner_name_ = query.value(9).toString();
d->owner_email_ = query.value(10).toString();
d->last_updated_ = QDateTime::fromSecsSinceEpoch(query.value(11).toUInt());
d->last_update_error_ = query.value(12).toString();
QDataStream extra_stream(query.value(13).toByteArray());
extra_stream >> d->extra_;
}
void Podcast::BindToQuery(QSqlQuery* query) const {
query->bindValue(":url", d->url_.toEncoded());
query->bindValue(":title", d->title_);
query->bindValue(":description", d->description_);
query->bindValue(":copyright", d->copyright_);
query->bindValue(":link", d->link_.toEncoded());
query->bindValue(":image_url_large", d->image_url_large_.toEncoded());
query->bindValue(":image_url_small", d->image_url_small_.toEncoded());
query->bindValue(":author", d->author_);
query->bindValue(":owner_name", d->owner_name_);
query->bindValue(":owner_email", d->owner_email_);
query->bindValue(":last_updated", d->last_updated_.toSecsSinceEpoch());
query->bindValue(":last_update_error", d->last_update_error_);
QByteArray extra;
QDataStream extra_stream(&extra, QIODevice::WriteOnly);
extra_stream << d->extra_;
query->bindValue(":extra", extra);
}
void Podcast::InitFromGpo(const mygpo::Podcast* podcast) {
d->url_ = podcast->url();
d->title_ = podcast->title();
d->description_ = podcast->description();
d->link_ = podcast->website();
d->image_url_large_ = podcast->logoUrl();
set_extra("gpodder:subscribers", podcast->subscribers());
set_extra("gpodder:subscribers_last_week", podcast->subscribersLastWeek());
set_extra("gpodder:page", podcast->mygpoUrl());
}

View File

@@ -1,114 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 PODCAST_H
#define PODCAST_H
#include <QSharedData>
#include <QSharedDataPointer>
#include <QVariant>
#include <QVariantMap>
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QSqlQuery>
#include "podcastepisode.h"
namespace mygpo {
class Podcast;
} // namespace mygpo
class Podcast {
public:
Podcast();
Podcast(const Podcast &other);
~Podcast();
static const QStringList kColumns;
static const QString kColumnSpec;
static const QString kJoinSpec;
static const QString kBindSpec;
static const QString kUpdateSpec;
void InitFromQuery(const QSqlQuery &query);
void InitFromGpo(const mygpo::Podcast *podcast);
void BindToQuery(QSqlQuery *query) const;
bool is_valid() const { return database_id() != -1; }
int database_id() const;
const QUrl &url() const;
const QString &title() const;
const QString &description() const;
const QString &copyright() const;
const QUrl &link() const;
const QUrl &image_url_large() const;
const QUrl &image_url_small() const;
const QString &author() const;
const QString &owner_name() const;
const QString &owner_email() const;
const QDateTime &last_updated() const;
const QString &last_update_error() const;
const QVariantMap &extra() const;
QVariant extra(const QString &key) const;
void set_database_id(const int v);
void set_url(const QUrl &v);
void set_title(const QString &v);
void set_description(const QString &v);
void set_copyright(const QString &v);
void set_link(const QUrl &v);
void set_image_url_large(const QUrl &v);
void set_image_url_small(const QUrl &v);
void set_author(const QString &v);
void set_owner_name(const QString &v);
void set_owner_email(const QString &v);
void set_last_updated(const QDateTime &v);
void set_last_update_error(const QString &v);
void set_extra(const QVariantMap &v);
void set_extra(const QString &key, const QVariant &value);
// Small images are suitable for 16x16 icons in lists. Large images are used in detailed information displays.
const QUrl &ImageUrlLarge() const { return image_url_large().isValid() ? image_url_large() : image_url_small(); }
const QUrl &ImageUrlSmall() const { return image_url_small().isValid() ? image_url_small() : image_url_large(); }
// These are stored in a different database table, and aren't loaded or persisted by InitFromQuery or BindToQuery.
const PodcastEpisodeList &episodes() const;
PodcastEpisodeList *mutable_episodes();
void set_episodes(const PodcastEpisodeList &v);
void add_episode(const PodcastEpisode &episode);
Podcast &operator=(const Podcast &other);
private:
struct Private;
QSharedDataPointer<Private> d;
};
Q_DECLARE_METATYPE(Podcast)
typedef QList<Podcast> PodcastList;
Q_DECLARE_METATYPE(QList<Podcast>)
#endif // PODCAST_H

View File

@@ -1,368 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 <QMutexLocker>
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QUrl>
#include "core/application.h"
#include "core/database.h"
#include "core/logging.h"
#include "core/scopedtransaction.h"
#include "podcastbackend.h"
PodcastBackend::PodcastBackend(Application *app, QObject *parent)
: QObject(parent), app_(app), db_(app->database()) {}
void PodcastBackend::Subscribe(Podcast *podcast) {
// If this podcast is already in the database, do nothing
if (podcast->is_valid()) {
return;
}
// If there's an entry in the database with the same URL, take its data.
Podcast existing_podcast = GetSubscriptionByUrl(podcast->url());
if (existing_podcast.is_valid()) {
*podcast = existing_podcast;
return;
}
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
ScopedTransaction t(&db);
// Insert the podcast.
QSqlQuery q(db);
q.prepare("INSERT INTO podcasts (" + Podcast::kColumnSpec + ") VALUES (" + Podcast::kBindSpec + ")");
podcast->BindToQuery(&q);
q.exec();
if (db_->CheckErrors(q)) return;
// Update the database ID.
const int database_id = q.lastInsertId().toInt();
podcast->set_database_id(database_id);
// Update the IDs of any episodes.
PodcastEpisodeList *episodes = podcast->mutable_episodes();
for (auto it = episodes->begin(); it != episodes->end(); ++it) {
it->set_podcast_database_id(database_id);
}
// Add those episodes to the database.
AddEpisodes(episodes, &db);
t.Commit();
emit SubscriptionAdded(*podcast);
}
void PodcastBackend::Unsubscribe(const Podcast &podcast) {
// If this podcast is not already in the database, do nothing
if (!podcast.is_valid()) {
return;
}
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
ScopedTransaction t(&db);
// Remove the podcast.
QSqlQuery q(db);
q.prepare("DELETE FROM podcasts WHERE ROWID = :id");
q.bindValue(":id", podcast.database_id());
q.exec();
if (db_->CheckErrors(q)) return;
// Remove all episodes in the podcast
q.prepare("DELETE FROM podcast_episodes WHERE podcast_id = :id");
q.bindValue(":id", podcast.database_id());
q.exec();
if (db_->CheckErrors(q)) return;
t.Commit();
emit SubscriptionRemoved(podcast);
}
void PodcastBackend::AddEpisodes(PodcastEpisodeList *episodes, QSqlDatabase *db) {
QSqlQuery q(*db);
q.prepare("INSERT INTO podcast_episodes (" + PodcastEpisode::kColumnSpec + ") VALUES (" + PodcastEpisode::kBindSpec + ")");
for (auto it = episodes->begin(); it != episodes->end(); ++it) {
it->BindToQuery(&q);
q.exec();
if (db_->CheckErrors(q)) continue;
const int database_id = q.lastInsertId().toInt();
it->set_database_id(database_id);
}
}
void PodcastBackend::AddEpisodes(PodcastEpisodeList *episodes) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
ScopedTransaction t(&db);
AddEpisodes(episodes, &db);
t.Commit();
emit EpisodesAdded(*episodes);
}
void PodcastBackend::UpdateEpisodes(const PodcastEpisodeList &episodes) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
ScopedTransaction t(&db);
QSqlQuery q(db);
q.prepare("UPDATE podcast_episodes SET listened = :listened, listened_date = :listened_date, downloaded = :downloaded, local_url = :local_url WHERE ROWID = :id");
for (const PodcastEpisode &episode : episodes) {
q.bindValue(":listened", episode.listened());
q.bindValue(":listened_date", episode.listened_date().toSecsSinceEpoch());
q.bindValue(":downloaded", episode.downloaded());
q.bindValue(":local_url", episode.local_url().toEncoded());
q.bindValue(":id", episode.database_id());
q.exec();
db_->CheckErrors(q);
}
t.Commit();
emit EpisodesUpdated(episodes);
}
PodcastList PodcastBackend::GetAllSubscriptions() {
PodcastList ret;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare("SELECT ROWID, " + Podcast::kColumnSpec + " FROM podcasts");
q.exec();
if (db_->CheckErrors(q)) return ret;
while (q.next()) {
Podcast podcast;
podcast.InitFromQuery(q);
ret << podcast;
}
return ret;
}
Podcast PodcastBackend::GetSubscriptionById(const int id) {
Podcast ret;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare("SELECT ROWID, " + Podcast::kColumnSpec + " FROM podcasts WHERE ROWID = :id");
q.bindValue(":id", id);
q.exec();
if (!db_->CheckErrors(q) && q.next()) {
ret.InitFromQuery(q);
}
return ret;
}
Podcast PodcastBackend::GetSubscriptionByUrl(const QUrl &url) {
Podcast ret;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare("SELECT ROWID, " + Podcast::kColumnSpec + " FROM podcasts WHERE url = :url");
q.bindValue(":url", url.toEncoded());
q.exec();
if (!db_->CheckErrors(q) && q.next()) {
ret.InitFromQuery(q);
}
return ret;
}
PodcastEpisodeList PodcastBackend::GetEpisodes(const int podcast_id) {
PodcastEpisodeList ret;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare("SELECT ROWID, " + PodcastEpisode::kColumnSpec + " FROM podcast_episodes WHERE podcast_id = :id ORDER BY publication_date DESC");
q.bindValue(":id", podcast_id);
q.exec();
if (db_->CheckErrors(q)) return ret;
while (q.next()) {
PodcastEpisode episode;
episode.InitFromQuery(q);
ret << episode;
}
return ret;
}
PodcastEpisode PodcastBackend::GetEpisodeById(const int id) {
PodcastEpisode ret;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare("SELECT ROWID, " + PodcastEpisode::kColumnSpec + " FROM podcast_episodes WHERE ROWID = :id");
q.bindValue(":id", id);
q.exec();
if (!db_->CheckErrors(q) && q.next()) {
ret.InitFromQuery(q);
}
return ret;
}
PodcastEpisode PodcastBackend::GetEpisodeByUrl(const QUrl &url) {
PodcastEpisode ret;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare("SELECT ROWID, " + PodcastEpisode::kColumnSpec + " FROM podcast_episodes WHERE url = :url");
q.bindValue(":url", url.toEncoded());
q.exec();
if (!db_->CheckErrors(q) && q.next()) {
ret.InitFromQuery(q);
}
return ret;
}
PodcastEpisode PodcastBackend::GetEpisodeByUrlOrLocalUrl(const QUrl &url) {
PodcastEpisode ret;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare("SELECT ROWID, " + PodcastEpisode::kColumnSpec + " FROM podcast_episodes WHERE url = :url OR local_url = :url");
q.bindValue(":url", url.toEncoded());
q.exec();
if (!db_->CheckErrors(q) && q.next()) {
ret.InitFromQuery(q);
}
return ret;
}
PodcastEpisodeList PodcastBackend::GetOldDownloadedEpisodes(const QDateTime &max_listened_date) {
PodcastEpisodeList ret;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare("SELECT ROWID, " + PodcastEpisode::kColumnSpec + " FROM podcast_episodes WHERE downloaded = 'true' AND listened_date <= :max_listened_date");
q.bindValue(":max_listened_date", max_listened_date.toSecsSinceEpoch());
q.exec();
if (db_->CheckErrors(q)) return ret;
while (q.next()) {
PodcastEpisode episode;
episode.InitFromQuery(q);
ret << episode;
}
return ret;
}
PodcastEpisode PodcastBackend::GetOldestDownloadedListenedEpisode() {
PodcastEpisode ret;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare("SELECT ROWID, " + PodcastEpisode::kColumnSpec + " FROM podcast_episodes WHERE downloaded = 'true' AND listened = 'true' ORDER BY listened_date ASC");
q.exec();
if (db_->CheckErrors(q)) return ret;
q.next();
ret.InitFromQuery(q);
return ret;
}
PodcastEpisodeList PodcastBackend::GetNewDownloadedEpisodes() {
PodcastEpisodeList ret;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare("SELECT ROWID, " + PodcastEpisode::kColumnSpec + " FROM podcast_episodes WHERE downloaded = 'true' AND listened = 'false'");
q.exec();
if (db_->CheckErrors(q)) return ret;
while (q.next()) {
PodcastEpisode episode;
episode.InitFromQuery(q);
ret << episode;
}
return ret;
}

View File

@@ -1,98 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 PODCASTBACKEND_H
#define PODCASTBACKEND_H
#include <QObject>
#include <QDateTime>
#include <QUrl>
#include "podcast.h"
class QSqlDatabase;
class Application;
class Database;
class PodcastBackend : public QObject {
Q_OBJECT
public:
explicit PodcastBackend(Application *app, QObject *parent = nullptr);
// Adds the podcast and any included Episodes to the database.
// Updates the podcast with a database ID.
// If this podcast already has an ID set, this function does nothing.
// If a podcast with this URL already exists in the database,
// this function just updates the ID field in the provided podcast.
void Subscribe(Podcast *podcast);
// Removes the Podcast with the given ID from the database.
// Also removes any episodes associated with this podcast.
void Unsubscribe(const Podcast &podcast);
// Returns a list of all the subscribed podcasts.
// For efficiency the Podcast objects returned won't contain any PodcastEpisode objects - get them separately if you want them.
PodcastList GetAllSubscriptions();
Podcast GetSubscriptionById(const int id);
Podcast GetSubscriptionByUrl(const QUrl &url);
// Returns podcast episodes that match various keys. All these queries are indexed.
PodcastEpisodeList GetEpisodes(const int podcast_id);
PodcastEpisode GetEpisodeById(const int id);
PodcastEpisode GetEpisodeByUrl(const QUrl &url);
PodcastEpisode GetEpisodeByUrlOrLocalUrl(const QUrl &url);
PodcastEpisode GetOldestDownloadedListenedEpisode();
// Returns a list of episodes that have local data (downloaded=true) but were last listened to before the given QDateTime.
// This query is NOT indexed so it involves a full search of the table.
PodcastEpisodeList GetOldDownloadedEpisodes(const QDateTime &max_listened_date);
PodcastEpisodeList GetNewDownloadedEpisodes();
// Adds episodes to the database. Every episode must have a valid podcast_database_id set already.
void AddEpisodes(PodcastEpisodeList *episodes);
// Updates the editable fields (listened, listened_date, downloaded, and local_url) on episodes that must already exist in the database.
void UpdateEpisodes(const PodcastEpisodeList &episodes);
signals:
void SubscriptionAdded(const Podcast &podcast);
void SubscriptionRemoved(const Podcast &podcast);
// Emitted when episodes are added to a subscription that *already exists*.
void EpisodesAdded(const PodcastEpisodeList &episodes);
// Emitted when existing episodes are updated.
void EpisodesUpdated(const PodcastEpisodeList &episodes);
private:
// Adds each episode to the database, setting their IDs after inserting each one.
void AddEpisodes(PodcastEpisodeList *episodes, QSqlDatabase *db);
private:
Application *app_;
Database *db_;
};
#endif // PODCASTBACKEND_H

View File

@@ -1,124 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 <QFile>
#include <QDateTime>
#include <QTimer>
#include <QSettings>
#include "core/application.h"
#include "core/logging.h"
#include "core/timeconstants.h"
#include "core/utilities.h"
#include "collection/collectiondirectorymodel.h"
#include "collection/collectionmodel.h"
#include "podcastbackend.h"
#include "podcastdeleter.h"
const char *PodcastDeleter::kSettingsGroup = "Podcasts";
const int PodcastDeleter::kAutoDeleteCheckIntervalMsec = 60 * 6 * 60 * kMsecPerSec;
PodcastDeleter::PodcastDeleter(Application *app, QObject *parent)
: QObject(parent),
app_(app),
backend_(app_->podcast_backend()),
delete_after_secs_(0),
auto_delete_timer_(new QTimer(this)) {
ReloadSettings();
auto_delete_timer_->setSingleShot(true);
AutoDelete();
QObject::connect(auto_delete_timer_, &QTimer::timeout, this, &PodcastDeleter::AutoDelete);
QObject::connect(app_, &Application::SettingsChanged, this, &PodcastDeleter::ReloadSettings);
}
void PodcastDeleter::DeleteEpisode(const PodcastEpisode &episode) {
// Delete the local file
if (!QFile::remove(episode.local_url().toLocalFile())) {
qLog(Warning) << "The local file" << episode.local_url().toLocalFile() << "could not be removed";
}
// Update the episode in the DB
PodcastEpisode episode_copy(episode);
episode_copy.set_downloaded(false);
episode_copy.set_local_url(QUrl());
episode_copy.set_listened_date(QDateTime());
backend_->UpdateEpisodes(PodcastEpisodeList() << episode_copy);
}
void PodcastDeleter::ReloadSettings() {
QSettings s;
s.beginGroup(kSettingsGroup);
delete_after_secs_ = s.value("delete_after", 0).toInt();
s.endGroup();
AutoDelete();
}
void PodcastDeleter::AutoDelete() {
if (delete_after_secs_ <= 0) {
return;
}
auto_delete_timer_->stop();
QDateTime max_date = QDateTime::currentDateTime();
qint64 timeout_ms;
PodcastEpisode oldest_episode;
QDateTime oldest_episode_time;
max_date = max_date.addSecs(-delete_after_secs_);
PodcastEpisodeList old_episodes = backend_->GetOldDownloadedEpisodes(max_date);
qLog(Info) << "Deleting" << old_episodes.count()
<< "episodes because they were last listened to"
<< (delete_after_secs_ / kSecsPerDay) << "days ago";
for (const PodcastEpisode& episode : old_episodes) {
DeleteEpisode(episode);
}
oldest_episode = backend_->GetOldestDownloadedListenedEpisode();
if (!oldest_episode.listened_date().isValid()) {
oldest_episode_time = QDateTime::currentDateTime();
}
else {
oldest_episode_time = oldest_episode.listened_date();
}
timeout_ms = QDateTime::currentDateTime().toMSecsSinceEpoch();
timeout_ms -= oldest_episode_time.toMSecsSinceEpoch();
timeout_ms = (delete_after_secs_ * kMsecPerSec) - timeout_ms;
qLog(Info) << "Timeout for autodelete set to:" << timeout_ms << "ms";
if (timeout_ms >= 0) {
auto_delete_timer_->setInterval(timeout_ms);
}
else {
auto_delete_timer_->setInterval(kAutoDeleteCheckIntervalMsec);
}
auto_delete_timer_->start();
}

View File

@@ -1,56 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 PODCASTDELETER_H
#define PODCASTDELETER_H
#include <QObject>
#include "podcast.h"
#include "podcastepisode.h"
class QTimer;
class Application;
class PodcastBackend;
class PodcastDeleter : public QObject {
Q_OBJECT
public:
explicit PodcastDeleter(Application *app, QObject *parent = nullptr);
static const char *kSettingsGroup;
static const int kAutoDeleteCheckIntervalMsec;
public slots:
// Deletes downloaded data for this episode
void DeleteEpisode(const PodcastEpisode &episode);
void AutoDelete();
void ReloadSettings();
private:
Application *app_;
PodcastBackend *backend_;
int delete_after_secs_;
QTimer *auto_delete_timer_;
};
#endif // PODCASTDELETER_H

View File

@@ -1,125 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 "podcastdiscoverymodel.h"
#include <QStandardItemModel>
#include <QStandardItem>
#include <QSet>
#include <QString>
#include <QUrl>
#include <QIcon>
#include "core/application.h"
#include "core/iconloader.h"
#include "core/standarditemiconloader.h"
#include "opmlcontainer.h"
#include "podcast.h"
PodcastDiscoveryModel::PodcastDiscoveryModel(Application *app, QObject *parent)
: QStandardItemModel(parent),
app_(app),
icon_loader_(new StandardItemIconLoader(app->album_cover_loader(), this)),
default_icon_(IconLoader::Load("podcast")) {
icon_loader_->SetModel(this);
}
QVariant PodcastDiscoveryModel::data(const QModelIndex &idx, int role) const {
if (idx.isValid() && role == Qt::DecorationRole && !QStandardItemModel::data(idx, Role_StartedLoadingImage).toBool()) {
const QUrl image_url = QStandardItemModel::data(idx, Role_ImageUrl).toUrl();
if (image_url.isValid()) {
const_cast<PodcastDiscoveryModel*>(this)->LazyLoadImage(image_url, idx);
}
}
return QStandardItemModel::data(idx, role);
}
QStandardItem *PodcastDiscoveryModel::CreatePodcastItem(const Podcast &podcast) {
QStandardItem *item = new QStandardItem;
item->setIcon(default_icon_);
item->setText(podcast.title());
item->setData(QVariant::fromValue(podcast), Role_Podcast);
item->setData(Type_Podcast, Role_Type);
item->setData(podcast.ImageUrlSmall(), Role_ImageUrl);
return item;
}
QStandardItem *PodcastDiscoveryModel::CreateFolder(const QString &name) {
if (folder_icon_.isNull()) {
folder_icon_ = IconLoader::Load("folder");
}
QStandardItem *item = new QStandardItem;
item->setIcon(folder_icon_);
item->setText(name);
item->setData(Type_Folder, Role_Type);
return item;
}
QStandardItem *PodcastDiscoveryModel::CreateOpmlContainerItem(const OpmlContainer &container) {
QStandardItem *item = CreateFolder(container.name);
CreateOpmlContainerItems(container, item);
return item;
}
void PodcastDiscoveryModel::CreateOpmlContainerItems(const OpmlContainer &container, QStandardItem *parent) {
for (const OpmlContainer &child : container.containers) {
QStandardItem *child_item = CreateOpmlContainerItem(child);
parent->appendRow(child_item);
}
for (const Podcast &child : container.feeds) {
QStandardItem *child_item = CreatePodcastItem(child);
parent->appendRow(child_item);
}
}
void PodcastDiscoveryModel::LazyLoadImage(const QUrl &url, const QModelIndex &idx) {
QStandardItem *item = itemFromIndex(idx);
item->setData(true, Role_StartedLoadingImage);
icon_loader_->LoadIcon(url, QUrl(), item);
}
QStandardItem *PodcastDiscoveryModel::CreateLoadingIndicator() {
QStandardItem *item = new QStandardItem;
item->setText(tr("Loading..."));
item->setData(Type_LoadingIndicator, Role_Type);
return item;
}

View File

@@ -1,79 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 PODCASTDISCOVERYMODEL_H
#define PODCASTDISCOVERYMODEL_H
#include <QStandardItemModel>
#include <QString>
#include <QUrl>
#include <QIcon>
#include "covermanager/albumcoverloaderoptions.h"
class Application;
class OpmlContainer;
class OpmlFeed;
class Podcast;
class StandardItemIconLoader;
class PodcastDiscoveryModel : public QStandardItemModel {
Q_OBJECT
public:
explicit PodcastDiscoveryModel(Application *app, QObject *parent = nullptr);
enum Type {
Type_Folder,
Type_Podcast,
Type_LoadingIndicator
};
enum Role {
Role_Podcast = Qt::UserRole,
Role_Type,
Role_ImageUrl,
Role_StartedLoadingImage,
RoleCount
};
void CreateOpmlContainerItems(const OpmlContainer &container, QStandardItem *parent);
QStandardItem *CreateOpmlContainerItem(const OpmlContainer &container);
QStandardItem *CreatePodcastItem(const Podcast &podcast);
QStandardItem *CreateFolder(const QString &name);
QStandardItem *CreateLoadingIndicator();
QVariant data(const QModelIndex &idx, int role) const override;
private:
void LazyLoadImage(const QUrl &url, const QModelIndex &idx);
private:
Application *app_;
StandardItemIconLoader *icon_loader_;
QIcon default_icon_;
QIcon folder_icon_;
};
#endif // PODCASTDISCOVERYMODEL_H

View File

@@ -1,288 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 "podcastdownloader.h"
#include <QString>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QList>
#include <QString>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QSettings>
#include <QTimer>
#include "core/application.h"
#include "core/logging.h"
#include "core/networkaccessmanager.h"
#include "core/tagreaderclient.h"
#include "core/timeconstants.h"
#include "core/utilities.h"
#include "podcastbackend.h"
const char *PodcastDownloader::kSettingsGroup = "Podcasts";
Task::Task(const PodcastEpisode &episode, QFile *file, PodcastBackend *backend)
: file_(file),
episode_(episode),
backend_(backend),
network_(new NetworkAccessManager(this)),
req_(QNetworkRequest(episode.url())),
reply_(network_->get(req_)) {
QObject::connect(reply_, &QNetworkReply::readyRead, this, &Task::reading);
QObject::connect(reply_, &QNetworkReply::finished, this, &Task::finishedInternal);
QObject::connect(reply_, &QNetworkReply::downloadProgress, this, &Task::downloadProgressInternal);
emit ProgressChanged(episode_, PodcastDownload::Queued, 0);
}
PodcastEpisode Task::episode() const { return episode_; }
void Task::reading() {
qint64 bytes = 0;
forever {
bytes = reply_->bytesAvailable();
if (bytes <= 0) break;
file_->write(reply_->read(bytes));
}
}
void Task::finishedPublic() {
disconnect(reply_, &QNetworkReply::readyRead, nullptr, nullptr);
disconnect(reply_, &QNetworkReply::downloadProgress, nullptr, nullptr);
disconnect(reply_, &QNetworkReply::finished, nullptr, nullptr);
emit ProgressChanged(episode_, PodcastDownload::NotDownloading, 0);
// Delete the file
file_->remove();
emit finished(this);
}
void Task::finishedInternal() {
reply_->deleteLater();
if (reply_->error() != QNetworkReply::NoError) {
qLog(Warning) << "Error downloading episode:" << reply_->errorString();
emit ProgressChanged(episode_, PodcastDownload::NotDownloading, 0);
// Delete the file
file_->remove();
emit finished(this);
reply_ = nullptr;
return;
}
qLog(Info) << "Download of" << file_->fileName() << "finished";
// Tell the database the episode has been updated. Get it from the DB again in case the listened field changed in the mean time.
PodcastEpisode episode = episode_;
episode.set_downloaded(true);
episode.set_local_url(QUrl::fromLocalFile(file_->fileName()));
backend_->UpdateEpisodes(PodcastEpisodeList() << episode);
Podcast podcast = backend_->GetSubscriptionById(episode.podcast_database_id());
Song song = episode_.ToSong(podcast);
emit ProgressChanged(episode_, PodcastDownload::Finished, 0);
// I didn't ecountered even a single podcast with a correct metadata
TagReaderClient::Instance()->SaveFileBlocking(file_->fileName(), song);
emit finished(this);
reply_ = nullptr;
}
void Task::downloadProgressInternal(qint64 received, qint64 total) {
if (total <= 0) {
emit ProgressChanged(episode_, PodcastDownload::Downloading, 0);
}
else {
emit ProgressChanged(episode_, PodcastDownload::Downloading, static_cast<float>(received) / total * 100);
}
}
PodcastDownloader::PodcastDownloader(Application *app, QObject *parent)
: QObject(parent),
app_(app),
backend_(app_->podcast_backend()),
network_(new NetworkAccessManager(this)),
disallowed_filename_characters_("[^a-zA-Z0-9_~ -]"),
auto_download_(false) {
QObject::connect(backend_, &PodcastBackend::EpisodesAdded, this, &PodcastDownloader::EpisodesAdded);
QObject::connect(backend_, &PodcastBackend::SubscriptionAdded, this, &PodcastDownloader::SubscriptionAdded);
QObject::connect(app_, &Application::SettingsChanged, this, &PodcastDownloader::ReloadSettings);
ReloadSettings();
}
QString PodcastDownloader::DefaultDownloadDir() const {
return QDir::homePath() + "/Podcasts";
}
void PodcastDownloader::ReloadSettings() {
QSettings s;
s.beginGroup(kSettingsGroup);
auto_download_ = s.value("auto_download", false).toBool();
download_dir_ = s.value("download_dir", DefaultDownloadDir()).toString();
}
QString PodcastDownloader::FilenameForEpisode(const QString &directory, const PodcastEpisode &episode) const {
const QString file_extension = QFileInfo(episode.url().path()).suffix();
int count = 0;
// The file name contains the publication date and episode title
QString base_filename = episode.publication_date().date().toString(Qt::ISODate) + "-" + SanitiseFilenameComponent(episode.title());
// Add numbers on to the end of the filename until we find one that doesn't exist.
forever {
QString filename;
if (count == 0) {
filename = QString("%1/%2.%3").arg(directory, base_filename, file_extension);
}
else {
filename = QString("%1/%2 (%3).%4").arg(directory, base_filename, QString::number(count), file_extension);
}
if (!QFile::exists(filename)) {
return filename;
}
++count;
}
}
void PodcastDownloader::DownloadEpisode(const PodcastEpisode &episode) {
for (Task *tas : list_tasks_) {
if (tas->episode().database_id() == episode.database_id()) {
return;
}
}
Podcast podcast = backend_->GetSubscriptionById(episode.podcast_database_id());
if (!podcast.is_valid()) {
qLog(Warning) << "The podcast that contains episode" << episode.url() << "doesn't exist any more";
return;
}
const QString directory = download_dir_ + "/" + SanitiseFilenameComponent(podcast.title());
const QString filepath = FilenameForEpisode(directory, episode);
// Open the output file
if (!QDir(directory).exists()) QDir().mkpath(directory);
QFile *file = new QFile(filepath);
if (!file->open(QIODevice::WriteOnly)) {
qLog(Warning) << "Could not open the file" << filepath << "for writing";
return;
}
Task *task = new Task(episode, file, backend_);
list_tasks_ << task;
qLog(Info) << "Downloading" << task->episode().url() << "to" << filepath;
QObject::connect(task, &Task::finished, this, &PodcastDownloader::ReplyFinished);
QObject::connect(task, &Task::ProgressChanged, this, &PodcastDownloader::ProgressChanged);
}
void PodcastDownloader::ReplyFinished(Task *task) {
list_tasks_.removeAll(task);
delete task;
}
QString PodcastDownloader::SanitiseFilenameComponent(const QString &text) const {
return QString(text).replace(disallowed_filename_characters_, " ") .simplified();
}
void PodcastDownloader::SubscriptionAdded(const Podcast &podcast) {
EpisodesAdded(podcast.episodes());
}
void PodcastDownloader::EpisodesAdded(const PodcastEpisodeList &episodes) {
if (auto_download_) {
for (const PodcastEpisode &episode : episodes) {
DownloadEpisode(episode);
}
}
}
PodcastEpisodeList PodcastDownloader::EpisodesDownloading(const PodcastEpisodeList &episodes) {
PodcastEpisodeList ret;
for (Task *tas : list_tasks_) {
for (const PodcastEpisode &episode : episodes) {
if (tas->episode().database_id() == episode.database_id()) {
ret << episode;
}
}
}
return ret;
}
void PodcastDownloader::cancelDownload(const PodcastEpisodeList &episodes) {
QList<Task*> ta;
for (Task *tas : list_tasks_) {
for (const PodcastEpisode &episode : episodes) {
if (tas->episode().database_id() == episode.database_id()) {
ta << tas;
}
}
}
for (Task *tas : ta) {
tas->finishedPublic();
list_tasks_.removeAll(tas);
}
}

View File

@@ -1,129 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 PODCASTDOWNLOADER_H
#define PODCASTDOWNLOADER_H
#include <memory>
#include <QObject>
#include <QFile>
#include <QSet>
#include <QList>
#include <QQueue>
#include <QString>
#include <QRegularExpression>
#include <QNetworkRequest>
#include "core/networkaccessmanager.h"
#include "podcast.h"
#include "podcastepisode.h"
class Application;
class PodcastBackend;
class NetworkAccessManager;
class QNetworkReply;
namespace PodcastDownload {
enum State {
NotDownloading,
Queued,
Downloading,
Finished
};
}
class Task : public QObject {
Q_OBJECT
public:
Task(const PodcastEpisode &episode, QFile *file, PodcastBackend *backend);
PodcastEpisode episode() const;
signals:
void ProgressChanged(const PodcastEpisode &episode, const PodcastDownload::State state, const int percent);
void finished(Task *task);
public slots:
void finishedPublic();
private slots:
void reading();
void downloadProgressInternal(qint64 received, qint64 total);
void finishedInternal();
private:
std::unique_ptr<QFile> file_;
PodcastEpisode episode_;
PodcastBackend *backend_;
std::unique_ptr<NetworkAccessManager> network_;
QNetworkRequest req_;
QNetworkReply *reply_;
};
class PodcastDownloader : public QObject {
Q_OBJECT
public:
explicit PodcastDownloader(Application *app, QObject *parent = nullptr);
PodcastEpisodeList EpisodesDownloading(const PodcastEpisodeList &episodes);
QString DefaultDownloadDir() const;
public slots:
// Adds the episode to the download queue
void DownloadEpisode(const PodcastEpisode &episode);
void cancelDownload(const PodcastEpisodeList &episodes);
signals:
void ProgressChanged(const PodcastEpisode &episode, const PodcastDownload::State state, const int percent);
private slots:
void ReloadSettings();
void SubscriptionAdded(const Podcast &podcast);
void EpisodesAdded(const PodcastEpisodeList &episodes);
void ReplyFinished(Task *task);
private:
QString FilenameForEpisode(const QString &directory, const PodcastEpisode &episode) const;
QString SanitiseFilenameComponent(const QString &text) const;
private:
static const char *kSettingsGroup;
Application *app_;
PodcastBackend *backend_;
NetworkAccessManager *network_;
QRegularExpression disallowed_filename_characters_;
bool auto_download_;
QString download_dir_;
QList<Task*> list_tasks_;
};
#endif // PODCASTDOWNLOADER_H

View File

@@ -1,231 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 <limits>
#include <QSharedData>
#include <QIODevice>
#include <QFile>
#include <QFileInfo>
#include <QDataStream>
#include <QVariant>
#include <QVariantMap>
#include <QByteArray>
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QDateTime>
#include <QSqlQuery>
#include "core/logging.h"
#include "core/timeconstants.h"
#include "core/utilities.h"
#include "podcast.h"
#include "podcastepisode.h"
const QStringList PodcastEpisode::kColumns = QStringList() << "podcast_id"
<< "title"
<< "description"
<< "author"
<< "publication_date"
<< "duration_secs"
<< "url"
<< "listened"
<< "listened_date"
<< "downloaded"
<< "local_url"
<< "extra";
const QString PodcastEpisode::kColumnSpec = PodcastEpisode::kColumns.join(", ");
const QString PodcastEpisode::kJoinSpec = Utilities::Prepend("e.", PodcastEpisode::kColumns).join(", ");
const QString PodcastEpisode::kBindSpec = Utilities::Prepend(":", PodcastEpisode::kColumns).join(", ");
const QString PodcastEpisode::kUpdateSpec = Utilities::Updateify(PodcastEpisode::kColumns).join(", ");
struct PodcastEpisode::Private : public QSharedData {
Private();
int database_id_;
int podcast_database_id_;
QString title_;
QString description_;
QString author_;
QDateTime publication_date_;
int duration_secs_;
QUrl url_;
bool listened_;
QDateTime listened_date_;
bool downloaded_;
QUrl local_url_;
QVariantMap extra_;
};
PodcastEpisode::Private::Private()
: database_id_(-1),
podcast_database_id_(-1),
duration_secs_(-1),
listened_(false),
downloaded_(false) {}
PodcastEpisode::PodcastEpisode() : d(new Private) {}
PodcastEpisode::PodcastEpisode(const PodcastEpisode &other) : d(other.d) {}
PodcastEpisode::~PodcastEpisode() {}
PodcastEpisode &PodcastEpisode::operator=(const PodcastEpisode &other) {
d = other.d;
return *this;
}
int PodcastEpisode::database_id() const { return d->database_id_; }
int PodcastEpisode::podcast_database_id() const {
return d->podcast_database_id_;
}
const QString &PodcastEpisode::title() const { return d->title_; }
const QString &PodcastEpisode::description() const { return d->description_; }
const QString &PodcastEpisode::author() const { return d->author_; }
const QDateTime &PodcastEpisode::publication_date() const { return d->publication_date_; }
int PodcastEpisode::duration_secs() const { return d->duration_secs_; }
const QUrl &PodcastEpisode::url() const { return d->url_; }
bool PodcastEpisode::listened() const { return d->listened_; }
const QDateTime &PodcastEpisode::listened_date() const { return d->listened_date_; }
bool PodcastEpisode::downloaded() const { return d->downloaded_; }
const QUrl &PodcastEpisode::local_url() const { return d->local_url_; }
const QVariantMap &PodcastEpisode::extra() const { return d->extra_; }
QVariant PodcastEpisode::extra(const QString &key) const { return d->extra_[key]; }
void PodcastEpisode::set_database_id(const int v) { d->database_id_ = v; }
void PodcastEpisode::set_podcast_database_id(const int v) { d->podcast_database_id_ = v; }
void PodcastEpisode::set_title(const QString &v) { d->title_ = v; }
void PodcastEpisode::set_description(const QString &v) { d->description_ = v; }
void PodcastEpisode::set_author(const QString &v) { d->author_ = v; }
void PodcastEpisode::set_publication_date(const QDateTime &v) { d->publication_date_ = v; }
void PodcastEpisode::set_duration_secs(int v) { d->duration_secs_ = v; }
void PodcastEpisode::set_url(const QUrl &v) { d->url_ = v; }
void PodcastEpisode::set_listened(const bool v) { d->listened_ = v; }
void PodcastEpisode::set_listened_date(const QDateTime &v) { d->listened_date_ = v; }
void PodcastEpisode::set_downloaded(const bool v) { d->downloaded_ = v; }
void PodcastEpisode::set_local_url(const QUrl &v) { d->local_url_ = v; }
void PodcastEpisode::set_extra(const QVariantMap &v) { d->extra_ = v; }
void PodcastEpisode::set_extra(const QString &key, const QVariant &value) { d->extra_[key] = value; }
void PodcastEpisode::InitFromQuery(const QSqlQuery &query) {
d->database_id_ = query.value(0).toInt();
d->podcast_database_id_ = query.value(1).toInt();
d->title_ = query.value(2).toString();
d->description_ = query.value(3).toString();
d->author_ = query.value(4).toString();
d->publication_date_ = QDateTime::fromSecsSinceEpoch(query.value(5).toUInt());
d->duration_secs_ = query.value(6).toInt();
d->url_ = QUrl::fromEncoded(query.value(7).toByteArray());
d->listened_ = query.value(8).toBool();
// After setting QDateTime to invalid state, it's saved into database as time_t,
// when this number std::numeric_limits<unsigned int>::max() (4294967295) is read back from database, it creates a valid QDateTime.
// So to make it behave consistently, this change is needed.
if (query.value(9).toUInt() == std::numeric_limits<unsigned int>::max()) {
d->listened_date_ = QDateTime();
}
else {
d->listened_date_ = QDateTime::fromSecsSinceEpoch(query.value(9).toUInt());
}
d->downloaded_ = query.value(10).toBool();
d->local_url_ = QUrl::fromEncoded(query.value(11).toByteArray());
QDataStream extra_stream(query.value(12).toByteArray());
extra_stream >> d->extra_;
}
void PodcastEpisode::BindToQuery(QSqlQuery* query) const {
query->bindValue(":podcast_id", d->podcast_database_id_);
query->bindValue(":title", d->title_);
query->bindValue(":description", d->description_);
query->bindValue(":author", d->author_);
query->bindValue(":publication_date", d->publication_date_.toSecsSinceEpoch());
query->bindValue(":duration_secs", d->duration_secs_);
query->bindValue(":url", d->url_.toEncoded());
query->bindValue(":listened", d->listened_);
query->bindValue(":listened_date", d->listened_date_.toSecsSinceEpoch());
query->bindValue(":downloaded", d->downloaded_);
query->bindValue(":local_url", d->local_url_.toEncoded());
QByteArray extra;
QDataStream extra_stream(&extra, QIODevice::WriteOnly);
extra_stream << d->extra_;
query->bindValue(":extra", extra);
}
Song PodcastEpisode::ToSong(const Podcast &podcast) const {
Song ret;
ret.set_valid(true);
ret.set_title(title().simplified());
ret.set_artist(author().simplified());
ret.set_length_nanosec(kNsecPerSec * duration_secs());
ret.set_year(publication_date().date().year());
ret.set_comment(description());
ret.set_id(database_id());
ret.set_ctime(publication_date().toSecsSinceEpoch());
ret.set_genre(QString("Podcast"));
//ret.set_genre_id3(186);
if (listened() && listened_date().isValid()) {
ret.set_mtime(listened_date().toSecsSinceEpoch());
}
else {
ret.set_mtime(publication_date().toSecsSinceEpoch());
}
if (ret.length_nanosec() < 0) {
ret.set_length_nanosec(-1);
}
if (downloaded() && QFile::exists(local_url().toLocalFile())) {
ret.set_url(local_url());
}
else {
ret.set_url(url());
}
ret.set_basefilename(QFileInfo(ret.url().path()).fileName());
// Use information from the podcast if it's set
if (podcast.is_valid()) {
ret.set_album(podcast.title().simplified());
ret.set_art_automatic(podcast.ImageUrlLarge());
if (author().isEmpty()) ret.set_artist(podcast.title().simplified());
}
return ret;
}

View File

@@ -1,100 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 PODCASTEPISODE_H
#define PODCASTEPISODE_H
#include <QSharedDataPointer>
#include <QVariant>
#include <QVariantMap>
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QDateTime>
#include <QSqlQuery>
#include "core/song.h"
class Podcast;
class PodcastEpisode {
public:
PodcastEpisode();
PodcastEpisode(const PodcastEpisode &other);
~PodcastEpisode();
static const QStringList kColumns;
static const QString kColumnSpec;
static const QString kJoinSpec;
static const QString kBindSpec;
static const QString kUpdateSpec;
void InitFromQuery(const QSqlQuery &query);
void BindToQuery(QSqlQuery *query) const;
Song ToSong(const Podcast &podcast) const;
bool is_valid() const { return database_id() != -1; }
int database_id() const;
int podcast_database_id() const;
const QString &title() const;
const QString &description() const;
const QString &author() const;
const QDateTime &publication_date() const;
int duration_secs() const;
const QUrl &url() const;
bool listened() const;
const QDateTime &listened_date() const;
bool downloaded() const;
const QUrl &local_url() const;
const QVariantMap &extra() const;
QVariant extra(const QString &key) const;
void set_database_id(const int v);
void set_podcast_database_id(int v);
void set_title(const QString &v);
void set_description(const QString &v);
void set_author(const QString &v);
void set_publication_date(const QDateTime &v);
void set_duration_secs(int v);
void set_url(const QUrl &v);
void set_listened(const bool v);
void set_listened_date(const QDateTime &v);
void set_downloaded(const bool v);
void set_local_url(const QUrl &v);
void set_extra(const QVariantMap &v);
void set_extra(const QString &key, const QVariant &value);
PodcastEpisode &operator=(const PodcastEpisode &other);
private:
struct Private;
QSharedDataPointer<Private> d;
};
Q_DECLARE_METATYPE(PodcastEpisode)
typedef QList<PodcastEpisode> PodcastEpisodeList;
Q_DECLARE_METATYPE(QList<PodcastEpisode>)
#endif // PODCASTEPISODE_H

View File

@@ -1,59 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
Copyright 2018, Jim Broadus <jbroadus@gmail.com>
* Copyright 2019-2021, 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 <QDialog>
#include "core/application.h"
#include "podcastepisode.h"
#include "podcastinfodialog.h"
#include "ui_podcastinfodialog.h"
PodcastInfoDialog::PodcastInfoDialog(Application *app, QWidget *parent)
: QDialog(parent), app_(app), ui_(new Ui_PodcastInfoDialog) {
ui_->setupUi(this);
ui_->podcast_details->SetApplication(app);
ui_->episode_details->SetApplication(app);
}
PodcastInfoDialog::~PodcastInfoDialog() { delete ui_; }
void PodcastInfoDialog::ShowPodcast(const Podcast &podcast) {
ui_->episode_info_scroll_area->hide();
ui_->podcast_url->setText(podcast.url().toString());
ui_->podcast_url->setReadOnly(true);
ui_->podcast_details->SetPodcast(podcast);
show();
}
void PodcastInfoDialog::ShowEpisode(const PodcastEpisode &episode, const Podcast &podcast) {
ui_->episode_info_scroll_area->show();
ui_->podcast_url->setText(episode.url().toString());
ui_->podcast_url->setReadOnly(true);
ui_->podcast_details->SetPodcast(podcast);
ui_->episode_details->SetEpisode(episode);
show();
}

View File

@@ -1,48 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
Copyright 2018, Jim Broadus <jbroadus@gmail.com>
* Copyright 2019-2021, 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 PODCASTINFODIALOG_H
#define PODCASTINFODIALOG_H
#include <QDialog>
class Application;
class Podcast;
class PodcastEpisode;
class Ui_PodcastInfoDialog;
class PodcastInfoDialog : public QDialog {
Q_OBJECT
public:
explicit PodcastInfoDialog(Application *app, QWidget *parent = nullptr);
~PodcastInfoDialog();
void ShowPodcast(const Podcast &podcast);
void ShowEpisode(const PodcastEpisode &episode, const Podcast &podcast);
private:
Application *app_;
Ui_PodcastInfoDialog *ui_;
};
#endif // PODCASTINFODIALOG_H

View File

@@ -1,142 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PodcastInfoDialog</class>
<widget class="QDialog" name="PodcastInfoDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>493</width>
<height>415</height>
</rect>
</property>
<property name="windowTitle">
<string>Podcast Information</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLineEdit" name="podcast_url"/>
</item>
<item>
<widget class="QScrollArea" name="episode_info_scroll_area">
<property name="enabled">
<bool>true</bool>
</property>
<property name="minimumSize">
<size>
<width>250</width>
<height>100</height>
</size>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="EpisodeInfoWidget" name="episode_details">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>473</width>
<height>158</height>
</rect>
</property>
</widget>
</widget>
</item>
<item>
<widget class="QScrollArea" name="podcast_info_scroll_area">
<property name="minimumSize">
<size>
<width>250</width>
<height>100</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="PodcastInfoWidget" name="podcast_details">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>473</width>
<height>157</height>
</rect>
</property>
</widget>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>PodcastInfoWidget</class>
<extends>QWidget</extends>
<header>podcasts/podcastinfowidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>EpisodeInfoWidget</class>
<extends>QWidget</extends>
<header location="global">podcasts/episodeinfowidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>PodcastInfoDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>PodcastInfoDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -1,134 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 <QWidget>
#include <QImage>
#include <QPixmap>
#include <QColor>
#include <QPalette>
#include <QLabel>
#include "core/application.h"
#include "covermanager/albumcoverloader.h"
#include "covermanager/albumcoverloaderoptions.h"
#include "covermanager/albumcoverloaderresult.h"
#include "podcastinfowidget.h"
#include "ui_podcastinfowidget.h"
PodcastInfoWidget::PodcastInfoWidget(QWidget *parent)
: QWidget(parent),
ui_(new Ui_PodcastInfoWidget),
app_(nullptr),
image_id_(0) {
ui_->setupUi(this);
cover_options_.desired_height_ = 180;
ui_->image->setFixedSize(cover_options_.desired_height_, cover_options_.desired_height_);
// Set the colour of all the labels
const bool light = palette().color(QPalette::Base).value() > 128;
const QColor color = palette().color(QPalette::Dark);
QPalette label_palette(palette());
label_palette.setColor(QPalette::WindowText, light ? color.darker(150) : color.lighter(125));
for (QLabel* label : findChildren<QLabel*>()) {
if (label->property("field_label").toBool()) {
label->setPalette(label_palette);
}
}
}
PodcastInfoWidget::~PodcastInfoWidget() { delete ui_; }
void PodcastInfoWidget::SetApplication(Application *app) {
app_ = app;
connect(app_->album_cover_loader(), &AlbumCoverLoader::AlbumCoverLoaded, this, &PodcastInfoWidget::AlbumCoverLoaded);
}
namespace {
template<typename T>
void SetText(const QString& value, T* label, QLabel* buddy_label = nullptr) {
const bool visible = !value.isEmpty();
label->setVisible(visible);
if (buddy_label) {
buddy_label->setVisible(visible);
}
if (visible) {
label->setText(value);
}
}
} // namespace
void PodcastInfoWidget::SetPodcast(const Podcast &podcast) {
if (image_id_) {
app_->album_cover_loader()->CancelTask(image_id_);
image_id_ = 0;
}
podcast_ = podcast;
if (podcast.ImageUrlLarge().isValid()) {
// Start loading an image for this item.
image_id_ = app_->album_cover_loader()->LoadImageAsync(cover_options_, podcast.ImageUrlLarge(), QUrl());
}
ui_->image->hide();
SetText(podcast.title(), ui_->title);
SetText(podcast.description(), ui_->description);
SetText(podcast.copyright(), ui_->copyright, ui_->copyright_label);
SetText(podcast.author(), ui_->author, ui_->author_label);
SetText(podcast.owner_name(), ui_->owner, ui_->owner_label);
SetText(podcast.link().toString(), ui_->website, ui_->website_label);
SetText(podcast.extra("gpodder:subscribers").toString(), ui_->subscribers, ui_->subscribers_label);
if (!image_id_) {
emit LoadingFinished();
}
}
void PodcastInfoWidget::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result) {
if (id != image_id_) {
return;
}
image_id_ = 0;
if (result.success && !result.image_scaled.isNull()) {
ui_->image->setPixmap(QPixmap::fromImage(result.image_scaled));
ui_->image->show();
}
emit LoadingFinished();
}

View File

@@ -1,65 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 PODCASTINFOWIDGET_H
#define PODCASTINFOWIDGET_H
#include <QWidget>
#include "podcast.h"
#include "covermanager/albumcoverloaderoptions.h"
#include "covermanager/albumcoverloaderresult.h"
class Application;
class Ui_PodcastInfoWidget;
class QLabel;
class PodcastInfoWidget : public QWidget {
Q_OBJECT
public:
explicit PodcastInfoWidget(QWidget *parent = nullptr);
~PodcastInfoWidget();
void SetApplication(Application *app);
void SetPodcast(const Podcast& podcast);
signals:
void LoadingFinished();
private slots:
void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result);
private:
Ui_PodcastInfoWidget *ui_;
AlbumCoverLoaderOptions cover_options_;
Application *app_;
Podcast podcast_;
quint64 image_id_;
};
#endif // PODCASTINFOWIDGET_H

View File

@@ -1,220 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PodcastInfoWidget</class>
<widget class="QWidget" name="PodcastInfoWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>398</width>
<height>551</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<property name="styleSheet">
<string notr="true">#title {
font-weight: bold;
}
#description {
font-size: smaller;
}
QLineEdit {
background: transparent;
}</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="sizeConstraint">
<enum>QLayout::SetMinAndMaxSize</enum>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
</property>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="image">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="title">
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="description">
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout">
<property name="sizeConstraint">
<enum>QLayout::SetMinAndMaxSize</enum>
</property>
<item row="4" column="1">
<widget class="QLineEdit" name="website">
<property name="frame">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="author_label">
<property name="text">
<string>Author</string>
</property>
<property name="field_label" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="owner">
<property name="frame">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="website_label">
<property name="text">
<string>Website</string>
</property>
<property name="field_label" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="copyright">
<property name="frame">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="copyright_label">
<property name="text">
<string>Copyright</string>
</property>
<property name="field_label" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="author">
<property name="frame">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="owner_label">
<property name="text">
<string>Owner</string>
</property>
<property name="field_label" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="subscribers_label">
<property name="text">
<string>Subscribers</string>
</property>
<property name="field_label" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="subscribers">
<property name="frame">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -1,376 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <QByteArray>
#include <QString>
#include <QUrl>
#include <QDateTime>
#include <QXmlStreamReader>
#include <QRegularExpression>
#include "core/logging.h"
#include "core/utilities.h"
#include "podcastparser.h"
#include "opmlcontainer.h"
// Namespace constants must be lower case.
const char *PodcastParser::kAtomNamespace = "http://www.w3.org/2005/atom";
const char *PodcastParser::kItunesNamespace = "http://www.itunes.com/dtds/podcast-1.0.dtd";
PodcastParser::PodcastParser() {
supported_mime_types_ << "application/rss+xml"
<< "application/xml"
<< "text/x-opml"
<< "text/xml";
}
bool PodcastParser::SupportsContentType(const QString &content_type) const {
if (content_type.isEmpty()) {
// Why not have a go.
return true;
}
for (const QString &mime_type : supported_mime_types()) {
if (content_type.contains(mime_type)) {
return true;
}
}
return false;
}
bool PodcastParser::TryMagic(const QByteArray &data) const {
QString str(QString::fromUtf8(data));
return str.contains(QRegularExpression("<rss\\b")) || str.contains(QRegularExpression("<opml\\b"));
}
QVariant PodcastParser::Load(QIODevice *device, const QUrl &url) const {
QXmlStreamReader reader(device);
while (!reader.atEnd()) {
switch (reader.readNext()) {
case QXmlStreamReader::StartElement: {
const QString name = reader.name().toString();
if (name == "rss") {
Podcast podcast;
if (!ParseRss(&reader, &podcast)) {
return QVariant();
}
else {
podcast.set_url(url);
return QVariant::fromValue(podcast);
}
}
else if (name == "opml") {
OpmlContainer container;
if (!ParseOpml(&reader, &container)) {
return QVariant();
}
else {
container.url = url;
return QVariant::fromValue(container);
}
}
return QVariant();
}
default:
break;
}
}
return QVariant();
}
bool PodcastParser::ParseRss(QXmlStreamReader *reader, Podcast *ret) const {
if (!Utilities::ParseUntilElement(reader, "channel")) {
return false;
}
ParseChannel(reader, ret);
return true;
}
void PodcastParser::ParseChannel(QXmlStreamReader *reader, Podcast *ret) const {
while (!reader->atEnd()) {
QXmlStreamReader::TokenType type = reader->readNext();
switch (type) {
case QXmlStreamReader::StartElement: {
const QString name = reader->name().toString();
const QString lower_namespace = reader->namespaceUri().toString().toLower();
if (name == "title") {
ret->set_title(reader->readElementText());
}
else if (name == "link" && lower_namespace.isEmpty()) {
ret->set_link(QUrl::fromEncoded(reader->readElementText().toLatin1()));
}
else if (name == "description") {
ret->set_description(reader->readElementText());
}
else if (name == "owner" && lower_namespace == kItunesNamespace) {
ParseItunesOwner(reader, ret);
}
else if (name == "image") {
ParseImage(reader, ret);
}
else if (name == "copyright") {
ret->set_copyright(reader->readElementText());
}
else if (name == "link" && lower_namespace == kAtomNamespace && ret->url().isEmpty() && reader->attributes().value("rel").toString() == "self") {
ret->set_url(QUrl::fromEncoded(reader->readElementText().toLatin1()));
}
else if (name == "item") {
ParseItem(reader, ret);
}
else {
Utilities::ConsumeCurrentElement(reader);
}
break;
}
case QXmlStreamReader::EndElement:
return;
default:
break;
}
}
}
void PodcastParser::ParseImage(QXmlStreamReader *reader, Podcast *ret) const {
while (!reader->atEnd()) {
QXmlStreamReader::TokenType type = reader->readNext();
switch (type) {
case QXmlStreamReader::StartElement: {
const QString name = reader->name().toString();
if (name == "url") {
ret->set_image_url_large(
QUrl::fromEncoded(reader->readElementText().toLatin1()));
}
else {
Utilities::ConsumeCurrentElement(reader);
}
break;
}
case QXmlStreamReader::EndElement:
return;
default:
break;
}
}
}
void PodcastParser::ParseItunesOwner(QXmlStreamReader *reader, Podcast *ret) const {
while (!reader->atEnd()) {
QXmlStreamReader::TokenType type = reader->readNext();
switch (type) {
case QXmlStreamReader::StartElement: {
const QString name = reader->name().toString();
if (name == "name") {
ret->set_owner_name(reader->readElementText());
}
else if (name == "email") {
ret->set_owner_email(reader->readElementText());
}
else {
Utilities::ConsumeCurrentElement(reader);
}
break;
}
case QXmlStreamReader::EndElement:
return;
default:
break;
}
}
}
void PodcastParser::ParseItem(QXmlStreamReader *reader, Podcast *ret) const {
PodcastEpisode episode;
while (!reader->atEnd()) {
QXmlStreamReader::TokenType type = reader->readNext();
switch (type) {
case QXmlStreamReader::StartElement: {
const QString name = reader->name().toString();
const QString lower_namespace = reader->namespaceUri().toString().toLower();
if (name == "title") {
episode.set_title(reader->readElementText());
}
else if (name == "description") {
episode.set_description(reader->readElementText());
}
else if (name == "pubDate") {
QString date = reader->readElementText();
episode.set_publication_date(Utilities::ParseRFC822DateTime(date));
if (!episode.publication_date().isValid()) {
qLog(Error) << "Unable to parse date:" << date;
}
}
else if (name == "duration" && lower_namespace == kItunesNamespace) {
// http://www.apple.com/itunes/podcasts/specs.html
QStringList parts = reader->readElementText().split(':');
if (parts.count() == 2) {
episode.set_duration_secs(parts[0].toInt() * 60 + parts[1].toInt());
}
else if (parts.count() >= 3) {
episode.set_duration_secs(parts[0].toInt() * 60 * 60 + parts[1].toInt() * 60 + parts[2].toInt());
}
}
else if (name == "enclosure") {
const QString type2 = reader->attributes().value("type").toString();
const QUrl url = QUrl::fromEncoded(reader->attributes().value("url").toString().toLatin1());
if (type2.startsWith("audio/") || type2.startsWith("x-audio/")) {
episode.set_url(url);
}
// If the URL doesn't have a type, see if it's one of the obvious types
else if (type2.isEmpty() && (url.path().endsWith(".mp3", Qt::CaseInsensitive) || url.path().endsWith(".m4a", Qt::CaseInsensitive) || url.path().endsWith(".wav", Qt::CaseInsensitive))) {
episode.set_url(url);
}
Utilities::ConsumeCurrentElement(reader);
}
else if (name == "author" && lower_namespace == kItunesNamespace) {
episode.set_author(reader->readElementText());
}
else {
Utilities::ConsumeCurrentElement(reader);
}
break;
}
case QXmlStreamReader::EndElement:
if (!episode.publication_date().isValid()) {
episode.set_publication_date(QDateTime::currentDateTime());
}
if (!episode.url().isEmpty()) {
ret->add_episode(episode);
}
return;
default:
break;
}
}
}
bool PodcastParser::ParseOpml(QXmlStreamReader *reader, OpmlContainer *ret) const {
if (!Utilities::ParseUntilElement(reader, "body")) {
return false;
}
ParseOutline(reader, ret);
// OPML files sometimes consist of a single top level container.
OpmlContainer *top = ret;
while (top->feeds.count() == 0 && top->containers.count() == 1) {
top = &top->containers[0];
}
if (top != ret) {
// Copy the sub-container to a temporary location first.
OpmlContainer tmp = *top;
*ret = tmp;
}
return true;
}
void PodcastParser::ParseOutline(QXmlStreamReader *reader, OpmlContainer *ret) const {
while (!reader->atEnd()) {
QXmlStreamReader::TokenType type = reader->readNext();
switch (type) {
case QXmlStreamReader::StartElement: {
const QString name = reader->name().toString();
if (name != "outline") {
Utilities::ConsumeCurrentElement(reader);
continue;
}
QXmlStreamAttributes attributes = reader->attributes();
if (attributes.value("type").toString() == "rss") {
// Parse the feed and add it to this container
Podcast podcast;
podcast.set_description(attributes.value("description").toString());
QString title = attributes.value("title").toString();
if (title.isEmpty()) {
title = attributes.value("text").toString();
}
podcast.set_title(title);
podcast.set_image_url_large(QUrl::fromEncoded(attributes.value("imageHref").toString().toLatin1()));
podcast.set_url(QUrl::fromEncoded(attributes.value("xmlUrl").toString().toLatin1()));
ret->feeds.append(podcast);
// Consume any children and the EndElement.
Utilities::ConsumeCurrentElement(reader);
}
else {
// Create a new child container
OpmlContainer child;
// Take the name from the fullname attribute first if it exists.
child.name = attributes.value("fullname").toString();
if (child.name.isEmpty()) {
child.name = attributes.value("text").toString();
}
// Parse its contents and add it to this container
ParseOutline(reader, &child);
ret->containers.append(child);
}
break;
}
case QXmlStreamReader::EndElement:
return;
default:
break;
}
}
}

View File

@@ -1,72 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 PODCASTPARSER_H
#define PODCASTPARSER_H
#include <QByteArray>
#include <QString>
#include <QStringList>
#include <QUrl>
#include "podcast.h"
class QIODevice;
class QXmlStreamReader;
class OpmlContainer;
// Reads XML data from a QIODevice.
// Returns either a Podcast or an OpmlContainer depending on what was inside the XML document.
class PodcastParser {
public:
PodcastParser();
static const char *kAtomNamespace;
static const char *kItunesNamespace;
const QStringList &supported_mime_types() const { return supported_mime_types_; }
bool SupportsContentType(const QString &content_type) const;
// You should check the type of the returned QVariant to see whether it contains a Podcast or an OpmlContainer.
// If the QVariant isNull then an error occurred parsing the XML.
QVariant Load(QIODevice *device, const QUrl &url) const;
// Really quick test to see if some data might be supported. Load() might still return a null QVariant.
bool TryMagic(const QByteArray &data) const;
private:
bool ParseRss(QXmlStreamReader *reader, Podcast *ret) const;
void ParseChannel(QXmlStreamReader *reader, Podcast *ret) const;
void ParseImage(QXmlStreamReader *reader, Podcast *ret) const;
void ParseItunesOwner(QXmlStreamReader *reader, Podcast *ret) const;
void ParseItem(QXmlStreamReader *reader, Podcast *ret) const;
bool ParseOpml(QXmlStreamReader *reader, OpmlContainer *ret) const;
void ParseOutline(QXmlStreamReader *reader, OpmlContainer *ret) const;
private:
QStringList supported_mime_types_;
};
#endif // PODCASTPARSER_H

View File

@@ -1,919 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012-2013, David Sansome <me@davidsansome.com>
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2013-2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2014, Simeon Bird <sbird@andrew.cmu.edu>
* Copyright 2019-2021, 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 <memory>
#include "podcastservice.h"
#include <QObject>
#include <QtConcurrentRun>
#include <QSet>
#include <QMap>
#include <QVariant>
#include <QString>
#include <QIcon>
#include <QDateTime>
#include <QFont>
#include <QStandardItemModel>
#include <QStandardItem>
#include <QSortFilterProxyModel>
#include <QMenu>
#include <QAction>
#include "core/application.h"
#include "core/logging.h"
#include "core/mergedproxymodel.h"
#include "core/iconloader.h"
#include "core/standarditemiconloader.h"
//#include "podcastsmodel.h"
#include "podcastservicemodel.h"
#include "collection/collectionview.h"
#include "opmlcontainer.h"
#include "podcastbackend.h"
#include "podcastdeleter.h"
#include "podcastdownloader.h"
#include "podcastinfodialog.h"
#include "podcastupdater.h"
#include "addpodcastdialog.h"
#include "organize/organizedialog.h"
#include "organize/organizeerrordialog.h"
#include "playlist/playlistmanager.h"
#include "device/devicemanager.h"
#include "device/devicestatefiltermodel.h"
#include "device/deviceview.h"
const char* PodcastService::kServiceName = "Podcasts";
const char *PodcastService::kSettingsGroup = "Podcasts";
class PodcastSortProxyModel : public QSortFilterProxyModel {
public:
explicit PodcastSortProxyModel(QObject *parent = nullptr);
protected:
bool lessThan(const QModelIndex &left, const QModelIndex &right) const;
};
PodcastService::PodcastService(Application *app, QObject *parent)
: InternetService(Song::Source_Unknown, kServiceName, QString(), QString(), SettingsDialog::Page_Appearance, app, parent),
use_pretty_covers_(true),
hide_listened_(false),
show_episodes_(0),
icon_loader_(new StandardItemIconLoader(app->album_cover_loader(), this)),
backend_(app->podcast_backend()),
model_(new PodcastServiceModel(this)),
proxy_(new PodcastSortProxyModel(this)),
root_(nullptr),
organize_dialog_(new OrganizeDialog(app_->task_manager())) {
icon_loader_->SetModel(model_);
proxy_->setSourceModel(model_);
proxy_->setDynamicSortFilter(true);
proxy_->sort(0);
QObject::connect(backend_, &PodcastBackend::SubscriptionAdded, this, &PodcastService::SubscriptionAdded);
QObject::connect(backend_, &PodcastBackend::SubscriptionRemoved, this, &PodcastService::SubscriptionRemoved);
QObject::connect(backend_, &PodcastBackend::EpisodesAdded, this, &PodcastService::EpisodesAdded);
QObject::connect(backend_, &PodcastBackend::EpisodesUpdated, this, &PodcastService::EpisodesUpdated);
QObject::connect(app_->playlist_manager(), &PlaylistManager::CurrentSongChanged, this, &PodcastService::CurrentSongChanged);
QObject::connect(organize_dialog_.get(), &OrganizeDialog::FileCopied, this, &PodcastService::FileCopied);
}
PodcastService::~PodcastService() {}
PodcastSortProxyModel::PodcastSortProxyModel(QObject *parent) : QSortFilterProxyModel(parent) {}
bool PodcastSortProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const {
Q_UNUSED(left)
Q_UNUSED(right)
#if 0
const int left_type = left.data(InternetModel::Role_Type).toInt();
const int right_type = right.data(InternetModel::Role_Type).toInt();
// The special Add Podcast item comes first
if (left_type == PodcastService::Type_AddPodcast)
return true;
else if (right_type == PodcastService::Type_AddPodcast)
return false;
// Otherwise we only compare identical typed items.
if (left_type != right_type)
return QSortFilterProxyModel::lessThan(left, right);
switch (left_type) {
case PodcastService::Type_Podcast:
return left.data().toString().localeAwareCompare(right.data().toString()) < 0;
case PodcastService::Type_Episode: {
const PodcastEpisode left_episode = left.data(PodcastService::Role_Episode).value<PodcastEpisode>();
const PodcastEpisode right_episode = right.data(PodcastService::Role_Episode).value<PodcastEpisode>();
return left_episode.publication_date() > right_episode.publication_date();
}
default:
return QSortFilterProxyModel::lessThan(left, right);
}
#endif
return false;
}
QStandardItem *PodcastService::CreateRootItem() {
#if 0
root_ = new QStandardItem(IconLoader::Load("podcast"), tr("Podcasts"));
root_->setData(true, InternetModel::Role_CanLazyLoad);
return root_;
#endif
return nullptr;
}
void PodcastService::CopyToDevice() {
if (selected_episodes_.isEmpty() && explicitly_selected_podcasts_.isEmpty()) {
CopyToDevice(backend_->GetNewDownloadedEpisodes());
}
else {
CopyToDevice(selected_episodes_, explicitly_selected_podcasts_);
}
}
void PodcastService::CopyToDevice(const PodcastEpisodeList &episodes_list) {
SongList songs;
Podcast podcast;
for (const PodcastEpisode &episode : episodes_list) {
podcast = backend_->GetSubscriptionById(episode.podcast_database_id());
songs.append(episode.ToSong(podcast));
}
if (songs.isEmpty()) return;
organize_dialog_->SetDestinationModel(app_->device_manager()->connected_devices_model(), true);
organize_dialog_->SetCopy(true);
if (organize_dialog_->SetSongs(songs)) organize_dialog_->show();
}
void PodcastService::CopyToDevice(const QModelIndexList &episode_indexes, const QModelIndexList &podcast_indexes) {
PodcastEpisodeList episodes;
for (const QModelIndex &idx : episode_indexes) {
PodcastEpisode episode_tmp = idx.data(Role_Episode).value<PodcastEpisode>();
if (episode_tmp.downloaded()) episodes << episode_tmp;
}
for (const QModelIndex &idx : podcast_indexes) {
for (int i = 0; i < idx.model()->rowCount(idx); ++i) {
const QModelIndex &idx2 = idx.model()->index(i, 0, idx);
PodcastEpisode episode_tmp = idx2.data(Role_Episode).value<PodcastEpisode>();
if (episode_tmp.downloaded() && !episode_tmp.listened())
episodes << episode_tmp;
}
}
SongList songs;
for (const PodcastEpisode &episode : episodes) {
Podcast podcast = backend_->GetSubscriptionById(episode.podcast_database_id());
songs.append(episode.ToSong(podcast));
}
organize_dialog_->SetDestinationModel(app_->device_manager()->connected_devices_model(), true);
organize_dialog_->SetCopy(true);
if (organize_dialog_->SetSongs(songs)) organize_dialog_->show();
}
void PodcastService::CancelDownload() {
CancelDownload(selected_episodes_, explicitly_selected_podcasts_);
}
void PodcastService::CancelDownload(const QModelIndexList &episode_indexes, const QModelIndexList &podcast_indexes) {
PodcastEpisodeList episodes;
for (const QModelIndex &idx : episode_indexes) {
if (!idx.isValid()) continue;
PodcastEpisode episode_tmp = idx.data(Role_Episode).value<PodcastEpisode>();
episodes << episode_tmp;
}
for (const QModelIndex &idx : podcast_indexes) {
if (!idx.isValid()) continue;
for (int i = 0; i < idx.model()->rowCount(idx); ++i) {
const QModelIndex &idx2 = idx.model()->index(i, 0, idx);
if (!idx2.isValid()) continue;
PodcastEpisode episode_tmp = idx2.data(Role_Episode).value<PodcastEpisode>();
episodes << episode_tmp;
}
}
episodes = app_->podcast_downloader()->EpisodesDownloading(episodes);
app_->podcast_downloader()->cancelDownload(episodes);
}
void PodcastService::LazyPopulate(QStandardItem *parent) {
Q_UNUSED(parent)
#if 0
switch (parent->data(InternetModel::Role_Type).toInt()) {
case InternetModel::Type_Service:
PopulatePodcastList(model_->invisibleRootItem());
model()->merged_model()->AddSubModel(parent->index(), proxy_);
break;
}
#endif
}
void PodcastService::PopulatePodcastList(QStandardItem *parent) {
// Do this here since the downloader won't be created yet in the ctor.
QObject::connect(app_->podcast_downloader(), &PodcastDownloader::ProgressChanged, this, &PodcastService::DownloadProgressChanged);
if (default_icon_.isNull()) {
default_icon_ = IconLoader::Load("podcast");
}
PodcastList podcasts = backend_->GetAllSubscriptions();
for (const Podcast &podcast : podcasts) {
parent->appendRow(CreatePodcastItem(podcast));
}
}
void PodcastService::ClearPodcastList(QStandardItem *parent) {
parent->removeRows(0, parent->rowCount());
}
void PodcastService::UpdatePodcastText(QStandardItem *item, const int unlistened_count) const {
const Podcast podcast = item->data(Role_Podcast).value<Podcast>();
QString title = podcast.title().simplified();
QFont font;
if (unlistened_count > 0) {
// Add the number of new episodes after the title.
title.append(QString(" (%1)").arg(unlistened_count));
// Set a bold font
font.setBold(true);
}
item->setFont(font);
item->setText(title);
}
void PodcastService::UpdateEpisodeText(QStandardItem *item, const PodcastDownload::State state, const int percent) {
const PodcastEpisode episode = item->data(Role_Episode).value<PodcastEpisode>();
QString title = episode.title().simplified();
QString tooltip;
QFont font;
QIcon icon;
// Unlistened episodes are bold
if (!episode.listened()) {
font.setBold(true);
}
// Downloaded episodes get an icon
if (episode.downloaded()) {
if (downloaded_icon_.isNull()) {
downloaded_icon_ = IconLoader::Load("document-save");
}
icon = downloaded_icon_;
}
// Queued or downloading episodes get icons, tooltips, and maybe a title.
switch (state) {
case PodcastDownload::Queued:
if (queued_icon_.isNull()) {
queued_icon_ = IconLoader::Load("user-away");
}
icon = queued_icon_;
tooltip = tr("Download queued");
break;
case PodcastDownload::Downloading:
if (downloading_icon_.isNull()) {
downloading_icon_ = IconLoader::Load("go-down");
}
icon = downloading_icon_;
tooltip = tr("Downloading (%1%)...").arg(percent);
title = QString("[ %1% ] %2").arg(QString::number(percent), episode.title());
break;
case PodcastDownload::Finished:
case PodcastDownload::NotDownloading:
break;
}
item->setFont(font);
item->setText(title);
item->setIcon(icon);
}
void PodcastService::UpdatePodcastText(QStandardItem *item, const PodcastDownload::State state, const int percent) {
const Podcast podcast = item->data(Role_Podcast).value<Podcast>();
QString tooltip;
QIcon icon;
// Queued or downloading podcasts get icons, tooltips, and maybe a title.
switch (state) {
case PodcastDownload::Queued:
if (queued_icon_.isNull()) {
queued_icon_ = IconLoader::Load("user-away");
}
icon = queued_icon_;
item->setIcon(icon);
tooltip = tr("Download queued");
break;
case PodcastDownload::Downloading:
if (downloading_icon_.isNull()) {
downloading_icon_ = IconLoader::Load("go-down");
}
icon = downloading_icon_;
item->setIcon(icon);
tooltip = tr("Downloading (%1%)...").arg(percent);
break;
case PodcastDownload::Finished:
case PodcastDownload::NotDownloading:
if (podcast.ImageUrlSmall().isValid()) {
icon_loader_->LoadIcon(podcast.ImageUrlSmall(), QUrl(), item);
}
else {
item->setIcon(default_icon_);
}
break;
}
}
QStandardItem *PodcastService::CreatePodcastItem(const Podcast &podcast) {
QStandardItem *item = new QStandardItem;
// Add the episodes in this podcast and gather aggregate stats.
int unlistened_count = 0;
qint64 number = 0;
for (const PodcastEpisode &episode :
backend_->GetEpisodes(podcast.database_id())) {
if (!episode.listened()) {
unlistened_count++;
}
if (episode.listened() && hide_listened_) {
continue;
}
else {
item->appendRow(CreatePodcastEpisodeItem(episode));
++number;
}
if ((number >= show_episodes_) && (show_episodes_ != 0)) {
break;
}
}
item->setIcon(default_icon_);
//item->setData(Type_Podcast, InternetModel::Role_Type);
item->setData(QVariant::fromValue(podcast), Role_Podcast);
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsDragEnabled | Qt::ItemIsSelectable);
UpdatePodcastText(item, unlistened_count);
// Load the podcast's image if it has one
if (podcast.ImageUrlSmall().isValid()) {
icon_loader_->LoadIcon(podcast.ImageUrlSmall(), QUrl(), item);
}
podcasts_by_database_id_[podcast.database_id()] = item;
return item;
}
QStandardItem *PodcastService::CreatePodcastEpisodeItem(const PodcastEpisode &episode) {
QStandardItem *item = new QStandardItem;
item->setText(episode.title().simplified());
//item->setData(Type_Episode, InternetModel::Role_Type);
item->setData(QVariant::fromValue(episode), Role_Episode);
//item->setData(InternetModel::PlayBehaviour_UseSongLoader, InternetModel::Role_PlayBehaviour);
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsDragEnabled | Qt::ItemIsSelectable);
UpdateEpisodeText(item);
episodes_by_database_id_[episode.database_id()] = item;
return item;
}
void PodcastService::ShowContextMenu(const QPoint &global_pos) {
if (!context_menu_) {
context_menu_ = new QMenu;
context_menu_->addAction(IconLoader::Load("list-add"), tr("Add podcast..."), this, &PodcastService::AddPodcast);
context_menu_->addAction(IconLoader::Load("view-refresh"), tr("Update all podcasts"), app_->podcast_updater(), &PodcastUpdater::UpdateAllPodcastsNow);
context_menu_->addSeparator();
//context_menu_->addActions(GetPlaylistActions());
context_menu_->addSeparator();
update_selected_action_ = context_menu_->addAction(IconLoader::Load("view-refresh"), tr("Update this podcast"), this, &PodcastService::UpdateSelectedPodcast);
download_selected_action_ = context_menu_->addAction(IconLoader::Load("download"), "", this, &PodcastService::DownloadSelectedEpisode);
info_selected_action_ = context_menu_->addAction(IconLoader::Load("about-info"), tr("Podcast information"), this, &PodcastService::PodcastInfo);
delete_downloaded_action_ = context_menu_->addAction(IconLoader::Load("edit-delete"), tr("Delete downloaded data"), this, &PodcastService::DeleteDownloadedData);
copy_to_device_ = context_menu_->addAction(IconLoader::Load("multimedia-player-ipod-mini-blue"), tr("Copy to device..."), this, QOverload<>::of(&PodcastService::CopyToDevice));
cancel_download_ = context_menu_->addAction(IconLoader::Load("cancel"), tr("Cancel download"), this, QOverload<>::of(&PodcastService::CancelDownload));
remove_selected_action_ = context_menu_->addAction(IconLoader::Load("list-remove"), tr("Unsubscribe"), this, QOverload<>::of(&PodcastService::RemoveSelectedPodcast));
context_menu_->addSeparator();
set_new_action_ = context_menu_->addAction(tr("Mark as new"), this, &PodcastService::SetNew);
set_listened_action_ = context_menu_->addAction(tr("Mark as listened"), this, QOverload<>::of(&PodcastService::SetListened));
context_menu_->addSeparator();
context_menu_->addAction(IconLoader::Load("configure"), tr("Configure podcasts..."), this, &PodcastService::ShowConfig);
copy_to_device_->setDisabled(app_->device_manager()->connected_devices_model()->rowCount() == 0);
connect(app_->device_manager()->connected_devices_model(), &DeviceStateFilterModel::IsEmptyChanged, copy_to_device_, &QAction::setDisabled);
}
selected_episodes_.clear();
selected_podcasts_.clear();
explicitly_selected_podcasts_.clear();
QSet<int> podcast_ids;
#if 0
for (const QModelIndex &index : model()->selected_indexes()) {
switch (index.data(InternetModel::Role_Type).toInt()) {
case Type_Podcast: {
const int id = index.data(Role_Podcast).value<Podcast>().database_id();
if (!podcast_ids.contains(id)) {
selected_podcasts_.append(index);
explicitly_selected_podcasts_.append(index);
podcast_ids.insert(id);
}
break;
}
case Type_Episode: {
selected_episodes_.append(index);
// Add the parent podcast as well.
const QModelIndex parent = index.parent();
const int id = parent.data(Role_Podcast).value<Podcast>().database_id();
if (!podcast_ids.contains(id)) {
selected_podcasts_.append(parent);
podcast_ids.insert(id);
}
break;
}
}
}
#endif
const bool episodes = !selected_episodes_.isEmpty();
const bool podcasts = !selected_podcasts_.isEmpty();
update_selected_action_->setEnabled(podcasts);
remove_selected_action_->setEnabled(podcasts);
set_new_action_->setEnabled(episodes || podcasts);
set_listened_action_->setEnabled(episodes || podcasts);
cancel_download_->setEnabled(episodes || podcasts);
if (selected_episodes_.count() == 1) {
const PodcastEpisode episode = selected_episodes_[0].data(Role_Episode).value<PodcastEpisode>();
const bool downloaded = episode.downloaded();
const bool listened = episode.listened();
download_selected_action_->setEnabled(!downloaded);
delete_downloaded_action_->setEnabled(downloaded);
if (explicitly_selected_podcasts_.isEmpty()) {
set_new_action_->setEnabled(listened);
set_listened_action_->setEnabled(!listened || !episode.listened_date().isValid());
}
}
else {
download_selected_action_->setEnabled(episodes);
delete_downloaded_action_->setEnabled(episodes);
}
if (selected_podcasts_.count() == 1) {
if (selected_episodes_.count() == 1) {
info_selected_action_->setText(tr("Episode information"));
info_selected_action_->setEnabled(true);
}
else {
info_selected_action_->setText(tr("Podcast information"));
info_selected_action_->setEnabled(true);
}
}
else {
info_selected_action_->setText(tr("Podcast information"));
info_selected_action_->setEnabled(false);
}
if (explicitly_selected_podcasts_.isEmpty() && selected_episodes_.isEmpty()) {
PodcastEpisodeList epis = backend_->GetNewDownloadedEpisodes();
set_listened_action_->setEnabled(!epis.isEmpty());
}
if (selected_episodes_.count() > 1) {
download_selected_action_->setText(
tr("Download %n episodes", "", selected_episodes_.count()));
}
else {
download_selected_action_->setText(tr("Download this episode"));
}
//GetAppendToPlaylistAction()->setEnabled(episodes || podcasts);
//GetReplacePlaylistAction()->setEnabled(episodes || podcasts);
//GetOpenInNewPlaylistAction()->setEnabled(episodes || podcasts);
context_menu_->popup(global_pos);
}
void PodcastService::UpdateSelectedPodcast() {
for (const QModelIndex &index : selected_podcasts_) {
app_->podcast_updater()->UpdatePodcastNow(
index.data(Role_Podcast).value<Podcast>());
}
}
void PodcastService::RemoveSelectedPodcast() {
for (const QModelIndex &index : selected_podcasts_) {
backend_->Unsubscribe(index.data(Role_Podcast).value<Podcast>());
}
}
void PodcastService::ReloadSettings() {
InitialLoadSettings();
ClearPodcastList(model_->invisibleRootItem());
PopulatePodcastList(model_->invisibleRootItem());
}
void PodcastService::InitialLoadSettings() {
QSettings s;
s.beginGroup(CollectionSettingsPage::kSettingsGroup);
use_pretty_covers_ = s.value("pretty_covers", true).toBool();
s.endGroup();
s.beginGroup(kSettingsGroup);
hide_listened_ = s.value("hide_listened", false).toBool();
show_episodes_ = s.value("show_episodes", 0).toInt();
s.endGroup();
// TODO(notme): reload the podcast icons that are already loaded?
}
void PodcastService::EnsureAddPodcastDialogCreated() {
add_podcast_dialog_.reset(new AddPodcastDialog(app_));
}
void PodcastService::AddPodcast() {
EnsureAddPodcastDialogCreated();
add_podcast_dialog_->show();
}
void PodcastService::FileCopied(int database_id) {
SetListened(PodcastEpisodeList() << backend_->GetEpisodeById(database_id), true);
}
void PodcastService::SubscriptionAdded(const Podcast &podcast) {
// Ensure the root item is lazy loaded already
LazyLoadRoot();
// The podcast might already be in the list - maybe the LazyLoadRoot() above
// added it.
QStandardItem *item = podcasts_by_database_id_[podcast.database_id()];
if (!item) {
item = CreatePodcastItem(podcast);
model_->appendRow(item);
}
//emit ScrollToIndex(MapToMergedModel(item->index()));
}
void PodcastService::SubscriptionRemoved(const Podcast &podcast) {
QStandardItem *item = podcasts_by_database_id_.take(podcast.database_id());
if (item) {
// Remove any episode ID -> item mappings for the episodes in this podcast.
for (int i = 0; i < item->rowCount(); ++i) {
QStandardItem *episode_item = item->child(i);
const int episode_id = episode_item->data(Role_Episode).value<PodcastEpisode>().database_id();
episodes_by_database_id_.remove(episode_id);
}
// Remove this episode's row
model_->removeRow(item->row());
}
}
void PodcastService::EpisodesAdded(const PodcastEpisodeList &episodes) {
QSet<int> seen_podcast_ids;
for (const PodcastEpisode &episode : episodes) {
const int database_id = episode.podcast_database_id();
QStandardItem *parent = podcasts_by_database_id_[database_id];
if (!parent) continue;
parent->appendRow(CreatePodcastEpisodeItem(episode));
if (!seen_podcast_ids.contains(database_id)) {
// Update the unlistened count text once for each podcast
int unlistened_count = 0;
for (const PodcastEpisode &i : backend_->GetEpisodes(database_id)) {
if (!i.listened()) {
++unlistened_count;
}
}
UpdatePodcastText(parent, unlistened_count);
seen_podcast_ids.insert(database_id);
}
const Podcast podcast = parent->data(Role_Podcast).value<Podcast>();
ReloadPodcast(podcast);
}
}
void PodcastService::EpisodesUpdated(const PodcastEpisodeList &episodes) {
QSet<int> seen_podcast_ids;
QMap<int, Podcast> podcasts_map;
for (const PodcastEpisode &episode : episodes) {
const int podcast_database_id = episode.podcast_database_id();
QStandardItem *item = episodes_by_database_id_[episode.database_id()];
QStandardItem *parent = podcasts_by_database_id_[podcast_database_id];
if (!item || !parent) continue;
// Update the episode data on the item, and update the item's text.
item->setData(QVariant::fromValue(episode), Role_Episode);
UpdateEpisodeText(item);
// Update the parent podcast's text too.
if (!seen_podcast_ids.contains(podcast_database_id)) {
// Update the unlistened count text once for each podcast
int unlistened_count = 0;
for (const PodcastEpisode &i : backend_->GetEpisodes(podcast_database_id)) {
if (!i.listened()) {
++unlistened_count;
}
}
UpdatePodcastText(parent, unlistened_count);
seen_podcast_ids.insert(podcast_database_id);
}
const Podcast podcast = parent->data(Role_Podcast).value<Podcast>();
podcasts_map[podcast.database_id()] = podcast;
}
QList<Podcast> podcast_values = podcasts_map.values();
for (const Podcast &podcast_tmp : podcast_values) {
ReloadPodcast(podcast_tmp);
}
}
void PodcastService::DownloadSelectedEpisode() {
for (const QModelIndex &idx : selected_episodes_) {
app_->podcast_downloader()->DownloadEpisode(idx.data(Role_Episode).value<PodcastEpisode>());
}
}
void PodcastService::PodcastInfo() {
if (selected_podcasts_.isEmpty()) {
// Should never happen.
return;
}
const Podcast podcast = selected_podcasts_[0].data(Role_Podcast).value<Podcast>();
podcast_info_dialog_.reset(new PodcastInfoDialog(app_));
if (selected_episodes_.count() == 1) {
const PodcastEpisode episode = selected_episodes_[0].data(Role_Episode).value<PodcastEpisode>();
podcast_info_dialog_->ShowEpisode(episode, podcast);
}
else {
podcast_info_dialog_->ShowPodcast(podcast);
}
}
void PodcastService::DeleteDownloadedData() {
for (const QModelIndex &idx : selected_episodes_) {
app_->podcast_deleter()->DeleteEpisode(idx.data(Role_Episode).value<PodcastEpisode>());
}
}
void PodcastService::DownloadProgressChanged(const PodcastEpisode &episode, const PodcastDownload::State state, const int percent) {
QStandardItem *item = episodes_by_database_id_[episode.database_id()];
QStandardItem *item2 = podcasts_by_database_id_[episode.podcast_database_id()];
if (!item || !item2) return;
UpdateEpisodeText(item, state, percent);
UpdatePodcastText(item2, state, percent);
}
void PodcastService::ShowConfig() {
//app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Podcasts);
}
void PodcastService::CurrentSongChanged(const Song &metadata) {
// This does two db queries, and we are called on every song change, so run this off the main thread.
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
(void)QtConcurrent::run(&PodcastService::UpdatePodcastListenedStateAsync, this, metadata);
#else
(void)QtConcurrent::run(this, &PodcastService::UpdatePodcastListenedStateAsync, metadata);
#endif
}
void PodcastService::UpdatePodcastListenedStateAsync(const Song &metadata) {
// Check whether this song is one of our podcast episodes.
PodcastEpisode episode = backend_->GetEpisodeByUrlOrLocalUrl(metadata.url());
if (!episode.is_valid()) return;
// Mark it as listened if it's not already
if (!episode.listened() || !episode.listened_date().isValid()) {
episode.set_listened(true);
episode.set_listened_date(QDateTime::currentDateTime());
backend_->UpdateEpisodes(PodcastEpisodeList() << episode);
}
}
void PodcastService::SetNew() {
SetListened(selected_episodes_, explicitly_selected_podcasts_, false);
}
void PodcastService::SetListened() {
if (selected_episodes_.isEmpty() && explicitly_selected_podcasts_.isEmpty()) {
SetListened(backend_->GetNewDownloadedEpisodes(), true);
}
else {
SetListened(selected_episodes_, explicitly_selected_podcasts_, true);
}
}
void PodcastService::SetListened(const PodcastEpisodeList &episodes_list, const bool listened) {
PodcastEpisodeList episodes;
QDateTime current_date_time = QDateTime::currentDateTime();
for (PodcastEpisode episode : episodes_list) {
episode.set_listened(listened);
if (listened) {
episode.set_listened_date(current_date_time);
}
episodes << episode;
}
backend_->UpdateEpisodes(episodes);
}
void PodcastService::SetListened(const QModelIndexList &episode_indexes, const QModelIndexList& podcast_indexes, const bool listened) {
PodcastEpisodeList episodes;
// Get all the episodes from the indexes.
for (const QModelIndex& index : episode_indexes) {
episodes << index.data(Role_Episode).value<PodcastEpisode>();
}
for (const QModelIndex& podcast : podcast_indexes) {
for (int i = 0; i < podcast.model()->rowCount(podcast); ++i) {
const QModelIndex& index = podcast.model()->index(i, 0, podcast);
episodes << index.data(Role_Episode).value<PodcastEpisode>();
}
}
// Update each one with the new state and maybe the listened time.
QDateTime current_date_time = QDateTime::currentDateTime();
for (int i = 0; i < episodes.count(); ++i) {
PodcastEpisode *episode = &episodes[i];
episode->set_listened(listened);
if (listened) {
episode->set_listened_date(current_date_time);
}
}
backend_->UpdateEpisodes(episodes);
}
QModelIndex PodcastService::MapToMergedModel(const QModelIndex &idx) const {
Q_UNUSED(idx)
//return model()->merged_model()->mapFromSource(proxy_->mapFromSource(index));
return QModelIndex();
}
void PodcastService::LazyLoadRoot() {
#if 0
if (root_->data(InternetModel::Role_CanLazyLoad).toBool()) {
root_->setData(false, InternetModel::Role_CanLazyLoad);
LazyPopulate(root_);
}
#endif
}
void PodcastService::SubscribeAndShow(const QVariant &podcast_or_opml) {
if (podcast_or_opml.canConvert<Podcast>()) {
Podcast podcast(podcast_or_opml.value<Podcast>());
backend_->Subscribe(&podcast);
// Lazy load the root item if it hasn't been already
LazyLoadRoot();
QStandardItem *item = podcasts_by_database_id_[podcast.database_id()];
if (item) {
// There will be an item already if this podcast was already there, otherwise it'll be scrolled to when the item is created.
//emit ScrollToIndex(MapToMergedModel(item->index()));
}
}
else if (podcast_or_opml.canConvert<OpmlContainer>()) {
EnsureAddPodcastDialogCreated();
add_podcast_dialog_->ShowWithOpml(podcast_or_opml.value<OpmlContainer>());
}
}
void PodcastService::ReloadPodcast(const Podcast &podcast) {
if (!(hide_listened_ || (show_episodes_ > 0))) {
return;
}
QStandardItem *item = podcasts_by_database_id_[podcast.database_id()];
model_->invisibleRootItem()->removeRow(item->row());
model_->invisibleRootItem()->appendRow(CreatePodcastItem(podcast));
}

View File

@@ -1,178 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012-2013, David Sansome <me@davidsansome.com>
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2013-2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2014, Simeon Bird <sbird@andrew.cmu.edu>
* Copyright 2019-2021, 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 PODCASTSERVICE_H
#define PODCASTSERVICE_H
#include <memory>
#include <QMap>
#include <QIcon>
#include <QScopedPointer>
//#include "internet/internetmodel.h"
#include "internet/internetservice.h"
#include "podcastdeleter.h"
#include "podcastdownloader.h"
class QMenu;
class QAction;
class AddPodcastDialog;
class PodcastInfoDialog;
class OrganizeDialog;
class Podcast;
class PodcastBackend;
class PodcastEpisode;
class StandardItemIconLoader;
class QStandardItemModel;
class QStandardItem;
class QSortFilterProxyModel;
class PodcastService : public InternetService {
Q_OBJECT
public:
PodcastService(Application *app, QObject *parent);
~PodcastService();
static const char *kServiceName;
static const char *kSettingsGroup;
enum Type {
Type_AddPodcast = 0,
Type_Podcast,
Type_Episode
};
enum Role {
Role_Podcast = 0,
Role_Episode
};
QStandardItem *CreateRootItem();
void LazyPopulate(QStandardItem *parent);
bool has_initial_load_settings() const { return true; }
void ShowContextMenu(const QPoint &global_pos);
void ReloadSettings();
void InitialLoadSettings();
// Called by SongLoader when the user adds a Podcast URL directly.
// Adds a subscription to the podcast and displays it in the UI.
// If the QVariant contains an OPML file then this displays it in the Add Podcast dialog.
void SubscribeAndShow(const QVariant &podcast_or_opml);
public slots:
void AddPodcast();
void FileCopied(const int database_id);
private slots:
void UpdateSelectedPodcast();
void ReloadPodcast(const Podcast &podcast);
void RemoveSelectedPodcast();
void DownloadSelectedEpisode();
void PodcastInfo();
void DeleteDownloadedData();
void SetNew();
void SetListened();
void ShowConfig();
void SubscriptionAdded(const Podcast &podcast);
void SubscriptionRemoved(const Podcast &podcast);
void EpisodesAdded(const PodcastEpisodeList &episodes);
void EpisodesUpdated(const PodcastEpisodeList &episodes);
void DownloadProgressChanged(const PodcastEpisode &episode, PodcastDownload::State state, int percent);
void CurrentSongChanged(const Song &metadata);
void CopyToDevice();
void CopyToDevice(const PodcastEpisodeList &episodes_list);
void CopyToDevice(const QModelIndexList &episode_indexes, const QModelIndexList &podcast_indexes);
void CancelDownload();
void CancelDownload(const QModelIndexList &episode_indexes, const QModelIndexList &podcast_indexes);
private:
void EnsureAddPodcastDialogCreated();
void UpdatePodcastListenedStateAsync(const Song &metadata);
void PopulatePodcastList(QStandardItem *parent);
void ClearPodcastList(QStandardItem *parent);
void UpdatePodcastText(QStandardItem *item, const int unlistened_count) const;
void UpdateEpisodeText(QStandardItem *item, const PodcastDownload::State state = PodcastDownload::NotDownloading, const int percent = 0);
void UpdatePodcastText(QStandardItem *item, const PodcastDownload::State state = PodcastDownload::NotDownloading, const int percent = 0);
QStandardItem *CreatePodcastItem(const Podcast &podcast);
QStandardItem *CreatePodcastEpisodeItem(const PodcastEpisode &episode);
QModelIndex MapToMergedModel(const QModelIndex &idx) const;
void SetListened(const QModelIndexList &episode_indexes, const QModelIndexList &podcast_indexes, const bool listened);
void SetListened(const PodcastEpisodeList &episodes_list, bool listened);
void LazyLoadRoot();
private:
bool use_pretty_covers_;
bool hide_listened_;
qint64 show_episodes_;
StandardItemIconLoader *icon_loader_;
// The podcast icon
QIcon default_icon_;
// Episodes get different icons depending on their state
QIcon queued_icon_;
QIcon downloading_icon_;
QIcon downloaded_icon_;
PodcastBackend *backend_;
QStandardItemModel *model_;
QSortFilterProxyModel *proxy_;
QMenu *context_menu_;
QAction *update_selected_action_;
QAction *remove_selected_action_;
QAction *download_selected_action_;
QAction *info_selected_action_;
QAction *delete_downloaded_action_;
QAction *set_new_action_;
QAction *set_listened_action_;
QAction *copy_to_device_;
QAction *cancel_download_;
QStandardItem *root_;
std::unique_ptr<OrganizeDialog> organize_dialog_;
QModelIndexList explicitly_selected_podcasts_;
QModelIndexList selected_podcasts_;
QModelIndexList selected_episodes_;
QMap<int, QStandardItem*> podcasts_by_database_id_;
QMap<int, QStandardItem*> episodes_by_database_id_;
std::unique_ptr<AddPodcastDialog> add_podcast_dialog_;
std::unique_ptr<PodcastInfoDialog> podcast_info_dialog_;
};
#endif // PODCASTSERVICE_H

View File

@@ -1,101 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 <QList>
#include <QVariant>
#include <QUrl>
#include <QMimeData>
#include "podcastservicemodel.h"
#include "podcastservice.h"
#include "playlist/songmimedata.h"
PodcastServiceModel::PodcastServiceModel(QObject* parent) : QStandardItemModel(parent) {}
QMimeData* PodcastServiceModel::mimeData(const QModelIndexList &indexes) const {
SongMimeData *data = new SongMimeData;
QList<QUrl> urls;
#if 0
for (const QModelIndex& index : indexes) {
switch (index.data(InternetModel::Role_Type).toInt()) {
case PodcastService::Type_Episode:
MimeDataForEpisode(index, data, &urls);
break;
case PodcastService::Type_Podcast:
MimeDataForPodcast(index, data, &urls);
break;
}
}
#endif
data->setUrls(urls);
return data;
}
void PodcastServiceModel::MimeDataForEpisode(const QModelIndex &idx, SongMimeData *data, QList<QUrl>* urls) const {
QVariant episode_variant = idx.data(PodcastService::Role_Episode);
if (!episode_variant.isValid()) return;
PodcastEpisode episode(episode_variant.value<PodcastEpisode>());
// Get the podcast from the index's parent
Podcast podcast;
QVariant podcast_variant = idx.parent().data(PodcastService::Role_Podcast);
if (podcast_variant.isValid()) {
podcast = podcast_variant.value<Podcast>();
}
Song song = episode.ToSong(podcast);
data->songs << song;
*urls << song.url();
}
void PodcastServiceModel::MimeDataForPodcast(const QModelIndex &idx, SongMimeData *data, QList<QUrl> *urls) const {
// Get the podcast
Podcast podcast;
QVariant podcast_variant = idx.data(PodcastService::Role_Podcast);
if (podcast_variant.isValid()) {
podcast = podcast_variant.value<Podcast>();
}
// Add each child episode
const int children = idx.model()->rowCount(idx);
for (int i = 0; i < children; ++i) {
QVariant episode_variant = idx.model()->index(i, 0, idx).data(PodcastService::Role_Episode);
if (!episode_variant.isValid()) continue;
PodcastEpisode episode(episode_variant.value<PodcastEpisode>());
Song song = episode.ToSong(podcast);
data->songs << song;
*urls << song.url();
}
}

View File

@@ -1,46 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 PODCASTSERVICEMODEL_H
#define PODCASTSERVICEMODEL_H
#include <QStandardItemModel>
#include <QList>
#include <QUrl>
class SongMimeData;
class PodcastServiceModel : public QStandardItemModel {
Q_OBJECT
public:
explicit PodcastServiceModel(QObject *parent = nullptr);
QMimeData* mimeData(const QModelIndexList &indexes) const;
private:
void MimeDataForPodcast(const QModelIndex &idx, SongMimeData* data, QList<QUrl> *urls) const;
void MimeDataForEpisode(const QModelIndex &idx, SongMimeData* data, QList<QUrl> *urls) const;
};
#endif // PODCASTSERVICEMODEL_H

View File

@@ -1,194 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 "podcastupdater.h"
#include <QObject>
#include <QSet>
#include <QList>
#include <QUrl>
#include <QDateTime>
#include <QTimer>
#include <QSettings>
#include "core/application.h"
#include "core/logging.h"
#include "core/timeconstants.h"
#include "podcastbackend.h"
#include "podcasturlloader.h"
const char *PodcastUpdater::kSettingsGroup = "Podcasts";
PodcastUpdater::PodcastUpdater(Application *app, QObject *parent)
: QObject(parent),
app_(app),
update_interval_secs_(0),
update_timer_(new QTimer(this)),
loader_(new PodcastUrlLoader(this)),
pending_replies_(0) {
update_timer_->setSingleShot(true);
QObject::connect(app_, &Application::SettingsChanged, this, &PodcastUpdater::ReloadSettings);
QObject::connect(update_timer_, &QTimer::timeout, this, &PodcastUpdater::UpdateAllPodcastsNow);
QObject::connect(app_->podcast_backend(), &PodcastBackend::SubscriptionAdded, this, &PodcastUpdater::SubscriptionAdded);
ReloadSettings();
}
void PodcastUpdater::ReloadSettings() {
QSettings s;
s.beginGroup(kSettingsGroup);
last_full_update_ = s.value("last_full_update").toDateTime();
update_interval_secs_ = s.value("update_interval_secs").toInt();
s.endGroup();
RestartTimer();
}
void PodcastUpdater::SaveSettings() {
QSettings s;
s.beginGroup(kSettingsGroup);
s.setValue("last_full_update", last_full_update_);
s.endGroup();
}
void PodcastUpdater::RestartTimer() {
// Stop any existing timer
update_timer_->stop();
if (pending_replies_ > 0) {
// We're still waiting for replies from the last update - don't do anything.
return;
}
if (update_interval_secs_ > 0) {
if (!last_full_update_.isValid()) {
// Updates are enabled and we've never updated before. Do it now.
qLog(Info) << "Updating podcasts for the first time";
UpdateAllPodcastsNow();
}
else {
const QDateTime next_update = last_full_update_.addSecs(update_interval_secs_);
const int secs_until_next_update = QDateTime::currentDateTime().secsTo(next_update);
if (secs_until_next_update < 0) {
qLog(Info) << "Updating podcasts" << (-secs_until_next_update) << "seconds late";
UpdateAllPodcastsNow();
}
else {
qLog(Info) << "Updating podcasts at" << next_update << "(in" << secs_until_next_update << "seconds)";
update_timer_->start(secs_until_next_update * kMsecPerSec);
}
}
}
}
void PodcastUpdater::SubscriptionAdded(const Podcast& podcast) {
// Only update a new podcast immediately if it doesn't have an episode list.
// We assume that the episode list has already been fetched recently otherwise.
if (podcast.episodes().isEmpty()) {
UpdatePodcastNow(podcast);
}
}
void PodcastUpdater::UpdatePodcastNow(const Podcast& podcast) {
PodcastUrlLoaderReply *reply = loader_->Load(podcast.url());
QObject::connect(reply, &PodcastUrlLoaderReply::Finished, this, [this, reply, podcast]() { PodcastLoaded(reply, podcast, false); });
}
void PodcastUpdater::UpdateAllPodcastsNow() {
PodcastList podcasts = app_->podcast_backend()->GetAllSubscriptions();
for (const Podcast &podcast : podcasts) {
PodcastUrlLoaderReply *reply = loader_->Load(podcast.url());
QObject::connect(reply, &PodcastUrlLoaderReply::Finished, this, [this, reply, podcast]() { PodcastLoaded(reply, podcast, true); });
++pending_replies_;
}
}
void PodcastUpdater::PodcastLoaded(PodcastUrlLoaderReply *reply, const Podcast& podcast, bool one_of_many) {
reply->deleteLater();
if (one_of_many) {
--pending_replies_;
if (pending_replies_ == 0) {
// This was the last reply we were waiting for. Save this time as being
// the last successful update and restart the timer.
last_full_update_ = QDateTime::currentDateTime();
SaveSettings();
RestartTimer();
}
}
if (!reply->is_success()) {
qLog(Warning) << "Error fetching podcast at" << podcast.url() << ":" << reply->error_text();
return;
}
if (reply->result_type() != PodcastUrlLoaderReply::Type_Podcast) {
qLog(Warning) << "The URL" << podcast.url() << "no longer contains a podcast";
return;
}
// Get the episode URLs we had for this podcast already.
QSet<QUrl> existing_urls;
for (const PodcastEpisode &episode :
app_->podcast_backend()->GetEpisodes(podcast.database_id())) {
existing_urls.insert(episode.url());
}
// Add any new episodes
PodcastEpisodeList new_episodes;
PodcastList reply_podcasts = reply->podcast_results();
for (const Podcast &reply_podcast : reply_podcasts) {
PodcastEpisodeList episodes = reply_podcast.episodes();
for (const PodcastEpisode &episode : episodes) {
if (!existing_urls.contains(episode.url())) {
PodcastEpisode episode_copy(episode);
episode_copy.set_podcast_database_id(podcast.database_id());
new_episodes.append(episode_copy);
}
}
}
app_->podcast_backend()->AddEpisodes(&new_episodes);
qLog(Info) << "Added" << new_episodes.count() << "new episodes for" << podcast.url();
}

View File

@@ -1,71 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 PODCASTUPDATER_H
#define PODCASTUPDATER_H
#include <QObject>
#include <QDateTime>
class Application;
class Podcast;
class PodcastUrlLoader;
class PodcastUrlLoaderReply;
class QTimer;
// Responsible for updating podcasts when they're first subscribed to, and then updating them at regular intervals afterwards.
class PodcastUpdater : public QObject {
Q_OBJECT
public:
explicit PodcastUpdater(Application *app, QObject *parent = nullptr);
public slots:
void UpdateAllPodcastsNow();
void UpdatePodcastNow(const Podcast &podcast);
private slots:
void ReloadSettings();
void SubscriptionAdded(const Podcast &podcast);
void PodcastLoaded(PodcastUrlLoaderReply *reply, const Podcast &podcast, const bool one_of_many);
private:
void RestartTimer();
void SaveSettings();
private:
static const char *kSettingsGroup;
Application *app_;
QDateTime last_full_update_;
int update_interval_secs_;
QTimer *update_timer_;
PodcastUrlLoader *loader_;
int pending_replies_;
};
#endif // PODCASTUPDATER_H

View File

@@ -1,250 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 <QUrl>
#include <QUrlQuery>
#include <QNetworkRequest>
#include <QNetworkReply>
#include "core/logging.h"
#include "core/networkaccessmanager.h"
#include "core/utilities.h"
#include "podcasturlloader.h"
#include "podcastparser.h"
const int PodcastUrlLoader::kMaxRedirects = 5;
PodcastUrlLoader::PodcastUrlLoader(QObject* parent)
: QObject(parent),
network_(new NetworkAccessManager(this)),
parser_(new PodcastParser),
html_link_re_("<link (.*)>"),
html_link_rel_re_("rel\\s*=\\s*['\"]?\\s*alternate"),
html_link_type_re_("type\\s*=\\s*['\"]?([^'\" ]+)"),
html_link_href_re_("href\\s*=\\s*['\"]?([^'\" ]+)") {
//html_link_re_.setMinimal(true);
//html_link_re_.setCaseSensitivity(Qt::CaseInsensitive);
}
PodcastUrlLoader::~PodcastUrlLoader() { delete parser_; }
QUrl PodcastUrlLoader::FixPodcastUrl(const QString& url_text) {
QString url_text_copy(url_text.trimmed());
// Thanks gpodder!
QuickPrefixList quick_prefixes = QuickPrefixList()
<< QuickPrefix("fb:", "http://feeds.feedburner.com/%1")
<< QuickPrefix("yt:", "https://www.youtube.com/rss/user/%1/videos.rss")
<< QuickPrefix("sc:", "https://soundcloud.com/%1")
<< QuickPrefix("fm4od:", "http://onapp1.orf.at/webcam/fm4/fod/%1.xspf")
<< QuickPrefix("ytpl:", "https://gdata.youtube.com/feeds/api/playlists/%1");
// Check if it matches one of the quick prefixes.
for (QuickPrefixList::const_iterator it = quick_prefixes.constBegin(); it != quick_prefixes.constEnd(); ++it) {
if (url_text_copy.startsWith(it->first)) {
url_text_copy = it->second.arg(url_text_copy.mid(it->first.length()));
}
}
if (!url_text_copy.contains("://")) {
url_text_copy.prepend("http://");
}
return FixPodcastUrl(QUrl(url_text_copy));
}
QUrl PodcastUrlLoader::FixPodcastUrl(const QUrl& url_orig) {
QUrl url(url_orig);
QUrlQuery url_query(url);
// Replace schemes
if (url.scheme().isEmpty() || url.scheme() == "feed" || url.scheme() == "itpc" || url.scheme() == "itms") {
url.setScheme("http");
}
else if (url.scheme() == "zune" && url.host() == "subscribe" &&
!url_query.queryItems().isEmpty()) {
url = QUrl(url_query.queryItems()[0].second);
}
return url;
}
PodcastUrlLoaderReply* PodcastUrlLoader::Load(const QString& url_text) {
return Load(FixPodcastUrl(url_text));
}
PodcastUrlLoaderReply* PodcastUrlLoader::Load(const QUrl& url) {
// Create a reply
PodcastUrlLoaderReply* reply = new PodcastUrlLoaderReply(url, this);
// Create a state object to track this request
RequestState* state = new RequestState;
state->redirects_remaining_ = kMaxRedirects + 1;
state->reply_ = reply;
// Start the first request
NextRequest(url, state);
return reply;
}
void PodcastUrlLoader::SendErrorAndDelete(const QString& error_text, RequestState* state) {
state->reply_->SetFinished(error_text);
delete state;
}
void PodcastUrlLoader::NextRequest(const QUrl& url, RequestState* state) {
// Stop the request if there have been too many redirects already.
if (state->redirects_remaining_-- == 0) {
SendErrorAndDelete(tr("Too many redirects"), state);
return;
}
qLog(Debug) << "Loading URL" << url;
QNetworkRequest req(url);
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
QNetworkReply* network_reply = network_->get(req);
QObject::connect(network_reply, &QNetworkReply::finished, this, [this, state, network_reply]() { RequestFinished(state, network_reply); });
}
void PodcastUrlLoader::RequestFinished(RequestState* state, QNetworkReply* reply) {
reply->deleteLater();
if (reply->attribute(QNetworkRequest::RedirectionTargetAttribute).isValid()) {
const QUrl next_url = reply->url().resolved(reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl());
NextRequest(next_url, state);
return;
}
// Check for errors.
if (reply->error() != QNetworkReply::NoError) {
SendErrorAndDelete(reply->errorString(), state);
return;
}
const QVariant http_status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
if (http_status.isValid() && http_status.toInt() != 200) {
SendErrorAndDelete(
QString("HTTP %1: %2").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) .toString(), reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString()),
state);
return;
}
// Check the mime type.
const QString content_type = reply->header(QNetworkRequest::ContentTypeHeader).toString();
if (parser_->SupportsContentType(content_type)) {
const QVariant ret = parser_->Load(reply, reply->url());
if (ret.canConvert<Podcast>()) {
state->reply_->SetFinished(PodcastList() << ret.value<Podcast>());
}
else if (ret.canConvert<OpmlContainer>()) {
state->reply_->SetFinished(ret.value<OpmlContainer>());
}
else {
SendErrorAndDelete(tr("Failed to parse the XML for this RSS feed"),
state);
return;
}
delete state;
return;
}
else if (content_type.contains("text/html")) {
// I don't want a full HTML parser here, so do this the dirty way.
const QString page_text = QString::fromUtf8(reply->readAll());
//int pos = 0;
#if 0
while ((pos = html_link_re_.indexIn(page_text, pos)) != -1) {
const QString link = html_link_re_.cap(1).toLower();
pos += html_link_re_.matchedLength();
if (html_link_rel_re_.indexIn(link) == -1 ||
html_link_type_re_.indexIn(link) == -1 ||
html_link_href_re_.indexIn(link) == -1) {
continue;
}
const QString link_type = html_link_type_re_.cap(1);
const QString href = Utilities::DecodeHtmlEntities(html_link_href_re_.cap(1));
if (parser_->supported_mime_types().contains(link_type)) {
NextRequest(QUrl(href), state);
return;
}
}
#endif
SendErrorAndDelete(tr("HTML page did not contain any RSS feeds"), state);
}
else {
SendErrorAndDelete(tr("Unknown content-type") + ": " + content_type, state);
}
}
PodcastUrlLoaderReply::PodcastUrlLoaderReply(const QUrl& url, QObject* parent)
: QObject(parent), url_(url), finished_(false) {}
void PodcastUrlLoaderReply::SetFinished(const PodcastList& results) {
result_type_ = Type_Podcast;
podcast_results_ = results;
finished_ = true;
emit Finished(true);
}
void PodcastUrlLoaderReply::SetFinished(const OpmlContainer& results) {
result_type_ = Type_Opml;
opml_results_ = results;
finished_ = true;
emit Finished(true);
}
void PodcastUrlLoaderReply::SetFinished(const QString& error_text) {
error_text_ = error_text;
finished_ = true;
emit Finished(false);
}

View File

@@ -1,119 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, 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 PODCASTURLLOADER_H
#define PODCASTURLLOADER_H
#include <QObject>
#include <QRegularExpression>
#include "opmlcontainer.h"
#include "podcast.h"
class PodcastParser;
class QNetworkAccessManager;
class QNetworkReply;
class PodcastUrlLoaderReply : public QObject {
Q_OBJECT
public:
PodcastUrlLoaderReply(const QUrl& url, QObject* parent);
enum ResultType { Type_Podcast,
Type_Opml };
const QUrl& url() const { return url_; }
bool is_finished() const { return finished_; }
bool is_success() const { return error_text_.isEmpty(); }
const QString& error_text() const { return error_text_; }
ResultType result_type() const { return result_type_; }
const PodcastList& podcast_results() const { return podcast_results_; }
const OpmlContainer& opml_results() const { return opml_results_; }
void SetFinished(const QString& error_text);
void SetFinished(const PodcastList& results);
void SetFinished(const OpmlContainer& results);
signals:
void Finished(bool success);
private:
QUrl url_;
bool finished_;
QString error_text_;
ResultType result_type_;
PodcastList podcast_results_;
OpmlContainer opml_results_;
};
class PodcastUrlLoader : public QObject {
Q_OBJECT
public:
explicit PodcastUrlLoader(QObject* parent = nullptr);
~PodcastUrlLoader();
static const int kMaxRedirects;
PodcastUrlLoaderReply* Load(const QString& url_text);
PodcastUrlLoaderReply* Load(const QUrl& url);
// Both the FixPodcastUrl functions replace common podcatcher URL schemes
// like itpc:// or zune:// with their http:// equivalents. The QString
// overload also cleans up user-entered text a bit - stripping whitespace and
// applying shortcuts like sc:tag.
static QUrl FixPodcastUrl(const QString& url_text);
static QUrl FixPodcastUrl(const QUrl& url);
private:
struct RequestState {
int redirects_remaining_;
PodcastUrlLoaderReply* reply_;
};
typedef QPair<QString, QString> QuickPrefix;
typedef QList<QuickPrefix> QuickPrefixList;
private slots:
void RequestFinished(RequestState* state, QNetworkReply* reply);
private:
void SendErrorAndDelete(const QString& error_text, RequestState* state);
void NextRequest(const QUrl& url, RequestState* state);
private:
QNetworkAccessManager* network_;
PodcastParser* parser_;
QRegularExpression html_link_re_;
QRegularExpression whitespace_re_;
QRegularExpression html_link_rel_re_;
QRegularExpression html_link_type_re_;
QRegularExpression html_link_href_re_;
};
#endif // PODCASTURLLOADER_H

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

@@ -1,146 +0,0 @@
/* This file is part of Clementine.
Copyright 2012, David Sansome <me@davidsansome.com>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Copyright 2014, John Maguire <john.maguire@gmail.com>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "podcastsettingspage.h"
#include <QFileDialog>
#include <QSettings>
#include "core/application.h"
#include "core/timeconstants.h"
#include "core/iconloader.h"
#include "collection/collectiondirectorymodel.h"
#include "collection/collectionmodel.h"
#include "podcasts/gpoddersync.h"
#include "podcasts/podcastdownloader.h"
#include "settingsdialog.h"
#include "ui_podcastsettingspage.h"
const char* PodcastSettingsPage::kSettingsGroup = "Podcasts";
PodcastSettingsPage::PodcastSettingsPage(SettingsDialog* dialog)
: SettingsPage(dialog), ui_(new Ui_PodcastSettingsPage) {
ui_->setupUi(this);
setWindowIcon(IconLoader::Load("podcast"));
connect(ui_->login, SIGNAL(clicked()), SLOT(LoginClicked()));
connect(ui_->login_state, SIGNAL(LoginClicked()), SLOT(LoginClicked()));
connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(LogoutClicked()));
connect(ui_->download_dir_browse, SIGNAL(clicked()),
SLOT(DownloadDirBrowse()));
GPodderSync* gsync = dialog->app()->gpodder_sync();
connect(gsync, SIGNAL(LoginSuccess()), SLOT(GpodderLoginSuccess()));
connect(gsync, SIGNAL(LoginFailure(const QString&)), SLOT(GpodderLoginFailure(const QString&)));
ui_->login_state->AddCredentialField(ui_->username);
ui_->login_state->AddCredentialField(ui_->device_name);
ui_->login_state->AddCredentialField(ui_->password);
ui_->login_state->AddCredentialGroup(ui_->login_group);
ui_->check_interval->setItemData(0, 0); // manually
ui_->check_interval->setItemData(1, 10 * 60); // 10 minutes
ui_->check_interval->setItemData(2, 20 * 60); // 20 minutes
ui_->check_interval->setItemData(3, 30 * 60); // 30 minutes
ui_->check_interval->setItemData(4, 60 * 60); // 1 hour
ui_->check_interval->setItemData(5, 2 * 60 * 60); // 2 hours
ui_->check_interval->setItemData(6, 6 * 60 * 60); // 6 hours
ui_->check_interval->setItemData(7, 12 * 60 * 60); // 12 hours
}
PodcastSettingsPage::~PodcastSettingsPage() { delete ui_; }
void PodcastSettingsPage::Load() {
QSettings s;
s.beginGroup(kSettingsGroup);
const int update_interval = s.value("update_interval_secs", 0).toInt();
ui_->check_interval->setCurrentIndex(
ui_->check_interval->findData(update_interval));
const QString default_download_dir =
dialog()->app()->podcast_downloader()->DefaultDownloadDir();
ui_->download_dir->setText(QDir::toNativeSeparators(
s.value("download_dir", default_download_dir).toString()));
ui_->auto_download->setChecked(s.value("auto_download", false).toBool());
ui_->hide_listened->setChecked(s.value("hide_listened", false).toBool());
ui_->delete_after->setValue(s.value("delete_after", 0).toInt() / kSecsPerDay);
ui_->show_episodes->setValue(s.value("show_episodes", 0).toInt());
ui_->username->setText(s.value("gpodder_username").toString());
ui_->device_name->setText(
s.value("gpodder_device_name", GPodderSync::DefaultDeviceName())
.toString());
if (dialog()->app()->gpodder_sync()->is_logged_in()) {
ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn,
ui_->username->text());
}
else {
ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut);
}
}
void PodcastSettingsPage::Save() {
QSettings s;
s.beginGroup(kSettingsGroup);
s.setValue("update_interval_secs", ui_->check_interval->itemData(ui_->check_interval->currentIndex()));
s.setValue("download_dir",
QDir::fromNativeSeparators(ui_->download_dir->text()));
s.setValue("auto_download", ui_->auto_download->isChecked());
s.setValue("hide_listened", ui_->hide_listened->isChecked());
s.setValue("delete_after", ui_->delete_after->value() * kSecsPerDay);
s.setValue("show_episodes", ui_->show_episodes->value());
s.setValue("gpodder_device_name", ui_->device_name->text());
}
void PodcastSettingsPage::LoginClicked() {
ui_->login_state->SetLoggedIn(LoginStateWidget::LoginInProgress);
dialog()->app()->gpodder_sync()->Login(
ui_->username->text(), ui_->password->text(), ui_->device_name->text());
}
void PodcastSettingsPage::GpodderLoginSuccess() {
ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn,
ui_->username->text());
ui_->login_state->SetAccountTypeVisible(false);
}
void PodcastSettingsPage::GpodderLoginFailure(const QString& error) {
ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut,
ui_->username->text());
ui_->login_state->SetAccountTypeVisible(true);
ui_->login_state->SetAccountTypeText(tr("Login failed") + ": " + error);
}
void PodcastSettingsPage::LogoutClicked() {
ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut);
ui_->password->clear();
dialog()->app()->gpodder_sync()->Logout();
}
void PodcastSettingsPage::DownloadDirBrowse() {
QString directory = QFileDialog::getExistingDirectory(
this, tr("Choose podcast download directory"), ui_->download_dir->text());
if (directory.isEmpty()) return;
ui_->download_dir->setText(QDir::toNativeSeparators(directory));
}

View File

@@ -1,52 +0,0 @@
/* This file is part of Clementine.
Copyright 2012, David Sansome <me@davidsansome.com>
Copyright 2014, John Maguire <john.maguire@gmail.com>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef PODCASTSETTINGSPAGE_H
#define PODCASTSETTINGSPAGE_H
#include "settingspage.h"
class Ui_PodcastSettingsPage;
class PodcastSettingsPage : public SettingsPage {
Q_OBJECT
public:
explicit PodcastSettingsPage(SettingsDialog* dialog);
~PodcastSettingsPage();
static const char* kSettingsGroup;
void Load();
void Save();
private slots:
void LoginClicked();
void LogoutClicked();
void GpodderLoginSuccess();
void GpodderLoginFailure(const QString& error);
void DownloadDirBrowse();
private:
Ui_PodcastSettingsPage* ui_;
};
#endif // PODCASTSETTINGSPAGE_H

View File

@@ -1,290 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PodcastSettingsPage</class>
<widget class="QWidget" name="PodcastSettingsPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>616</width>
<height>656</height>
</rect>
</property>
<property name="windowTitle">
<string>Podcasts</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Updating</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Check for new episodes</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="check_interval">
<item>
<property name="text">
<string>Manually</string>
</property>
</item>
<item>
<property name="text">
<string>Every 10 minutes</string>
</property>
</item>
<item>
<property name="text">
<string>Every 20 minutes</string>
</property>
</item>
<item>
<property name="text">
<string>Every 30 minutes</string>
</property>
</item>
<item>
<property name="text">
<string>Every hour</string>
</property>
</item>
<item>
<property name="text">
<string>Every 2 hours</string>
</property>
</item>
<item>
<property name="text">
<string>Every 6 hours</string>
</property>
</item>
<item>
<property name="text">
<string>Every 12 hours</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Download episodes to</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="auto_download">
<property name="text">
<string>Download new episodes automatically</string>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLineEdit" name="download_dir"/>
</item>
<item>
<widget class="QPushButton" name="download_dir_browse">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Cleaning up</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Delete played episodes</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="delete_after">
<property name="specialValueText">
<string>Manually</string>
</property>
<property name="suffix">
<string> days</string>
</property>
<property name="prefix">
<string>After </string>
</property>
<property name="maximum">
<number>30</number>
</property>
<property name="empty_text" stdset="0">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>Appearance</string>
</property>
<layout class="QFormLayout" name="formLayout_4">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QCheckBox" name="hide_listened">
<property name="text">
<string>Don't show listened episodes</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="show_episodes">
<property name="specialValueText">
<string>All</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Number of episodes to show</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>gpodder.net</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Clementine can synchronize your subscription list with your other computers and podcast applications. &lt;a href=&quot;https://gpodder.net/register/&quot;&gt;Create an account&lt;/a&gt;.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="LoginStateWidget" name="login_state" native="true"/>
</item>
<item>
<widget class="QWidget" name="login_group" native="true">
<layout class="QFormLayout" name="formLayout_3">
<property name="margin">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Username</string>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="username"/>
</item>
<item>
<widget class="QPushButton" name="login">
<property name="text">
<string>Sign in</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Password</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="password">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Device name</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="device_name"/>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>LoginStateWidget</class>
<extends>QWidget</extends>
<header>widgets/loginstatewidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>check_interval</tabstop>
<tabstop>download_dir</tabstop>
<tabstop>download_dir_browse</tabstop>
<tabstop>auto_download</tabstop>
<tabstop>delete_after</tabstop>
<tabstop>username</tabstop>
<tabstop>password</tabstop>
<tabstop>device_name</tabstop>
<tabstop>login</tabstop>
</tabstops>
<connections/>
</ui>

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

@@ -1,10 +1,7 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, Jonas Kvinge <jonas@jkvinge.net>
* 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
@@ -21,25 +18,35 @@
*
*/
#ifndef OPMLCONTAINER_H
#define OPMLCONTAINER_H
#ifndef INFOTEXTVIEW_H
#define INFOTEXTVIEW_H
#include <QList>
#include <QTextBrowser>
#include <QString>
#include <QUrl>
#include "podcast.h"
class QResizeEvent;
class QWheelEvent;
class InfoTextView : public QTextBrowser {
Q_OBJECT
class OpmlContainer {
public:
// Only set for the top-level container
QUrl url;
explicit InfoTextView(QWidget *parent = nullptr);
QString name;
QList<OpmlContainer> containers;
PodcastList feeds;
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_;
};
Q_DECLARE_METATYPE(OpmlContainer)
#endif // OPMLCONTAINER_H
#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

@@ -1,37 +1,44 @@
/* This file is part of Clementine.
Copyright 2010, David Sansome <me@davidsansome.com>
/*
* 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/>.
*
*/
Clementine 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.
Clementine 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 Clementine. 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 <QPainter>
#include <QResizeEvent>
#include <QTimeLine>
#include <QtDebug>
#include "core/qt_blurimage.h"
const int WidgetFadeHelper::kLoadingPadding = 9;
const int WidgetFadeHelper::kLoadingBorderRadius = 10;
WidgetFadeHelper::WidgetFadeHelper(QWidget* parent, const int msec)
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()));
@@ -39,16 +46,18 @@ WidgetFadeHelper::WidgetFadeHelper(QWidget* parent, const int msec)
connect(fade_timeline_, SIGNAL(finished()), SLOT(FadeFinished()));
hide();
}
bool WidgetFadeHelper::eventFilter(QObject* obj, QEvent* event) {
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);
QResizeEvent *re = static_cast<QResizeEvent*>(event);
if (re->oldSize() == re->size()) {
// Ignore phoney resize events
return false;
@@ -59,9 +68,11 @@ bool WidgetFadeHelper::eventFilter(QObject* obj, QEvent* event) {
CaptureParent();
show();
return false;
}
void WidgetFadeHelper::StartBlur() {
CaptureParent();
// Cover the parent
@@ -73,9 +84,11 @@ void WidgetFadeHelper::StartBlur() {
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();
@@ -95,25 +108,23 @@ void WidgetFadeHelper::CaptureParent() {
QFontMetrics loading_font_metrics(loading_font);
const QString loading_text = tr("Loading...");
const QSize loading_size(
kLoadingPadding * 2 + loading_font_metrics.width(loading_text),
kLoadingPadding * 2 + loading_font_metrics.height());
const QRect loading_rect((blurred.width() - loading_size.width()) / 2, 100,
loading_size.width(), loading_size.height());
#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.setRenderHint(QPainter::HighQualityAntialiasing);
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.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.drawText(loading_rect.translated(-1, -1), Qt::AlignCenter, loading_text);
blur_painter.translate(-0.5, -0.5);
blur_painter.end();
@@ -121,6 +132,7 @@ void WidgetFadeHelper::CaptureParent() {
blurred_pixmap_ = QPixmap::fromImage(blurred);
resize(parent_->size());
}
void WidgetFadeHelper::StartFade() {
@@ -146,23 +158,30 @@ void WidgetFadeHelper::StartFade() {
}
void WidgetFadeHelper::paintEvent(QPaintEvent*) {
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 {
}
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

@@ -1,42 +1,48 @@
/* This file is part of Clementine.
Copyright 2010, David Sansome <me@davidsansome.com>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* 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);
WidgetFadeHelper(QWidget *parent, const int msec = 500);
public slots:
public Q_SLOTS:
void StartBlur();
void StartFade();
protected:
void paintEvent(QPaintEvent*);
bool eventFilter(QObject* obj, QEvent* event);
void paintEvent(QPaintEvent *event) override;
bool eventFilter(QObject *obj, QEvent *event) override;
private slots:
private Q_SLOTS:
void FadeFinished();
private:
@@ -46,9 +52,9 @@ class WidgetFadeHelper : public QWidget {
static const int kLoadingPadding;
static const int kLoadingBorderRadius;
QWidget* parent_;
QTimeLine* blur_timeline_;
QTimeLine* fade_timeline_;
QWidget *parent_;
QTimeLine *blur_timeline_;
QTimeLine *fade_timeline_;
QPixmap original_pixmap_;
QPixmap blurred_pixmap_;