]> git.sur5r.net Git - minitube/blob - src/ytchannel.cpp
use standard cache location
[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 void YTChannel::unsubscribe() {
203     YTChannel::unsubscribe(channelId);
204 }
205
206 void YTChannel::storeThumbnail(const QByteArray &bytes) {
207     thumbnail.loadFromData(bytes);
208     static const int maxWidth = 88;
209
210     QDir dir;
211     dir.mkpath(getThumbnailDir());
212
213     if (thumbnail.width() > maxWidth) {
214         thumbnail = thumbnail.scaledToWidth(maxWidth, Qt::SmoothTransformation);
215         thumbnail.save(getThumbnailLocation(), "JPG");
216     } else {
217         QFile file(getThumbnailLocation());
218         if (!file.open(QIODevice::WriteOnly))
219             qWarning() << "Error opening file for writing" << file.fileName();
220         QDataStream stream(&file);
221         stream.writeRawData(bytes.constData(), bytes.size());
222     }
223
224     emit thumbnailLoaded();
225     loadingThumbnail = false;
226 }
227
228 void YTChannel::requestError(QNetworkReply *reply) {
229     emit error(reply->errorString());
230     qWarning() << reply->errorString();
231     loading = false;
232     loadingThumbnail = false;
233 }
234
235 void YTChannel::storeInfo() {
236     if (channelId.isEmpty()) return;
237     QSqlDatabase db = Database::instance().getConnection();
238     QSqlQuery query(db);
239     query.prepare("update subscriptions set "
240                   "user_name=?, name=?, description=?, thumb_url=?, loaded=? "
241                   "where user_id=?");
242     qDebug() << userName;
243     query.bindValue(0, userName);
244     query.bindValue(1, displayName);
245     query.bindValue(2, description);
246     query.bindValue(3, thumbnailUrl);
247     query.bindValue(4, QDateTime::currentDateTime().toTime_t());
248     query.bindValue(5, channelId);
249     bool success = query.exec();
250     if (!success) qWarning() << query.lastQuery() << query.lastError().text();
251
252     loadThumbnail();
253 }
254
255 void YTChannel::subscribe(const QString &channelId) {
256     if (channelId.isEmpty()) return;
257
258     uint now = QDateTime::currentDateTime().toTime_t();
259
260     QSqlDatabase db = Database::instance().getConnection();
261     QSqlQuery query(db);
262     query.prepare("insert into subscriptions "
263                   "(user_id,added,watched,checked,views,notify_count)"
264                   " values (?,?,?,0,0,0)");
265     query.bindValue(0, channelId);
266     query.bindValue(1, now);
267     query.bindValue(2, now);
268     bool success = query.exec();
269     if (!success) qWarning() << query.lastQuery() << query.lastError().text();
270
271     // This will call maybeLoadFromApi
272     YTChannel::forId(channelId);
273 }
274
275 void YTChannel::unsubscribe(const QString &channelId) {
276     if (channelId.isEmpty()) return;
277     QSqlDatabase db = Database::instance().getConnection();
278     QSqlQuery query(db);
279     query.prepare("delete from subscriptions where user_id=?");
280     query.bindValue(0, channelId);
281     bool success = query.exec();
282     if (!success) qWarning() << query.lastQuery() << query.lastError().text();
283
284     query = QSqlQuery(db);
285     query.prepare("delete from subscriptions_videos where user_id=?");
286     query.bindValue(0, channelId);
287     success = query.exec();
288     if (!success) qWarning() << query.lastQuery() << query.lastError().text();
289
290     YTChannel *user = cache.take(channelId);
291     if (user) user->deleteLater();
292 }
293
294 bool YTChannel::isSubscribed(const QString &channelId) {
295     if (!Database::exists()) return false;
296     if (channelId.isEmpty()) return false;
297     QSqlDatabase db = Database::instance().getConnection();
298     QSqlQuery query(db);
299     query.prepare("select count(*) from subscriptions where user_id=?");
300     query.bindValue(0, channelId);
301     bool success = query.exec();
302     if (!success) qWarning() << query.lastQuery() << query.lastError().text();
303     if (query.next())
304         return query.value(0).toInt() > 0;
305     return false;
306 }
307
308 void YTChannel::updateChecked() {
309     if (channelId.isEmpty()) return;
310
311     uint now = QDateTime::currentDateTime().toTime_t();
312     checked = now;
313
314     QSqlDatabase db = Database::instance().getConnection();
315     QSqlQuery query(db);
316     query.prepare("update subscriptions set checked=? where user_id=?");
317     query.bindValue(0, now);
318     query.bindValue(1, channelId);
319     bool success = query.exec();
320     if (!success) qWarning() << query.lastQuery() << query.lastError().text();
321 }
322
323 void YTChannel::updateWatched() {
324     if (channelId.isEmpty()) return;
325
326     uint now = QDateTime::currentDateTime().toTime_t();
327     watched = now;
328     notifyCount = 0;
329     emit notifyCountChanged();
330
331     QSqlDatabase db = Database::instance().getConnection();
332     QSqlQuery query(db);
333     query.prepare("update subscriptions set watched=?, notify_count=0, views=views+1 where user_id=?");
334     query.bindValue(0, now);
335     query.bindValue(1, channelId);
336     bool success = query.exec();
337     if (!success) qWarning() << query.lastQuery() << query.lastError().text();
338 }
339
340 void YTChannel::storeNotifyCount(int count) {
341     if (notifyCount != count)
342         emit notifyCountChanged();
343     notifyCount = count;
344
345     QSqlDatabase db = Database::instance().getConnection();
346     QSqlQuery query(db);
347     query.prepare("update subscriptions set notify_count=? where user_id=?");
348     query.bindValue(0, count);
349     query.bindValue(1, channelId);
350     bool success = query.exec();
351     if (!success) qWarning() << query.lastQuery() << query.lastError().text();
352 }
353
354 bool YTChannel::updateNotifyCount() {
355     QSqlDatabase db = Database::instance().getConnection();
356     QSqlQuery query(db);
357     query.prepare("select count(*) from subscriptions_videos "
358                   "where channel_id=? and added>? and published>? and watched=0");
359     query.bindValue(0, id);
360     query.bindValue(1, watched);
361     query.bindValue(2, watched);
362     bool success = query.exec();
363     if (!success) qWarning() << query.lastQuery() << query.lastError().text();
364     if (!query.next()) {
365         qWarning() << __PRETTY_FUNCTION__ << "Count failed";
366         return false;
367     }
368     int count = query.value(0).toInt();
369     storeNotifyCount(count);
370     return count != notifyCount;
371 }