/* This file is part of Clementine. Copyright 2012, David Sansome 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 . */ #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 #include #include #include #include #include const char* PodcastDownloader::kSettingsGroup = "Podcasts"; const int PodcastDownloader::kAutoDeleteCheckIntervalMsec = 15 * 60 * kMsecPerSec; // 15 minutes struct PodcastDownloader::Task { Task() : file(nullptr) {} ~Task() { delete file; } PodcastEpisode episode; QFile* file; }; 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), delete_after_secs_(0), current_task_(nullptr), last_progress_signal_(0), auto_delete_timer_(new QTimer(this)) { 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(); } 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); 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 { 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::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; } const QString directory = download_dir_ + "/" + SanitiseFilenameComponent(podcast.title()); const QString filepath = FilenameForEpisode(directory, task->episode); // 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"; FinishAndDelete(task); 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_ = nullptr; if (!queued_tasks_.isEmpty()) { StartDownloading(queued_tasks_.dequeue()); } } void PodcastDownloader::ReplyReadyRead() { QNetworkReply* reply = qobject_cast(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(received) / total * 100); } void PodcastDownloader::ReplyFinished() { RedirectFollower* reply = qobject_cast(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_); } 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); } } } void PodcastDownloader::AutoDelete() { if (delete_after_secs_ <= 0) { return; } 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); } }