6 #include "jsfunctions.h"
8 #include "videodefinition.h"
16 static const QString jsNameChars = "a-zA-Z0-9\\$_";
19 YTVideo::YTVideo(const QString &videoId, QObject *parent)
20 : QObject(parent), videoId(videoId), definitionCode(0), elIndex(0), ageGate(false),
21 loadingStreamUrl(false) {}
23 void YTVideo::loadStreamUrl() {
24 if (loadingStreamUrl) {
25 qDebug() << "Already loading stream URL for" << videoId;
28 loadingStreamUrl = true;
35 void YTVideo::getVideoInfo() {
36 static const QStringList elTypes = {"&el=embedded", "&el=detailpage", "&el=vevo", ""};
39 if (elIndex == elTypes.size()) {
40 qDebug() << "Trying special embedded el param";
41 url = QUrl("https://www.youtube.com/get_video_info");
43 q.addQueryItem("video_id", videoId);
44 q.addQueryItem("el", "embedded");
45 q.addQueryItem("gl", "US");
46 q.addQueryItem("hl", "en");
47 q.addQueryItem("eurl", "https://youtube.googleapis.com/v/" + videoId);
48 q.addQueryItem("asv", "3");
49 q.addQueryItem("sts", "1588");
51 } else if (elIndex > elTypes.size() - 1) {
52 qWarning() << "Cannot get video info";
53 loadingStreamUrl = false;
54 emit errorStreamUrl("Cannot get video info");
57 // qDebug() << "Trying el param:" << elTypes.at(elIndex) << elIndex;
58 url = QUrl(QString("https://www.youtube.com/"
59 "get_video_info?video_id=%1%2&ps=default&eurl=&gl=US&hl=en")
60 .arg(videoId, elTypes.at(elIndex)));
63 QObject *reply = HttpUtils::yt().get(url);
64 connect(reply, SIGNAL(data(QByteArray)), SLOT(gotVideoInfo(QByteArray)));
65 connect(reply, SIGNAL(error(QString)), SLOT(errorVideoInfo(QString)));
67 // see you in gotVideoInfo...
70 void YTVideo::gotVideoInfo(const QByteArray &bytes) {
71 QString videoInfo = QString::fromUtf8(bytes);
72 // qDebug() << "videoInfo" << videoInfo;
75 static const QRegExp videoTokeRE(JsFunctions::instance()->videoTokenRE());
76 if (videoTokeRE.indexIn(videoInfo) == -1) {
77 qDebug() << "Cannot get token. Trying next el param" << videoInfo << videoTokeRE.pattern();
78 // Don't panic! We're gonna try another magic "el" param
84 QString videoToken = videoTokeRE.cap(1);
85 while (videoToken.contains('%'))
86 videoToken = QByteArray::fromPercentEncoding(videoToken.toLatin1());
87 qDebug() << "videoToken" << videoToken;
88 this->videoToken = videoToken;
91 static const QRegExp fmtMapRE(JsFunctions::instance()->videoInfoFmtMapRE());
92 if (fmtMapRE.indexIn(videoInfo) == -1) {
93 qDebug() << "Cannot get urlMap. Trying next el param";
94 // Don't panic! We're gonna try another magic "el" param
100 QString fmtUrlMap = fmtMapRE.cap(1);
101 // qDebug() << "got fmtUrlMap" << fmtUrlMap;
102 fmtUrlMap = QByteArray::fromPercentEncoding(fmtUrlMap.toUtf8());
104 qDebug() << "Got token and urlMap" << elIndex << videoToken << fmtUrlMap;
105 parseFmtUrlMap(fmtUrlMap);
108 void YTVideo::parseFmtUrlMap(const QString &fmtUrlMap, bool fromWebPage) {
110 const VideoDefinition &definition = YT3::instance().maxVideoDefinition();
112 // qDebug() << "fmtUrlMap" << fmtUrlMap;
113 const QVector<QStringRef> formatUrls = fmtUrlMap.splitRef(',', QString::SkipEmptyParts);
115 for (const QStringRef &formatUrl : formatUrls) {
116 // qDebug() << "formatUrl" << formatUrl;
117 const QVector<QStringRef> urlParams = formatUrl.split('&', QString::SkipEmptyParts);
118 // qDebug() << "urlParams" << urlParams;
124 for (const QStringRef &urlParam : urlParams) {
125 qDebug() << "urlParam" << urlParam;
126 if (sp.isNull() && urlParam.startsWith(QLatin1String("sp"))) {
127 int separator = urlParam.indexOf('=');
128 sp = urlParam.mid(separator + 1);
130 if (urlParam.startsWith(QLatin1String("itag="))) {
131 int separator = urlParam.indexOf('=');
132 format = urlParam.mid(separator + 1).toInt();
133 } else if (urlParam.startsWith(QLatin1String("url="))) {
134 int separator = urlParam.indexOf('=');
135 url = QByteArray::fromPercentEncoding(urlParam.mid(separator + 1).toUtf8());
136 } else if (urlParam.startsWith(QLatin1String("sig="))) {
137 int separator = urlParam.indexOf('=');
138 sig = QByteArray::fromPercentEncoding(urlParam.mid(separator + 1).toUtf8());
139 } else if (urlParam.startsWith(QLatin1String("s="))) {
140 if (fromWebPage || ageGate) {
141 int separator = urlParam.indexOf('=');
142 sig = QByteArray::fromPercentEncoding(urlParam.mid(separator + 1).toUtf8());
143 sig = decryptSignature(sig);
144 if (sig.isEmpty()) sig = JsFunctions::instance()->decryptSignature(sig);
145 if (sig.isEmpty()) qWarning() << "Empty signature";
152 if (format == -1 || url.isNull()) continue;
154 if (!sig.isEmpty()) {
156 url += QLatin1String("&signature=") + sig;
158 url += '&' + sp + '=' + sig;
161 if (!url.contains(QLatin1String("ratebypass"))) url += QLatin1String("&ratebypass=yes");
164 if (format == definition.getCode()) {
165 qDebug() << "Found format" << format;
166 if (definition.hasAudio()) {
167 // we found the exact match with an audio/video stream
168 saveDefinitionForUrl(url, definition);
171 videoFormat = format;
173 urlMap.insert(format, url);
176 if (!fromWebPage && !ageGate) {
181 if (videoFormat != 0) {
182 // exact match with video stream was found
183 const VideoDefinition &definition = VideoDefinition::forCode(videoFormat);
184 saveDefinitionForUrl(urlMap.value(videoFormat), definition);
188 const QVector<VideoDefinition> &definitions = VideoDefinition::getDefinitions();
189 int previousIndex = std::max(definitions.indexOf(definition) - 1, 0);
190 for (; previousIndex >= 0; previousIndex--) {
191 const VideoDefinition &previousDefinition = definitions.at(previousIndex);
192 qDebug() << "Testing format" << previousDefinition.getCode();
193 if (urlMap.contains(previousDefinition.getCode())) {
194 qDebug() << "Found format" << previousDefinition.getCode();
195 saveDefinitionForUrl(urlMap.value(previousDefinition.getCode()), previousDefinition);
200 emit errorStreamUrl(tr("Cannot get video stream for %1").arg(videoId));
203 void YTVideo::loadWebPage() {
204 QUrl url("https://www.youtube.com/watch");
206 q.addQueryItem("v", videoId);
207 q.addQueryItem("gl", "US");
208 q.addQueryItem("hl", "en");
209 q.addQueryItem("has_verified", "1");
210 q.addQueryItem("bpctr", "9999999999");
212 qDebug() << "Loading webpage" << url;
213 QObject *reply = HttpUtils::yt().get(url);
214 connect(reply, SIGNAL(data(QByteArray)), SLOT(scrapeWebPage(QByteArray)));
215 connect(reply, SIGNAL(error(QString)), SLOT(errorVideoInfo(QString)));
216 // see you in scrapWebPage(QByteArray)
219 void YTVideo::errorVideoInfo(const QString &message) {
220 loadingStreamUrl = false;
221 emit errorStreamUrl(message);
224 void YTVideo::scrapeWebPage(const QByteArray &bytes) {
225 qDebug() << "scrapeWebPage";
227 const QString html = QString::fromUtf8(bytes);
229 static const QRegExp ageGateRE(JsFunctions::instance()->ageGateRE());
230 if (ageGateRE.indexIn(html) != -1) {
231 qDebug() << "Found ageGate";
238 // "\"url_encoded_fmt_stream_map\":\s*\"([^\"]+)\""
239 static const QRegExp fmtMapRE(JsFunctions::instance()->webPageFmtMapRE());
240 if (fmtMapRE.indexIn(html) != -1) {
241 fmtUrlMap = fmtMapRE.cap(1);
242 fmtUrlMap.replace("\\u0026", "&");
245 QRegExp adaptiveFormatsRE("\"adaptive_fmts\":\\s*\"([^\"]+)\"");
246 if (adaptiveFormatsRE.indexIn(html) != -1) {
247 qDebug() << "Found adaptive_fmts";
248 if (!fmtUrlMap.isEmpty()) fmtUrlMap += ',';
249 fmtUrlMap += adaptiveFormatsRE.cap(1).replace("\\u0026", "&");
252 if (fmtUrlMap.isEmpty()) {
253 qWarning() << "Cannot get fmtUrlMap from video page. Trying next el";
259 static const QRegExp jsPlayerRe(JsFunctions::instance()->jsPlayerRE());
260 if (jsPlayerRe.indexIn(html) != -1) {
261 QString jsPlayerUrl = jsPlayerRe.cap(1);
262 jsPlayerUrl.remove('\\');
263 if (jsPlayerUrl.startsWith(QLatin1String("//"))) {
264 jsPlayerUrl = QLatin1String("https:") + jsPlayerUrl;
265 } else if (jsPlayerUrl.startsWith("/")) {
266 jsPlayerUrl = QLatin1String("https://youtube.com") + jsPlayerUrl;
268 // qDebug() << "jsPlayerUrl" << jsPlayerUrl;
270 QRegExp jsPlayerIdRe("-(.+)\\.js");
271 jsPlayerIdRe.indexIn(jsPlayerUrl);
272 QString jsPlayerId = jsPlayerRe.cap(1);
274 QObject *reply = HttpUtils::yt().get(jsPlayerUrl);
275 connect(reply, SIGNAL(data(QByteArray)), SLOT(parseJsPlayer(QByteArray)));
276 connect(reply, SIGNAL(error(QString)), SLOT(errorVideoInfo(QString)));
280 void YTVideo::parseJsPlayer(const QByteArray &bytes) {
281 jsPlayer = QString::fromUtf8(bytes);
282 // qDebug() << "jsPlayer" << jsPlayer;
284 // QRegExp funcNameRe("[\"']signature[\"']\\s*,\\s*([" + jsNameChars + "]+)\\(");
285 static const QVector<QRegExp> funcNameRes = [] {
286 QVector<QRegExp> res;
287 for (const QString &s : JsFunctions::instance()->signatureFunctionNameREs()) {
288 res << QRegExp(s.arg(jsNameChars));
292 for (const QRegExp &funcNameRe : funcNameRes) {
293 if (funcNameRe.indexIn(jsPlayer) == -1) {
294 qWarning() << "Cannot capture signature function name" << funcNameRe;
297 sigFuncName = funcNameRe.cap(1);
298 qDebug() << "Captures" << funcNameRe.captureCount() << funcNameRe.capturedTexts();
299 if (sigFuncName.isEmpty()) {
300 qDebug() << "Empty capture for" << funcNameRe;
303 captureFunction(sigFuncName, jsPlayer);
304 qDebug() << sigFunctions << sigObjects;
308 if (sigFuncName.isEmpty()) qDebug() << "Empty signature function name";
310 parseFmtUrlMap(fmtUrlMap, true);
313 void YTVideo::captureFunction(const QString &name, const QString &js) {
314 qDebug() << __PRETTY_FUNCTION__ << name;
315 const QString argsAndBody =
316 QLatin1String("\\s*\\([") + jsNameChars + QLatin1String(",\\s]*\\)\\s*\\{[^\\}]+\\}");
318 QRegExp funcRe(QLatin1String("function\\s+") + QRegExp::escape(name) + argsAndBody);
319 if (funcRe.indexIn(js) != -1) {
320 func = funcRe.cap(0);
322 // try var foo = function(bar) { };
323 funcRe = QRegExp(QLatin1String("var\\s+") + QRegExp::escape(name) +
324 QLatin1String("\\s*=\\s*function") + argsAndBody);
325 if (funcRe.indexIn(js) != -1) {
326 func = funcRe.cap(0);
328 // try ,gr= function(bar) { };
329 funcRe = QRegExp(QLatin1String("[,\\s;}\\.\\)](") + QRegExp::escape(name) +
330 QLatin1String("\\s*=\\s*function") + argsAndBody + ")");
331 if (funcRe.indexIn(js) != -1) {
332 func = funcRe.cap(1);
334 qWarning() << "Cannot capture function" << name;
339 sigFunctions.insert(name, func);
341 // capture inner functions
342 static const QRegExp invokedFuncRe(QLatin1String("[\\s=;\\(]([") + jsNameChars +
343 QLatin1String("]+)\\s*\\([") + jsNameChars +
344 QLatin1String(",\\s]+\\)"));
345 int pos = name.length() + 9;
346 while ((pos = invokedFuncRe.indexIn(func, pos)) != -1) {
347 QString funcName = invokedFuncRe.cap(1);
348 if (!sigFunctions.contains(funcName)) captureFunction(funcName, js);
349 pos += invokedFuncRe.matchedLength();
352 // capture referenced objects
353 static const QRegExp objRe(QLatin1String("[\\s=;\\(]([") + jsNameChars +
354 QLatin1String("]+)\\.[") + jsNameChars + QLatin1String("]+"));
355 pos = name.length() + 9;
356 while ((pos = objRe.indexIn(func, pos)) != -1) {
357 QString objName = objRe.cap(1);
358 if (!sigObjects.contains(objName)) captureObject(objName, js);
359 pos += objRe.matchedLength();
363 void YTVideo::captureObject(const QString &name, const QString &js) {
364 QRegExp re(QLatin1String("var\\s+") + QRegExp::escape(name) +
365 QLatin1String("\\s*=\\s*\\{.*\\}\\s*;"));
367 if (re.indexIn(js) == -1) {
368 qWarning() << "Cannot capture object" << name;
371 QString obj = re.cap(0);
372 sigObjects.insert(name, obj);
375 QString YTVideo::decryptSignature(const QString &s) {
376 qDebug() << "decryptSignature" << sigFuncName << sigFunctions << sigObjects;
377 if (sigFuncName.isEmpty()) return QString();
379 for (const QString &f : sigObjects) {
380 QJSValue value = engine.evaluate(f);
381 if (value.isError()) qWarning() << "Error in" << f << value.toString();
383 for (const QString &f : sigFunctions) {
384 QJSValue value = engine.evaluate(f);
385 if (value.isError()) qWarning() << "Error in" << f << value.toString();
387 QString js = sigFuncName + "('" + s + "');";
388 QJSValue value = engine.evaluate(js);
390 if (value.isUndefined()) {
391 qWarning() << "Undefined result for" << js;
394 if (value.isError()) {
395 qWarning() << "Error in" << js << value.toString();
400 engine2.evaluate(jsPlayer);
401 value = engine2.evaluate(js);
402 if (value.isUndefined()) {
403 qWarning() << "Undefined result for" << js;
406 if (value.isError()) {
407 qWarning() << "Error in" << js << value.toString();
411 if (error) return QString();
412 return value.toString();
415 void YTVideo::saveDefinitionForUrl(const QString &url, const VideoDefinition &definition) {
416 qDebug() << "Selected video format" << definition.getCode() << definition.getName()
417 << definition.hasAudio();
419 definitionCode = definition.getCode();
422 if (!definition.hasAudio()) {
423 qDebug() << "Finding audio format";
424 static const QVector<int> audioFormats({251, 171, 140});
425 for (int audioFormat : audioFormats) {
426 qDebug() << "Trying audio format" << audioFormat;
427 auto i = urlMap.constFind(audioFormat);
428 if (i != urlMap.constEnd()) {
429 qDebug() << "Found audio format" << i.value();
430 audioUrl = i.value();
436 loadingStreamUrl = false;
437 emit gotStreamUrl(url, audioUrl);