Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f1bda0546 |
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
BIN
data/icons/128x128/guitar.png
Normal file
BIN
data/icons/128x128/guitar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
data/icons/22x22/guitar.png
Normal file
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
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
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
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
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
7
data/style/artistbio.css
Normal file
@@ -0,0 +1,7 @@
|
||||
QScrollArea {
|
||||
background: qpalette(base);
|
||||
}
|
||||
|
||||
QTextEdit {
|
||||
border: 0px;
|
||||
}
|
||||
126
src/artistbio/artistbiofetcher.cpp
Normal file
126
src/artistbio/artistbiofetcher.cpp
Normal 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);
|
||||
|
||||
}
|
||||
75
src/artistbio/artistbiofetcher.h
Normal file
75
src/artistbio/artistbiofetcher.h
Normal 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
|
||||
@@ -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()); }
|
||||
@@ -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
|
||||
287
src/artistbio/artistbioview.cpp
Normal file
287
src/artistbio/artistbioview.cpp
Normal 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()));
|
||||
}
|
||||
|
||||
}
|
||||
104
src/artistbio/artistbioview.h
Normal file
104
src/artistbio/artistbioview.h
Normal 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
|
||||
209
src/artistbio/lastfmartistbio.cpp
Normal file
209
src/artistbio/lastfmartistbio.cpp
Normal 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 ¶m : 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;
|
||||
|
||||
}
|
||||
66
src/artistbio/lastfmartistbio.h
Normal file
66
src/artistbio/lastfmartistbio.h
Normal 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
|
||||
319
src/artistbio/wikipediaartistbio.cpp
Normal file
319
src/artistbio/wikipediaartistbio.cpp
Normal 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> ¶ms) {
|
||||
|
||||
params << Param(QLatin1String("format"), QLatin1String("json"));
|
||||
params << Param(QLatin1String("action"), QLatin1String("query"));
|
||||
|
||||
QUrlQuery url_query;
|
||||
for (const Param ¶m : 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;
|
||||
|
||||
}
|
||||
69
src/artistbio/wikipediaartistbio.h
Normal file
69
src/artistbio/wikipediaartistbio.h
Normal 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> ¶ms);
|
||||
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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
39
src/core/latch.cpp
Normal 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
39
src/core/latch.h
Normal 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
|
||||
@@ -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();
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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 ¤t) {
|
||||
|
||||
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(¤t_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(¤t_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_));
|
||||
|
||||
}
|
||||
@@ -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 ¤t);
|
||||
|
||||
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
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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()));
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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(); }
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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(); }
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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());
|
||||
|
||||
}
|
||||
@@ -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 ©right() 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
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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));
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
|
||||
@@ -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. <a href="https://gpodder.net/register/">Create an account</a>.</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>
|
||||
174
src/widgets/collapsibleinfoheader.cpp
Normal file
174
src/widgets/collapsibleinfoheader.cpp
Normal 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_);
|
||||
}
|
||||
82
src/widgets/collapsibleinfoheader.h
Normal file
82
src/widgets/collapsibleinfoheader.h
Normal 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
|
||||
64
src/widgets/collapsibleinfopane.cpp
Normal file
64
src/widgets/collapsibleinfopane.cpp
Normal 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;
|
||||
|
||||
}
|
||||
72
src/widgets/collapsibleinfopane.h
Normal file
72
src/widgets/collapsibleinfopane.h
Normal 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
|
||||
89
src/widgets/infotextview.cpp
Normal file
89
src/widgets/infotextview.cpp
Normal 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);
|
||||
|
||||
}
|
||||
@@ -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
257
src/widgets/prettyimage.cpp
Normal 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
92
src/widgets/prettyimage.h
Normal 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
|
||||
189
src/widgets/prettyimageview.cpp
Normal file
189
src/widgets/prettyimageview.cpp
Normal 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);
|
||||
|
||||
}
|
||||
74
src/widgets/prettyimageview.h
Normal file
74
src/widgets/prettyimageview.h
Normal 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
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
@@ -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_;
|
||||
|
||||
Reference in New Issue
Block a user