Add podcasts support WIP
This commit is contained in:
parent
4ea5eb8292
commit
9564836c5a
|
@ -347,6 +347,12 @@ if(WIN32)
|
|||
add_definitions(-DSTATIC_GETOPT -D_UNICODE)
|
||||
endif()
|
||||
|
||||
if(BUILD_WITH_QT6)
|
||||
pkg_check_modules(LIBMYGPO libmygpo-qt6)
|
||||
else()
|
||||
pkg_check_modules(LIBMYGPO libmygpo-qt5)
|
||||
endif()
|
||||
|
||||
if(WIN32 AND NOT MSVC)
|
||||
# RC compiler
|
||||
string(REPLACE "gcc" "windres" CMAKE_RC_COMPILER_INIT ${CMAKE_C_COMPILER})
|
||||
|
@ -461,6 +467,8 @@ optional_component(EBUR128 ON "EBU R 128 loudness normalization"
|
|||
DEPENDS "gstreamer" HAVE_GSTREAMER
|
||||
)
|
||||
|
||||
optional_component(PODCASTS ON "Podcasts support" DEPENDS "libmygpo" LIBMYGPO_FOUND)
|
||||
|
||||
if(APPLE OR WIN32)
|
||||
set(USE_BUNDLE_DEFAULT ON)
|
||||
else()
|
||||
|
|
|
@ -1000,11 +1000,75 @@ optional_source(HAVE_MOODBAR
|
|||
settings/moodbarsettingspage.ui
|
||||
)
|
||||
|
||||
|
||||
# EBU R 128
|
||||
optional_source(HAVE_EBUR128
|
||||
SOURCES engine/ebur128analysis.cpp
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config.h.in ${CMAKE_CURRENT_BINARY_DIR}/config.h)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h)
|
||||
|
||||
|
@ -1215,6 +1279,11 @@ if(HAVE_LIBMTP)
|
|||
target_link_libraries(strawberry_lib PRIVATE ${LIBMTP_LIBRARIES})
|
||||
endif()
|
||||
|
||||
if(HAVE_PODCASTS)
|
||||
target_include_directories(strawberry_lib SYSTEM PRIVATE ${LIBMYGPO_INCLUDE_DIRS})
|
||||
target_link_libraries(strawberry_lib PRIVATE ${LIBMYGPO_LIBRARIES})
|
||||
endif(HAVE_PODCASTS)
|
||||
|
||||
if(APPLE)
|
||||
target_link_libraries(strawberry_lib PRIVATE
|
||||
"-framework AppKit"
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
#cmakedefine HAVE_QOBUZ
|
||||
|
||||
#cmakedefine HAVE_MOODBAR
|
||||
#cmakedefine HAVE_PODCASTS
|
||||
|
||||
#cmakedefine HAVE_KEYSYMDEF_H
|
||||
#cmakedefine HAVE_XF86KEYSYM_H
|
||||
|
|
|
@ -109,7 +109,16 @@
|
|||
#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 {
|
||||
|
@ -214,6 +223,21 @@ class ApplicationImpl {
|
|||
#ifdef HAVE_MOODBAR
|
||||
moodbar_loader_([app]() { return new MoodbarLoader(app); }),
|
||||
moodbar_controller_([app]() { return new MoodbarController(app); }),
|
||||
#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()); })
|
||||
{}
|
||||
|
@ -240,6 +264,13 @@ 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_;
|
||||
|
||||
|
@ -377,3 +408,10 @@ 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
|
||||
|
|
|
@ -64,6 +64,14 @@ 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
|
||||
|
@ -104,6 +112,13 @@ 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;
|
||||
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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
|
|
@ -0,0 +1,61 @@
|
|||
<?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>
|
|
@ -0,0 +1,270 @@
|
|||
/*
|
||||
* 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_));
|
||||
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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
|
|
@ -0,0 +1,276 @@
|
|||
<?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>
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 "addpodcastpage.h"
|
||||
|
||||
#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;
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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()));
|
||||
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 EPISODEINFOWIDGET_H
|
||||
#define EPISODEINFOWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#include "podcastepisode.h"
|
||||
|
||||
class Application;
|
||||
class Ui_EpisodeInfoWidget;
|
||||
|
||||
class EpisodeInfoWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit EpisodeInfoWidget(QWidget *parent = nullptr);
|
||||
~EpisodeInfoWidget();
|
||||
|
||||
void SetApplication(Application *app);
|
||||
|
||||
void SetEpisode(const PodcastEpisode &episode);
|
||||
|
||||
private:
|
||||
Ui_EpisodeInfoWidget *ui_;
|
||||
|
||||
Application *app_;
|
||||
PodcastEpisode episode_;
|
||||
};
|
||||
|
||||
#endif // EPISODEINFOWIDGET_H
|
|
@ -0,0 +1,137 @@
|
|||
<?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>
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* 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(); }
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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
|
|
@ -0,0 +1,61 @@
|
|||
<?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>
|
|
@ -0,0 +1,415 @@
|
|||
/*
|
||||
* 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();
|
||||
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* 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
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* 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);
|
||||
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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();
|
||||
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* 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(); }
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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
|
|
@ -0,0 +1,61 @@
|
|||
<?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>
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 OPMLCONTAINER_H
|
||||
#define OPMLCONTAINER_H
|
||||
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
#include "podcast.h"
|
||||
|
||||
class OpmlContainer {
|
||||
public:
|
||||
// Only set for the top-level container
|
||||
QUrl url;
|
||||
|
||||
QString name;
|
||||
QList<OpmlContainer> containers;
|
||||
PodcastList feeds;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(OpmlContainer)
|
||||
|
||||
#endif // OPMLCONTAINER_H
|
|
@ -0,0 +1,194 @@
|
|||
/*
|
||||
* 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());
|
||||
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* 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
|
|
@ -0,0 +1,368 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* 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();
|
||||
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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
|
|
@ -0,0 +1,288 @@
|
|||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* 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
|
|
@ -0,0 +1,231 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* 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
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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();
|
||||
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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
|
|
@ -0,0 +1,142 @@
|
|||
<?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>
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* 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();
|
||||
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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
|
|
@ -0,0 +1,220 @@
|
|||
<?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>
|
|
@ -0,0 +1,376 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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
|
|
@ -0,0 +1,919 @@
|
|||
/*
|
||||
* 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));
|
||||
|
||||
}
|
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
* 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
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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
|
|
@ -0,0 +1,194 @@
|
|||
/*
|
||||
* 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();
|
||||
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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
|
|
@ -0,0 +1,250 @@
|
|||
/*
|
||||
* 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);
|
||||
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* 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
|
|
@ -0,0 +1,146 @@
|
|||
/* 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));
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/* 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
|
|
@ -0,0 +1,290 @@
|
|||
<?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>
|
|
@ -0,0 +1,168 @@
|
|||
/* 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/>.
|
||||
*/
|
||||
|
||||
#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)
|
||||
: QWidget(parent),
|
||||
parent_(parent),
|
||||
blur_timeline_(new QTimeLine(msec, this)),
|
||||
fade_timeline_(new QTimeLine(msec, this)) {
|
||||
parent->installEventFilter(this);
|
||||
|
||||
connect(blur_timeline_, SIGNAL(valueChanged(qreal)), SLOT(update()));
|
||||
connect(fade_timeline_, SIGNAL(valueChanged(qreal)), SLOT(update()));
|
||||
connect(fade_timeline_, SIGNAL(finished()), SLOT(FadeFinished()));
|
||||
|
||||
hide();
|
||||
}
|
||||
|
||||
bool WidgetFadeHelper::eventFilter(QObject* obj, QEvent* event) {
|
||||
// We're only interested in our parent's resize events
|
||||
if (obj != parent_ || event->type() != QEvent::Resize) return false;
|
||||
|
||||
// Don't care if we're hidden
|
||||
if (!isVisible()) return false;
|
||||
|
||||
QResizeEvent* re = static_cast<QResizeEvent*>(event);
|
||||
if (re->oldSize() == re->size()) {
|
||||
// Ignore phoney resize events
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get a new capture of the parent
|
||||
hide();
|
||||
CaptureParent();
|
||||
show();
|
||||
return false;
|
||||
}
|
||||
|
||||
void WidgetFadeHelper::StartBlur() {
|
||||
CaptureParent();
|
||||
|
||||
// Cover the parent
|
||||
raise();
|
||||
show();
|
||||
|
||||
// Start the timeline
|
||||
blur_timeline_->stop();
|
||||
blur_timeline_->start();
|
||||
|
||||
setAttribute(Qt::WA_TransparentForMouseEvents, false);
|
||||
}
|
||||
|
||||
void WidgetFadeHelper::CaptureParent() {
|
||||
// Take a "screenshot" of the window
|
||||
original_pixmap_ = parent_->grab();
|
||||
QImage original_image = original_pixmap_.toImage();
|
||||
|
||||
// Blur it
|
||||
QImage blurred(original_image.size(), QImage::Format_ARGB32_Premultiplied);
|
||||
blurred.fill(Qt::transparent);
|
||||
|
||||
QPainter blur_painter(&blurred);
|
||||
blur_painter.save();
|
||||
qt_blurImage(&blur_painter, original_image, 10.0, true, false);
|
||||
blur_painter.restore();
|
||||
|
||||
// Draw some loading text over the top
|
||||
QFont loading_font(font());
|
||||
loading_font.setBold(true);
|
||||
QFontMetrics loading_font_metrics(loading_font);
|
||||
|
||||
const QString loading_text = tr("Loading...");
|
||||
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());
|
||||
|
||||
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.setPen(palette().brush(QPalette::Text).color());
|
||||
blur_painter.setFont(loading_font);
|
||||
blur_painter.drawText(loading_rect.translated(-1, -1), Qt::AlignCenter,
|
||||
loading_text);
|
||||
blur_painter.translate(-0.5, -0.5);
|
||||
|
||||
blur_painter.end();
|
||||
|
||||
blurred_pixmap_ = QPixmap::fromImage(blurred);
|
||||
|
||||
resize(parent_->size());
|
||||
}
|
||||
|
||||
void WidgetFadeHelper::StartFade() {
|
||||
|
||||
if (blur_timeline_->state() == QTimeLine::Running) {
|
||||
// Blur timeline is still running, so we need render the current state
|
||||
// into a new pixmap.
|
||||
QPixmap pixmap(original_pixmap_);
|
||||
QPainter painter(&pixmap);
|
||||
painter.setOpacity(blur_timeline_->currentValue());
|
||||
painter.drawPixmap(0, 0, blurred_pixmap_);
|
||||
painter.end();
|
||||
blurred_pixmap_ = pixmap;
|
||||
}
|
||||
blur_timeline_->stop();
|
||||
original_pixmap_ = QPixmap();
|
||||
|
||||
// Start the timeline
|
||||
fade_timeline_->stop();
|
||||
fade_timeline_->start();
|
||||
|
||||
setAttribute(Qt::WA_TransparentForMouseEvents, true);
|
||||
|
||||
}
|
||||
|
||||
void WidgetFadeHelper::paintEvent(QPaintEvent*) {
|
||||
QPainter p(this);
|
||||
|
||||
if (fade_timeline_->state() != QTimeLine::Running) {
|
||||
// We're fading in the blur
|
||||
p.drawPixmap(0, 0, original_pixmap_);
|
||||
p.setOpacity(blur_timeline_->currentValue());
|
||||
} else {
|
||||
// Fading out the blur into the new image
|
||||
p.setOpacity(1.0 - fade_timeline_->currentValue());
|
||||
}
|
||||
|
||||
p.drawPixmap(0, 0, blurred_pixmap_);
|
||||
}
|
||||
|
||||
void WidgetFadeHelper::FadeFinished() {
|
||||
hide();
|
||||
original_pixmap_ = QPixmap();
|
||||
blurred_pixmap_ = QPixmap();
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/* 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/>.
|
||||
*/
|
||||
|
||||
#ifndef WIDGETFADEHELPER_H
|
||||
#define WIDGETFADEHELPER_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class QTimeLine;
|
||||
|
||||
class WidgetFadeHelper : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
WidgetFadeHelper(QWidget* parent, const int msec = 500);
|
||||
|
||||
public slots:
|
||||
void StartBlur();
|
||||
void StartFade();
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent*);
|
||||
bool eventFilter(QObject* obj, QEvent* event);
|
||||
|
||||
private slots:
|
||||
void FadeFinished();
|
||||
|
||||
private:
|
||||
void CaptureParent();
|
||||
|
||||
private:
|
||||
static const int kLoadingPadding;
|
||||
static const int kLoadingBorderRadius;
|
||||
|
||||
QWidget* parent_;
|
||||
QTimeLine* blur_timeline_;
|
||||
QTimeLine* fade_timeline_;
|
||||
|
||||
QPixmap original_pixmap_;
|
||||
QPixmap blurred_pixmap_;
|
||||
};
|
||||
|
||||
#endif // WIDGETFADEHELPER_H
|
Loading…
Reference in New Issue