]> git.sur5r.net Git - minitube/blob - src/ytchannel.cpp
bcebcfed0ed4e1ada2677d4ba1491e593a5cbd81
[minitube] / src / ytchannel.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 "ytchannel.h"
22 #include "networkaccess.h"
23 #include "database.h"
24 #include <QtSql>
25
26 #ifdef APP_YT3
27 #include "yt3.h"
28 #include <QtScript>
29 #endif
30 #include "compatibility/qurlqueryhelper.h"
31 #include "compatibility/pathsservice.h"
32
33 namespace The {
34 NetworkAccess* http();
35 }
36
37 YTChannel::YTChannel(const QString &channelId, QObject *parent) : QObject(parent),
38     id(0),
39     channelId(channelId),
40     loadingThumbnail(false),
41     notifyCount(0),
42     checked(0),
43     watched(0),
44     loaded(0),
45     loading(false) { }
46
47 QHash<QString, YTChannel*> YTChannel::cache;
48
49 YTChannel* YTChannel::forId(const QString &channelId) {
50     if (channelId.isEmpty()) return 0;
51
52     if (cache.contains(channelId))
53         return cache.value(channelId);
54
55     QSqlDatabase db = Database::instance().getConnection();
56     QSqlQuery query(db);
57     query.prepare("select id,name,description,thumb_url,notify_count,watched,checked,loaded "
58                   "from subscriptions where user_id=?");
59     query.bindValue(0, channelId);
60     bool success = query.exec();
61     if (!success) qWarning() << query.lastQuery() << query.lastError().text();
62
63     YTChannel* channel = 0;
64     if (query.next()) {
65         // Change userId to ChannelId
66
67         channel = new YTChannel(channelId);
68         channel->id = query.value(0).toInt();
69         channel->displayName = query.value(1).toString();
70         channel->description = query.value(2).toString();
71         channel->thumbnailUrl = query.value(3).toString();
72         channel->notifyCount = query.value(4).toInt();
73         channel->watched = query.value(5).toUInt();
74         channel->checked = query.value(6).toUInt();
75         channel->loaded = query.value(7).toUInt();
76         channel->thumbnail = QPixmap(channel->getThumbnailLocation());
77         channel->maybeLoadfromAPI();
78         cache.insert(channelId, channel);
79     }
80
81     return channel;
82 }
83
84 void YTChannel::maybeLoadfromAPI() {
85     if (loading) return;
86     if (channelId.isEmpty()) return;
87
88     uint now = QDateTime::currentDateTime().toTime_t();
89     static const int refreshInterval = 60 * 60 * 24 * 10;
90     if (loaded > now - refreshInterval) return;
91
92     loading = true;
93
94 #ifdef APP_YT3
95
96     QUrl url = YT3::instance().method("channels");
97     {
98         QUrlQueryHelper urlHelper(url);
99         urlHelper.addQueryItem("id", channelId);
100         urlHelper.addQueryItem("part", "snippet");
101     }
102
103 #else
104
105     QUrl url("http://gdata.youtube.com/feeds/api/users/" + channelId);
106     {
107         QUrlQueryHelper urlHelper(url);
108         urlHelper.addQueryItem("v", "2");
109     }
110
111 #endif
112
113     QObject *reply = The::http()->get(url);
114     connect(reply, SIGNAL(data(QByteArray)), SLOT(parseResponse(QByteArray)));
115     connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
116 }
117
118 #ifdef APP_YT3
119
120 void YTChannel::parseResponse(const QByteArray &bytes) {
121     QScriptEngine engine;
122     QScriptValue json = engine.evaluate("(" + QString::fromUtf8(bytes) + ")");
123     QScriptValue items = json.property("items");
124     if (items.isArray()) {
125         QScriptValueIterator it(items);
126         while (it.hasNext()) {
127             it.next();
128             QScriptValue item = it.value();
129             // For some reason the array has an additional element containing its size.
130             if (item.isObject()) {
131                 QScriptValue snippet = item.property("snippet");
132                 displayName = snippet.property("title").toString();
133                 description = snippet.property("description").toString();
134                 QScriptValue thumbnails = snippet.property("thumbnails");
135                 thumbnailUrl = thumbnails.property("default").property("url").toString();
136                 qDebug() << displayName << description << thumbnailUrl;
137             }
138         }
139     }
140
141     emit infoLoaded();
142     storeInfo();
143     loading = false;
144 }
145
146 #else
147
148 void YTChannel::parseResponse(const QByteArray &bytes) {
149     QXmlStreamReader xml(bytes);
150     xml.readNextStartElement();
151     if (xml.name() == QLatin1String("entry"))
152         while(xml.readNextStartElement()) {
153             const QStringRef n = xml.name();
154             if (n == QLatin1String("summary"))
155                 description = xml.readElementText().simplified();
156             else if (n == QLatin1String("title"))
157                 displayName = xml.readElementText();
158             else if (n == QLatin1String("thumbnail")) {
159                 thumbnailUrl = xml.attributes().value("url").toString();
160                 xml.skipCurrentElement();
161             } else if (n == QLatin1String("username"))
162                 userName = xml.readElementText();
163             else xml.skipCurrentElement();
164         }
165
166     if (xml.hasError()) {
167         emit error(xml.errorString());
168         qWarning() << xml.errorString();
169     }
170
171     emit infoLoaded();
172     storeInfo();
173     loading = false;
174 }
175
176 #endif
177
178 void YTChannel::loadThumbnail() {
179     if (loadingThumbnail) return;
180     if (thumbnailUrl.isEmpty()) return;
181     loadingThumbnail = true;
182
183 #ifdef Q_OS_WIN
184     thumbnailUrl.replace(QLatin1String("https://"), QLatin1String("http://"));
185 #endif
186
187     QUrl url(thumbnailUrl);
188     QObject *reply = The::http()->get(url);
189     connect(reply, SIGNAL(data(QByteArray)), SLOT(storeThumbnail(QByteArray)));
190     connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
191 }
192
193 const QString & YTChannel::getThumbnailDir() {
194     static const QString thumbDir = Paths::getCacheLocation() + "/channels/";
195     return thumbDir;
196 }
197
198 QString YTChannel::getThumbnailLocation() {
199     return getThumbnailDir() + channelId;
200 }
201
202 QString YTChannel::latestVideoId() {
203     QSqlDatabase db = Database::instance().getConnection();
204     QSqlQuery query(db);
205     query.prepare("select video_id from subscriptions_videos where user_id=? order by published desc limit 1");
206     query.bindValue(0, channelId);
207     bool success = query.exec();
208     if (!success) qWarning() << query.lastQuery() << query.lastError().text();
209     if (!query.next()) return QString();
210     return query.value(0).toString();
211 }
212
213 void YTChannel::unsubscribe() {
214     YTChannel::unsubscribe(channelId);
215 }
216
217 void YTChannel::storeThumbnail(const QByteArray &bytes) {
218     thumbnail.loadFromData(bytes);
219     static const int maxWidth = 88;
220
221     QDir dir;
222     dir.mkpath(getThumbnailDir());
223
224     if (thumbnail.width() > maxWidth) {
225         thumbnail = thumbnail.scaledToWidth(maxWidth, Qt::SmoothTransformation);
226         thumbnail.save(getThumbnailLocation(), "JPG");
227     } else {
228         QFile file(getThumbnailLocation());
229         if (!file.open(QIODevice::WriteOnly))
230             qWarning() << "Error opening file for writing" << file.fileName();
231         QDataStream stream(&file);
232         stream.writeRawData(bytes.constData(), bytes.size());
233     }
234
235     emit thumbnailLoaded();
236     loadingThumbnail = false;
237 }
238
239 void YTChannel::requestError(QNetworkReply *reply) {
240     emit error(reply->errorString());
241     qWarning() << reply->errorString();
242     loading = false;
243     loadingThumbnail = false;
244 }
245
246 void YTChannel::storeInfo() {
247     if (channelId.isEmpty()) return;
248     QSqlDatabase db = Database::instance().getConnection();
249     QSqlQuery query(db);
250     query.prepare("update subscriptions set "
251                   "user_name=?, name=?, description=?, thumb_url=?, loaded=? "
252                   "where user_id=?");
253     qDebug() << userName;
254     query.bindValue(0, userName);
255     query.bindValue(1, displayName);
256     query.bindValue(2, description);
257     query.bindValue(3, thumbnailUrl);
258     query.bindValue(4, QDateTime::currentDateTime().toTime_t());
259     query.bindValue(5, channelId);
260     bool success = query.exec();
261     if (!success) qWarning() << query.lastQuery() << query.lastError().text();
262
263     loadThumbnail();
264 }
265
266 void YTChannel::subscribe(const QString &channelId) {
267     if (channelId.isEmpty()) return;
268
269     uint now = QDateTime::currentDateTime().toTime_t();
270
271     QSqlDatabase db = Database::instance().getConnection();
272     QSqlQuery query(db);
273     query.prepare("insert into subscriptions "
274                   "(user_id,added,watched,checked,views,notify_count)"
275                   " values (?,?,?,0,0,0)");
276     query.bindValue(0, channelId);
277     query.bindValue(1, now);
278     query.bindValue(2, now);
279     bool success = query.exec();
280     if (!success) qWarning() << query.lastQuery() << query.lastError().text();
281
282     // This will call maybeLoadFromApi
283     YTChannel::forId(channelId);
284 }
285
286 void YTChannel::unsubscribe(const QString &channelId) {
287     if (channelId.isEmpty()) return;
288     QSqlDatabase db = Database::instance().getConnection();
289     QSqlQuery query(db);
290     query.prepare("delete from subscriptions where user_id=?");
291     query.bindValue(0, channelId);
292     bool success = query.exec();
293     if (!success) qWarning() << query.lastQuery() << query.lastError().text();
294
295     query = QSqlQuery(db);
296     query.prepare("delete from subscriptions_videos where user_id=?");
297     query.bindValue(0, channelId);
298     success = query.exec();
299     if (!success) qWarning() << query.lastQuery() << query.lastError().text();
300
301     YTChannel *user = cache.take(channelId);
302     if (user) user->deleteLater();
303 }
304
305 bool YTChannel::isSubscribed(const QString &channelId) {
306     if (!Database::exists()) return false;
307     if (channelId.isEmpty()) return false;
308     QSqlDatabase db = Database::instance().getConnection();
309     QSqlQuery query(db);
310     query.prepare("select count(*) from subscriptions where user_id=?");
311     query.bindValue(0, channelId);
312     bool success = query.exec();
313     if (!success) qWarning() << query.lastQuery() << query.lastError().text();
314     if (query.next())
315         return query.value(0).toInt() > 0;
316     return false;
317 }
318
319 void YTChannel::updateChecked() {
320     if (channelId.isEmpty()) return;
321
322     uint now = QDateTime::currentDateTime().toTime_t();
323     checked = now;
324
325     QSqlDatabase db = Database::instance().getConnection();
326     QSqlQuery query(db);
327     query.prepare("update subscriptions set checked=? where user_id=?");
328     query.bindValue(0, now);
329     query.bindValue(1, channelId);
330     bool success = query.exec();
331     if (!success) qWarning() << query.lastQuery() << query.lastError().text();
332 }
333
334 void YTChannel::updateWatched() {
335     if (channelId.isEmpty()) return;
336
337     uint now = QDateTime::currentDateTime().toTime_t();
338     watched = now;
339     notifyCount = 0;
340     emit notifyCountChanged();
341
342     QSqlDatabase db = Database::instance().getConnection();
343     QSqlQuery query(db);
344     query.prepare("update subscriptions set watched=?, notify_count=0, views=views+1 where user_id=?");
345     query.bindValue(0, now);
346     query.bindValue(1, channelId);
347     bool success = query.exec();
348     if (!success) qWarning() << query.lastQuery() << query.lastError().text();
349 }
350
351 void YTChannel::storeNotifyCount(int count) {
352     if (notifyCount != count)
353         emit notifyCountChanged();
354     notifyCount = count;
355
356     QSqlDatabase db = Database::instance().getConnection();
357     QSqlQuery query(db);
358     query.prepare("update subscriptions set notify_count=? where user_id=?");
359     query.bindValue(0, count);
360     query.bindValue(1, channelId);
361     bool success = query.exec();
362     if (!success) qWarning() << query.lastQuery() << query.lastError().text();
363 }
364
365 bool YTChannel::updateNotifyCount() {
366     QSqlDatabase db = Database::instance().getConnection();
367     QSqlQuery query(db);
368     query.prepare("select count(*) from subscriptions_videos "
369                   "where channel_id=? and added>? and published>? and watched=0");
370     query.bindValue(0, id);
371     query.bindValue(1, watched);
372     query.bindValue(2, watched);
373     bool success = query.exec();
374     if (!success) qWarning() << query.lastQuery() << query.lastError().text();
375     if (!query.next()) {
376         qWarning() << __PRETTY_FUNCTION__ << "Count failed";
377         return false;
378     }
379     int count = query.value(0).toInt();
380     storeNotifyCount(count);
381     return count != notifyCount;
382 }