]> git.sur5r.net Git - minitube/blob - src/ytjs/ytjssearch.cpp
New upstream version 3.6.1
[minitube] / src / ytjs / ytjssearch.cpp
1 #include "ytjssearch.h"
2
3 #include "mainwindow.h"
4 #include "searchparams.h"
5 #include "video.h"
6 #include "ytjs.h"
7 #include "ytsearch.h"
8
9 namespace {
10
11 int parseDuration(const QString &s) {
12     const auto parts = s.splitRef(':');
13     int secs = 0;
14     int p = 0;
15     for (auto i = parts.crbegin(); i != parts.crend(); ++i) {
16         if (p == 0) {
17             secs = i->toInt();
18         } else if (p == 1) {
19             secs += i->toInt() * 60;
20         } else if (p == 2) {
21             secs += i->toInt() * 60 * 60;
22         }
23         p++;
24     }
25     return secs;
26 }
27
28 QString parseChannelId(const QString &channelUrl) {
29     int pos = channelUrl.lastIndexOf('/');
30     if (pos >= 0) return channelUrl.mid(pos + 1);
31     return QString();
32 }
33
34 QDateTime parsePublishedText(const QString &s) {
35     int num = 0;
36     const auto parts = s.splitRef(' ');
37     for (const auto &part : parts) {
38         num = part.toInt();
39         if (num > 0) break;
40     }
41     if (num == 0) return QDateTime();
42
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);
52     }
53     return QDateTime();
54 }
55
56 } // namespace
57
58 YTJSSearch::YTJSSearch(SearchParams *searchParams, QObject *parent)
59     : VideoSource(parent), searchParams(searchParams) {}
60
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); });
65         return;
66     }
67     auto &engine = ytjs.getEngine();
68
69     aborted = false;
70
71     auto function = engine.evaluate("search");
72     if (!function.isCallable()) {
73         qWarning() << function.toString() << " is not callable";
74         emit error(function.toString());
75         return;
76     }
77
78     QString q;
79     if (!searchParams->keywords().isEmpty()) {
80         if (searchParams->keywords().startsWith("http://") ||
81             searchParams->keywords().startsWith("https://")) {
82             q = YTSearch::videoIdFromUrl(searchParams->keywords());
83         } else
84             q = searchParams->keywords();
85     }
86
87     // Options
88
89     QJSValue options = engine.newObject();
90
91     if (startIndex > 1 && !nextpageRef.isEmpty()) options.setProperty("nextpageRef", nextpageRef);
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     case SearchParams::DurationLong:
131         addFilter("Duration", "Long");
132         break;
133     }
134
135     switch (searchParams->time()) {
136     case SearchParams::TimeToday:
137         addFilter("Upload date", "Today");
138         break;
139     case SearchParams::TimeWeek:
140         addFilter("Upload date", "This week");
141         break;
142     case SearchParams::TimeMonth:
143         addFilter("Upload date", "This month");
144         break;
145     }
146
147     switch (searchParams->quality()) {
148     case SearchParams::QualityHD:
149         addFilter("Features", "HD");
150         break;
151     case SearchParams::Quality4K:
152         addFilter("Features", "4K");
153         break;
154     case SearchParams::QualityHDR:
155         addFilter("Features", "HDR");
156         break;
157     }
158
159     auto handler = new ResultHandler;
160     connect(handler, &ResultHandler::error, this, &VideoSource::error);
161     connect(handler, &ResultHandler::data, this, [this](const QJsonDocument &doc) {
162         if (aborted) return;
163
164         auto obj = doc.object();
165
166         /*
167         QFile jsonFile("/Users/flavio/test.json");
168         jsonFile.open(QFile::WriteOnly);
169         jsonFile.write(doc.toJson());
170         */
171
172         nextpageRef = obj["nextpageRef"].toString();
173
174         const auto items = obj["items"].toArray();
175         QVector<Video *> videos;
176         videos.reserve(items.size());
177
178         for (const auto &i : items) {
179             QString type = i["type"].toString();
180             if (type != "video") continue;
181
182             Video *video = new Video();
183
184             QString id = YTSearch::videoIdFromUrl(i["link"].toString());
185             video->setId(id);
186
187             QString title = i["title"].toString();
188             video->setTitle(title);
189
190             QString desc = i["description"].toString();
191             video->setDescription(desc);
192
193             QString thumb = i["thumbnail"].toString();
194             video->setThumbnailUrl(thumb);
195
196             int views = i["views"].toInt();
197             video->setViewCount(views);
198
199             int duration = parseDuration(i["duration"].toString());
200             video->setDuration(duration);
201
202             auto published = parsePublishedText(i["uploaded_at"].toString());
203             if (published.isValid()) video->setPublished(published);
204
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);
210
211             videos << video;
212         }
213
214         emit gotVideos(videos);
215         emit finished(videos.size());
216     });
217     QJSValue h = engine.newQObject(handler);
218     auto value = function.call({h, q, options, filterMap});
219     if (ytjs.checkError(value)) emit error(value.toString());
220 }
221
222 QString YTJSSearch::getName() {
223     if (!name.isEmpty()) return name;
224     if (!searchParams->keywords().isEmpty()) return searchParams->keywords();
225     return QString();
226 }
227
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;
233         return noActions;
234     }
235     return channelActions;
236 }