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