3 This file is part of Minitube.
4 Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
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.
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.
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/>.
22 #include "constants.h"
24 #include "httputils.h"
25 #include "searchparams.h"
27 #include "ytchannel.h"
29 #include "datautils.h"
30 #include "mainwindow.h"
32 #include "yt3listparser.h"
36 QString RFC3339toString(const QDateTime &dt) {
37 return dt.toString(QStringLiteral("yyyy-MM-ddThh:mm:ssZ"));
41 YTSearch::YTSearch(SearchParams *searchParams, QObject *parent)
42 : PaginatedVideoSource(parent), searchParams(searchParams) {
43 searchParams->setParent(this);
46 void YTSearch::loadVideos(int max, int startIndex) {
49 QUrl url = YT3::instance().method("search");
52 q.addQueryItem("part", "snippet");
53 q.addQueryItem("type", "video");
54 q.addQueryItem("maxResults", QString::number(max));
57 if (maybeReloadToken(max, startIndex)) return;
58 q.addQueryItem("pageToken", nextPageToken);
61 // TODO interesting params
62 // urlHelper.addQueryItem("videoSyndicated", "true");
63 // urlHelper.addQueryItem("regionCode", "IT");
64 // urlHelper.addQueryItem("videoType", "movie");
66 if (!searchParams->keywords().isEmpty()) {
67 if (searchParams->keywords().startsWith("http://") ||
68 searchParams->keywords().startsWith("https://")) {
69 q.addQueryItem("q", YTSearch::videoIdFromUrl(searchParams->keywords()));
71 q.addQueryItem("q", searchParams->keywords());
74 if (!searchParams->channelId().isEmpty())
75 q.addQueryItem("channelId", searchParams->channelId());
77 switch (searchParams->sortBy()) {
78 case SearchParams::SortByNewest:
79 q.addQueryItem("order", "date");
81 case SearchParams::SortByViewCount:
82 q.addQueryItem("order", "viewCount");
84 case SearchParams::SortByRating:
85 q.addQueryItem("order", "rating");
89 switch (searchParams->duration()) {
90 case SearchParams::DurationShort:
91 q.addQueryItem("videoDuration", "short");
93 case SearchParams::DurationMedium:
94 q.addQueryItem("videoDuration", "medium");
96 case SearchParams::DurationLong:
97 q.addQueryItem("videoDuration", "long");
101 switch (searchParams->time()) {
102 case SearchParams::TimeToday:
103 q.addQueryItem("publishedAfter",
104 RFC3339toString(QDateTime::currentDateTimeUtc().addSecs(-60 * 60 * 24)));
106 case SearchParams::TimeWeek:
107 q.addQueryItem("publishedAfter",
108 RFC3339toString(QDateTime::currentDateTimeUtc().addSecs(-60 * 60 * 24 * 7)));
110 case SearchParams::TimeMonth:
111 q.addQueryItem("publishedAfter", RFC3339toString(QDateTime::currentDateTimeUtc().addSecs(
112 -60 * 60 * 24 * 30)));
116 if (searchParams->publishedAfter()) {
119 RFC3339toString(QDateTime::fromTime_t(searchParams->publishedAfter()).toUTC()));
122 switch (searchParams->quality()) {
123 case SearchParams::QualityHD:
124 q.addQueryItem("videoDefinition", "high");
128 switch (searchParams->safeSearch()) {
129 case SearchParams::None:
130 q.addQueryItem("safeSearch", "none");
132 case SearchParams::Strict:
133 q.addQueryItem("safeSearch", "strict");
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)));
147 void YTSearch::parseResults(const QByteArray &data) {
150 YT3ListParser parser(data);
151 const QVector<Video *> &videos = parser.getVideos();
153 bool tryingWithNewToken = setPageToken(parser.getNextPageToken());
154 if (tryingWithNewToken) return;
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();
163 emit nameChanged(name);
167 emit gotVideos(videos);
168 emit finished(videos.size());
170 loadVideoDetails(videos);
173 void YTSearch::abort() {
177 QString YTSearch::getName() {
178 if (!name.isEmpty()) return name;
179 if (!searchParams->keywords().isEmpty()) return searchParams->keywords();
183 void YTSearch::requestError(const QString &message) {
184 QString msg = message;
185 msg.remove(QRegularExpression("key=[^ &]+"));
189 QString YTSearch::videoIdFromUrl(const QString &url) {
190 static const QVector<QRegExp> res = {QRegExp("^.*[\\?&]v=([^&#]+).*$"),
191 QRegExp("^.*://.*/([^&#\\?]+).*$"),
192 QRegExp("^.*/shorts/([^&#\\?/]+)$")};
193 for (const auto &re : res) {
194 if (re.exactMatch(url)) return re.cap(1);
199 QTime YTSearch::videoTimestampFromUrl(const QString &url) {
202 // TODO: should we make this accept h/m/s in any order?
203 // timestamps returned by youtube always seem to be
205 QRegExp re = QRegExp(".*t=([0-9]*h)?([0-9]*m)?([0-9]*s)?.*");
207 if (!re.exactMatch(url)) {
211 const auto captured = re.capturedTexts();
212 for (const QString &str : captured) {
213 if (str.length() <= 1) continue;
215 QString truncated = str;
219 int value = truncated.toInt(&ok);
221 char unit = str.at(str.length() - 1).toLatin1();
225 value *= 60 * 60; // hours -> seconds
229 value *= 60; // minutes -> seconds
239 res = res.addSecs(value);
245 const QList<QAction *> &YTSearch::getActions() {
246 static const QList<QAction *> channelActions = {
247 MainWindow::instance()->getAction("subscribeChannel")};
248 if (searchParams->channelId().isEmpty()) {
249 static const QList<QAction *> noActions;
252 return channelActions;