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 "ytchannel.h"
24 #include "httputils.h"
29 #include "iconutils.h"
31 #include "ivchannel.h"
33 #include "ytjschannel.h"
35 YTChannel::YTChannel(const QString &channelId, QObject *parent)
36 : QObject(parent), id(0), channelId(channelId), loadingThumbnail(false), notifyCount(0),
37 checked(0), watched(0), loaded(0), loading(false) {}
39 QHash<QString, YTChannel *> YTChannel::cache;
41 YTChannel *YTChannel::forId(const QString &channelId) {
42 if (channelId.isEmpty()) return 0;
44 auto i = cache.constFind(channelId);
45 if (i != cache.constEnd()) return i.value();
47 QSqlDatabase db = Database::instance().getConnection();
49 query.prepare("select id,name,description,thumb_url,notify_count,watched,checked,loaded "
50 "from subscriptions where user_id=?");
51 query.bindValue(0, channelId);
52 bool success = query.exec();
53 if (!success) qWarning() << query.lastQuery() << query.lastError().text();
55 YTChannel *channel = 0;
57 // Change userId to ChannelId
59 channel = new YTChannel(channelId);
60 channel->id = query.value(0).toInt();
61 channel->displayName = query.value(1).toString();
62 channel->description = query.value(2).toString();
63 channel->thumbnailUrl = query.value(3).toString();
64 channel->notifyCount = query.value(4).toInt();
65 channel->watched = query.value(5).toUInt();
66 channel->checked = query.value(6).toUInt();
67 channel->loaded = query.value(7).toUInt();
68 channel->thumbnail = QPixmap(channel->getThumbnailLocation());
69 channel->thumbnail.setDevicePixelRatio(2.0);
70 channel->maybeLoadfromAPI();
71 cache.insert(channelId, channel);
77 void YTChannel::maybeLoadfromAPI() {
79 if (channelId.isEmpty()) return;
81 uint now = QDateTime::currentDateTime().toTime_t();
82 static const int refreshInterval = 60 * 60 * 24 * 10;
83 if (loaded > now - refreshInterval) return;
87 if (VideoAPI::impl() == VideoAPI::YT3) {
88 QUrl url = YT3::instance().method("channels");
90 q.addQueryItem("id", channelId);
91 q.addQueryItem("part", "snippet");
94 QObject *reply = HttpUtils::yt().get(url);
95 connect(reply, SIGNAL(data(QByteArray)), SLOT(parseResponse(QByteArray)));
96 connect(reply, SIGNAL(error(QString)), SLOT(requestError(QString)));
97 } else if (VideoAPI::impl() == VideoAPI::IV) {
98 auto ivChannel = new IVChannel(channelId);
99 connect(ivChannel, &IVChannel::error, this, &YTChannel::requestError);
100 connect(ivChannel, &IVChannel::loaded, this, [this, ivChannel] {
101 displayName = ivChannel->getDisplayName();
102 description = ivChannel->getDescription();
103 thumbnailUrl = ivChannel->getThumbnailUrl();
104 ivChannel->deleteLater();
109 } else if (VideoAPI::impl() == VideoAPI::JS) {
110 auto ivChannel = new YTJSChannel(channelId);
111 connect(ivChannel, &YTJSChannel::error, this, &YTChannel::requestError);
112 connect(ivChannel, &YTJSChannel::loaded, this, [this, ivChannel] {
113 displayName = ivChannel->getDisplayName();
114 description = ivChannel->getDescription();
115 thumbnailUrl = ivChannel->getThumbnailUrl();
116 ivChannel->deleteLater();
124 void YTChannel::parseResponse(const QByteArray &bytes) {
125 QJsonDocument doc = QJsonDocument::fromJson(bytes);
126 QJsonObject obj = doc.object();
127 const QJsonArray items = obj["items"].toArray();
128 for (const QJsonValue &v : items) {
129 QJsonObject item = v.toObject();
130 QJsonObject snippet = item["snippet"].toObject();
131 displayName = snippet["title"].toString();
132 description = snippet["description"].toString();
133 QJsonObject thumbnails = snippet["thumbnails"].toObject();
134 thumbnailUrl = thumbnails["medium"].toObject()["url"].toString();
135 qDebug() << displayName << description << thumbnailUrl;
143 void YTChannel::loadThumbnail() {
144 if (loadingThumbnail) return;
145 if (thumbnailUrl.isEmpty()) return;
146 loadingThumbnail = true;
148 QUrl url(thumbnailUrl);
149 QObject *reply = HttpUtils::yt().get(url);
150 connect(reply, SIGNAL(data(QByteArray)), SLOT(storeThumbnail(QByteArray)));
151 connect(reply, SIGNAL(error(QString)), SLOT(requestError(QString)));
154 const QString &YTChannel::getThumbnailDir() {
155 static const QString thumbDir =
156 QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/channels/";
160 QString YTChannel::getThumbnailLocation() {
161 return getThumbnailDir() + channelId;
164 QString YTChannel::latestVideoId() {
165 QSqlDatabase db = Database::instance().getConnection();
167 query.prepare("select video_id from subscriptions_videos where user_id=? order by published "
169 query.bindValue(0, channelId);
170 bool success = query.exec();
171 if (!success) qWarning() << query.lastQuery() << query.lastError().text();
172 if (!query.next()) return QString();
173 return query.value(0).toString();
176 void YTChannel::unsubscribe() {
177 YTChannel::unsubscribe(channelId);
180 void YTChannel::storeThumbnail(const QByteArray &bytes) {
181 thumbnail.loadFromData(bytes);
182 qreal maxRatio = 2.0;
183 thumbnail.setDevicePixelRatio(maxRatio);
184 const int maxWidth = 88 * maxRatio;
187 dir.mkpath(getThumbnailDir());
189 if (thumbnail.width() > maxWidth) {
190 thumbnail = thumbnail.scaledToWidth(maxWidth, Qt::SmoothTransformation);
191 thumbnail.setDevicePixelRatio(maxRatio);
192 thumbnail.save(getThumbnailLocation(), "JPG");
194 QFile file(getThumbnailLocation());
195 if (!file.open(QIODevice::WriteOnly))
196 qWarning() << "Error opening file for writing" << file.fileName();
197 QDataStream stream(&file);
198 stream.writeRawData(bytes.constData(), bytes.size());
201 emit thumbnailLoaded();
202 loadingThumbnail = false;
205 void YTChannel::requestError(const QString &message) {
207 qWarning() << message;
209 loadingThumbnail = false;
212 void YTChannel::storeInfo() {
213 if (channelId.isEmpty()) return;
214 QSqlDatabase db = Database::instance().getConnection();
216 query.prepare("update subscriptions set "
217 "user_name=?, name=?, description=?, thumb_url=?, loaded=? "
219 qDebug() << userName;
220 query.bindValue(0, userName);
221 query.bindValue(1, displayName);
222 query.bindValue(2, description);
223 query.bindValue(3, thumbnailUrl);
224 query.bindValue(4, QDateTime::currentDateTime().toTime_t());
225 query.bindValue(5, channelId);
226 bool success = query.exec();
227 if (!success) qWarning() << query.lastQuery() << query.lastError().text();
232 bool YTChannel::subscribe(const QString &channelId) {
233 if (channelId.isEmpty()) return false;
235 uint now = QDateTime::currentDateTime().toTime_t();
237 QSqlDatabase db = Database::instance().getConnection();
239 query.prepare("insert into subscriptions "
240 "(user_id,added,watched,checked,views,notify_count)"
241 " values (?,?,?,0,0,0)");
242 query.bindValue(0, channelId);
243 query.bindValue(1, now);
244 query.bindValue(2, now);
245 bool success = query.exec();
246 if (!success) qWarning() << query.lastQuery() << query.lastError().text();
249 // This will call maybeLoadFromApi
250 YTChannel::forId(channelId);
253 void YTChannel::unsubscribe(const QString &channelId) {
254 if (channelId.isEmpty()) return;
255 QSqlDatabase db = Database::instance().getConnection();
257 query.prepare("delete from subscriptions where user_id=?");
258 query.bindValue(0, channelId);
259 bool success = query.exec();
260 if (!success) qWarning() << query.lastQuery() << query.lastError().text();
262 query = QSqlQuery(db);
263 query.prepare("delete from subscriptions_videos where user_id=?");
264 query.bindValue(0, channelId);
265 success = query.exec();
266 if (!success) qWarning() << query.lastQuery() << query.lastError().text();
268 YTChannel *user = cache.take(channelId);
269 if (user) user->deleteLater();
272 bool YTChannel::isSubscribed(const QString &channelId) {
273 if (!Database::exists()) return false;
274 if (channelId.isEmpty()) return false;
275 QSqlDatabase db = Database::instance().getConnection();
277 query.prepare("select count(*) from subscriptions where user_id=?");
278 query.bindValue(0, channelId);
279 bool success = query.exec();
280 if (!success) qWarning() << query.lastQuery() << query.lastError().text();
281 if (query.next()) return query.value(0).toInt() > 0;
285 void YTChannel::updateChecked() {
286 if (channelId.isEmpty()) return;
288 uint now = QDateTime::currentDateTime().toTime_t();
291 QSqlDatabase db = Database::instance().getConnection();
293 query.prepare("update subscriptions set checked=? where user_id=?");
294 query.bindValue(0, now);
295 query.bindValue(1, channelId);
296 bool success = query.exec();
297 if (!success) qWarning() << query.lastQuery() << query.lastError().text();
300 void YTChannel::updateWatched() {
301 if (channelId.isEmpty()) return;
303 uint now = QDateTime::currentDateTime().toTime_t();
306 emit notifyCountChanged();
308 QSqlDatabase db = Database::instance().getConnection();
311 "update subscriptions set watched=?, notify_count=0, views=views+1 where user_id=?");
312 query.bindValue(0, now);
313 query.bindValue(1, channelId);
314 bool success = query.exec();
315 if (!success) qWarning() << query.lastQuery() << query.lastError().text();
318 void YTChannel::storeNotifyCount(int count) {
319 if (notifyCount != count) emit notifyCountChanged();
322 QSqlDatabase db = Database::instance().getConnection();
324 query.prepare("update subscriptions set notify_count=? where user_id=?");
325 query.bindValue(0, count);
326 query.bindValue(1, channelId);
327 bool success = query.exec();
328 if (!success) qWarning() << query.lastQuery() << query.lastError().text();
331 bool YTChannel::updateNotifyCount() {
332 QSqlDatabase db = Database::instance().getConnection();
334 query.prepare("select count(*) from subscriptions_videos "
335 "where channel_id=? and added>? and published>? and watched=0");
336 query.bindValue(0, id);
337 query.bindValue(1, watched);
338 query.bindValue(2, watched);
339 bool success = query.exec();
340 if (!success) qWarning() << query.lastQuery() << query.lastError().text();
342 qWarning() << __PRETTY_FUNCTION__ << "Count failed";
345 int count = query.value(0).toInt();
346 storeNotifyCount(count);
347 return count != notifyCount;