Add a podcast downloader
This commit is contained in:
parent
71fe57b3ae
commit
b0dd1bd284
|
@ -240,6 +240,7 @@ set(SOURCES
|
||||||
podcasts/podcast.cpp
|
podcasts/podcast.cpp
|
||||||
podcasts/podcastbackend.cpp
|
podcasts/podcastbackend.cpp
|
||||||
podcasts/podcastdiscoverymodel.cpp
|
podcasts/podcastdiscoverymodel.cpp
|
||||||
|
podcasts/podcastdownloader.cpp
|
||||||
podcasts/podcastepisode.cpp
|
podcasts/podcastepisode.cpp
|
||||||
podcasts/podcastinfowidget.cpp
|
podcasts/podcastinfowidget.cpp
|
||||||
podcasts/podcastservice.cpp
|
podcasts/podcastservice.cpp
|
||||||
|
@ -494,6 +495,7 @@ set(HEADERS
|
||||||
podcasts/itunessearchpage.h
|
podcasts/itunessearchpage.h
|
||||||
podcasts/podcastbackend.h
|
podcasts/podcastbackend.h
|
||||||
podcasts/podcastdiscoverymodel.h
|
podcasts/podcastdiscoverymodel.h
|
||||||
|
podcasts/podcastdownloader.h
|
||||||
podcasts/podcastinfowidget.h
|
podcasts/podcastinfowidget.h
|
||||||
podcasts/podcastservice.h
|
podcasts/podcastservice.h
|
||||||
podcasts/podcastsettingspage.h
|
podcasts/podcastsettingspage.h
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
#include "playlist/playlistmanager.h"
|
#include "playlist/playlistmanager.h"
|
||||||
#include "podcasts/gpoddersync.h"
|
#include "podcasts/gpoddersync.h"
|
||||||
#include "podcasts/podcastbackend.h"
|
#include "podcasts/podcastbackend.h"
|
||||||
|
#include "podcasts/podcastdownloader.h"
|
||||||
#include "podcasts/podcastupdater.h"
|
#include "podcasts/podcastupdater.h"
|
||||||
|
|
||||||
Application::Application(QObject* parent)
|
Application::Application(QObject* parent)
|
||||||
|
@ -53,6 +54,7 @@ Application::Application(QObject* parent)
|
||||||
library_(NULL),
|
library_(NULL),
|
||||||
device_manager_(NULL),
|
device_manager_(NULL),
|
||||||
podcast_updater_(NULL),
|
podcast_updater_(NULL),
|
||||||
|
podcast_downloader_(NULL),
|
||||||
gpodder_sync_(NULL)
|
gpodder_sync_(NULL)
|
||||||
{
|
{
|
||||||
tag_reader_client_ = new TagReaderClient(this);
|
tag_reader_client_ = new TagReaderClient(this);
|
||||||
|
@ -82,6 +84,7 @@ Application::Application(QObject* parent)
|
||||||
library_ = new Library(this, this);
|
library_ = new Library(this, this);
|
||||||
device_manager_ = new DeviceManager(this, this);
|
device_manager_ = new DeviceManager(this, this);
|
||||||
podcast_updater_ = new PodcastUpdater(this, this);
|
podcast_updater_ = new PodcastUpdater(this, this);
|
||||||
|
podcast_downloader_ = new PodcastDownloader(this, this);
|
||||||
gpodder_sync_ = new GPodderSync(this, this);
|
gpodder_sync_ = new GPodderSync(this, this);
|
||||||
|
|
||||||
library_->Init();
|
library_->Init();
|
||||||
|
|
|
@ -34,6 +34,7 @@ class LibraryBackend;
|
||||||
class LibraryModel;
|
class LibraryModel;
|
||||||
class Player;
|
class Player;
|
||||||
class PlaylistBackend;
|
class PlaylistBackend;
|
||||||
|
class PodcastDownloader;
|
||||||
class PlaylistManager;
|
class PlaylistManager;
|
||||||
class PodcastBackend;
|
class PodcastBackend;
|
||||||
class PodcastUpdater;
|
class PodcastUpdater;
|
||||||
|
@ -64,6 +65,7 @@ public:
|
||||||
Library* library() const { return library_; }
|
Library* library() const { return library_; }
|
||||||
DeviceManager* device_manager() const { return device_manager_; }
|
DeviceManager* device_manager() const { return device_manager_; }
|
||||||
PodcastUpdater* podcast_updater() const { return podcast_updater_; }
|
PodcastUpdater* podcast_updater() const { return podcast_updater_; }
|
||||||
|
PodcastDownloader* podcast_downloader() const { return podcast_downloader_; }
|
||||||
GPodderSync* gpodder_sync() const { return gpodder_sync_; }
|
GPodderSync* gpodder_sync() const { return gpodder_sync_; }
|
||||||
|
|
||||||
LibraryBackend* library_backend() const;
|
LibraryBackend* library_backend() const;
|
||||||
|
@ -97,6 +99,7 @@ private:
|
||||||
Library* library_;
|
Library* library_;
|
||||||
DeviceManager* device_manager_;
|
DeviceManager* device_manager_;
|
||||||
PodcastUpdater* podcast_updater_;
|
PodcastUpdater* podcast_updater_;
|
||||||
|
PodcastDownloader* podcast_downloader_;
|
||||||
GPodderSync* gpodder_sync_;
|
GPodderSync* gpodder_sync_;
|
||||||
|
|
||||||
QList<QObject*> objects_in_threads_;
|
QList<QObject*> objects_in_threads_;
|
||||||
|
|
|
@ -134,3 +134,42 @@ void NetworkTimeouts::timerEvent(QTimerEvent* e) {
|
||||||
reply->abort();
|
reply->abort();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
RedirectFollower::RedirectFollower(QNetworkReply* first_reply, int max_redirects)
|
||||||
|
: QObject(NULL),
|
||||||
|
current_reply_(first_reply),
|
||||||
|
redirects_remaining_(max_redirects) {
|
||||||
|
ConnectReply(first_reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RedirectFollower::ConnectReply(QNetworkReply* reply) {
|
||||||
|
connect(reply, SIGNAL(readyRead()), SIGNAL(readyRead()));
|
||||||
|
connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), SIGNAL(error(QNetworkReply::NetworkError)));
|
||||||
|
connect(reply, SIGNAL(downloadProgress(qint64,qint64)), SIGNAL(downloadProgress(qint64,qint64)));
|
||||||
|
connect(reply, SIGNAL(uploadProgress(qint64,qint64)), SIGNAL(uploadProgress(qint64,qint64)));
|
||||||
|
connect(reply, SIGNAL(finished()), SLOT(ReplyFinished()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void RedirectFollower::ReplyFinished() {
|
||||||
|
current_reply_->deleteLater();
|
||||||
|
|
||||||
|
if (current_reply_->attribute(QNetworkRequest::RedirectionTargetAttribute).isValid()) {
|
||||||
|
if (redirects_remaining_-- == 0) {
|
||||||
|
emit finished();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QUrl next_url = current_reply_->url().resolved(
|
||||||
|
current_reply_->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl());
|
||||||
|
|
||||||
|
QNetworkRequest req(current_reply_->request());
|
||||||
|
req.setUrl(next_url);
|
||||||
|
|
||||||
|
current_reply_ = current_reply_->manager()->get(req);
|
||||||
|
ConnectReply(current_reply_);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit finished();
|
||||||
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
#include <QAbstractNetworkCache>
|
#include <QAbstractNetworkCache>
|
||||||
#include <QMutex>
|
#include <QMutex>
|
||||||
#include <QNetworkAccessManager>
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
|
||||||
class QNetworkDiskCache;
|
class QNetworkDiskCache;
|
||||||
|
|
||||||
|
@ -43,6 +44,7 @@ private:
|
||||||
static QNetworkDiskCache* sCache;
|
static QNetworkDiskCache* sCache;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
class NetworkAccessManager : public QNetworkAccessManager {
|
class NetworkAccessManager : public QNetworkAccessManager {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
|
@ -54,6 +56,7 @@ protected:
|
||||||
QIODevice* outgoingData);
|
QIODevice* outgoingData);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
class NetworkTimeouts : public QObject {
|
class NetworkTimeouts : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
|
@ -74,4 +77,41 @@ private:
|
||||||
QMap<QNetworkReply*, int> timers_;
|
QMap<QNetworkReply*, int> timers_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
class RedirectFollower : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
RedirectFollower(QNetworkReply* first_reply, int max_redirects = 5);
|
||||||
|
|
||||||
|
bool hit_redirect_limit() const { return redirects_remaining_ < 0; }
|
||||||
|
QNetworkReply* reply() const { return current_reply_; }
|
||||||
|
|
||||||
|
// These are all forwarded to the current reply.
|
||||||
|
QNetworkReply::NetworkError error() const { return current_reply_->error(); }
|
||||||
|
QString errorString() const { return current_reply_->errorString(); }
|
||||||
|
QVariant attribute(QNetworkRequest::Attribute code) const { return current_reply_->attribute(code); }
|
||||||
|
QVariant header(QNetworkRequest::KnownHeaders header) const { return current_reply_->header(header); }
|
||||||
|
|
||||||
|
signals:
|
||||||
|
// These are all forwarded from the current reply.
|
||||||
|
void readyRead();
|
||||||
|
void error(QNetworkReply::NetworkError);
|
||||||
|
void uploadProgress(qint64 bytesSent, qint64 bytesTotal);
|
||||||
|
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
|
||||||
|
|
||||||
|
// This is NOT emitted when a request that has a redirect finishes.
|
||||||
|
void finished();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void ReplyFinished();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void ConnectReply(QNetworkReply* reply);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QNetworkReply* current_reply_;
|
||||||
|
int redirects_remaining_;
|
||||||
|
};
|
||||||
|
|
||||||
#endif // NETWORK_H
|
#endif // NETWORK_H
|
||||||
|
|
|
@ -0,0 +1,211 @@
|
||||||
|
/* This file is part of Clementine.
|
||||||
|
Copyright 2012, 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 "podcastbackend.h"
|
||||||
|
#include "podcastdownloader.h"
|
||||||
|
#include "core/application.h"
|
||||||
|
#include "core/logging.h"
|
||||||
|
#include "core/network.h"
|
||||||
|
#include "core/utilities.h"
|
||||||
|
#include "library/librarydirectorymodel.h"
|
||||||
|
#include "library/librarymodel.h"
|
||||||
|
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QSettings>
|
||||||
|
|
||||||
|
const char* PodcastDownloader::kSettingsGroup = "Podcasts";
|
||||||
|
|
||||||
|
|
||||||
|
struct PodcastDownloader::Task {
|
||||||
|
Task() : file(NULL) {}
|
||||||
|
~Task() { delete file; }
|
||||||
|
|
||||||
|
PodcastEpisode episode;
|
||||||
|
QFile* file;
|
||||||
|
};
|
||||||
|
|
||||||
|
PodcastDownloader::PodcastDownloader(Application* app, QObject* parent)
|
||||||
|
: QObject(parent),
|
||||||
|
app_(app),
|
||||||
|
network_(new NetworkAccessManager(this)),
|
||||||
|
disallowed_filename_characters_("[^a-zA-Z0-9_~ -]"),
|
||||||
|
current_task_(NULL),
|
||||||
|
last_progress_signal_(0)
|
||||||
|
{
|
||||||
|
connect(app_->podcast_backend(), SIGNAL(EpisodesAdded(QList<PodcastEpisode>)),
|
||||||
|
SLOT(EpisodesAdded(QList<PodcastEpisode>)));
|
||||||
|
connect(app_->podcast_backend(), SIGNAL(SubscriptionAdded(Podcast)),
|
||||||
|
SLOT(SubscriptionAdded(Podcast)));
|
||||||
|
connect(app_, SIGNAL(SettingsChanged()), SLOT(ReloadSettings()));
|
||||||
|
|
||||||
|
ReloadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString PodcastDownloader::DefaultDownloadDir() const {
|
||||||
|
QString prefix = QDir::homePath();
|
||||||
|
|
||||||
|
LibraryDirectoryModel* model = app_->library_model()->directory_model();
|
||||||
|
if (model->rowCount() > 0) {
|
||||||
|
// Download to the first library directory if there is one set
|
||||||
|
prefix = model->index(0, 0).data().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return prefix + "/Podcasts";
|
||||||
|
}
|
||||||
|
|
||||||
|
void PodcastDownloader::ReloadSettings() {
|
||||||
|
QSettings s;
|
||||||
|
s.beginGroup(kSettingsGroup);
|
||||||
|
|
||||||
|
download_dir_ = s.value("download_dir", DefaultDownloadDir()).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PodcastDownloader::DownloadEpisode(const PodcastEpisode& episode) {
|
||||||
|
Task* task = new Task;
|
||||||
|
task->episode = episode;
|
||||||
|
|
||||||
|
if (current_task_) {
|
||||||
|
// Add it to the queue
|
||||||
|
queued_tasks_.enqueue(task);
|
||||||
|
emit ProgressChanged(episode, Queued, 0);
|
||||||
|
} else {
|
||||||
|
// Start downloading now
|
||||||
|
StartDownloading(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PodcastDownloader::StartDownloading(Task* task) {
|
||||||
|
current_task_ = task;
|
||||||
|
|
||||||
|
// Need to get the name of the podcast to use in the directory name.
|
||||||
|
Podcast podcast =
|
||||||
|
app_->podcast_backend()->GetSubscriptionById(task->episode.podcast_database_id());
|
||||||
|
if (!podcast.is_valid()) {
|
||||||
|
qLog(Warning) << "The podcast that contains episode" << task->episode.url()
|
||||||
|
<< "doesn't exist any more";
|
||||||
|
emit ProgressChanged(task->episode, Finished, 0);
|
||||||
|
delete task;
|
||||||
|
NextTask();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString file_extension = QFileInfo(task->episode.url().path()).suffix();
|
||||||
|
const QString directory = download_dir_ + "/" +
|
||||||
|
SanitiseFilenameComponent(podcast.title());
|
||||||
|
const QString filename =
|
||||||
|
SanitiseFilenameComponent(task->episode.title()) + "." + file_extension;
|
||||||
|
const QString filepath = directory + "/" + filename;
|
||||||
|
|
||||||
|
// Open the output file
|
||||||
|
QDir().mkpath(directory);
|
||||||
|
task->file = new QFile(filepath);
|
||||||
|
if (!task->file->open(QIODevice::WriteOnly)) {
|
||||||
|
qLog(Warning) << "Could not open the file" << filepath << "for writing";
|
||||||
|
emit ProgressChanged(task->episode, Finished, 0);
|
||||||
|
delete task;
|
||||||
|
NextTask();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qLog(Info) << "Downloading" << task->episode.url() << "to" << filepath;
|
||||||
|
|
||||||
|
// Get the URL
|
||||||
|
QNetworkRequest req(task->episode.url());
|
||||||
|
RedirectFollower* reply = new RedirectFollower(network_->get(req));
|
||||||
|
connect(reply, SIGNAL(readyRead()), SLOT(ReplyReadyRead()));
|
||||||
|
connect(reply, SIGNAL(finished()), SLOT(ReplyFinished()));
|
||||||
|
connect(reply, SIGNAL(downloadProgress(qint64,qint64)),
|
||||||
|
SLOT(ReplyDownloadProgress(qint64,qint64)));
|
||||||
|
|
||||||
|
emit ProgressChanged(task->episode, Downloading, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PodcastDownloader::NextTask() {
|
||||||
|
current_task_ = NULL;
|
||||||
|
|
||||||
|
if (!queued_tasks_.isEmpty()) {
|
||||||
|
StartDownloading(queued_tasks_.dequeue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PodcastDownloader::ReplyReadyRead() {
|
||||||
|
QNetworkReply* reply = qobject_cast<RedirectFollower*>(sender())->reply();
|
||||||
|
if (!reply || !current_task_ || !current_task_->file)
|
||||||
|
return;
|
||||||
|
|
||||||
|
forever {
|
||||||
|
const qint64 bytes = reply->bytesAvailable();
|
||||||
|
if (bytes <= 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
current_task_->file->write(reply->read(bytes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PodcastDownloader::ReplyDownloadProgress(qint64 received, qint64 total) {
|
||||||
|
if (!current_task_ || !current_task_->file)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const time_t current_time = QDateTime::currentDateTime().toTime_t();
|
||||||
|
if (last_progress_signal_ == current_time)
|
||||||
|
return;
|
||||||
|
last_progress_signal_ = current_time;
|
||||||
|
|
||||||
|
emit ProgressChanged(current_task_->episode, Downloading,
|
||||||
|
float(received) / total * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PodcastDownloader::ReplyFinished() {
|
||||||
|
RedirectFollower* reply = qobject_cast<RedirectFollower*>(sender());
|
||||||
|
if (!reply || !current_task_ || !current_task_->file)
|
||||||
|
return;
|
||||||
|
|
||||||
|
reply->deleteLater();
|
||||||
|
|
||||||
|
if (reply->error() != QNetworkReply::NoError) {
|
||||||
|
qLog(Warning) << "Error downloading episode:" << reply->errorString();
|
||||||
|
|
||||||
|
// Delete the file
|
||||||
|
current_task_->file->remove();
|
||||||
|
|
||||||
|
emit ProgressChanged(current_task_->episode, Finished, 0);
|
||||||
|
delete current_task_;
|
||||||
|
NextTask();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qLog(Info) << "Download of" << current_task_->file->fileName() << "finished";
|
||||||
|
emit ProgressChanged(current_task_->episode, Finished, 100);
|
||||||
|
|
||||||
|
delete current_task_;
|
||||||
|
NextTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString PodcastDownloader::SanitiseFilenameComponent(const QString& text) const {
|
||||||
|
return QString(text).replace(disallowed_filename_characters_, " ").simplified();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PodcastDownloader::SubscriptionAdded(const Podcast& podcast) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
void PodcastDownloader::EpisodesAdded(const QList<PodcastEpisode>& episodes) {
|
||||||
|
// TOOD
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
/* This file is part of Clementine.
|
||||||
|
Copyright 2012, 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 PODCASTDOWNLOADER_H
|
||||||
|
#define PODCASTDOWNLOADER_H
|
||||||
|
|
||||||
|
#include "podcast.h"
|
||||||
|
#include "podcastepisode.h"
|
||||||
|
|
||||||
|
#include <QList>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QQueue>
|
||||||
|
#include <QRegExp>
|
||||||
|
|
||||||
|
class Application;
|
||||||
|
|
||||||
|
class QNetworkAccessManager;
|
||||||
|
|
||||||
|
class PodcastDownloader : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
PodcastDownloader(Application* app, QObject* parent = 0);
|
||||||
|
|
||||||
|
enum State {
|
||||||
|
Queued,
|
||||||
|
Downloading,
|
||||||
|
Finished
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char* kSettingsGroup;
|
||||||
|
|
||||||
|
QString DefaultDownloadDir() const;
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
// Adds the episode to the download queue
|
||||||
|
void DownloadEpisode(const PodcastEpisode& episode);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void ProgressChanged(const PodcastEpisode& episode, State state, int percent);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void ReloadSettings();
|
||||||
|
|
||||||
|
void SubscriptionAdded(const Podcast& podcast);
|
||||||
|
void EpisodesAdded(const QList<PodcastEpisode>& episodes);
|
||||||
|
|
||||||
|
void ReplyReadyRead();
|
||||||
|
void ReplyFinished();
|
||||||
|
void ReplyDownloadProgress(qint64 received, qint64 total);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Task;
|
||||||
|
|
||||||
|
void StartDownloading(Task* task);
|
||||||
|
void NextTask();
|
||||||
|
|
||||||
|
QString FilenameForEpisode(const PodcastEpisode& episode) const;
|
||||||
|
QString SanitiseFilenameComponent(const QString& text) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Application* app_;
|
||||||
|
QNetworkAccessManager* network_;
|
||||||
|
|
||||||
|
QRegExp disallowed_filename_characters_;
|
||||||
|
|
||||||
|
QString download_dir_;
|
||||||
|
|
||||||
|
Task* current_task_;
|
||||||
|
QQueue<Task*> queued_tasks_;
|
||||||
|
|
||||||
|
time_t last_progress_signal_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // PODCASTDOWNLOADER_H
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
#include "addpodcastdialog.h"
|
#include "addpodcastdialog.h"
|
||||||
#include "podcastbackend.h"
|
#include "podcastbackend.h"
|
||||||
|
#include "podcastdownloader.h"
|
||||||
#include "podcastservice.h"
|
#include "podcastservice.h"
|
||||||
#include "podcastupdater.h"
|
#include "podcastupdater.h"
|
||||||
#include "core/application.h"
|
#include "core/application.h"
|
||||||
|
@ -202,9 +203,13 @@ void PodcastService::ShowContextMenu(const QModelIndex& index,
|
||||||
remove_selected_action_ = context_menu_->addAction(IconLoader::Load("list-remove"),
|
remove_selected_action_ = context_menu_->addAction(IconLoader::Load("list-remove"),
|
||||||
tr("Unsubscribe"),
|
tr("Unsubscribe"),
|
||||||
this, SLOT(RemoveSelectedPodcast()));
|
this, SLOT(RemoveSelectedPodcast()));
|
||||||
|
download_selected_action_ = context_menu_->addAction(IconLoader::Load("download"),
|
||||||
|
tr("Download this episode"),
|
||||||
|
this, SLOT(DownloadSelectedEpisode()));
|
||||||
}
|
}
|
||||||
|
|
||||||
current_index_ = index;
|
current_index_ = index;
|
||||||
|
bool is_episode = false;
|
||||||
|
|
||||||
switch (index.data(InternetModel::Role_Type).toInt()) {
|
switch (index.data(InternetModel::Role_Type).toInt()) {
|
||||||
case Type_Podcast:
|
case Type_Podcast:
|
||||||
|
@ -213,6 +218,7 @@ void PodcastService::ShowContextMenu(const QModelIndex& index,
|
||||||
|
|
||||||
case Type_Episode:
|
case Type_Episode:
|
||||||
current_podcast_index_ = index.parent();
|
current_podcast_index_ = index.parent();
|
||||||
|
is_episode = true;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -222,6 +228,7 @@ void PodcastService::ShowContextMenu(const QModelIndex& index,
|
||||||
|
|
||||||
update_selected_action_->setVisible(current_podcast_index_.isValid());
|
update_selected_action_->setVisible(current_podcast_index_.isValid());
|
||||||
remove_selected_action_->setVisible(current_podcast_index_.isValid());
|
remove_selected_action_->setVisible(current_podcast_index_.isValid());
|
||||||
|
download_selected_action_->setVisible(is_episode);
|
||||||
context_menu_->popup(global_pos);
|
context_menu_->popup(global_pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -301,3 +308,8 @@ void PodcastService::EpisodesAdded(const QList<PodcastEpisode>& episodes) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PodcastService::DownloadSelectedEpisode() {
|
||||||
|
app_->podcast_downloader()->DownloadEpisode(
|
||||||
|
current_index_.data(Role_Episode).value<PodcastEpisode>());
|
||||||
|
}
|
||||||
|
|
|
@ -66,6 +66,8 @@ private slots:
|
||||||
void UpdateSelectedPodcast();
|
void UpdateSelectedPodcast();
|
||||||
void RemoveSelectedPodcast();
|
void RemoveSelectedPodcast();
|
||||||
|
|
||||||
|
void DownloadSelectedEpisode();
|
||||||
|
|
||||||
void SubscriptionAdded(const Podcast& podcast);
|
void SubscriptionAdded(const Podcast& podcast);
|
||||||
void SubscriptionRemoved(const Podcast& podcast);
|
void SubscriptionRemoved(const Podcast& podcast);
|
||||||
void EpisodesAdded(const QList<PodcastEpisode>& episodes);
|
void EpisodesAdded(const QList<PodcastEpisode>& episodes);
|
||||||
|
@ -89,6 +91,7 @@ private:
|
||||||
QMenu* context_menu_;
|
QMenu* context_menu_;
|
||||||
QAction* update_selected_action_;
|
QAction* update_selected_action_;
|
||||||
QAction* remove_selected_action_;
|
QAction* remove_selected_action_;
|
||||||
|
QAction* download_selected_action_;
|
||||||
QStandardItem* root_;
|
QStandardItem* root_;
|
||||||
|
|
||||||
QModelIndex current_index_;
|
QModelIndex current_index_;
|
||||||
|
|
|
@ -16,12 +16,16 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "gpoddersync.h"
|
#include "gpoddersync.h"
|
||||||
|
#include "podcastdownloader.h"
|
||||||
#include "podcastsettingspage.h"
|
#include "podcastsettingspage.h"
|
||||||
#include "ui_podcastsettingspage.h"
|
#include "ui_podcastsettingspage.h"
|
||||||
#include "core/application.h"
|
#include "core/application.h"
|
||||||
#include "core/closure.h"
|
#include "core/closure.h"
|
||||||
|
#include "library/librarydirectorymodel.h"
|
||||||
|
#include "library/librarymodel.h"
|
||||||
#include "ui/settingsdialog.h"
|
#include "ui/settingsdialog.h"
|
||||||
|
|
||||||
|
#include <QFileDialog>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
|
|
||||||
|
@ -35,6 +39,7 @@ PodcastSettingsPage::PodcastSettingsPage(SettingsDialog* dialog)
|
||||||
connect(ui_->login, SIGNAL(clicked()), SLOT(LoginClicked()));
|
connect(ui_->login, SIGNAL(clicked()), SLOT(LoginClicked()));
|
||||||
connect(ui_->login_state, SIGNAL(LoginClicked()), SLOT(LoginClicked()));
|
connect(ui_->login_state, SIGNAL(LoginClicked()), SLOT(LoginClicked()));
|
||||||
connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(LogoutClicked()));
|
connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(LogoutClicked()));
|
||||||
|
connect(ui_->download_dir_browse, SIGNAL(clicked()), SLOT(DownloadDirBrowse()));
|
||||||
|
|
||||||
ui_->login_state->AddCredentialField(ui_->username);
|
ui_->login_state->AddCredentialField(ui_->username);
|
||||||
ui_->login_state->AddCredentialField(ui_->device_name);
|
ui_->login_state->AddCredentialField(ui_->device_name);
|
||||||
|
@ -62,12 +67,18 @@ void PodcastSettingsPage::Load() {
|
||||||
const int update_interval = s.value("update_interval_secs", 0).toInt();
|
const int update_interval = s.value("update_interval_secs", 0).toInt();
|
||||||
ui_->check_interval->setCurrentIndex(ui_->check_interval->findData(update_interval));
|
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_->auto_download->setChecked(s.value("auto_download", false).toBool());
|
||||||
ui_->delete_after->setValue(s.value("delete_after", 0).toInt() / (24*60*60));
|
ui_->delete_after->setValue(s.value("delete_after", 0).toInt() / (24*60*60));
|
||||||
ui_->delete_unplayed->setChecked(s.value("delete_unplayed", false).toBool());
|
ui_->delete_unplayed->setChecked(s.value("delete_unplayed", false).toBool());
|
||||||
ui_->username->setText(s.value("gpodder_username").toString());
|
ui_->username->setText(s.value("gpodder_username").toString());
|
||||||
|
|
||||||
ui_->device_name->setText(s.value("gpodder_device_name", GPodderSync::DefaultDeviceName()).toString());
|
ui_->device_name->setText(s.value("gpodder_device_name", GPodderSync::DefaultDeviceName()).toString());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PodcastSettingsPage::Save() {
|
void PodcastSettingsPage::Save() {
|
||||||
|
@ -76,6 +87,7 @@ void PodcastSettingsPage::Save() {
|
||||||
|
|
||||||
s.setValue("update_interval_secs",
|
s.setValue("update_interval_secs",
|
||||||
ui_->check_interval->itemData(ui_->check_interval->currentIndex()));
|
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("auto_download", ui_->auto_download->isChecked());
|
||||||
s.setValue("delete_after", ui_->delete_after->value());
|
s.setValue("delete_after", ui_->delete_after->value());
|
||||||
s.setValue("delete_unplayed", ui_->delete_unplayed->isChecked());
|
s.setValue("delete_unplayed", ui_->delete_unplayed->isChecked());
|
||||||
|
@ -111,3 +123,12 @@ void PodcastSettingsPage::LogoutClicked() {
|
||||||
ui_->password->clear();
|
ui_->password->clear();
|
||||||
dialog()->app()->gpodder_sync()->Logout();
|
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));
|
||||||
|
}
|
||||||
|
|
|
@ -41,6 +41,8 @@ private slots:
|
||||||
void LoginFinished(QNetworkReply* reply);
|
void LoginFinished(QNetworkReply* reply);
|
||||||
void LogoutClicked();
|
void LogoutClicked();
|
||||||
|
|
||||||
|
void DownloadDirBrowse();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Ui_PodcastSettingsPage* ui_;
|
Ui_PodcastSettingsPage* ui_;
|
||||||
};
|
};
|
||||||
|
|
|
@ -75,13 +75,34 @@
|
||||||
</item>
|
</item>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="0" colspan="2">
|
<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">
|
<widget class="QCheckBox" name="auto_download">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Download new episodes automatically</string>
|
<string>Download new episodes automatically</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</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>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
@ -231,6 +252,8 @@
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
<tabstops>
|
<tabstops>
|
||||||
<tabstop>check_interval</tabstop>
|
<tabstop>check_interval</tabstop>
|
||||||
|
<tabstop>download_dir</tabstop>
|
||||||
|
<tabstop>download_dir_browse</tabstop>
|
||||||
<tabstop>auto_download</tabstop>
|
<tabstop>auto_download</tabstop>
|
||||||
<tabstop>delete_after</tabstop>
|
<tabstop>delete_after</tabstop>
|
||||||
<tabstop>delete_unplayed</tabstop>
|
<tabstop>delete_unplayed</tabstop>
|
||||||
|
|
Loading…
Reference in New Issue