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"
32 #include "ivchannel.h"
34 YTChannel::YTChannel(const QString &channelId, QObject *parent)
35 : QObject(parent), id(0), channelId(channelId), loadingThumbnail(false), notifyCount(0),
36 checked(0), watched(0), loaded(0), loading(false) {}
38 QHash<QString, YTChannel *> YTChannel::cache;
40 YTChannel *YTChannel::forId(const QString &channelId) {
41 if (channelId.isEmpty()) return 0;
43 auto i = cache.constFind(channelId);
44 if (i != cache.constEnd()) return i.value();
46 QSqlDatabase db = Database::instance().getConnection();
48 query.prepare("select id,name,description,thumb_url,notify_count,watched,checked,loaded "
49 "from subscriptions where user_id=?");
50 query.bindValue(0, channelId);
51 bool success = query.exec();
52 if (!success) qWarning() << query.lastQuery() << query.lastError().text();
54 YTChannel *channel = 0;
56 // Change userId to ChannelId
58 channel = new YTChannel(channelId);
59 channel->id = query.value(0).toInt();
60 channel->displayName = query.value(1).toString();
61 channel->description = query.value(2).toString();
62 channel->thumbnailUrl = query.value(3).toString();
63 channel->notifyCount = query.value(4).toInt();
64 channel->watched = query.value(5).toUInt();
65 channel->checked = query.value(6).toUInt();
66 channel->loaded = query.value(7).toUInt();
67 channel->thumbnail = QPixmap(channel->getThumbnailLocation());
68 channel->thumbnail.setDevicePixelRatio(2.0);
69 channel->maybeLoadfromAPI();
70 cache.insert(channelId, channel);
76 void YTChannel::maybeLoadfromAPI() {
78 if (channelId.isEmpty()) return;
80 uint now = QDateTime::currentDateTime().toTime_t();
81 static const int refreshInterval = 60 * 60 * 24 * 10;
82 if (loaded > now - refreshInterval) return;
86 if (VideoAPI::impl() == VideoAPI::YT3) {
87 QUrl url = YT3::instance().method("channels");
89 q.addQueryItem("id", channelId);
90 q.addQueryItem("part", "snippet");
93 QObject *reply = HttpUtils::yt().get(url);
94 connect(reply, SIGNAL(data(QByteArray)), SLOT(parseResponse(QByteArray)));
95 connect(reply, SIGNAL(error(QString)), SLOT(requestError(QString)));
96 } else if (VideoAPI::impl() == VideoAPI::IV) {
97 auto ivChannel = new IVChannel(channelId);
98 connect(ivChannel, &IVChannel::error, this, &YTChannel::requestError);
99 connect(ivChannel, &IVChannel::loaded, this, [this, ivChannel] {
100 displayName = ivChannel->getDisplayName();
101 description = ivChannel->getDescription();
102 thumbnailUrl = ivChannel->getThumbnailUrl();
103 ivChannel->deleteLater();
111 void YTChannel::parseResponse(const QByteArray &bytes) {
112 QJsonDocument doc = QJsonDocument::fromJson(bytes);
113 QJsonObject obj = doc.object();
114 const QJsonArray items = obj["items"].toArray();
115 for (const QJsonValue &v : items) {
116 QJsonObject item = v.toObject();
117 QJsonObject snippet = item["snippet"].toObject();
118 displayName = snippet["title"].toString();
119 description = snippet["description"].toString();
120 QJsonObject thumbnails = snippet["thumbnails"].toObject();
121 thumbnailUrl = thumbnails["medium"].toObject()["url"].toString();
122 qDebug() << displayName << description << thumbnailUrl;
130 void YTChannel::loadThumbnail() {
131 if (loadingThumbnail) return;
132 if (thumbnailUrl.isEmpty()) return;
133 loadingThumbnail = true;
135 QUrl url(thumbnailUrl);
136 QObject *reply = HttpUtils::yt().get(url);
137 connect(reply, SIGNAL(data(QByteArray)), SLOT(storeThumbnail(QByteArray)));
138 connect(reply, SIGNAL(error(QString)), SLOT(requestError(QString)));
141 const QString &YTChannel::getThumbnailDir() {
142 static const QString thumbDir =
143 QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/channels/";
147 QString YTChannel::getThumbnailLocation() {
148 return getThumbnailDir() + channelId;
151 QString YTChannel::latestVideoId() {
152 QSqlDatabase db = Database::instance().getConnection();
154 query.prepare("select video_id from subscriptions_videos where user_id=? order by published "
156 query.bindValue(0, channelId);
157 bool success = query.exec();
158 if (!success) qWarning() << query.lastQuery() << query.lastError().text();
159 if (!query.next()) return QString();
160 return query.value(0).toString();
163 void YTChannel::unsubscribe() {
164 YTChannel::unsubscribe(channelId);
167 void YTChannel::storeThumbnail(const QByteArray &bytes) {
168 thumbnail.loadFromData(bytes);
169 qreal maxRatio = 2.0;
170 thumbnail.setDevicePixelRatio(maxRatio);
171 const int maxWidth = 88 * maxRatio;
174 dir.mkpath(getThumbnailDir());
176 if (thumbnail.width() > maxWidth) {
177 thumbnail = thumbnail.scaledToWidth(maxWidth, Qt::SmoothTransformation);
178 thumbnail.setDevicePixelRatio(maxRatio);
179 thumbnail.save(getThumbnailLocation(), "JPG");
181 QFile file(getThumbnailLocation());
182 if (!file.open(QIODevice::WriteOnly))
183 qWarning() << "Error opening file for writing" << file.fileName();
184 QDataStream stream(&file);
185 stream.writeRawData(bytes.constData(), bytes.size());
188 emit thumbnailLoaded();
189 loadingThumbnail = false;
192 void YTChannel::requestError(const QString &message) {
194 qWarning() << message;
196 loadingThumbnail = false;
199 void YTChannel::storeInfo() {
200 if (channelId.isEmpty()) return;
201 QSqlDatabase db = Database::instance().getConnection();
203 query.prepare("update subscriptions set "
204 "user_name=?, name=?, description=?, thumb_url=?, loaded=? "
206 qDebug() << userName;
207 query.bindValue(0, userName);
208 query.bindValue(1, displayName);
209 query.bindValue(2, description);
210 query.bindValue(3, thumbnailUrl);
211 query.bindValue(4, QDateTime::currentDateTime().toTime_t());
212 query.bindValue(5, channelId);
213 bool success = query.exec();
214 if (!success) qWarning() << query.lastQuery() << query.lastError().text();
219 void YTChannel::subscribe(const QString &channelId) {
220 if (channelId.isEmpty()) return;
222 uint now = QDateTime::currentDateTime().toTime_t();
224 QSqlDatabase db = Database::instance().getConnection();
226 query.prepare("insert into subscriptions "
227 "(user_id,added,watched,checked,views,notify_count)"
228 " values (?,?,?,0,0,0)");
229 query.bindValue(0, channelId);
230 query.bindValue(1, now);
231 query.bindValue(2, now);
232 bool success = query.exec();
233 if (!success) qWarning() << query.lastQuery() << query.lastError().text();
235 // This will call maybeLoadFromApi
236 YTChannel::forId(channelId);
239 void YTChannel::unsubscribe(const QString &channelId) {
240 if (channelId.isEmpty()) return;
241 QSqlDatabase db = Database::instance().getConnection();
243 query.prepare("delete from subscriptions where user_id=?");
244 query.bindValue(0, channelId);
245 bool success = query.exec();
246 if (!success) qWarning() << query.lastQuery() << query.lastError().text();
248 query = QSqlQuery(db);
249 query.prepare("delete from subscriptions_videos where user_id=?");
250 query.bindValue(0, channelId);
251 success = query.exec();
252 if (!success) qWarning() << query.lastQuery() << query.lastError().text();
254 YTChannel *user = cache.take(channelId);
255 if (user) user->deleteLater();
258 bool YTChannel::isSubscribed(const QString &channelId) {
259 if (!Database::exists()) return false;
260 if (channelId.isEmpty()) return false;
261 QSqlDatabase db = Database::instance().getConnection();
263 query.prepare("select count(*) from subscriptions where user_id=?");
264 query.bindValue(0, channelId);
265 bool success = query.exec();
266 if (!success) qWarning() << query.lastQuery() << query.lastError().text();
267 if (query.next()) return query.value(0).toInt() > 0;
271 void YTChannel::updateChecked() {
272 if (channelId.isEmpty()) return;
274 uint now = QDateTime::currentDateTime().toTime_t();
277 QSqlDatabase db = Database::instance().getConnection();
279 query.prepare("update subscriptions set checked=? where user_id=?");
280 query.bindValue(0, now);
281 query.bindValue(1, channelId);
282 bool success = query.exec();
283 if (!success) qWarning() << query.lastQuery() << query.lastError().text();
286 void YTChannel::updateWatched() {
287 if (channelId.isEmpty()) return;
289 uint now = QDateTime::currentDateTime().toTime_t();
292 emit notifyCountChanged();
294 QSqlDatabase db = Database::instance().getConnection();
297 "update subscriptions set watched=?, notify_count=0, views=views+1 where user_id=?");
298 query.bindValue(0, now);
299 query.bindValue(1, channelId);
300 bool success = query.exec();
301 if (!success) qWarning() << query.lastQuery() << query.lastError().text();
304 void YTChannel::storeNotifyCount(int count) {
305 if (notifyCount != count) emit notifyCountChanged();
308 QSqlDatabase db = Database::instance().getConnection();
310 query.prepare("update subscriptions set notify_count=? where user_id=?");
311 query.bindValue(0, count);
312 query.bindValue(1, channelId);
313 bool success = query.exec();
314 if (!success) qWarning() << query.lastQuery() << query.lastError().text();
317 bool YTChannel::updateNotifyCount() {
318 QSqlDatabase db = Database::instance().getConnection();
320 query.prepare("select count(*) from subscriptions_videos "
321 "where channel_id=? and added>? and published>? and watched=0");
322 query.bindValue(0, id);
323 query.bindValue(1, watched);
324 query.bindValue(2, watched);
325 bool success = query.exec();
326 if (!success) qWarning() << query.lastQuery() << query.lastError().text();
328 qWarning() << __PRETTY_FUNCTION__ << "Count failed";
331 int count = query.value(0).toInt();
332 storeNotifyCount(count);
333 return count != notifyCount;