/* * Strawberry Music Player * This file was part of Clementine. * Copyright 2012, David Sansome * Copyright 2014, John Maguire * Copyright 2014, Krzysztof Sobiecki * Copyright 2019-2021, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Strawberry is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Strawberry. If not, see . * */ #include "podcastdownloader.h" #include #include #include #include #include #include #include #include #include #include #include #include "core/application.h" #include "core/logging.h" #include "core/networkaccessmanager.h" #include "core/tagreaderclient.h" #include "core/timeconstants.h" #include "core/utilities.h" #include "podcastbackend.h" const char *PodcastDownloader::kSettingsGroup = "Podcasts"; Task::Task(const PodcastEpisode &episode, QFile *file, PodcastBackend *backend) : file_(file), episode_(episode), backend_(backend), network_(new NetworkAccessManager(this)), req_(QNetworkRequest(episode.url())), reply_(network_->get(req_)) { QObject::connect(reply_, &QNetworkReply::readyRead, this, &Task::reading); QObject::connect(reply_, &QNetworkReply::finished, this, &Task::finishedInternal); QObject::connect(reply_, &QNetworkReply::downloadProgress, this, &Task::downloadProgressInternal); emit ProgressChanged(episode_, PodcastDownload::Queued, 0); } PodcastEpisode Task::episode() const { return episode_; } void Task::reading() { qint64 bytes = 0; forever { bytes = reply_->bytesAvailable(); if (bytes <= 0) break; file_->write(reply_->read(bytes)); } } void Task::finishedPublic() { disconnect(reply_, &QNetworkReply::readyRead, nullptr, nullptr); disconnect(reply_, &QNetworkReply::downloadProgress, nullptr, nullptr); disconnect(reply_, &QNetworkReply::finished, nullptr, nullptr); emit ProgressChanged(episode_, PodcastDownload::NotDownloading, 0); // Delete the file file_->remove(); emit finished(this); } void Task::finishedInternal() { reply_->deleteLater(); if (reply_->error() != QNetworkReply::NoError) { qLog(Warning) << "Error downloading episode:" << reply_->errorString(); emit ProgressChanged(episode_, PodcastDownload::NotDownloading, 0); // Delete the file file_->remove(); emit finished(this); reply_ = nullptr; return; } qLog(Info) << "Download of" << file_->fileName() << "finished"; // Tell the database the episode has been updated. Get it from the DB again in case the listened field changed in the mean time. PodcastEpisode episode = episode_; episode.set_downloaded(true); episode.set_local_url(QUrl::fromLocalFile(file_->fileName())); backend_->UpdateEpisodes(PodcastEpisodeList() << episode); Podcast podcast = backend_->GetSubscriptionById(episode.podcast_database_id()); Song song = episode_.ToSong(podcast); emit ProgressChanged(episode_, PodcastDownload::Finished, 0); // I didn't ecountered even a single podcast with a correct metadata TagReaderClient::Instance()->SaveFileBlocking(file_->fileName(), song); emit finished(this); reply_ = nullptr; } void Task::downloadProgressInternal(qint64 received, qint64 total) { if (total <= 0) { emit ProgressChanged(episode_, PodcastDownload::Downloading, 0); } else { emit ProgressChanged(episode_, PodcastDownload::Downloading, static_cast(received) / total * 100); } } PodcastDownloader::PodcastDownloader(Application *app, QObject *parent) : QObject(parent), app_(app), backend_(app_->podcast_backend()), network_(new NetworkAccessManager(this)), disallowed_filename_characters_("[^a-zA-Z0-9_~ -]"), auto_download_(false) { QObject::connect(backend_, &PodcastBackend::EpisodesAdded, this, &PodcastDownloader::EpisodesAdded); QObject::connect(backend_, &PodcastBackend::SubscriptionAdded, this, &PodcastDownloader::SubscriptionAdded); QObject::connect(app_, &Application::SettingsChanged, this, &PodcastDownloader::ReloadSettings); ReloadSettings(); } QString PodcastDownloader::DefaultDownloadDir() const { return QDir::homePath() + "/Podcasts"; } void PodcastDownloader::ReloadSettings() { QSettings s; s.beginGroup(kSettingsGroup); auto_download_ = s.value("auto_download", false).toBool(); download_dir_ = s.value("download_dir", DefaultDownloadDir()).toString(); } QString PodcastDownloader::FilenameForEpisode(const QString &directory, const PodcastEpisode &episode) const { const QString file_extension = QFileInfo(episode.url().path()).suffix(); int count = 0; // The file name contains the publication date and episode title QString base_filename = episode.publication_date().date().toString(Qt::ISODate) + "-" + SanitiseFilenameComponent(episode.title()); // Add numbers on to the end of the filename until we find one that doesn't exist. forever { QString filename; if (count == 0) { filename = QString("%1/%2.%3").arg(directory, base_filename, file_extension); } else { filename = QString("%1/%2 (%3).%4").arg(directory, base_filename, QString::number(count), file_extension); } if (!QFile::exists(filename)) { return filename; } ++count; } } void PodcastDownloader::DownloadEpisode(const PodcastEpisode &episode) { for (Task *tas : list_tasks_) { if (tas->episode().database_id() == episode.database_id()) { return; } } Podcast podcast = backend_->GetSubscriptionById(episode.podcast_database_id()); if (!podcast.is_valid()) { qLog(Warning) << "The podcast that contains episode" << episode.url() << "doesn't exist any more"; return; } const QString directory = download_dir_ + "/" + SanitiseFilenameComponent(podcast.title()); const QString filepath = FilenameForEpisode(directory, episode); // Open the output file if (!QDir(directory).exists()) QDir().mkpath(directory); QFile *file = new QFile(filepath); if (!file->open(QIODevice::WriteOnly)) { qLog(Warning) << "Could not open the file" << filepath << "for writing"; return; } Task *task = new Task(episode, file, backend_); list_tasks_ << task; qLog(Info) << "Downloading" << task->episode().url() << "to" << filepath; QObject::connect(task, &Task::finished, this, &PodcastDownloader::ReplyFinished); QObject::connect(task, &Task::ProgressChanged, this, &PodcastDownloader::ProgressChanged); } void PodcastDownloader::ReplyFinished(Task *task) { list_tasks_.removeAll(task); delete task; } QString PodcastDownloader::SanitiseFilenameComponent(const QString &text) const { return QString(text).replace(disallowed_filename_characters_, " ") .simplified(); } void PodcastDownloader::SubscriptionAdded(const Podcast &podcast) { EpisodesAdded(podcast.episodes()); } void PodcastDownloader::EpisodesAdded(const PodcastEpisodeList &episodes) { if (auto_download_) { for (const PodcastEpisode &episode : episodes) { DownloadEpisode(episode); } } } PodcastEpisodeList PodcastDownloader::EpisodesDownloading(const PodcastEpisodeList &episodes) { PodcastEpisodeList ret; for (Task *tas : list_tasks_) { for (const PodcastEpisode &episode : episodes) { if (tas->episode().database_id() == episode.database_id()) { ret << episode; } } } return ret; } void PodcastDownloader::cancelDownload(const PodcastEpisodeList &episodes) { QList ta; for (Task *tas : list_tasks_) { for (const PodcastEpisode &episode : episodes) { if (tas->episode().database_id() == episode.database_id()) { ta << tas; } } } for (Task *tas : ta) { tas->finishedPublic(); list_tasks_.removeAll(tas); } }