1 #include "ytjssearch.h"
3 #include "mainwindow.h"
4 #include "searchparams.h"
12 int parseDuration(const QString &s) {
13 const auto parts = s.splitRef(':');
16 for (auto i = parts.crbegin(); i != parts.crend(); ++i) {
20 secs += i->toInt() * 60;
22 secs += i->toInt() * 60 * 60;
29 QString parseChannelId(const QString &channelUrl) {
30 int pos = channelUrl.lastIndexOf('/');
31 if (pos >= 0) return channelUrl.mid(pos + 1);
35 QDateTime parsePublishedText(const QString &s) {
37 const auto parts = s.splitRef(' ');
38 for (const auto &part : parts) {
42 if (num == 0) return QDateTime();
44 auto now = QDateTime::currentDateTimeUtc();
45 if (s.contains("hour")) {
46 return now.addSecs(-num * 3600);
47 } else if (s.contains("day")) {
48 return now.addDays(-num);
49 } else if (s.contains("week")) {
50 return now.addDays(-num * 7);
51 } else if (s.contains("month")) {
52 return now.addMonths(-num);
53 } else if (s.contains("year")) {
54 return now.addDays(-num * 365);
61 YTJSSearch::YTJSSearch(SearchParams *searchParams, QObject *parent)
62 : VideoSource(parent), searchParams(searchParams) {}
64 void YTJSSearch::loadVideos(int max, int startIndex) {
67 auto &js = JS::instance();
68 auto &engine = js.getEngine();
71 if (!searchParams->keywords().isEmpty()) {
72 if (searchParams->keywords().startsWith("http://") ||
73 searchParams->keywords().startsWith("https://")) {
74 q = YTSearch::videoIdFromUrl(searchParams->keywords());
76 q = searchParams->keywords();
81 QJSValue options = engine.newObject();
84 if (!nextpageRef.isEmpty())
85 options.setProperty("nextpageRef", nextpageRef);
87 // non-first page was requested but we have no continuation token
88 emit error("No pagination token");
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 addFilter("Duration", "Medium");
132 case SearchParams::DurationLong:
133 addFilter("Duration", "Long");
137 switch (searchParams->time()) {
138 case SearchParams::TimeToday:
139 addFilter("Upload date", "Today");
141 case SearchParams::TimeWeek:
142 addFilter("Upload date", "This week");
144 case SearchParams::TimeMonth:
145 addFilter("Upload date", "This month");
147 case SearchParams::TimeYear:
148 addFilter("Upload date", "This year");
152 switch (searchParams->quality()) {
153 case SearchParams::QualityHD:
154 addFilter("Features", "HD");
156 case SearchParams::Quality4K:
157 addFilter("Features", "4K");
159 case SearchParams::QualityHDR:
160 addFilter("Features", "HDR");
164 js.callFunction(new JSResult(this), "search", {q, options, filterMap})
165 .onJson([this](auto &doc) {
168 auto obj = doc.object();
170 nextpageRef = obj["nextpageRef"].toString();
172 const auto items = obj["items"].toArray();
173 QVector<Video *> videos;
174 videos.reserve(items.size());
176 for (const auto &i : items) {
177 QString type = i["type"].toString();
178 if (type != "video") continue;
180 Video *video = new Video();
182 QString id = YTSearch::videoIdFromUrl(i["link"].toString());
185 QString title = i["title"].toString();
186 video->setTitle(title);
188 QString desc = i["description"].toString();
189 video->setDescription(desc);
191 const auto thumbs = i["thumbnails"].toArray();
192 for (const auto &t : thumbs) {
193 video->addThumb(t["width"].toInt(), t["height"].toInt(),
194 t["url"].toString());
197 int views = i["views"].toInt();
198 video->setViewCount(views);
200 int duration = parseDuration(i["duration"].toString());
201 video->setDuration(duration);
203 auto published = parsePublishedText(i["uploaded_at"].toString());
204 if (published.isValid()) video->setPublished(published);
206 auto authorObj = i["author"];
207 QString channelName = authorObj["name"].toString();
208 video->setChannelTitle(channelName);
209 QString channelId = parseChannelId(authorObj["ref"].toString());
210 video->setChannelId(channelId);
215 if (videos.isEmpty()) {
216 emit error("No results");
218 emit gotVideos(videos);
219 emit finished(videos.size());
222 .onError([this, max, startIndex](auto &msg) {
223 static int retries = 0;
225 qDebug() << "Retrying...";
226 QTimer::singleShot(0, this,
227 [this, max, startIndex] { loadVideos(max, startIndex); });
235 QString YTJSSearch::getName() {
236 if (!name.isEmpty()) return name;
237 if (!searchParams->keywords().isEmpty()) return searchParams->keywords();
241 const QList<QAction *> &YTJSSearch::getActions() {
242 static const QList<QAction *> channelActions = {
243 MainWindow::instance()->getAction("subscribeChannel")};
244 if (searchParams->channelId().isEmpty()) {
245 static const QList<QAction *> noActions;
248 return channelActions;