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 "podcastbackend.h"
21
22 #include <QMutexLocker>
23
24 #include "core/application.h"
25 #include "core/database.h"
26 #include "core/logging.h"
27 #include "core/scopedtransaction.h"
28
PodcastBackend(Application * app,QObject * parent)29 PodcastBackend::PodcastBackend(Application* app, QObject* parent)
30 : QObject(parent), app_(app), db_(app->database()) {}
31
Subscribe(Podcast * podcast)32 void PodcastBackend::Subscribe(Podcast* podcast) {
33 // If this podcast is already in the database, do nothing
34 if (podcast->is_valid()) {
35 return;
36 }
37
38 // If there's an entry in the database with the same URL, take its data.
39 Podcast existing_podcast = GetSubscriptionByUrl(podcast->url());
40 if (existing_podcast.is_valid()) {
41 *podcast = existing_podcast;
42 return;
43 }
44
45 QMutexLocker l(db_->Mutex());
46 QSqlDatabase db(db_->Connect());
47 ScopedTransaction t(&db);
48
49 // Insert the podcast.
50 QSqlQuery q(db);
51 q.prepare("INSERT INTO podcasts (" + Podcast::kColumnSpec +
52 ")"
53 " VALUES (" +
54 Podcast::kBindSpec + ")");
55 podcast->BindToQuery(&q);
56
57 q.exec();
58 if (db_->CheckErrors(q)) return;
59
60 // Update the database ID.
61 const int database_id = q.lastInsertId().toInt();
62 podcast->set_database_id(database_id);
63
64 // Update the IDs of any episodes.
65 PodcastEpisodeList* episodes = podcast->mutable_episodes();
66 for (auto it = episodes->begin(); it != episodes->end(); ++it) {
67 it->set_podcast_database_id(database_id);
68 }
69
70 // Add those episodes to the database.
71 AddEpisodes(episodes, &db);
72
73 t.Commit();
74
75 emit SubscriptionAdded(*podcast);
76 }
77
Unsubscribe(const Podcast & podcast)78 void PodcastBackend::Unsubscribe(const Podcast& podcast) {
79 // If this podcast is not already in the database, do nothing
80 if (!podcast.is_valid()) {
81 return;
82 }
83
84 QMutexLocker l(db_->Mutex());
85 QSqlDatabase db(db_->Connect());
86 ScopedTransaction t(&db);
87
88 // Remove the podcast.
89 QSqlQuery q(db);
90 q.prepare("DELETE FROM podcasts WHERE ROWID = :id");
91 q.bindValue(":id", podcast.database_id());
92 q.exec();
93 if (db_->CheckErrors(q)) return;
94
95 // Remove all episodes in the podcast
96 q.prepare("DELETE FROM podcast_episodes WHERE podcast_id = :id");
97 q.bindValue(":id", podcast.database_id());
98 q.exec();
99 if (db_->CheckErrors(q)) return;
100
101 t.Commit();
102
103 emit SubscriptionRemoved(podcast);
104 }
105
AddEpisodes(PodcastEpisodeList * episodes,QSqlDatabase * db)106 void PodcastBackend::AddEpisodes(PodcastEpisodeList* episodes,
107 QSqlDatabase* db) {
108 QSqlQuery q(*db);
109 q.prepare("INSERT INTO podcast_episodes (" + PodcastEpisode::kColumnSpec +
110 ")"
111 " VALUES (" +
112 PodcastEpisode::kBindSpec + ")");
113
114 for (auto it = episodes->begin(); it != episodes->end(); ++it) {
115 it->BindToQuery(&q);
116 q.exec();
117 if (db_->CheckErrors(q)) continue;
118
119 const int database_id = q.lastInsertId().toInt();
120 it->set_database_id(database_id);
121 }
122 }
123
AddEpisodes(PodcastEpisodeList * episodes)124 void PodcastBackend::AddEpisodes(PodcastEpisodeList* episodes) {
125 QMutexLocker l(db_->Mutex());
126 QSqlDatabase db(db_->Connect());
127 ScopedTransaction t(&db);
128
129 AddEpisodes(episodes, &db);
130 t.Commit();
131
132 emit EpisodesAdded(*episodes);
133 }
134
UpdateEpisodes(const PodcastEpisodeList & episodes)135 void PodcastBackend::UpdateEpisodes(const PodcastEpisodeList& episodes) {
136 QMutexLocker l(db_->Mutex());
137 QSqlDatabase db(db_->Connect());
138 ScopedTransaction t(&db);
139
140 QSqlQuery q(db);
141 q.prepare("UPDATE podcast_episodes"
142 " SET listened = :listened,"
143 " listened_date = :listened_date,"
144 " downloaded = :downloaded,"
145 " local_url = :local_url"
146 " WHERE ROWID = :id");
147
148 for (const PodcastEpisode& episode : episodes) {
149 q.bindValue(":listened", episode.listened());
150 q.bindValue(":listened_date", episode.listened_date().toTime_t());
151 q.bindValue(":downloaded", episode.downloaded());
152 q.bindValue(":local_url", episode.local_url().toEncoded());
153 q.bindValue(":id", episode.database_id());
154 q.exec();
155 db_->CheckErrors(q);
156 }
157
158 t.Commit();
159
160 emit EpisodesUpdated(episodes);
161 }
162
GetAllSubscriptions()163 PodcastList PodcastBackend::GetAllSubscriptions() {
164 PodcastList ret;
165
166 QMutexLocker l(db_->Mutex());
167 QSqlDatabase db(db_->Connect());
168
169 QSqlQuery q(db);
170 q.prepare("SELECT ROWID, " + Podcast::kColumnSpec + " FROM podcasts");
171 q.exec();
172 if (db_->CheckErrors(q)) return ret;
173
174 while (q.next()) {
175 Podcast podcast;
176 podcast.InitFromQuery(q);
177 ret << podcast;
178 }
179
180 return ret;
181 }
182
GetSubscriptionById(int id)183 Podcast PodcastBackend::GetSubscriptionById(int id) {
184 Podcast ret;
185
186 QMutexLocker l(db_->Mutex());
187 QSqlDatabase db(db_->Connect());
188
189 QSqlQuery q(db);
190 q.prepare("SELECT ROWID, " + Podcast::kColumnSpec +
191 " FROM podcasts"
192 " WHERE ROWID = :id");
193 q.bindValue(":id", id);
194 q.exec();
195 if (!db_->CheckErrors(q) && q.next()) {
196 ret.InitFromQuery(q);
197 }
198
199 return ret;
200 }
201
GetSubscriptionByUrl(const QUrl & url)202 Podcast PodcastBackend::GetSubscriptionByUrl(const QUrl& url) {
203 Podcast ret;
204
205 QMutexLocker l(db_->Mutex());
206 QSqlDatabase db(db_->Connect());
207
208 QSqlQuery q(db);
209 q.prepare("SELECT ROWID, " + Podcast::kColumnSpec +
210 " FROM podcasts"
211 " WHERE url = :url");
212 q.bindValue(":url", url.toEncoded());
213 q.exec();
214 if (!db_->CheckErrors(q) && q.next()) {
215 ret.InitFromQuery(q);
216 }
217
218 return ret;
219 }
220
GetEpisodes(int podcast_id)221 PodcastEpisodeList PodcastBackend::GetEpisodes(int podcast_id) {
222 PodcastEpisodeList ret;
223
224 QMutexLocker l(db_->Mutex());
225 QSqlDatabase db(db_->Connect());
226
227 QSqlQuery q(db);
228 q.prepare("SELECT ROWID, " + PodcastEpisode::kColumnSpec +
229 " FROM podcast_episodes"
230 " WHERE podcast_id = :id"
231 " ORDER BY publication_date DESC");
232 q.bindValue(":id", podcast_id);
233 q.exec();
234 if (db_->CheckErrors(q)) return ret;
235
236 while (q.next()) {
237 PodcastEpisode episode;
238 episode.InitFromQuery(q);
239 ret << episode;
240 }
241
242 return ret;
243 }
244
GetEpisodeById(int id)245 PodcastEpisode PodcastBackend::GetEpisodeById(int id) {
246 PodcastEpisode ret;
247
248 QMutexLocker l(db_->Mutex());
249 QSqlDatabase db(db_->Connect());
250
251 QSqlQuery q(db);
252 q.prepare("SELECT ROWID, " + PodcastEpisode::kColumnSpec +
253 " FROM podcast_episodes"
254 " WHERE ROWID = :id");
255 q.bindValue(":id", id);
256 q.exec();
257 if (!db_->CheckErrors(q) && q.next()) {
258 ret.InitFromQuery(q);
259 }
260
261 return ret;
262 }
263
GetEpisodeByUrl(const QUrl & url)264 PodcastEpisode PodcastBackend::GetEpisodeByUrl(const QUrl& url) {
265 PodcastEpisode ret;
266
267 QMutexLocker l(db_->Mutex());
268 QSqlDatabase db(db_->Connect());
269
270 QSqlQuery q(db);
271 q.prepare("SELECT ROWID, " + PodcastEpisode::kColumnSpec +
272 " FROM podcast_episodes"
273 " WHERE url = :url");
274 q.bindValue(":url", url.toEncoded());
275 q.exec();
276 if (!db_->CheckErrors(q) && q.next()) {
277 ret.InitFromQuery(q);
278 }
279
280 return ret;
281 }
282
GetEpisodeByUrlOrLocalUrl(const QUrl & url)283 PodcastEpisode PodcastBackend::GetEpisodeByUrlOrLocalUrl(const QUrl& url) {
284 PodcastEpisode ret;
285
286 QMutexLocker l(db_->Mutex());
287 QSqlDatabase db(db_->Connect());
288
289 QSqlQuery q(db);
290 q.prepare("SELECT ROWID, " + PodcastEpisode::kColumnSpec +
291 " FROM podcast_episodes"
292 " WHERE url = :url"
293 " OR local_url = :url");
294 q.bindValue(":url", url.toEncoded());
295 q.exec();
296 if (!db_->CheckErrors(q) && q.next()) {
297 ret.InitFromQuery(q);
298 }
299
300 return ret;
301 }
302
GetOldDownloadedEpisodes(const QDateTime & max_listened_date)303 PodcastEpisodeList PodcastBackend::GetOldDownloadedEpisodes(
304 const QDateTime& max_listened_date) {
305 PodcastEpisodeList ret;
306
307 QMutexLocker l(db_->Mutex());
308 QSqlDatabase db(db_->Connect());
309
310 QSqlQuery q(db);
311 q.prepare("SELECT ROWID, " + PodcastEpisode::kColumnSpec +
312 " FROM podcast_episodes"
313 " WHERE downloaded = 'true'"
314 " AND listened_date <= :max_listened_date");
315 q.bindValue(":max_listened_date", max_listened_date.toTime_t());
316 q.exec();
317 if (db_->CheckErrors(q)) return ret;
318
319 while (q.next()) {
320 PodcastEpisode episode;
321 episode.InitFromQuery(q);
322 ret << episode;
323 }
324
325 return ret;
326 }
327
GetOldestDownloadedListenedEpisode()328 PodcastEpisode PodcastBackend::GetOldestDownloadedListenedEpisode() {
329 PodcastEpisode ret;
330
331 QMutexLocker l(db_->Mutex());
332 QSqlDatabase db(db_->Connect());
333
334 QSqlQuery q(db);
335 q.prepare("SELECT ROWID, " + PodcastEpisode::kColumnSpec +
336 " FROM podcast_episodes"
337 " WHERE downloaded = 'true'"
338 " AND listened = 'true'"
339 " ORDER BY listened_date ASC");
340 q.exec();
341 if (db_->CheckErrors(q)) return ret;
342 q.next();
343 ret.InitFromQuery(q);
344
345 return ret;
346 }
347
GetNewDownloadedEpisodes()348 PodcastEpisodeList PodcastBackend::GetNewDownloadedEpisodes() {
349 PodcastEpisodeList ret;
350
351 QMutexLocker l(db_->Mutex());
352 QSqlDatabase db(db_->Connect());
353
354 QSqlQuery q(db);
355 q.prepare("SELECT ROWID, " + PodcastEpisode::kColumnSpec +
356 " FROM podcast_episodes"
357 " WHERE downloaded = 'true'"
358 " AND listened = 'false'");
359 q.exec();
360 if (db_->CheckErrors(q)) return ret;
361
362 while (q.next()) {
363 PodcastEpisode episode;
364 episode.InitFromQuery(q);
365 ret << episode;
366 }
367
368 return ret;
369 }
370