1 /* This file is part of Clementine.
2 Copyright 2012, David Sansome <me@davidsansome.com>
3 Copyright 2014, Krzysztof A. Sobiecki <sobkas@gmail.com>
4 Copyright 2014, John Maguire <john.maguire@gmail.com>
5
6 Clementine is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10
11 Clementine is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with Clementine. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20 #include "podcastdownloader.h"
21
22 #include <QDateTime>
23 #include <QDir>
24 #include <QFile>
25 #include <QNetworkReply>
26 #include <QSettings>
27 #include <QTimer>
28
29 #include "core/application.h"
30 #include "core/logging.h"
31 #include "core/network.h"
32 #include "core/tagreaderclient.h"
33 #include "core/timeconstants.h"
34 #include "core/utilities.h"
35 #include "library/librarydirectorymodel.h"
36 #include "library/librarymodel.h"
37 #include "podcastbackend.h"
38
39 const char* PodcastDownloader::kSettingsGroup = "Podcasts";
40
Task(PodcastEpisode episode,QFile * file,PodcastBackend * backend)41 Task::Task(PodcastEpisode episode, QFile* file, PodcastBackend* backend)
42 : file_(file),
43 episode_(episode),
44 req_(QNetworkRequest(episode.url())),
45 backend_(backend),
46 network_(new NetworkAccessManager(this)),
47 repl(new RedirectFollower(network_->get(req_))) {
48 connect(repl.get(), SIGNAL(readyRead()), SLOT(reading()));
49 connect(repl.get(), SIGNAL(finished()), SLOT(finishedInternal()));
50 connect(repl.get(), SIGNAL(downloadProgress(qint64, qint64)),
51 SLOT(downloadProgressInternal(qint64, qint64)));
52 emit ProgressChanged(episode_, PodcastDownload::Queued, 0);
53 }
54
episode() const55 PodcastEpisode Task::episode() const { return episode_; }
56
reading()57 void Task::reading() {
58 qint64 bytes = 0;
59 forever {
60 bytes = repl->bytesAvailable();
61 if (bytes <= 0) break;
62
63 file_->write(repl->reply()->read(bytes));
64 }
65 }
finishedPublic()66 void Task::finishedPublic() {
67 disconnect(repl.get(), SIGNAL(readyRead()), 0, 0);
68 disconnect(repl.get(), SIGNAL(downloadProgress(qint64, qint64)), 0, 0);
69 disconnect(repl.get(), SIGNAL(finished()), 0, 0);
70 emit ProgressChanged(episode_, PodcastDownload::NotDownloading, 0);
71 // Delete the file
72 file_->remove();
73 emit finished(this);
74 }
75
finishedInternal()76 void Task::finishedInternal() {
77 if (repl->error() != QNetworkReply::NoError) {
78 qLog(Warning) << "Error downloading episode:" << repl->errorString();
79 emit ProgressChanged(episode_, PodcastDownload::NotDownloading, 0);
80 // Delete the file
81 file_->remove();
82 emit finished(this);
83 return;
84 }
85
86 qLog(Info) << "Download of" << file_->fileName() << "finished";
87
88 // Tell the database the episode has been updated. Get it from the DB again
89 // in case the listened field changed in the mean time.
90 PodcastEpisode episode = episode_;
91 episode.set_downloaded(true);
92 episode.set_local_url(QUrl::fromLocalFile(file_->fileName()));
93 backend_->UpdateEpisodes(PodcastEpisodeList() << episode);
94 Podcast podcast =
95 backend_->GetSubscriptionById(episode.podcast_database_id());
96 Song song = episode_.ToSong(podcast);
97
98 emit ProgressChanged(episode_, PodcastDownload::Finished, 0);
99
100 // I didn't ecountered even a single podcast with a correct metadata
101 TagReaderClient::Instance()->SaveFileBlocking(file_->fileName(), song);
102 emit finished(this);
103 }
104
downloadProgressInternal(qint64 received,qint64 total)105 void Task::downloadProgressInternal(qint64 received, qint64 total) {
106 if (total <= 0) {
107 emit ProgressChanged(episode_, PodcastDownload::Downloading, 0);
108 } else {
109 emit ProgressChanged(episode_, PodcastDownload::Downloading,
110 static_cast<float>(received) / total * 100);
111 }
112 }
113
PodcastDownloader(Application * app,QObject * parent)114 PodcastDownloader::PodcastDownloader(Application* app, QObject* parent)
115 : QObject(parent),
116 app_(app),
117 backend_(app_->podcast_backend()),
118 network_(new NetworkAccessManager(this)),
119 disallowed_filename_characters_("[^a-zA-Z0-9_~ -]"),
120 auto_download_(false) {
121 connect(backend_, SIGNAL(EpisodesAdded(PodcastEpisodeList)),
122 SLOT(EpisodesAdded(PodcastEpisodeList)));
123 connect(backend_, SIGNAL(SubscriptionAdded(Podcast)),
124 SLOT(SubscriptionAdded(Podcast)));
125 connect(app_, SIGNAL(SettingsChanged()), SLOT(ReloadSettings()));
126
127 ReloadSettings();
128 }
129
DefaultDownloadDir() const130 QString PodcastDownloader::DefaultDownloadDir() const {
131 QString prefix = QDir::homePath();
132
133 LibraryDirectoryModel* model = app_->library_model()->directory_model();
134 if (model->rowCount() > 0) {
135 // Download to the first library directory if there is one set
136 prefix = model->index(0, 0).data().toString();
137 }
138
139 return prefix + "/Podcasts";
140 }
141
ReloadSettings()142 void PodcastDownloader::ReloadSettings() {
143 QSettings s;
144 s.beginGroup(kSettingsGroup);
145
146 auto_download_ = s.value("auto_download", false).toBool();
147 download_dir_ = s.value("download_dir", DefaultDownloadDir()).toString();
148 }
149
FilenameForEpisode(const QString & directory,const PodcastEpisode & episode) const150 QString PodcastDownloader::FilenameForEpisode(const QString& directory,
151 const PodcastEpisode& episode) const {
152 const QString file_extension = QFileInfo(episode.url().path()).suffix();
153 int count = 0;
154
155 // The file name contains the publication date and episode title
156 QString base_filename =
157 episode.publication_date().date().toString(Qt::ISODate) + "-" +
158 SanitiseFilenameComponent(episode.title());
159
160 // Add numbers on to the end of the filename until we find one that doesn't
161 // exist.
162 forever {
163 QString filename;
164
165 if (count == 0) {
166 filename =
167 QString("%1/%2.%3").arg(directory, base_filename, file_extension);
168 } else {
169 filename = QString("%1/%2 (%3).%4").arg(
170 directory, base_filename, QString::number(count), file_extension);
171 }
172
173 if (!QFile::exists(filename)) {
174 return filename;
175 }
176
177 count++;
178 }
179 }
180
DownloadEpisode(const PodcastEpisode & episode)181 void PodcastDownloader::DownloadEpisode(const PodcastEpisode& episode) {
182 for (Task* tas : list_tasks_) {
183 if (tas->episode().database_id() == episode.database_id()) {
184 return;
185 }
186 }
187
188 Podcast podcast =
189 backend_->GetSubscriptionById(episode.podcast_database_id());
190 if (!podcast.is_valid()) {
191 qLog(Warning) << "The podcast that contains episode" << episode.url()
192 << "doesn't exist any more";
193 return;
194 }
195 const QString directory =
196 download_dir_ + "/" + SanitiseFilenameComponent(podcast.title());
197 const QString filepath = FilenameForEpisode(directory, episode);
198
199 // Open the output file
200 QDir().mkpath(directory);
201 QFile* file = new QFile(filepath);
202 if (!file->open(QIODevice::WriteOnly)) {
203 qLog(Warning) << "Could not open the file" << filepath << "for writing";
204 return;
205 }
206
207 Task* task = new Task(episode, file, backend_);
208
209 list_tasks_ << task;
210 qLog(Info) << "Downloading" << task->episode().url() << "to" << filepath;
211 connect(task, SIGNAL(finished(Task*)), SLOT(ReplyFinished(Task*)));
212 connect(task, SIGNAL(ProgressChanged(const PodcastEpisode&,
213 PodcastDownload::State, int)),
214 SIGNAL(ProgressChanged(const PodcastEpisode&,
215 PodcastDownload::State, int)));
216 }
217
ReplyFinished(Task * task)218 void PodcastDownloader::ReplyFinished(Task* task) {
219 list_tasks_.removeAll(task);
220 delete task;
221 }
222
SanitiseFilenameComponent(const QString & text) const223 QString PodcastDownloader::SanitiseFilenameComponent(const QString& text)
224 const {
225 return QString(text)
226 .replace(disallowed_filename_characters_, " ")
227 .simplified();
228 }
229
SubscriptionAdded(const Podcast & podcast)230 void PodcastDownloader::SubscriptionAdded(const Podcast& podcast) {
231 EpisodesAdded(podcast.episodes());
232 }
233
EpisodesAdded(const PodcastEpisodeList & episodes)234 void PodcastDownloader::EpisodesAdded(const PodcastEpisodeList& episodes) {
235 if (auto_download_) {
236 for (const PodcastEpisode& episode : episodes) {
237 DownloadEpisode(episode);
238 }
239 }
240 }
241
EpisodesDownloading(const PodcastEpisodeList & episodes)242 PodcastEpisodeList PodcastDownloader::EpisodesDownloading(const PodcastEpisodeList& episodes) {
243 PodcastEpisodeList ret;
244 for (Task* tas : list_tasks_) {
245 for (PodcastEpisode episode : episodes) {
246 if (tas->episode().database_id() == episode.database_id()) {
247 ret << episode;
248 }
249 }
250 }
251 return ret;
252 }
253
cancelDownload(const PodcastEpisodeList & episodes)254 void PodcastDownloader::cancelDownload(const PodcastEpisodeList& episodes) {
255 QList<Task*> ta;
256 for (Task* tas : list_tasks_) {
257 for (PodcastEpisode episode : episodes) {
258 if (tas->episode().database_id() == episode.database_id()) {
259 ta << tas;
260 }
261 }
262 }
263 for (Task* tas : ta) {
264 tas->finishedPublic();
265 list_tasks_.removeAll(tas);
266 }
267 }
268