Merge pull request #4648 from sobkas/master

PodcastDownloader changes
This commit is contained in:
John Maguire 2014-12-14 00:37:37 +01:00
commit 946dc0ce3e
32 changed files with 576 additions and 297 deletions

View File

@ -282,6 +282,7 @@ set(SOURCES
podcasts/podcast.cpp
podcasts/podcastbackend.cpp
podcasts/podcastdiscoverymodel.cpp
podcasts/podcastdeleter.cpp
podcasts/podcastdownloader.cpp
podcasts/podcastepisode.cpp
podcasts/podcastinfowidget.cpp
@ -578,6 +579,7 @@ set(HEADERS
podcasts/itunessearchpage.h
podcasts/podcastbackend.h
podcasts/podcastdiscoverymodel.h
podcasts/podcastdeleter.h
podcasts/podcastdownloader.h
podcasts/podcastinfowidget.h
podcasts/podcastservice.h

View File

@ -41,6 +41,7 @@
#include "playlist/playlistmanager.h"
#include "podcasts/gpoddersync.h"
#include "podcasts/podcastbackend.h"
#include "podcasts/podcastdeleter.h"
#include "podcasts/podcastdownloader.h"
#include "podcasts/podcastupdater.h"
@ -73,6 +74,7 @@ Application::Application(QObject* parent)
library_(nullptr),
device_manager_(nullptr),
podcast_updater_(nullptr),
podcast_deleter_(nullptr),
podcast_downloader_(nullptr),
gpodder_sync_(nullptr),
moodbar_loader_(nullptr),
@ -107,6 +109,10 @@ Application::Application(QObject* parent)
library_ = new Library(this, this);
device_manager_ = new DeviceManager(this, this);
podcast_updater_ = new PodcastUpdater(this, this);
podcast_deleter_ = new PodcastDeleter(this, this);
MoveToNewThread(podcast_deleter_);
podcast_downloader_ = new PodcastDownloader(this, this);
gpodder_sync_ = new GPodderSync(this, this);

View File

@ -44,6 +44,7 @@ class NetworkRemote;
class NetworkRemoteHelper;
class Player;
class PlaylistBackend;
class PodcastDeleter;
class PodcastDownloader;
class PlaylistManager;
class PodcastBackend;
@ -83,6 +84,7 @@ class Application : public QObject {
Library* library() const { return library_; }
DeviceManager* device_manager() const { return device_manager_; }
PodcastUpdater* podcast_updater() const { return podcast_updater_; }
PodcastDeleter* podcast_deleter() const { return podcast_deleter_; }
PodcastDownloader* podcast_downloader() const { return podcast_downloader_; }
GPodderSync* gpodder_sync() const { return gpodder_sync_; }
MoodbarLoader* moodbar_loader() const { return moodbar_loader_; }
@ -128,6 +130,7 @@ class Application : public QObject {
Library* library_;
DeviceManager* device_manager_;
PodcastUpdater* podcast_updater_;
PodcastDeleter* podcast_deleter_;
PodcastDownloader* podcast_downloader_;
GPodderSync* gpodder_sync_;
MoodbarLoader* moodbar_loader_;

View File

@ -1,5 +1,7 @@
/* This file is part of Clementine.
Copyright 2010, David Sansome <me@davidsansome.com>
Copyright 2010-2011, David Sansome <davidsansome@gmail.com>
Copyright 2010-2012, 2014, John Maguire <john.maguire@gmail.com>
Copyright 2011, Tyler Rhodes <tyler.s.rhodes@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

View File

@ -1,5 +1,6 @@
/* This file is part of Clementine.
Copyright 2012, David Sansome <me@davidsansome.com>
Copyright 2012, 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

View File

@ -1,5 +1,6 @@
/* This file is part of Clementine.
Copyright 2010, David Sansome <me@davidsansome.com>
Copyright 2010, David Sansome <davidsansome@gmail.com>
Copyright 2010-2011, 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

View File

@ -1,6 +1,7 @@
/* This file is part of Clementine.
Copyright 2010-2011, David Sansome <me@davidsansome.com>
Copyright 2014, Arnaud Bienner <arnaud.bienner@gmail.com>
Copyright 2014, Andreas <asfa194@gmail.com>
Copyright 2014, Krzysztof A. Sobiecki <sobkas@gmail.com>
Copyright 2014, John Maguire <john.maguire@gmail.com>

View File

@ -1,6 +1,7 @@
/* This file is part of Clementine.
Copyright 2010, David Sansome <davidsansome@gmail.com>
Copyright 2014, Arnaud Bienner <arnaud.bienner@gmail.com>
Copyright 2014, Andreas <asfa194@gmail.com>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Copyright 2014, John Maguire <john.maguire@gmail.com>

View File

@ -6,7 +6,7 @@
Copyright 2011, Angus Gratton <gus@projectgus.com>
Copyright 2012, Kacper "mattrick" Banasik <mattrick@jabster.pl>
Copyright 2013, Martin Brodbeck <martin@brodbeck-online.de>
Copyright 2013, Andreas <asfa194@gmail.com>
Copyright 2013-2014, Andreas <asfa194@gmail.com>
Copyright 2013, Joel Bradshaw <cincodenada@gmail.com>
Copyright 2013, Uwe Klotz <uwe.klotz@gmail.com>
Copyright 2013, Mateusz Kowalczyk <fuuzetsu@fuuzetsu.co.uk>

View File

@ -9,6 +9,7 @@
Copyright 2013, Joel Bradshaw <cincodenada@gmail.com>
Copyright 2013, Uwe Klotz <uwe.klotz@gmail.com>
Copyright 2013, Mateusz Kowalczyk <fuuzetsu@fuuzetsu.co.uk>
Copyright 2014, Andreas <asfa194@gmail.com>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Clementine is free software: you can redistribute it and/or modify

View File

@ -18,15 +18,16 @@
*/
#include "addpodcastbyurl.h"
#include "podcastdiscoverymodel.h"
#include "podcasturlloader.h"
#include "ui_addpodcastbyurl.h"
#include "core/closure.h"
#include <QClipboard>
#include <QNetworkReply>
#include <QMessageBox>
#include "podcastdiscoverymodel.h"
#include "podcasturlloader.h"
#include "ui_addpodcastbyurl.h"
#include "core/closure.h"
AddPodcastByUrl::AddPodcastByUrl(Application* app, QWidget* parent)
: AddPodcastPage(app, parent),
ui_(new Ui_AddPodcastByUrl),

View File

@ -18,6 +18,11 @@
*/
#include "addpodcastdialog.h"
#include <QFileDialog>
#include <QPushButton>
#include <QTimer>
#include "addpodcastbyurl.h"
#include "fixedopmlpage.h"
#include "gpoddersearchpage.h"
@ -30,10 +35,6 @@
#include "ui/iconloader.h"
#include "widgets/widgetfadehelper.h"
#include <QFileDialog>
#include <QPushButton>
#include <QTimer>
const char* AddPodcastDialog::kBbcOpmlUrl =
"http://www.bbc.co.uk/podcasts.opml";

View File

@ -18,12 +18,13 @@
*/
#include "fixedopmlpage.h"
#include <QMessageBox>
#include "podcastdiscoverymodel.h"
#include "podcasturlloader.h"
#include "core/closure.h"
#include <QMessageBox>
FixedOpmlPage::FixedOpmlPage(const QUrl& opml_url, const QString& title,
const QIcon& icon, Application* app,
QWidget* parent)

View File

@ -18,14 +18,15 @@
*/
#include "gpoddersearchpage.h"
#include <QMessageBox>
#include "podcast.h"
#include "podcastdiscoverymodel.h"
#include "ui_gpoddersearchpage.h"
#include "core/closure.h"
#include "core/network.h"
#include <QMessageBox>
GPodderSearchPage::GPodderSearchPage(Application* app, QWidget* parent)
: AddPodcastPage(app, parent),
ui_(new Ui_GPodderSearchPage),

View File

@ -18,6 +18,13 @@
*/
#include "gpoddersync.h"
#include <QCoreApplication>
#include <QHostInfo>
#include <QNetworkAccessManager>
#include <QSettings>
#include <QTimer>
#include "podcastbackend.h"
#include "podcasturlloader.h"
#include "core/application.h"
@ -28,12 +35,6 @@
#include "core/timeconstants.h"
#include "core/utilities.h"
#include <QCoreApplication>
#include <QHostInfo>
#include <QNetworkAccessManager>
#include <QSettings>
#include <QTimer>
const char* GPodderSync::kSettingsGroup = "Podcasts";
const int GPodderSync::kFlushUpdateQueueDelay = 30 * kMsecPerSec; // 30 seconds
const int GPodderSync::kGetUpdatesInterval =

View File

@ -18,14 +18,14 @@
*/
#include "gpoddertoptagsmodel.h"
#include <ApiRequest.h>
#include <QMessageBox>
#include "gpoddertoptagspage.h"
#include "podcast.h"
#include "core/closure.h"
#include <ApiRequest.h>
#include <QMessageBox>
GPodderTopTagsModel::GPodderTopTagsModel(mygpo::ApiRequest* api,
Application* app, QObject* parent)
: PodcastDiscoveryModel(app, parent), api_(api) {}

View File

@ -17,13 +17,14 @@
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "gpoddertoptagsmodel.h"
#include "gpoddertoptagspage.h"
#include "core/closure.h"
#include "core/network.h"
#include <QMessageBox>
#include "gpoddertoptagsmodel.h"
#include "core/closure.h"
#include "core/network.h"
const int GPodderTopTagsPage::kMaxTagCount = 100;
GPodderTopTagsPage::GPodderTopTagsPage(Application* app, QWidget* parent)

View File

@ -18,16 +18,16 @@
*/
#include "itunessearchpage.h"
#include <qjson/parser.h>
#include <QMessageBox>
#include <QNetworkReply>
#include "core/closure.h"
#include "core/network.h"
#include "podcast.h"
#include "podcastdiscoverymodel.h"
#include "ui_itunessearchpage.h"
#include "core/closure.h"
#include "core/network.h"
#include <qjson/parser.h>
#include <QMessageBox>
#include <QNetworkReply>
const char* ITunesSearchPage::kUrlBase =
"http://ax.phobos.apple.com.edgesuite.net/WebObjects/MZStoreServices.woa/"

View File

@ -18,13 +18,13 @@
*/
#include "podcast.h"
#include "core/utilities.h"
#include <QDataStream>
#include <QDateTime>
#include <Podcast.h>
#include "core/utilities.h"
const QStringList Podcast::kColumns = QStringList() << "url"
<< "title"
<< "description"

View File

@ -18,13 +18,14 @@
*/
#include "podcastbackend.h"
#include <QMutexLocker>
#include "core/application.h"
#include "core/database.h"
#include "core/logging.h"
#include "core/scopedtransaction.h"
#include <QMutexLocker>
PodcastBackend::PodcastBackend(Application* app, QObject* parent)
: QObject(parent), app_(app), db_(app->database()) {}
@ -322,6 +323,26 @@ PodcastEpisodeList PodcastBackend::GetOldDownloadedEpisodes(
return ret;
}
PodcastEpisode PodcastBackend::GetOldestDownloadedListenedEpisode() {
PodcastEpisode ret;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q("SELECT ROWID, " + PodcastEpisode::kColumnSpec +
" FROM podcast_episodes"
" WHERE downloaded = 'true'"
" AND listened = 'true'"
" ORDER BY listened_date ASC",
db);
q.exec();
if (db_->CheckErrors(q)) return ret;
q.next();
ret.InitFromQuery(q);
return ret;
}
PodcastEpisodeList PodcastBackend::GetNewDownloadedEpisodes() {
PodcastEpisodeList ret;

View File

@ -56,6 +56,7 @@ class PodcastBackend : public QObject {
PodcastEpisode GetEpisodeById(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

View File

@ -0,0 +1,112 @@
/* This file is part of Clementine.
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/>.
*/
#include "podcastdeleter.h"
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QNetworkReply>
#include <QSettings>
#include <QTimer>
#include "core/application.h"
#include "core/logging.h"
#include "core/timeconstants.h"
#include "core/utilities.h"
#include "library/librarydirectorymodel.h"
#include "library/librarymodel.h"
#include "podcastbackend.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();
connect(auto_delete_timer_, SIGNAL(timeout()), SLOT(AutoDelete()));
connect(app_, SIGNAL(SettingsChanged()), SLOT(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();
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;
if (timeout_ms >= 0) {
auto_delete_timer_->setInterval(timeout_ms);
} else {
auto_delete_timer_->setInterval(kAutoDeleteCheckIntervalMsec);
}
auto_delete_timer_->start();
}

View File

@ -0,0 +1,64 @@
/* This file is part of Clementine.
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 PODCASTS_PODCASTDELETER_H_
#define PODCASTS_PODCASTDELETER_H_
#include "core/network.h"
#include "podcast.h"
#include "podcastepisode.h"
#include <QFile>
#include <QList>
#include <QObject>
#include <QQueue>
#include <QRegExp>
#include <QSet>
#ifdef Q_OS_WIN
#include <time.h>
#else
#include <sys/time.h>
#endif
class Application;
class PodcastBackend;
class QNetworkAccessManager;
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 // PODCASTS_PODCASTDELETER_H_

View File

@ -17,16 +17,17 @@
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "opmlcontainer.h"
#include "podcast.h"
#include "podcastdiscoverymodel.h"
#include "core/application.h"
#include "ui/iconloader.h"
#include "ui/standarditemiconloader.h"
#include <QIcon>
#include <QSet>
#include "core/application.h"
#include "opmlcontainer.h"
#include "podcast.h"
#include "ui/iconloader.h"
#include "ui/standarditemiconloader.h"
PodcastDiscoveryModel::PodcastDiscoveryModel(Application* app, QObject* parent)
: QStandardItemModel(parent),
app_(app),

View File

@ -17,16 +17,7 @@
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/tagreaderclient.h"
#include "core/timeconstants.h"
#include "core/utilities.h"
#include "library/librarydirectorymodel.h"
#include "library/librarymodel.h"
#include <QDateTime>
#include <QDir>
@ -35,17 +26,90 @@
#include <QSettings>
#include <QTimer>
#include "core/application.h"
#include "core/logging.h"
#include "core/network.h"
#include "core/tagreaderclient.h"
#include "core/timeconstants.h"
#include "core/utilities.h"
#include "library/librarydirectorymodel.h"
#include "library/librarymodel.h"
#include "podcastbackend.h"
const char* PodcastDownloader::kSettingsGroup = "Podcasts";
const int PodcastDownloader::kAutoDeleteCheckIntervalMsec =
15 * 60 * kMsecPerSec; // 15 minutes
struct PodcastDownloader::Task {
Task() : file(nullptr) {}
~Task() { delete file; }
Task::Task(PodcastEpisode episode, QFile* file, PodcastBackend* backend)
: file_(file),
episode_(episode),
req_(QNetworkRequest(episode.url())),
backend_(backend),
network_(new NetworkAccessManager(this)),
repl(new RedirectFollower(network_->get(req_))) {
connect(repl.get(), SIGNAL(readyRead()), SLOT(reading()));
connect(repl.get(), SIGNAL(finished()), SLOT(finishedInternal()));
connect(repl.get(), SIGNAL(downloadProgress(qint64, qint64)),
SLOT(downloadProgressInternal(qint64, qint64)));
emit ProgressChanged(episode_, PodcastDownload::Downloading, 0);
}
PodcastEpisode episode;
QFile* file;
};
PodcastEpisode Task::episode() const { return episode_; }
void Task::reading() {
qint64 bytes = 0;
forever {
bytes = repl->bytesAvailable();
if (bytes <= 0) break;
file_->write(repl->reply()->read(bytes));
}
}
void Task::finishedPublic() {
disconnect(repl.get(), SIGNAL(readyRead()), 0, 0);
disconnect(repl.get(), SIGNAL(downloadProgress(qint64, qint64)), 0, 0);
disconnect(repl.get(), SIGNAL(finished()), 0, 0);
emit ProgressChanged(episode_, PodcastDownload::NotDownloading, 0);
// Delete the file
file_->remove();
emit finished(this);
}
void Task::finishedInternal() {
if (repl->error() != QNetworkReply::NoError) {
qLog(Warning) << "Error downloading episode:" << repl->errorString();
emit ProgressChanged(episode_, PodcastDownload::NotDownloading, 0);
// Delete the file
file_->remove();
emit finished(this);
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 corect metadata
TagReaderClient::Instance()->SaveFileBlocking(file_->fileName(), song);
emit finished(this);
}
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),
@ -53,20 +117,12 @@ PodcastDownloader::PodcastDownloader(Application* app, QObject* parent)
backend_(app_->podcast_backend()),
network_(new NetworkAccessManager(this)),
disallowed_filename_characters_("[^a-zA-Z0-9_~ -]"),
auto_download_(false),
delete_after_secs_(0),
current_task_(nullptr),
last_progress_signal_(0),
auto_delete_timer_(new QTimer(this)) {
auto_download_(false) {
connect(backend_, SIGNAL(EpisodesAdded(PodcastEpisodeList)),
SLOT(EpisodesAdded(PodcastEpisodeList)));
connect(backend_, SIGNAL(SubscriptionAdded(Podcast)),
SLOT(SubscriptionAdded(Podcast)));
connect(app_, SIGNAL(SettingsChanged()), SLOT(ReloadSettings()));
connect(auto_delete_timer_, SIGNAL(timeout()), SLOT(AutoDelete()));
auto_delete_timer_->setInterval(kAutoDeleteCheckIntervalMsec);
auto_delete_timer_->start();
ReloadSettings();
}
@ -89,61 +145,10 @@ void PodcastDownloader::ReloadSettings() {
auto_download_ = s.value("auto_download", false).toBool();
download_dir_ = s.value("download_dir", DefaultDownloadDir()).toString();
delete_after_secs_ = s.value("delete_after", 0).toInt();
}
void PodcastDownloader::DownloadEpisode(const PodcastEpisode& episode) {
if (downloading_episode_ids_.contains(episode.database_id())) return;
downloading_episode_ids_.insert(episode.database_id());
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::DeleteEpisode(const PodcastEpisode& episode) {
if (!episode.downloaded() ||
downloading_episode_ids_.contains(episode.database_id()))
return;
// 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());
backend_->UpdateEpisodes(PodcastEpisodeList() << episode_copy);
}
void PodcastDownloader::FinishAndDelete(Task* task) {
Podcast podcast =
backend_->GetSubscriptionById(task->episode.podcast_database_id());
Song song = task->episode.ToSong(podcast);
downloading_episode_ids_.remove(task->episode.database_id());
emit ProgressChanged(task->episode, Finished, 0);
// I didn't ecountered even a single podcast with a corect metadata
TagReaderClient::Instance()->SaveFileBlocking(task->file->fileName(), song);
delete task;
NextTask();
}
QString PodcastDownloader::FilenameForEpisode(
const QString& directory, const PodcastEpisode& episode) const {
QString PodcastDownloader::FilenameForEpisode(const QString& directory,
const PodcastEpisode& episode) const {
const QString file_extension = QFileInfo(episode.url().path()).suffix();
int count = 0;
@ -173,103 +178,46 @@ QString PodcastDownloader::FilenameForEpisode(
}
}
void PodcastDownloader::StartDownloading(Task* task) {
current_task_ = task;
// Need to get the name of the podcast to use in the directory name.
Podcast 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";
FinishAndDelete(task);
return;
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, task->episode);
const QString filepath = FilenameForEpisode(directory, episode);
// Open the output file
QDir().mkpath(directory);
task->file = new QFile(filepath);
if (!task->file->open(QIODevice::WriteOnly)) {
QFile* file = new QFile(filepath);
if (!file->open(QIODevice::WriteOnly)) {
qLog(Warning) << "Could not open the file" << filepath << "for writing";
FinishAndDelete(task);
return;
}
qLog(Info) << "Downloading" << task->episode.url() << "to" << filepath;
Task* task = new Task(episode, file, backend_);
// 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);
list_tasks_ << task;
qLog(Info) << "Downloading" << task->episode().url() << "to" << filepath;
connect(task, SIGNAL(finished(Task*)), SLOT(ReplyFinished(Task*)));
connect(task, SIGNAL(ProgressChanged(const PodcastEpisode&,
PodcastDownload::State, int)),
SIGNAL(ProgressChanged(const PodcastEpisode&,
PodcastDownload::State, int)));
}
void PodcastDownloader::NextTask() {
current_task_ = nullptr;
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 || total < 1024) 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,
static_cast<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();
FinishAndDelete(current_task_);
return;
}
qLog(Info) << "Download of" << current_task_->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 =
backend_->GetEpisodeById(current_task_->episode.database_id());
episode.set_downloaded(true);
episode.set_local_url(QUrl::fromLocalFile(current_task_->file->fileName()));
backend_->UpdateEpisodes(PodcastEpisodeList() << episode);
FinishAndDelete(current_task_);
void PodcastDownloader::ReplyFinished(Task* task) {
list_tasks_.removeAll(task);
delete task;
}
QString PodcastDownloader::SanitiseFilenameComponent(const QString& text)
@ -291,23 +239,29 @@ void PodcastDownloader::EpisodesAdded(const PodcastEpisodeList& episodes) {
}
}
void PodcastDownloader::AutoDelete() {
if (delete_after_secs_ <= 0) {
return;
PodcastEpisodeList PodcastDownloader::EpisodesDownloading(const PodcastEpisodeList& episodes) {
PodcastEpisodeList ret;
for (Task* tas : list_tasks_) {
for (PodcastEpisode episode : episodes) {
if (tas->episode().database_id() == episode.database_id()) {
ret << episode;
}
}
}
return ret;
}
QDateTime max_date = QDateTime::currentDateTime();
max_date.addSecs(-delete_after_secs_);
PodcastEpisodeList old_episodes =
backend_->GetOldDownloadedEpisodes(max_date);
if (old_episodes.isEmpty()) return;
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);
void PodcastDownloader::cancelDownload(const PodcastEpisodeList& episodes) {
QList<Task*> ta;
for (Task* tas : list_tasks_) {
for (PodcastEpisode episode : episodes) {
if (tas->episode().database_id() == episode.database_id()) {
ta << tas;
}
}
}
for (Task* tas : ta) {
tas->finishedPublic();
list_tasks_.removeAll(tas);
}
}

View File

@ -20,9 +20,12 @@
#ifndef PODCASTS_PODCASTDOWNLOADER_H_
#define PODCASTS_PODCASTDOWNLOADER_H_
#include "core/network.h"
#include "podcast.h"
#include "podcastepisode.h"
#include <memory>
#include <QFile>
#include <QList>
#include <QObject>
#include <QQueue>
@ -40,29 +43,57 @@ class PodcastBackend;
class QNetworkAccessManager;
namespace PodcastDownload {
enum State { NotDownloading, Queued, Downloading, Finished };
}
class Task : public QObject {
Q_OBJECT
public:
Task(PodcastEpisode episode, QFile* file, PodcastBackend* backend);
PodcastEpisode episode() const;
signals:
void ProgressChanged(const PodcastEpisode& episode,
PodcastDownload::State state, 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_;
QNetworkRequest req_;
PodcastBackend* backend_;
std::unique_ptr<NetworkAccessManager> network_;
std::unique_ptr<RedirectFollower> repl;
};
class PodcastDownloader : public QObject {
Q_OBJECT
public:
explicit PodcastDownloader(Application* app, QObject* parent = nullptr);
enum State { NotDownloading, Queued, Downloading, Finished };
static const char* kSettingsGroup;
static const int kAutoDeleteCheckIntervalMsec;
PodcastEpisodeList EpisodesDownloading(const PodcastEpisodeList& episodes);
QString DefaultDownloadDir() const;
public slots:
// Adds the episode to the download queue
void DownloadEpisode(const PodcastEpisode& episode);
// Deletes downloaded data for this episode
void DeleteEpisode(const PodcastEpisode& episode);
void cancelDownload(const PodcastEpisodeList& episodes);
signals:
void ProgressChanged(const PodcastEpisode& episode,
PodcastDownloader::State state, int percent);
PodcastDownload::State state, int percent);
private slots:
void ReloadSettings();
@ -70,19 +101,9 @@ class PodcastDownloader : public QObject {
void SubscriptionAdded(const Podcast& podcast);
void EpisodesAdded(const PodcastEpisodeList& episodes);
void ReplyReadyRead();
void ReplyFinished();
void ReplyDownloadProgress(qint64 received, qint64 total);
void AutoDelete();
void ReplyFinished(Task* task);
private:
struct Task;
void StartDownloading(Task* task);
void NextTask();
void FinishAndDelete(Task* task);
QString FilenameForEpisode(const QString& directory,
const PodcastEpisode& episode) const;
QString SanitiseFilenameComponent(const QString& text) const;
@ -96,15 +117,8 @@ class PodcastDownloader : public QObject {
bool auto_download_;
QString download_dir_;
int delete_after_secs_;
Task* current_task_;
QQueue<Task*> queued_tasks_;
QSet<int> downloading_episode_ids_;
time_t last_progress_signal_;
QTimer* auto_delete_timer_;
QList<Task*> list_tasks_;
};
#endif // PODCASTS_PODCASTDOWNLOADER_H_

View File

@ -17,17 +17,18 @@
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "podcast.h"
#include "podcastepisode.h"
#include "core/logging.h"
#include "core/timeconstants.h"
#include "core/utilities.h"
#include <QDataStream>
#include <QDateTime>
#include <QFile>
#include <QFileInfo>
#include "podcast.h"
#include "core/logging.h"
#include "core/timeconstants.h"
#include "core/utilities.h"
const QStringList PodcastEpisode::kColumns = QStringList() << "podcast_id"
<< "title"
<< "description"
@ -183,8 +184,10 @@ Song PodcastEpisode::ToSong(const Podcast& podcast) const {
ret.set_comment(description());
ret.set_id(database_id());
ret.set_ctime(publication_date().toTime_t());
ret.set_genre(QString("Podcast"));
ret.set_genre_id3(186);
if (listened()) {
if (listened() && listened_date().isValid()) {
ret.set_mtime(listened_date().toTime_t());
} else {
ret.set_mtime(publication_date().toTime_t());

View File

@ -17,14 +17,15 @@
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "opmlcontainer.h"
#include "podcastparser.h"
#include "core/logging.h"
#include "core/utilities.h"
#include <QDateTime>
#include <QXmlStreamReader>
#include "core/logging.h"
#include "core/utilities.h"
#include "opmlcontainer.h"
// Namespace constants must be lower case.
const char* PodcastParser::kAtomNamespace = "http://www.w3.org/2005/atom";
const char* PodcastParser::kItunesNamespace =

View File

@ -1,7 +1,8 @@
/* This file is part of Clementine.
Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
Copyright 2012-2013, David Sansome <me@davidsansome.com>
Copyright 2013-2014, Krzysztof Sobiecki <sobkas@gmail.com>
Copyright 2013-2014, Krzysztof A. Sobiecki <sobkas@gmail.com>
Copyright 2014, Simeon Bird <sbird@andrew.cmu.edu>
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -17,13 +18,13 @@
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "addpodcastdialog.h"
#include "opmlcontainer.h"
#include "podcastbackend.h"
#include "podcastdownloader.h"
#include "podcastservice.h"
#include "podcastservicemodel.h"
#include "podcastupdater.h"
#include <QMenu>
#include <QSortFilterProxyModel>
#include <QtConcurrentRun>
#include "addpodcastdialog.h"
#include "core/application.h"
#include "core/logging.h"
#include "core/mergedproxymodel.h"
@ -32,15 +33,17 @@
#include "devices/deviceview.h"
#include "internet/internetmodel.h"
#include "library/libraryview.h"
#include "opmlcontainer.h"
#include "podcastbackend.h"
#include "podcastdeleter.h"
#include "podcastdownloader.h"
#include "podcastservicemodel.h"
#include "podcastupdater.h"
#include "ui/iconloader.h"
#include "ui/organisedialog.h"
#include "ui/organiseerrordialog.h"
#include "ui/standarditemiconloader.h"
#include <QMenu>
#include <QSortFilterProxyModel>
#include <QtConcurrentRun>
const char* PodcastService::kServiceName = "Podcasts";
const char* PodcastService::kSettingsGroup = "Podcasts";
@ -179,6 +182,32 @@ void PodcastService::CopyToDevice(const QModelIndexList& episode_indexes,
if (organise_dialog_->SetSongs(songs)) organise_dialog_->show();
}
void PodcastService::CancelDownload() {
CancelDownload(selected_episodes_, explicitly_selected_podcasts_);
}
void PodcastService::CancelDownload(const QModelIndexList& episode_indexes,
const QModelIndexList& podcast_indexes) {
PodcastEpisode episode_tmp;
SongList songs;
PodcastEpisodeList episodes;
Podcast podcast;
for (const QModelIndex& index : episode_indexes) {
episode_tmp = index.data(Role_Episode).value<PodcastEpisode>();
episodes << episode_tmp;
}
for (const QModelIndex& podcast : podcast_indexes) {
for (int i = 0; i < podcast.model()->rowCount(podcast); ++i) {
const QModelIndex& index = podcast.child(i, 0);
episode_tmp = index.data(Role_Episode).value<PodcastEpisode>();
episodes << episode_tmp;
}
}
episodes = app_->podcast_downloader()->EpisodesDownloading(episodes);
app_->podcast_downloader()->cancelDownload(episodes);
}
void PodcastService::LazyPopulate(QStandardItem* parent) {
switch (parent->data(InternetModel::Role_Type).toInt()) {
case InternetModel::Type_Service:
@ -190,11 +219,9 @@ void PodcastService::LazyPopulate(QStandardItem* parent) {
void PodcastService::PopulatePodcastList(QStandardItem* parent) {
// Do this here since the downloader won't be created yet in the ctor.
connect(
app_->podcast_downloader(),
SIGNAL(ProgressChanged(PodcastEpisode, PodcastDownloader::State, int)),
SLOT(DownloadProgressChanged(PodcastEpisode, PodcastDownloader::State,
int)));
connect(app_->podcast_downloader(),
SIGNAL(ProgressChanged(PodcastEpisode, PodcastDownload::State, int)),
SLOT(DownloadProgressChanged(PodcastEpisode, PodcastDownload::State, int)));
if (default_icon_.isNull()) {
default_icon_ = QIcon(":providers/podcast16.png");
@ -225,7 +252,7 @@ void PodcastService::UpdatePodcastText(QStandardItem* item,
}
void PodcastService::UpdateEpisodeText(QStandardItem* item,
PodcastDownloader::State state,
PodcastDownload::State state,
int percent) {
const PodcastEpisode episode =
item->data(Role_Episode).value<PodcastEpisode>();
@ -250,7 +277,7 @@ void PodcastService::UpdateEpisodeText(QStandardItem* item,
// Queued or downloading episodes get icons, tooltips, and maybe a title.
switch (state) {
case PodcastDownloader::Queued:
case PodcastDownload::Queued:
if (queued_icon_.isNull()) {
queued_icon_ = QIcon(":icons/22x22/user-away.png");
}
@ -258,7 +285,7 @@ void PodcastService::UpdateEpisodeText(QStandardItem* item,
tooltip = tr("Download queued");
break;
case PodcastDownloader::Downloading:
case PodcastDownload::Downloading:
if (downloading_icon_.isNull()) {
downloading_icon_ = IconLoader::Load("go-down");
}
@ -268,8 +295,8 @@ void PodcastService::UpdateEpisodeText(QStandardItem* item,
QString("[ %1% ] %2").arg(QString::number(percent), episode.title());
break;
case PodcastDownloader::Finished:
case PodcastDownloader::NotDownloading:
case PodcastDownload::Finished:
case PodcastDownload::NotDownloading:
break;
}
@ -278,6 +305,45 @@ void PodcastService::UpdateEpisodeText(QStandardItem* item,
item->setIcon(icon);
}
void PodcastService::UpdatePodcastText(QStandardItem* item,
PodcastDownload::State state,
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_ = QIcon(":icons/22x22/user-away.png");
}
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().toString(), QString(), item);
} else {
item->setIcon(default_icon_);
}
break;
}
}
QStandardItem* PodcastService::CreatePodcastItem(const Podcast& podcast) {
QStandardItem* item = new QStandardItem;
@ -352,6 +418,9 @@ void PodcastService::ShowContextMenu(const QPoint& global_pos) {
copy_to_device_ = context_menu_->addAction(
IconLoader::Load("multimedia-player-ipod-mini-blue"),
tr("Copy to device..."), this, SLOT(CopyToDevice()));
cancel_download_ = context_menu_->addAction(
IconLoader::Load("cancel"),
tr("Cancel download"), this, SLOT(CancelDownload()));
remove_selected_action_ = context_menu_->addAction(
IconLoader::Load("list-remove"), tr("Unsubscribe"), this,
SLOT(RemoveSelectedPodcast()));
@ -413,6 +482,7 @@ void PodcastService::ShowContextMenu(const QPoint& global_pos) {
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 =
@ -583,18 +653,20 @@ void PodcastService::DownloadSelectedEpisode() {
void PodcastService::DeleteDownloadedData() {
for (const QModelIndex& index : selected_episodes_) {
app_->podcast_downloader()->DeleteEpisode(
app_->podcast_deleter()->DeleteEpisode(
index.data(Role_Episode).value<PodcastEpisode>());
}
}
void PodcastService::DownloadProgressChanged(const PodcastEpisode& episode,
PodcastDownloader::State state,
PodcastDownload::State state,
int percent) {
QStandardItem* item = episodes_by_database_id_[episode.database_id()];
if (!item) return;
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() {

View File

@ -1,7 +1,8 @@
/* This file is part of Clementine.
Copyright 2012-2013, David Sansome <me@davidsansome.com>
Copyright 2013-2014, Krzysztof Sobiecki <sobkas@gmail.com>
Copyright 2013-2014, Krzysztof A. Sobiecki <sobkas@gmail.com>
Copyright 2014, John Maguire <john.maguire@gmail.com>
Copyright 2014, Simeon Bird <sbird@andrew.cmu.edu>
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -20,6 +21,7 @@
#ifndef PODCASTS_PODCASTSERVICE_H_
#define PODCASTS_PODCASTSERVICE_H_
#include "podcastdeleter.h"
#include "podcastdownloader.h"
#include "internet/internetmodel.h"
#include "internet/internetservice.h"
@ -84,7 +86,7 @@ class PodcastService : public InternetService {
void EpisodesUpdated(const PodcastEpisodeList& episodes);
void DownloadProgressChanged(const PodcastEpisode& episode,
PodcastDownloader::State state, int percent);
PodcastDownload::State state, int percent);
void CurrentSongChanged(const Song& metadata);
@ -92,6 +94,9 @@ class PodcastService : public InternetService {
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();
@ -101,7 +106,11 @@ class PodcastService : public InternetService {
void UpdatePodcastText(QStandardItem* item, int unlistened_count) const;
void UpdateEpisodeText(
QStandardItem* item,
PodcastDownloader::State state = PodcastDownloader::NotDownloading,
PodcastDownload::State state = PodcastDownload::NotDownloading,
int percent = 0);
void UpdatePodcastText(
QStandardItem* item,
PodcastDownload::State state = PodcastDownload::NotDownloading,
int percent = 0);
QStandardItem* CreatePodcastItem(const Podcast& podcast);
@ -139,6 +148,7 @@ class PodcastService : public InternetService {
QAction* set_new_action_;
QAction* set_listened_action_;
QAction* copy_to_device_;
QAction* cancel_download_;
QStandardItem* root_;
std::unique_ptr<OrganiseDialog> organise_dialog_;

View File

@ -17,21 +17,22 @@
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "gpoddersync.h"
#include "podcastdownloader.h"
#include "podcastsettingspage.h"
#include "ui_podcastsettingspage.h"
#include "core/application.h"
#include "core/closure.h"
#include "core/timeconstants.h"
#include "library/librarydirectorymodel.h"
#include "library/librarymodel.h"
#include "ui/settingsdialog.h"
#include <QFileDialog>
#include <QNetworkReply>
#include <QSettings>
#include "core/application.h"
#include "core/closure.h"
#include "core/timeconstants.h"
#include "gpoddersync.h"
#include "library/librarydirectorymodel.h"
#include "library/librarymodel.h"
#include "podcastdownloader.h"
#include "ui/settingsdialog.h"
const char* PodcastSettingsPage::kSettingsGroup = "Podcasts";
PodcastSettingsPage::PodcastSettingsPage(SettingsDialog* dialog)

View File

@ -17,17 +17,18 @@
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "podcastbackend.h"
#include "podcastupdater.h"
#include "podcasturlloader.h"
#include <QSettings>
#include <QTimer>
#include "core/application.h"
#include "core/closure.h"
#include "core/logging.h"
#include "core/qhash_qurl.h"
#include "core/timeconstants.h"
#include <QSettings>
#include <QTimer>
#include "podcastbackend.h"
#include "podcasturlloader.h"
const char* PodcastUpdater::kSettingsGroup = "Podcasts";