]> git.sur5r.net Git - minitube/blob - src/ytsearch.cpp
Upload 3.9.3-2 to unstable
[minitube] / src / ytsearch.cpp
1 /* $BEGIN_LICENSE
2
3 This file is part of Minitube.
4 Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
5
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.
10
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.
15
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/>.
18
19 $END_LICENSE */
20
21 #include "ytsearch.h"
22 #include "constants.h"
23 #include "http.h"
24 #include "httputils.h"
25 #include "searchparams.h"
26 #include "video.h"
27 #include "ytchannel.h"
28
29 #include "datautils.h"
30 #include "mainwindow.h"
31 #include "yt3.h"
32 #include "yt3listparser.h"
33
34 namespace {
35
36 QString RFC3339toString(const QDateTime &dt) {
37     return dt.toString(QStringLiteral("yyyy-MM-ddThh:mm:ssZ"));
38 }
39 }
40
41 YTSearch::YTSearch(SearchParams *searchParams, QObject *parent)
42     : PaginatedVideoSource(parent), searchParams(searchParams) {
43     searchParams->setParent(this);
44 }
45
46 void YTSearch::loadVideos(int max, int startIndex) {
47     aborted = false;
48
49     QUrl url = YT3::instance().method("search");
50
51     QUrlQuery q(url);
52     q.addQueryItem("part", "snippet");
53     q.addQueryItem("type", "video");
54     q.addQueryItem("maxResults", QString::number(max));
55
56     if (startIndex > 1) {
57         if (maybeReloadToken(max, startIndex)) return;
58         q.addQueryItem("pageToken", nextPageToken);
59     }
60
61     // TODO interesting params
62     // urlHelper.addQueryItem("videoSyndicated", "true");
63     // urlHelper.addQueryItem("regionCode", "IT");
64     // urlHelper.addQueryItem("videoType", "movie");
65
66     if (!searchParams->keywords().isEmpty()) {
67         if (searchParams->keywords().startsWith("http://") ||
68             searchParams->keywords().startsWith("https://")) {
69             q.addQueryItem("q", YTSearch::videoIdFromUrl(searchParams->keywords()));
70         } else
71             q.addQueryItem("q", searchParams->keywords());
72     }
73
74     if (!searchParams->channelId().isEmpty())
75         q.addQueryItem("channelId", searchParams->channelId());
76
77     switch (searchParams->sortBy()) {
78     case SearchParams::SortByNewest:
79         q.addQueryItem("order", "date");
80         break;
81     case SearchParams::SortByViewCount:
82         q.addQueryItem("order", "viewCount");
83         break;
84     case SearchParams::SortByRating:
85         q.addQueryItem("order", "rating");
86         break;
87     }
88
89     switch (searchParams->duration()) {
90     case SearchParams::DurationShort:
91         q.addQueryItem("videoDuration", "short");
92         break;
93     case SearchParams::DurationMedium:
94         q.addQueryItem("videoDuration", "medium");
95         break;
96     case SearchParams::DurationLong:
97         q.addQueryItem("videoDuration", "long");
98         break;
99     }
100
101     switch (searchParams->time()) {
102     case SearchParams::TimeToday:
103         q.addQueryItem("publishedAfter",
104                        RFC3339toString(QDateTime::currentDateTimeUtc().addSecs(-60 * 60 * 24)));
105         break;
106     case SearchParams::TimeWeek:
107         q.addQueryItem("publishedAfter",
108                        RFC3339toString(QDateTime::currentDateTimeUtc().addSecs(-60 * 60 * 24 * 7)));
109         break;
110     case SearchParams::TimeMonth:
111         q.addQueryItem("publishedAfter", RFC3339toString(QDateTime::currentDateTimeUtc().addSecs(
112                                                  -60 * 60 * 24 * 30)));
113         break;
114     }
115
116     if (searchParams->publishedAfter()) {
117         q.addQueryItem(
118                 "publishedAfter",
119                 RFC3339toString(QDateTime::fromTime_t(searchParams->publishedAfter()).toUTC()));
120     }
121
122     switch (searchParams->quality()) {
123     case SearchParams::QualityHD:
124         q.addQueryItem("videoDefinition", "high");
125         break;
126     }
127
128     switch (searchParams->safeSearch()) {
129     case SearchParams::None:
130         q.addQueryItem("safeSearch", "none");
131         break;
132     case SearchParams::Strict:
133         q.addQueryItem("safeSearch", "strict");
134         break;
135     }
136
137     url.setQuery(q);
138
139     lastUrl = url;
140
141     // qWarning() << "YT3 search" << url.toString();
142     QObject *reply = HttpUtils::yt().get(url);
143     connect(reply, SIGNAL(data(QByteArray)), SLOT(parseResults(QByteArray)));
144     connect(reply, SIGNAL(error(QString)), SLOT(requestError(QString)));
145 }
146
147 void YTSearch::parseResults(const QByteArray &data) {
148     if (aborted) return;
149
150     YT3ListParser parser(data);
151     const QVector<Video *> &videos = parser.getVideos();
152
153     bool tryingWithNewToken = setPageToken(parser.getNextPageToken());
154     if (tryingWithNewToken) return;
155
156     if (name.isEmpty() && !searchParams->channelId().isEmpty()) {
157         if (!videos.isEmpty()) {
158             name = videos.at(0)->getChannelTitle();
159             if (!searchParams->keywords().isEmpty()) {
160                 name += QLatin1String(": ") + searchParams->keywords();
161             }
162         }
163         emit nameChanged(name);
164     }
165
166     if (asyncDetails) {
167         emit gotVideos(videos);
168         emit finished(videos.size());
169     }
170     loadVideoDetails(videos);
171 }
172
173 void YTSearch::abort() {
174     aborted = true;
175 }
176
177 QString YTSearch::getName() {
178     if (!name.isEmpty()) return name;
179     if (!searchParams->keywords().isEmpty()) return searchParams->keywords();
180     return QString();
181 }
182
183 void YTSearch::requestError(const QString &message) {
184     QString msg = message;
185     msg.remove(QRegularExpression("key=[^ &]+"));
186     emit error(msg);
187 }
188
189 QString YTSearch::videoIdFromUrl(const QString &url) {
190     QRegExp re = QRegExp("^.*[\\?&]v=([^&#]+).*$");
191     if (re.exactMatch(url)) return re.cap(1);
192     re = QRegExp("^.*://.*/([^&#\\?]+).*$");
193     if (re.exactMatch(url)) return re.cap(1);
194     return QString();
195 }
196
197 QTime YTSearch::videoTimestampFromUrl(const QString &url) {
198     QTime res(0, 0);
199
200     // TODO: should we make this accept h/m/s in any order?
201     //       timestamps returned by youtube always seem to be
202     //       ordered.
203     QRegExp re = QRegExp(".*t=([0-9]*h)?([0-9]*m)?([0-9]*s)?.*");
204
205     if (!re.exactMatch(url)) {
206         return res;
207     }
208
209     const auto captured = re.capturedTexts();
210     for (const QString &str : captured) {
211         if (str.length() <= 1) continue;
212
213         QString truncated = str;
214         truncated.chop(1);
215
216         bool ok = false;
217         int value = truncated.toInt(&ok);
218         if (!ok) continue;
219         char unit = str.at(str.length() - 1).toLatin1();
220
221         switch (unit) {
222         case 'h':
223             value *= 60 * 60; // hours -> seconds
224             break;
225
226         case 'm':
227             value *= 60; // minutes -> seconds
228             break;
229
230         case 's':
231             break;
232
233         default:
234             continue;
235         }
236
237         res = res.addSecs(value);
238     }
239
240     return res;
241 }
242
243 const QList<QAction *> &YTSearch::getActions() {
244     static const QList<QAction *> channelActions = {
245             MainWindow::instance()->getAction("subscribeChannel")};
246     if (searchParams->channelId().isEmpty()) {
247         static const QList<QAction *> noActions;
248         return noActions;
249     }
250     return channelActions;
251 }