2012-03-10 16:32:36 +01:00
|
|
|
/* This file is part of Clementine.
|
|
|
|
Copyright 2012, David Sansome <me@davidsansome.com>
|
2014-11-01 19:38:39 +01:00
|
|
|
Copyright 2014, Krzysztof A. Sobiecki <sobkas@gmail.com>
|
|
|
|
Copyright 2014, John Maguire <john.maguire@gmail.com>
|
2014-02-07 16:34:20 +01:00
|
|
|
|
2012-03-10 16:32:36 +01:00
|
|
|
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.
|
2014-02-07 16:34:20 +01:00
|
|
|
|
2012-03-10 16:32:36 +01:00
|
|
|
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.
|
2014-02-07 16:34:20 +01:00
|
|
|
|
2012-03-10 16:32:36 +01:00
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
2014-12-13 02:05:08 +01:00
|
|
|
#include "podcastdownloader.h"
|
|
|
|
|
|
|
|
#include <QDateTime>
|
|
|
|
#include <QDir>
|
|
|
|
#include <QFile>
|
|
|
|
#include <QNetworkReply>
|
|
|
|
#include <QSettings>
|
|
|
|
#include <QTimer>
|
|
|
|
|
2012-03-10 16:32:36 +01:00
|
|
|
#include "core/application.h"
|
|
|
|
#include "core/logging.h"
|
|
|
|
#include "core/network.h"
|
2014-01-28 04:16:51 +01:00
|
|
|
#include "core/tagreaderclient.h"
|
2012-03-12 20:35:47 +01:00
|
|
|
#include "core/timeconstants.h"
|
2012-03-10 16:32:36 +01:00
|
|
|
#include "core/utilities.h"
|
|
|
|
#include "library/librarydirectorymodel.h"
|
|
|
|
#include "library/librarymodel.h"
|
2014-12-10 21:57:09 +01:00
|
|
|
#include "podcastbackend.h"
|
2012-03-10 16:32:36 +01:00
|
|
|
|
|
|
|
const char* PodcastDownloader::kSettingsGroup = "Podcasts";
|
|
|
|
|
2014-12-06 19:44:12 +01:00
|
|
|
Task::Task(PodcastEpisode episode, QFile* file, PodcastBackend* backend)
|
2014-12-10 22:13:01 +01:00
|
|
|
: file_(file),
|
|
|
|
episode_(episode),
|
|
|
|
req_(QNetworkRequest(episode.url())),
|
|
|
|
backend_(backend),
|
|
|
|
network_(new NetworkAccessManager(this)),
|
|
|
|
repl(new RedirectFollower(network_->get(req_))) {
|
2014-12-10 21:57:09 +01:00
|
|
|
connect(repl.get(), SIGNAL(readyRead()), SLOT(reading()));
|
|
|
|
connect(repl.get(), SIGNAL(finished()), SLOT(finishedInternal()));
|
|
|
|
connect(repl.get(), SIGNAL(downloadProgress(qint64, qint64)),
|
2014-12-12 15:38:34 +01:00
|
|
|
SLOT(downloadProgressInternal(qint64, qint64)));
|
2015-02-06 20:30:16 +01:00
|
|
|
emit ProgressChanged(episode_, PodcastDownload::Queued, 0);
|
2014-12-06 19:44:12 +01:00
|
|
|
}
|
|
|
|
|
2014-12-12 15:38:34 +01:00
|
|
|
PodcastEpisode Task::episode() const { return episode_; }
|
2014-12-06 19:44:12 +01:00
|
|
|
|
|
|
|
void Task::reading() {
|
|
|
|
qint64 bytes = 0;
|
|
|
|
forever {
|
|
|
|
bytes = repl->bytesAvailable();
|
|
|
|
if (bytes <= 0) break;
|
|
|
|
|
|
|
|
file_->write(repl->reply()->read(bytes));
|
|
|
|
}
|
|
|
|
}
|
2014-12-12 15:38:34 +01:00
|
|
|
void Task::finishedPublic() {
|
2014-12-16 14:59:17 +01:00
|
|
|
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);
|
2014-12-12 15:38:34 +01:00
|
|
|
}
|
2014-12-06 19:44:12 +01:00
|
|
|
|
2014-12-10 21:57:09 +01:00
|
|
|
void Task::finishedInternal() {
|
2014-12-06 19:44:12 +01:00
|
|
|
if (repl->error() != QNetworkReply::NoError) {
|
|
|
|
qLog(Warning) << "Error downloading episode:" << repl->errorString();
|
|
|
|
emit ProgressChanged(episode_, PodcastDownload::NotDownloading, 0);
|
|
|
|
// Delete the file
|
|
|
|
file_->remove();
|
2014-12-13 20:05:45 +01:00
|
|
|
emit finished(this);
|
2014-12-06 19:44:12 +01:00
|
|
|
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 =
|
2014-12-10 22:13:01 +01:00
|
|
|
backend_->GetSubscriptionById(episode.podcast_database_id());
|
2014-12-06 19:44:12 +01:00
|
|
|
Song song = episode_.ToSong(podcast);
|
|
|
|
|
|
|
|
emit ProgressChanged(episode_, PodcastDownload::Finished, 0);
|
2012-03-10 16:32:36 +01:00
|
|
|
|
2019-08-22 05:43:16 +02:00
|
|
|
// I didn't ecountered even a single podcast with a correct metadata
|
2014-12-06 19:44:12 +01:00
|
|
|
TagReaderClient::Instance()->SaveFileBlocking(file_->fileName(), song);
|
2014-12-13 20:05:45 +01:00
|
|
|
emit finished(this);
|
2014-12-06 19:44:12 +01:00
|
|
|
}
|
|
|
|
|
2014-12-12 15:38:34 +01:00
|
|
|
void Task::downloadProgressInternal(qint64 received, qint64 total) {
|
2014-12-06 19:44:12 +01:00
|
|
|
if (total <= 0) {
|
|
|
|
emit ProgressChanged(episode_, PodcastDownload::Downloading, 0);
|
|
|
|
} else {
|
|
|
|
emit ProgressChanged(episode_, PodcastDownload::Downloading,
|
|
|
|
static_cast<float>(received) / total * 100);
|
|
|
|
}
|
|
|
|
}
|
2012-03-10 16:32:36 +01:00
|
|
|
|
|
|
|
PodcastDownloader::PodcastDownloader(Application* app, QObject* parent)
|
2014-02-07 16:34:20 +01:00
|
|
|
: QObject(parent),
|
|
|
|
app_(app),
|
|
|
|
backend_(app_->podcast_backend()),
|
|
|
|
network_(new NetworkAccessManager(this)),
|
|
|
|
disallowed_filename_characters_("[^a-zA-Z0-9_~ -]"),
|
2014-12-06 19:44:12 +01:00
|
|
|
auto_download_(false) {
|
2014-01-28 16:04:17 +01:00
|
|
|
connect(backend_, SIGNAL(EpisodesAdded(PodcastEpisodeList)),
|
|
|
|
SLOT(EpisodesAdded(PodcastEpisodeList)));
|
2012-03-10 22:05:57 +01:00
|
|
|
connect(backend_, SIGNAL(SubscriptionAdded(Podcast)),
|
2012-03-10 16:32:36 +01:00
|
|
|
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);
|
|
|
|
|
2012-03-10 23:26:53 +01:00
|
|
|
auto_download_ = s.value("auto_download", false).toBool();
|
2012-03-10 16:32:36 +01:00
|
|
|
download_dir_ = s.value("download_dir", DefaultDownloadDir()).toString();
|
2012-03-11 16:36:35 +01:00
|
|
|
}
|
|
|
|
|
2014-12-06 19:44:12 +01:00
|
|
|
QString PodcastDownloader::FilenameForEpisode(const QString& directory,
|
|
|
|
const PodcastEpisode& episode) const {
|
2012-06-14 18:07:21 +02:00
|
|
|
const QString file_extension = QFileInfo(episode.url().path()).suffix();
|
|
|
|
int count = 0;
|
2014-01-28 04:50:00 +01:00
|
|
|
|
2012-06-14 18:07:21 +02:00
|
|
|
// The file name contains the publication date and episode title
|
2014-02-07 16:34:20 +01:00
|
|
|
QString base_filename =
|
2012-06-14 18:07:21 +02:00
|
|
|
episode.publication_date().date().toString(Qt::ISODate) + "-" +
|
|
|
|
SanitiseFilenameComponent(episode.title());
|
2014-01-28 04:50:00 +01:00
|
|
|
|
2012-06-14 18:07:21 +02:00
|
|
|
// Add numbers on to the end of the filename until we find one that doesn't
|
|
|
|
// exist.
|
|
|
|
forever {
|
|
|
|
QString filename;
|
2014-01-28 04:50:00 +01:00
|
|
|
|
2012-06-14 18:07:21 +02:00
|
|
|
if (count == 0) {
|
2014-02-07 16:34:20 +01:00
|
|
|
filename =
|
|
|
|
QString("%1/%2.%3").arg(directory, base_filename, file_extension);
|
2012-06-14 18:07:21 +02:00
|
|
|
} else {
|
|
|
|
filename = QString("%1/%2 (%3).%4").arg(
|
2014-02-07 16:34:20 +01:00
|
|
|
directory, base_filename, QString::number(count), file_extension);
|
2012-06-14 18:07:21 +02:00
|
|
|
}
|
2014-01-28 04:50:00 +01:00
|
|
|
|
2012-06-14 18:07:21 +02:00
|
|
|
if (!QFile::exists(filename)) {
|
|
|
|
return filename;
|
|
|
|
}
|
2014-01-28 04:50:00 +01:00
|
|
|
|
|
|
|
count++;
|
2012-06-14 18:07:21 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-12-06 19:44:12 +01:00
|
|
|
void PodcastDownloader::DownloadEpisode(const PodcastEpisode& episode) {
|
2014-12-10 22:13:01 +01:00
|
|
|
for (Task* tas : list_tasks_) {
|
2014-12-06 19:44:12 +01:00
|
|
|
if (tas->episode().database_id() == episode.database_id()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
2012-03-10 16:32:36 +01:00
|
|
|
|
|
|
|
Podcast podcast =
|
2014-12-06 19:44:12 +01:00
|
|
|
backend_->GetSubscriptionById(episode.podcast_database_id());
|
2012-03-10 16:32:36 +01:00
|
|
|
if (!podcast.is_valid()) {
|
2014-12-10 21:57:09 +01:00
|
|
|
qLog(Warning) << "The podcast that contains episode" << episode.url()
|
2012-03-10 16:32:36 +01:00
|
|
|
<< "doesn't exist any more";
|
|
|
|
return;
|
|
|
|
}
|
2014-02-07 16:34:20 +01:00
|
|
|
const QString directory =
|
|
|
|
download_dir_ + "/" + SanitiseFilenameComponent(podcast.title());
|
2014-12-06 19:44:12 +01:00
|
|
|
const QString filepath = FilenameForEpisode(directory, episode);
|
2012-03-10 16:32:36 +01:00
|
|
|
|
|
|
|
// Open the output file
|
|
|
|
QDir().mkpath(directory);
|
2014-12-10 21:57:09 +01:00
|
|
|
QFile* file = new QFile(filepath);
|
2014-12-06 19:44:12 +01:00
|
|
|
if (!file->open(QIODevice::WriteOnly)) {
|
2012-03-10 16:32:36 +01:00
|
|
|
qLog(Warning) << "Could not open the file" << filepath << "for writing";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-12-10 21:57:09 +01:00
|
|
|
Task* task = new Task(episode, file, backend_);
|
2012-03-10 16:32:36 +01:00
|
|
|
|
2014-12-06 19:44:12 +01:00
|
|
|
list_tasks_ << task;
|
|
|
|
qLog(Info) << "Downloading" << task->episode().url() << "to" << filepath;
|
2014-12-13 20:05:45 +01:00
|
|
|
connect(task, SIGNAL(finished(Task*)), SLOT(ReplyFinished(Task*)));
|
2014-12-06 19:44:12 +01:00
|
|
|
connect(task, SIGNAL(ProgressChanged(const PodcastEpisode&,
|
|
|
|
PodcastDownload::State, int)),
|
|
|
|
SIGNAL(ProgressChanged(const PodcastEpisode&,
|
|
|
|
PodcastDownload::State, int)));
|
2012-03-10 16:32:36 +01:00
|
|
|
}
|
|
|
|
|
2014-12-13 20:05:45 +01:00
|
|
|
void PodcastDownloader::ReplyFinished(Task* task) {
|
2014-12-06 19:44:12 +01:00
|
|
|
list_tasks_.removeAll(task);
|
2014-12-12 15:38:34 +01:00
|
|
|
delete task;
|
2012-03-10 16:32:36 +01:00
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
QString PodcastDownloader::SanitiseFilenameComponent(const QString& text)
|
|
|
|
const {
|
|
|
|
return QString(text)
|
|
|
|
.replace(disallowed_filename_characters_, " ")
|
|
|
|
.simplified();
|
2012-03-10 16:32:36 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
void PodcastDownloader::SubscriptionAdded(const Podcast& podcast) {
|
2012-03-10 23:26:53 +01:00
|
|
|
EpisodesAdded(podcast.episodes());
|
2012-03-10 16:32:36 +01:00
|
|
|
}
|
|
|
|
|
2014-01-28 16:04:17 +01:00
|
|
|
void PodcastDownloader::EpisodesAdded(const PodcastEpisodeList& episodes) {
|
2012-03-10 23:26:53 +01:00
|
|
|
if (auto_download_) {
|
2014-01-28 16:04:17 +01:00
|
|
|
for (const PodcastEpisode& episode : episodes) {
|
2012-03-10 23:26:53 +01:00
|
|
|
DownloadEpisode(episode);
|
|
|
|
}
|
|
|
|
}
|
2012-03-10 16:32:36 +01:00
|
|
|
}
|
2014-12-12 15:38:34 +01:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|