]> git.sur5r.net Git - minitube/blob - src/yt/ytjs/ytjssearch.cpp
New upstream version 3.9.1
[minitube] / src / yt / ytjs / ytjssearch.cpp
1 #include "ytjssearch.h"
2
3 #include "mainwindow.h"
4 #include "searchparams.h"
5 #include "video.h"
6 #include "ytsearch.h"
7
8 #include "js.h"
9
10 namespace {
11
12 int parseDuration(const QString &s) {
13     const auto parts = s.splitRef(':');
14     int secs = 0;
15     int p = 0;
16     for (auto i = parts.crbegin(); i != parts.crend(); ++i) {
17         if (p == 0) {
18             secs = i->toInt();
19         } else if (p == 1) {
20             secs += i->toInt() * 60;
21         } else if (p == 2) {
22             secs += i->toInt() * 60 * 60;
23         }
24         p++;
25     }
26     return secs;
27 }
28
29 QString parseChannelId(const QString &channelUrl) {
30     int pos = channelUrl.lastIndexOf('/');
31     if (pos >= 0) return channelUrl.mid(pos + 1);
32     return QString();
33 }
34
35 QDateTime parsePublishedText(const QString &s) {
36     int num = 0;
37     const auto parts = s.splitRef(' ');
38     for (const auto &part : parts) {
39         num = part.toInt();
40         if (num > 0) break;
41     }
42     if (num == 0) return QDateTime();
43
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);
55     }
56     return QDateTime();
57 }
58
59 } // namespace
60
61 YTJSSearch::YTJSSearch(SearchParams *searchParams, QObject *parent)
62     : VideoSource(parent), searchParams(searchParams) {}
63
64 void YTJSSearch::loadVideos(int max, int startIndex) {
65     aborted = false;
66
67     auto &js = JS::instance();
68     auto &engine = js.getEngine();
69
70     QString q;
71     if (!searchParams->keywords().isEmpty()) {
72         if (searchParams->keywords().startsWith("http://") ||
73             searchParams->keywords().startsWith("https://")) {
74             q = YTSearch::videoIdFromUrl(searchParams->keywords());
75         } else
76             q = searchParams->keywords();
77     }
78
79     // Options
80
81     QJSValue options = engine.newObject();
82
83     if (startIndex > 1) {
84         if (!nextpageRef.isEmpty())
85             options.setProperty("nextpageRef", nextpageRef);
86         else {
87             // non-first page was requested but we have no continuation token
88             emit error("No pagination token");
89             return;
90         }
91     }
92     options.setProperty("limit", max);
93
94     switch (searchParams->safeSearch()) {
95     case SearchParams::None:
96         options.setProperty("safeSearch", false);
97         break;
98     case SearchParams::Strict:
99         options.setProperty("safeSearch", true);
100         break;
101     }
102
103     // Filters
104
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});
109     };
110
111     // addFilter("Type", "Video");
112
113     switch (searchParams->sortBy()) {
114     case SearchParams::SortByNewest:
115         addFilter("Sort by", "Upload date");
116         break;
117     case SearchParams::SortByViewCount:
118         addFilter("Sort by", "View count");
119         break;
120     case SearchParams::SortByRating:
121         addFilter("Sort by", "Rating");
122         break;
123     }
124
125     switch (searchParams->duration()) {
126     case SearchParams::DurationShort:
127         addFilter("Duration", "Short");
128         break;
129     case SearchParams::DurationMedium:
130         addFilter("Duration", "Medium");
131         break;
132     case SearchParams::DurationLong:
133         addFilter("Duration", "Long");
134         break;
135     }
136
137     switch (searchParams->time()) {
138     case SearchParams::TimeToday:
139         addFilter("Upload date", "Today");
140         break;
141     case SearchParams::TimeWeek:
142         addFilter("Upload date", "This week");
143         break;
144     case SearchParams::TimeMonth:
145         addFilter("Upload date", "This month");
146         break;
147     case SearchParams::TimeYear:
148         addFilter("Upload date", "This year");
149         break;
150     }
151
152     switch (searchParams->quality()) {
153     case SearchParams::QualityHD:
154         addFilter("Features", "HD");
155         break;
156     case SearchParams::Quality4K:
157         addFilter("Features", "4K");
158         break;
159     case SearchParams::QualityHDR:
160         addFilter("Features", "HDR");
161         break;
162     }
163
164     js.callFunction(new JSResult(this), "search", {q, options, filterMap})
165             .onJson([this](auto &doc) {
166                 if (aborted) return;
167
168                 auto obj = doc.object();
169
170                 nextpageRef = obj["nextpageRef"].toString();
171
172                 const auto items = obj["items"].toArray();
173                 QVector<Video *> videos;
174                 videos.reserve(items.size());
175
176                 for (const auto &i : items) {
177                     QString type = i["type"].toString();
178                     if (type != "video") continue;
179
180                     Video *video = new Video();
181
182                     QString id = YTSearch::videoIdFromUrl(i["link"].toString());
183                     video->setId(id);
184
185                     QString title = i["title"].toString();
186                     video->setTitle(title);
187
188                     QString desc = i["description"].toString();
189                     video->setDescription(desc);
190
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());
195                     }
196
197                     int views = i["views"].toInt();
198                     video->setViewCount(views);
199
200                     int duration = parseDuration(i["duration"].toString());
201                     video->setDuration(duration);
202
203                     auto published = parsePublishedText(i["uploaded_at"].toString());
204                     if (published.isValid()) video->setPublished(published);
205
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);
211
212                     videos << video;
213                 }
214
215                 if (videos.isEmpty()) {
216                     emit error("No results");
217                 } else {
218                     emit gotVideos(videos);
219                     emit finished(videos.size());
220                 }
221             })
222             .onError([this, max, startIndex](auto &msg) {
223                 static int retries = 0;
224                 if (retries < 3) {
225                     qDebug() << "Retrying...";
226                     QTimer::singleShot(0, this,
227                                        [this, max, startIndex] { loadVideos(max, startIndex); });
228                     retries++;
229                 } else {
230                     emit error(msg);
231                 }
232             });
233 }
234
235 QString YTJSSearch::getName() {
236     if (!name.isEmpty()) return name;
237     if (!searchParams->keywords().isEmpty()) return searchParams->keywords();
238     return QString();
239 }
240
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;
246         return noActions;
247     }
248     return channelActions;
249 }