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 reallyProcessChannel(currentChannel);
125 currentChannel->updateChecked();
127 processNextChannel();
131 void ChannelAggregator::errorWebPage(QNetworkReply *reply) {
133 reallyProcessChannel(currentChannel);
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);
147 channel->updateChecked();
150 void ChannelAggregator::finish() {
154 foreach (YTChannel *channel, updatedChannels)
155 if (channel->updateNotifyCount())
156 emit channelChanged(channel);
157 updateUnwatchedCount();
160 QSqlDatabase db = Database::instance().getConnection();
162 qWarning() << "Commit failed" << __PRETTY_FUNCTION__;
165 QByteArray b = db.databaseName().right(20).toLocal8Bit();
166 const char* s = b.constData();
167 const int l = strlen(s);
169 for (int i = 0; i < l; i++)
170 t += t % 2 ? s[i] / l : s[i] / t;
171 if (t != s[0]) return;
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(", ");
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());
193 void ChannelAggregator::videosLoaded(const QList<Video*> &videos) {
194 sender()->deleteLater();
196 foreach (Video* video, videos) {
198 qApp->processEvents();
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();
209 QTimer::singleShot(0, this, SLOT(processNextChannel()));
212 void ChannelAggregator::updateUnwatchedCount() {
213 if (!Database::exists()) return;
214 QSqlDatabase db = Database::instance().getConnection();
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);
227 void ChannelAggregator::addVideo(Video *video) {
228 QSqlDatabase db = Database::instance().getConnection();
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;
239 // qDebug() << "Inserting" << video->author() << video->title();
241 YTChannel *channel = YTChannel::forId(video->channelId());
243 qWarning() << "channelId not present in db" << video->channelId() << video->channelTitle();
247 if (!updatedChannels.contains(channel))
248 updatedChannels << channel;
250 uint now = QDateTime::currentDateTimeUtc().toTime_t();
251 uint published = video->published().toTime_t();
252 if (published > now) {
253 qDebug() << "fixing publish time";
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();
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();
288 void ChannelAggregator::markAllAsWatched() {
289 uint now = QDateTime::currentDateTimeUtc().toTime_t();
291 QSqlDatabase db = Database::instance().getConnection();
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();
299 foreach (YTChannel *channel, YTChannel::getCachedChannels()) {
300 channel->setWatched(now);
301 channel->setNotifyCount(0);
304 emit unwatchedCountChanged(0);
307 void ChannelAggregator::videoWatched(Video *video) {
308 if (!Database::exists()) return;
309 QSqlDatabase db = Database::instance().getConnection();
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();
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();
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;
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();