]> git.sur5r.net Git - minitube/blob - src/channelaggregator.cpp
f64272bf7cfaed3e1b8e1773eb915e4184974a8a
[minitube] / src / channelaggregator.cpp
1 /* $BEGIN_LICENSE
2
3 This file is part of Minitube.
4 Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
5
6 Minitube 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 Minitube 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 Minitube.  If not, see <http://www.gnu.org/licenses/>.
18
19 $END_LICENSE */
20
21 #include "channelaggregator.h"
22 #include "ytchannel.h"
23 #include "ytsearch.h"
24 #include "searchparams.h"
25 #include "database.h"
26 #include "video.h"
27 #ifdef APP_MAC
28 #include "macutils.h"
29 #endif
30 #include "networkaccess.h"
31
32 namespace The {
33     NetworkAccess* http();
34 }
35
36 ChannelAggregator::ChannelAggregator(QObject *parent) : QObject(parent),
37     unwatchedCount(-1),
38     running(false),
39     stopped(false),
40     currentChannel(0) {
41     checkInterval = 1800;
42
43     timer = new QTimer(this);
44     timer->setInterval(60000 * 5);
45     connect(timer, SIGNAL(timeout()), SLOT(run()));
46 }
47
48 ChannelAggregator* ChannelAggregator::instance() {
49     static ChannelAggregator* i = new ChannelAggregator();
50     return i;
51 }
52
53 void ChannelAggregator::start() {
54     stopped = false;
55     updateUnwatchedCount();
56     QTimer::singleShot(0, this, SLOT(run()));
57     if (!timer->isActive()) timer->start();
58 }
59
60 void ChannelAggregator::stop() {
61     timer->stop();
62     stopped = true;
63 }
64
65 YTChannel* ChannelAggregator::getChannelToCheck() {
66     if (stopped) return 0;
67     QSqlDatabase db = Database::instance().getConnection();
68     QSqlQuery query(db);
69     query.prepare("select user_id from subscriptions where checked<? "
70                   "order by checked limit 1");
71     query.bindValue(0, QDateTime::currentDateTimeUtc().toTime_t() - checkInterval);
72     bool success = query.exec();
73     if (!success) qWarning() << query.lastQuery() << query.lastError().text();
74     if (query.next())
75         return YTChannel::forId(query.value(0).toString());
76     return 0;
77 }
78
79 void ChannelAggregator::run() {
80     if (running) return;
81     if (!Database::exists()) return;
82     running = true;
83     newVideoCount = 0;
84     updatedChannels.clear();
85
86     if (!Database::instance().getConnection().transaction())
87         qWarning() << "Transaction failed" << __PRETTY_FUNCTION__;
88
89     processNextChannel();
90 }
91
92 void ChannelAggregator::processNextChannel() {
93     if (stopped) {
94         running = false;
95         return;
96     }
97     YTChannel* channel = getChannelToCheck();
98     if (channel) {
99         checkWebPage(channel);
100     } else finish();
101 }
102
103 void ChannelAggregator::checkWebPage(YTChannel *channel) {
104     currentChannel = channel;
105     QString url = "https://www.youtube.com/channel/" + channel->getChannelId() + "/videos";
106     QObject *reply = The::http()->get(url);
107
108     connect(reply, SIGNAL(data(QByteArray)), SLOT(parseWebPage(QByteArray)));
109     connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(errorWebPage(QNetworkReply*)));
110 }
111
112 void ChannelAggregator::parseWebPage(const QByteArray &bytes) {
113     bool hasNewVideos = true;
114     QRegExp re = QRegExp("[\\?&]v=([0-9A-Za-z_-]+)");
115     if (re.indexIn(bytes) != -1) {
116         QString videoId = re.cap(1);
117         QString latestVideoId = currentChannel->latestVideoId();
118         // qDebug() << "Comparing" << videoId << latestVideoId;
119         hasNewVideos = videoId != latestVideoId;
120     }
121     if (hasNewVideos) {
122         reallyProcessChannel(currentChannel);
123         currentChannel = 0;
124     } else {
125         currentChannel->updateChecked();
126         currentChannel = 0;
127         processNextChannel();
128     }
129 }
130
131 void ChannelAggregator::errorWebPage(QNetworkReply *reply) {
132     Q_UNUSED(reply);
133     reallyProcessChannel(currentChannel);
134     currentChannel = 0;
135 }
136
137 void ChannelAggregator::reallyProcessChannel(YTChannel *channel) {
138     SearchParams *params = new SearchParams();
139     params->setChannelId(channel->getChannelId());
140     params->setSortBy(SearchParams::SortByNewest);
141     params->setTransient(true);
142     params->setPublishedAfter(channel->getChecked());
143     YTSearch *videoSource = new YTSearch(params, this);
144     connect(videoSource, SIGNAL(gotVideos(QList<Video*>)), SLOT(videosLoaded(QList<Video*>)));
145     videoSource->loadVideos(50, 1);
146
147     channel->updateChecked();
148 }
149
150 void ChannelAggregator::finish() {
151     currentChannel = 0;
152
153     /*
154     foreach (YTChannel *channel, updatedChannels)
155         if (channel->updateNotifyCount())
156             emit channelChanged(channel);
157     updateUnwatchedCount();
158     */
159
160     QSqlDatabase db = Database::instance().getConnection();
161     if (!db.commit())
162         qWarning() << "Commit failed" << __PRETTY_FUNCTION__;
163
164     /*
165     QByteArray b = db.databaseName().right(20).toLocal8Bit();
166     const char* s = b.constData();
167     const int l = strlen(s);
168     int t = 1;
169     for (int i = 0; i < l; i++)
170         t += t % 2 ? s[i] / l : s[i] / t;
171     if (t != s[0]) return;
172     */
173
174 #ifdef Q_OS_MAC
175     if (newVideoCount > 0 && unwatchedCount > 0 && mac::canNotify()) {
176         QString channelNames;
177         const int total = updatedChannels.size();
178         for (int i = 0; i < total; ++i) {
179             YTChannel *channel = updatedChannels.at(i);
180             channelNames += channel->getDisplayName();
181             if (i < total-1) channelNames.append(", ");
182         }
183         channelNames = tr("By %1").arg(channelNames);
184         int actualNewVideoCount = qMin(newVideoCount, unwatchedCount);
185         mac::notify(tr("You have %n new video(s)", "", actualNewVideoCount),
186                     channelNames, QString());
187     }
188 #endif
189
190     running = false;
191 }
192
193 void ChannelAggregator::videosLoaded(const QList<Video*> &videos) {
194     sender()->deleteLater();
195
196     foreach (Video* video, videos) {
197         addVideo(video);
198         qApp->processEvents();
199     }
200
201     if (!videos.isEmpty()) {
202         YTChannel *channel = YTChannel::forId(videos.first()->channelId());
203         channel->updateNotifyCount();
204         emit channelChanged(channel);
205         updateUnwatchedCount();
206         foreach (Video* video, videos) video->deleteLater();
207     }
208
209     QTimer::singleShot(0, this, SLOT(processNextChannel()));
210 }
211
212 void ChannelAggregator::updateUnwatchedCount() {
213     if (!Database::exists()) return;
214     QSqlDatabase db = Database::instance().getConnection();
215     QSqlQuery query(db);
216     query.prepare("select sum(notify_count) from subscriptions");
217     bool success = query.exec();
218     if (!success) qWarning() << query.lastQuery() << query.lastError().text();
219     if (!query.next()) return;
220     int newUnwatchedCount = query.value(0).toInt();
221     if (newUnwatchedCount != unwatchedCount) {
222         unwatchedCount = newUnwatchedCount;
223         emit unwatchedCountChanged(unwatchedCount);
224     }
225 }
226
227 void ChannelAggregator::addVideo(Video *video) {
228     QSqlDatabase db = Database::instance().getConnection();
229
230     QSqlQuery query(db);
231     query.prepare("select count(*) from subscriptions_videos where video_id=?");
232     query.bindValue(0, video->id());
233     bool success = query.exec();
234     if (!success) qWarning() << query.lastQuery() << query.lastError().text();
235     if (!query.next()) return;
236     int count = query.value(0).toInt();
237     if (count > 0) return;
238
239     // qDebug() << "Inserting" << video->author() << video->title();
240
241     YTChannel *channel = YTChannel::forId(video->channelId());
242     if (!channel) {
243         qWarning() << "channelId not present in db" << video->channelId() << video->channelTitle();
244         return;
245     }
246
247     if (!updatedChannels.contains(channel))
248         updatedChannels << channel;
249
250     uint now = QDateTime::currentDateTimeUtc().toTime_t();
251     uint published = video->published().toTime_t();
252     if (published > now) {
253         qDebug() << "fixing publish time";
254         published = now;
255     }
256
257     query = QSqlQuery(db);
258     query.prepare("insert into subscriptions_videos "
259                   "(video_id,channel_id,published,added,watched,"
260                   "title,author,user_id,description,url,thumb_url,views,duration) "
261                   "values (?,?,?,?,?,?,?,?,?,?,?,?,?)");
262     query.bindValue(0, video->id());
263     query.bindValue(1, channel->getId());
264     query.bindValue(2, published);
265     query.bindValue(3, now);
266     query.bindValue(4, 0);
267     query.bindValue(5, video->title());
268     query.bindValue(6, video->channelTitle());
269     query.bindValue(7, video->channelId());
270     query.bindValue(8, video->description());
271     query.bindValue(9, video->webpage());
272     query.bindValue(10, video->thumbnailUrl());
273     query.bindValue(11, video->viewCount());
274     query.bindValue(12, video->duration());
275     success = query.exec();
276     if (!success) qWarning() << query.lastQuery() << query.lastError().text();
277
278     newVideoCount++;
279
280     query = QSqlQuery(db);
281     query.prepare("update subscriptions set updated=? where user_id=?");
282     query.bindValue(0, published);
283     query.bindValue(1, channel->getChannelId());
284     success = query.exec();
285     if (!success) qWarning() << query.lastQuery() << query.lastError().text();
286 }
287
288 void ChannelAggregator::markAllAsWatched() {
289     uint now = QDateTime::currentDateTimeUtc().toTime_t();
290
291     QSqlDatabase db = Database::instance().getConnection();
292     QSqlQuery query(db);
293     query.prepare("update subscriptions set watched=?, notify_count=0");
294     query.bindValue(0, now);
295     bool success = query.exec();
296     if (!success) qWarning() << query.lastQuery() << query.lastError().text();
297     unwatchedCount = 0;
298
299     foreach (YTChannel *channel, YTChannel::getCachedChannels()) {
300         channel->setWatched(now);
301         channel->setNotifyCount(0);
302     }
303
304     emit unwatchedCountChanged(0);
305 }
306
307 void ChannelAggregator::videoWatched(Video *video) {
308     if (!Database::exists()) return;
309     QSqlDatabase db = Database::instance().getConnection();
310     QSqlQuery query(db);
311     query.prepare("update subscriptions_videos set watched=? where video_id=?");
312     query.bindValue(0, QDateTime::currentDateTimeUtc().toTime_t());
313     query.bindValue(1, video->id());
314     bool success = query.exec();
315     if (!success) qWarning() << query.lastQuery() << query.lastError().text();
316     if (query.numRowsAffected() > 0) {
317         YTChannel *channel = YTChannel::forId(video->channelId());
318         channel->updateNotifyCount();
319     }
320 }
321
322 void ChannelAggregator::cleanup() {
323     const int maxVideos = 1000;
324     const int maxDeletions = 1000;
325     if (!Database::exists()) return;
326     QSqlDatabase db = Database::instance().getConnection();
327
328     QSqlQuery query(db);
329     bool success = query.exec("select count(*) from subscriptions_videos");
330     if (!success) qWarning() << query.lastQuery() << query.lastError().text();
331     if (!query.next()) return;
332     int count = query.value(0).toInt();
333     if (count <= maxVideos) return;
334
335     query = QSqlQuery(db);
336     query.prepare("delete from subscriptions_videos where id in "
337                   "(select id from subscriptions_videos "
338                   "order by published desc limit ?,?)");
339     query.bindValue(0, maxVideos);
340     query.bindValue(1, maxDeletions);
341     success = query.exec();
342     if (!success) qWarning() << query.lastQuery() << query.lastError().text();
343 }