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