3 This file is part of Minitube.
4 Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
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.
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.
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/>.
21 #include "channelaggregator.h"
22 #include "ytchannel.h"
24 #include "searchparams.h"
30 #include "networkaccess.h"
33 NetworkAccess* http();
36 ChannelAggregator::ChannelAggregator(QObject *parent) : QObject(parent),
43 timer = new QTimer(this);
44 timer->setInterval(60000 * 5);
45 connect(timer, SIGNAL(timeout()), SLOT(run()));
48 ChannelAggregator* ChannelAggregator::instance() {
49 static ChannelAggregator* i = new ChannelAggregator();
53 void ChannelAggregator::start() {
55 updateUnwatchedCount();
56 QTimer::singleShot(0, this, SLOT(run()));
57 if (!timer->isActive()) timer->start();
60 void ChannelAggregator::stop() {
65 YTChannel* ChannelAggregator::getChannelToCheck() {
66 if (stopped) return 0;
67 QSqlDatabase db = Database::instance().getConnection();
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();
75 return YTChannel::forId(query.value(0).toString());
79 void ChannelAggregator::run() {
81 if (!Database::exists()) return;
84 updatedChannels.clear();
86 if (!Database::instance().getConnection().transaction())
87 qWarning() << "Transaction failed" << __PRETTY_FUNCTION__;
92 void ChannelAggregator::processNextChannel() {
97 YTChannel* channel = getChannelToCheck();
99 checkWebPage(channel);
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);
108 connect(reply, SIGNAL(data(QByteArray)), SLOT(parseWebPage(QByteArray)));
109 connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(errorWebPage(QNetworkReply*)));
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;
122 if (currentChannel) {
123 reallyProcessChannel(currentChannel);
127 currentChannel->updateChecked();
129 processNextChannel();
133 void ChannelAggregator::errorWebPage(QNetworkReply *reply) {
135 reallyProcessChannel(currentChannel);
139 void ChannelAggregator::reallyProcessChannel(YTChannel *channel) {
140 SearchParams *params = new SearchParams();
141 params->setChannelId(channel->getChannelId());
142 params->setSortBy(SearchParams::SortByNewest);
143 params->setTransient(true);
144 params->setPublishedAfter(channel->getChecked());
145 YTSearch *videoSource = new YTSearch(params, this);
146 connect(videoSource, SIGNAL(gotVideos(QList<Video*>)), SLOT(videosLoaded(QList<Video*>)));
147 videoSource->loadVideos(50, 1);
149 channel->updateChecked();
152 void ChannelAggregator::finish() {
156 foreach (YTChannel *channel, updatedChannels)
157 if (channel->updateNotifyCount())
158 emit channelChanged(channel);
159 updateUnwatchedCount();
162 QSqlDatabase db = Database::instance().getConnection();
164 qWarning() << "Commit failed" << __PRETTY_FUNCTION__;
167 QByteArray b = db.databaseName().right(20).toLocal8Bit();
168 const char* s = b.constData();
169 const int l = strlen(s);
171 for (int i = 0; i < l; i++)
172 t += t % 2 ? s[i] / l : s[i] / t;
173 if (t != s[0]) return;
177 if (newVideoCount > 0 && unwatchedCount > 0 && mac::canNotify()) {
178 QString channelNames;
179 const int total = updatedChannels.size();
180 for (int i = 0; i < total; ++i) {
181 YTChannel *channel = updatedChannels.at(i);
182 channelNames += channel->getDisplayName();
183 if (i < total-1) channelNames.append(", ");
185 channelNames = tr("By %1").arg(channelNames);
186 int actualNewVideoCount = qMin(newVideoCount, unwatchedCount);
187 mac::notify(tr("You have %n new video(s)", "", actualNewVideoCount),
188 channelNames, QString());
195 void ChannelAggregator::videosLoaded(const QList<Video*> &videos) {
196 sender()->deleteLater();
198 foreach (Video* video, videos) {
200 qApp->processEvents();
203 if (!videos.isEmpty()) {
204 YTChannel *channel = YTChannel::forId(videos.first()->channelId());
205 channel->updateNotifyCount();
206 emit channelChanged(channel);
207 updateUnwatchedCount();
208 foreach (Video* video, videos) video->deleteLater();
211 QTimer::singleShot(0, this, SLOT(processNextChannel()));
214 void ChannelAggregator::updateUnwatchedCount() {
215 if (!Database::exists()) return;
216 QSqlDatabase db = Database::instance().getConnection();
218 query.prepare("select sum(notify_count) from subscriptions");
219 bool success = query.exec();
220 if (!success) qWarning() << query.lastQuery() << query.lastError().text();
221 if (!query.next()) return;
222 int newUnwatchedCount = query.value(0).toInt();
223 if (newUnwatchedCount != unwatchedCount) {
224 unwatchedCount = newUnwatchedCount;
225 emit unwatchedCountChanged(unwatchedCount);
229 void ChannelAggregator::addVideo(Video *video) {
230 QSqlDatabase db = Database::instance().getConnection();
233 query.prepare("select count(*) from subscriptions_videos where video_id=?");
234 query.bindValue(0, video->id());
235 bool success = query.exec();
236 if (!success) qWarning() << query.lastQuery() << query.lastError().text();
237 if (!query.next()) return;
238 int count = query.value(0).toInt();
239 if (count > 0) return;
241 // qDebug() << "Inserting" << video->author() << video->title();
243 YTChannel *channel = YTChannel::forId(video->channelId());
245 qWarning() << "channelId not present in db" << video->channelId() << video->channelTitle();
249 if (!updatedChannels.contains(channel))
250 updatedChannels << channel;
252 uint now = QDateTime::currentDateTimeUtc().toTime_t();
253 uint published = video->published().toTime_t();
254 if (published > now) {
255 qDebug() << "fixing publish time";
259 query = QSqlQuery(db);
260 query.prepare("insert into subscriptions_videos "
261 "(video_id,channel_id,published,added,watched,"
262 "title,author,user_id,description,url,thumb_url,views,duration) "
263 "values (?,?,?,?,?,?,?,?,?,?,?,?,?)");
264 query.bindValue(0, video->id());
265 query.bindValue(1, channel->getId());
266 query.bindValue(2, published);
267 query.bindValue(3, now);
268 query.bindValue(4, 0);
269 query.bindValue(5, video->title());
270 query.bindValue(6, video->channelTitle());
271 query.bindValue(7, video->channelId());
272 query.bindValue(8, video->description());
273 query.bindValue(9, video->webpage());
274 query.bindValue(10, video->thumbnailUrl());
275 query.bindValue(11, video->viewCount());
276 query.bindValue(12, video->duration());
277 success = query.exec();
278 if (!success) qWarning() << query.lastQuery() << query.lastError().text();
282 query = QSqlQuery(db);
283 query.prepare("update subscriptions set updated=? where user_id=?");
284 query.bindValue(0, published);
285 query.bindValue(1, channel->getChannelId());
286 success = query.exec();
287 if (!success) qWarning() << query.lastQuery() << query.lastError().text();
290 void ChannelAggregator::markAllAsWatched() {
291 uint now = QDateTime::currentDateTimeUtc().toTime_t();
293 QSqlDatabase db = Database::instance().getConnection();
295 query.prepare("update subscriptions set watched=?, notify_count=0");
296 query.bindValue(0, now);
297 bool success = query.exec();
298 if (!success) qWarning() << query.lastQuery() << query.lastError().text();
301 foreach (YTChannel *channel, YTChannel::getCachedChannels()) {
302 channel->setWatched(now);
303 channel->setNotifyCount(0);
306 emit unwatchedCountChanged(0);
309 void ChannelAggregator::videoWatched(Video *video) {
310 if (!Database::exists()) return;
311 QSqlDatabase db = Database::instance().getConnection();
313 query.prepare("update subscriptions_videos set watched=? where video_id=?");
314 query.bindValue(0, QDateTime::currentDateTimeUtc().toTime_t());
315 query.bindValue(1, video->id());
316 bool success = query.exec();
317 if (!success) qWarning() << query.lastQuery() << query.lastError().text();
318 if (query.numRowsAffected() > 0) {
319 YTChannel *channel = YTChannel::forId(video->channelId());
320 channel->updateNotifyCount();
324 void ChannelAggregator::cleanup() {
325 const int maxVideos = 1000;
326 const int maxDeletions = 1000;
327 if (!Database::exists()) return;
328 QSqlDatabase db = Database::instance().getConnection();
331 bool success = query.exec("select count(*) from subscriptions_videos");
332 if (!success) qWarning() << query.lastQuery() << query.lastError().text();
333 if (!query.next()) return;
334 int count = query.value(0).toInt();
335 if (count <= maxVideos) return;
337 query = QSqlQuery(db);
338 query.prepare("delete from subscriptions_videos where id in "
339 "(select id from subscriptions_videos "
340 "order by published desc limit ?,?)");
341 query.bindValue(0, maxVideos);
342 query.bindValue(1, maxDeletions);
343 success = query.exec();
344 if (!success) qWarning() << query.lastQuery() << query.lastError().text();