1 #include "ytjssearch.h"
3 #include "mainwindow.h"
4 #include "searchparams.h"
11 int parseDuration(const QString &s) {
12 const auto parts = s.splitRef(':');
15 for (auto i = parts.crbegin(); i != parts.crend(); ++i) {
19 secs += i->toInt() * 60;
21 secs += i->toInt() * 60 * 60;
28 QString parseChannelId(const QString &channelUrl) {
29 int pos = channelUrl.lastIndexOf('/');
30 if (pos >= 0) return channelUrl.mid(pos + 1);
34 QDateTime parsePublishedText(const QString &s) {
36 const auto parts = s.splitRef(' ');
37 for (const auto &part : parts) {
41 if (num == 0) return QDateTime();
43 auto now = QDateTime::currentDateTimeUtc();
44 if (s.contains("day")) {
45 return now.addDays(-num);
46 } else if (s.contains("week")) {
47 return now.addDays(-num * 7);
48 } else if (s.contains("month")) {
49 return now.addMonths(-num);
50 } else if (s.contains("year")) {
51 return now.addDays(-num * 365);
58 YTJSSearch::YTJSSearch(SearchParams *searchParams, QObject *parent)
59 : VideoSource(parent), searchParams(searchParams) {}
61 void YTJSSearch::loadVideos(int max, int startIndex) {
62 auto &ytjs = YTJS::instance();
63 if (!ytjs.isInitialized()) {
64 QTimer::singleShot(500, this, [this, max, startIndex] { loadVideos(max, startIndex); });
67 auto &engine = ytjs.getEngine();
71 auto function = engine.evaluate("search");
72 if (!function.isCallable()) {
73 qWarning() << function.toString() << " is not callable";
74 emit error(function.toString());
79 if (!searchParams->keywords().isEmpty()) {
80 if (searchParams->keywords().startsWith("http://") ||
81 searchParams->keywords().startsWith("https://")) {
82 q = YTSearch::videoIdFromUrl(searchParams->keywords());
84 q = searchParams->keywords();
89 QJSValue options = engine.newObject();
91 if (startIndex > 1 && !nextpageRef.isEmpty()) options.setProperty("nextpageRef", nextpageRef);
92 options.setProperty("limit", max);
94 switch (searchParams->safeSearch()) {
95 case SearchParams::None:
96 options.setProperty("safeSearch", false);
98 case SearchParams::Strict:
99 options.setProperty("safeSearch", true);
105 auto filterMap = engine.evaluate("new Map()");
106 auto jsMapSet = filterMap.property("set");
107 auto addFilter = [&filterMap, &jsMapSet](QString name, QString value) {
108 jsMapSet.callWithInstance(filterMap, {name, value});
111 addFilter("Type", "Video");
113 switch (searchParams->sortBy()) {
114 case SearchParams::SortByNewest:
115 addFilter("Sort by", "Upload date");
117 case SearchParams::SortByViewCount:
118 addFilter("Sort by", "View count");
120 case SearchParams::SortByRating:
121 addFilter("Sort by", "Rating");
125 switch (searchParams->duration()) {
126 case SearchParams::DurationShort:
127 addFilter("Duration", "Short");
129 case SearchParams::DurationMedium:
130 case SearchParams::DurationLong:
131 addFilter("Duration", "Long");
135 switch (searchParams->time()) {
136 case SearchParams::TimeToday:
137 addFilter("Upload date", "Today");
139 case SearchParams::TimeWeek:
140 addFilter("Upload date", "This week");
142 case SearchParams::TimeMonth:
143 addFilter("Upload date", "This month");
147 switch (searchParams->quality()) {
148 case SearchParams::QualityHD:
149 addFilter("Features", "HD");
151 case SearchParams::Quality4K:
152 addFilter("Features", "4K");
154 case SearchParams::QualityHDR:
155 addFilter("Features", "HDR");
159 auto handler = new ResultHandler;
160 connect(handler, &ResultHandler::error, this, &VideoSource::error);
161 connect(handler, &ResultHandler::data, this, [this](const QJsonDocument &doc) {
164 auto obj = doc.object();
167 QFile jsonFile("/Users/flavio/test.json");
168 jsonFile.open(QFile::WriteOnly);
169 jsonFile.write(doc.toJson());
172 nextpageRef = obj["nextpageRef"].toString();
174 const auto items = obj["items"].toArray();
175 QVector<Video *> videos;
176 videos.reserve(items.size());
178 for (const auto &i : items) {
179 QString type = i["type"].toString();
180 if (type != "video") continue;
182 Video *video = new Video();
184 QString id = YTSearch::videoIdFromUrl(i["link"].toString());
187 QString title = i["title"].toString();
188 video->setTitle(title);
190 QString desc = i["description"].toString();
191 video->setDescription(desc);
193 QString thumb = i["thumbnail"].toString();
194 video->setThumbnailUrl(thumb);
196 int views = i["views"].toInt();
197 video->setViewCount(views);
199 int duration = parseDuration(i["duration"].toString());
200 video->setDuration(duration);
202 auto published = parsePublishedText(i["uploaded_at"].toString());
203 if (published.isValid()) video->setPublished(published);
205 auto authorObj = i["author"];
206 QString channelName = authorObj["name"].toString();
207 video->setChannelTitle(channelName);
208 QString channelId = parseChannelId(authorObj["ref"].toString());
209 video->setChannelId(channelId);
214 emit gotVideos(videos);
215 emit finished(videos.size());
217 QJSValue h = engine.newQObject(handler);
218 auto value = function.call({h, q, options, filterMap});
219 if (ytjs.checkError(value)) emit error(value.toString());
222 QString YTJSSearch::getName() {
223 if (!name.isEmpty()) return name;
224 if (!searchParams->keywords().isEmpty()) return searchParams->keywords();
228 const QList<QAction *> &YTJSSearch::getActions() {
229 static const QList<QAction *> channelActions = {
230 MainWindow::instance()->getAction("subscribeChannel")};
231 if (searchParams->channelId().isEmpty()) {
232 static const QList<QAction *> noActions;
235 return channelActions;