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