6 #include "jsfunctions.h"
8 #include "videodefinition.h"
15 static const QString jsNameChars = "a-zA-Z0-9\\$_";
18 YTVideo::YTVideo(const QString &videoId, QObject *parent)
19 : QObject(parent), videoId(videoId), definitionCode(0), elIndex(0), ageGate(false),
20 loadingStreamUrl(false) {}
22 void YTVideo::loadStreamUrl() {
23 if (loadingStreamUrl) {
24 qDebug() << "Already loading stream URL for" << videoId;
27 loadingStreamUrl = true;
34 void YTVideo::getVideoInfo() {
35 static const QStringList elTypes = {"&el=embedded", "&el=detailpage", "&el=vevo", ""};
38 if (elIndex == elTypes.size()) {
39 // qDebug() << "Trying special embedded el param";
40 url = QUrl("https://www.youtube.com/get_video_info");
42 q.addQueryItem("video_id", videoId);
43 q.addQueryItem("el", "embedded");
44 q.addQueryItem("gl", "US");
45 q.addQueryItem("hl", "en");
46 q.addQueryItem("eurl", "https://youtube.googleapis.com/v/" + videoId);
47 q.addQueryItem("asv", "3");
48 q.addQueryItem("sts", "1588");
50 } else if (elIndex > elTypes.size() - 1) {
51 qWarning() << "Cannot get video info";
52 loadingStreamUrl = false;
53 emit errorStreamUrl("Cannot get video info");
56 // qDebug() << "Trying el param:" << elTypes.at(elIndex) << elIndex;
57 url = QUrl(QString("https://www.youtube.com/"
58 "get_video_info?video_id=%1%2&ps=default&eurl=&gl=US&hl=en")
59 .arg(videoId, elTypes.at(elIndex)));
62 QObject *reply = HttpUtils::yt().get(url);
63 connect(reply, SIGNAL(data(QByteArray)), SLOT(gotVideoInfo(QByteArray)));
64 connect(reply, SIGNAL(error(QString)), SLOT(errorVideoInfo(QString)));
66 // see you in gotVideoInfo...
69 void YTVideo::gotVideoInfo(const QByteArray &bytes) {
70 QString videoInfo = QString::fromUtf8(bytes);
71 // qDebug() << "videoInfo" << videoInfo;
74 static const QRegExp videoTokeRE(JsFunctions::instance()->videoTokenRE());
75 if (videoTokeRE.indexIn(videoInfo) == -1) {
76 qDebug() << "Cannot get token. Trying next el param" << videoInfo << videoTokeRE.pattern();
77 // Don't panic! We're gonna try another magic "el" param
83 QString videoToken = videoTokeRE.cap(1);
84 qDebug() << "got token" << videoToken;
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) {
109 const QString definitionName = QSettings().value("definition", "360p").toString();
110 const VideoDefinition &definition = VideoDefinition::forName(definitionName);
112 qDebug() << "fmtUrlMap" << fmtUrlMap;
113 const QVector<QStringRef> formatUrls = fmtUrlMap.splitRef(',', QString::SkipEmptyParts);
114 QMap<int, QString> urlMap;
115 for (const QStringRef &formatUrl : formatUrls) {
116 // qDebug() << "formatUrl" << formatUrl;
117 const QVector<QStringRef> urlParams = formatUrl.split('&', QString::SkipEmptyParts);
118 // qDebug() << "urlParams" << urlParams;
123 for (const QStringRef &urlParam : urlParams) {
124 // qWarning() << urlParam;
125 if (urlParam.startsWith(QLatin1String("itag="))) {
126 int separator = urlParam.indexOf('=');
127 format = urlParam.mid(separator + 1).toInt();
128 } else if (urlParam.startsWith(QLatin1String("url="))) {
129 int separator = urlParam.indexOf('=');
130 url = QByteArray::fromPercentEncoding(urlParam.mid(separator + 1).toUtf8());
131 } else if (urlParam.startsWith(QLatin1String("sig="))) {
132 int separator = urlParam.indexOf('=');
133 sig = QByteArray::fromPercentEncoding(urlParam.mid(separator + 1).toUtf8());
134 } else if (urlParam.startsWith(QLatin1String("s="))) {
135 if (fromWebPage || ageGate) {
136 int separator = urlParam.indexOf('=');
137 sig = QByteArray::fromPercentEncoding(urlParam.mid(separator + 1).toUtf8());
139 sig = JsFunctions::instance()->decryptAgeSignature(sig);
141 sig = decryptSignature(sig);
142 if (sig.isEmpty()) sig = JsFunctions::instance()->decryptSignature(sig);
145 QUrl url("https://www.youtube.com/watch");
147 q.addQueryItem("v", videoId);
148 q.addQueryItem("gl", "US");
149 q.addQueryItem("hl", "en");
150 q.addQueryItem("has_verified", "1");
152 qDebug() << "Loading webpage" << url;
153 QObject *reply = HttpUtils::yt().get(url);
154 connect(reply, SIGNAL(data(QByteArray)), SLOT(scrapeWebPage(QByteArray)));
155 connect(reply, SIGNAL(error(QString)), SLOT(errorVideoInfo(QString)));
156 // see you in scrapWebPage(QByteArray)
161 if (format == -1 || url.isNull()) continue;
163 url += QLatin1String("&signature=") + sig;
165 if (!url.contains(QLatin1String("ratebypass"))) url += QLatin1String("&ratebypass=yes");
169 if (format == definition.getCode()) {
170 qDebug() << "Found format" << definitionCode;
171 saveDefinitionForUrl(url, definition);
175 urlMap.insert(format, url);
178 const QVector<VideoDefinition> &definitions = VideoDefinition::getDefinitions();
179 int previousIndex = std::max(definitions.indexOf(definition) - 1, 0);
180 for (; previousIndex >= 0; previousIndex--) {
181 const VideoDefinition &previousDefinition = definitions.at(previousIndex);
182 if (urlMap.contains(previousDefinition.getCode())) {
183 // qDebug() << "Found format" << definitionCode;
184 saveDefinitionForUrl(urlMap.value(previousDefinition.getCode()), previousDefinition);
189 emit errorStreamUrl(tr("Cannot get video stream for %1").arg(videoId));
192 void YTVideo::errorVideoInfo(const QString &message) {
193 loadingStreamUrl = false;
194 emit errorStreamUrl(message);
197 void YTVideo::scrapeWebPage(const QByteArray &bytes) {
198 const QString html = QString::fromUtf8(bytes);
200 static const QRegExp ageGateRE(JsFunctions::instance()->ageGateRE());
201 if (ageGateRE.indexIn(html) != -1) {
202 // qDebug() << "Found ageGate";
209 static const QRegExp fmtMapRE(JsFunctions::instance()->webPageFmtMapRE());
210 if (fmtMapRE.indexIn(html) == -1) {
211 qWarning() << "Error parsing video page";
212 // emit errorStreamUrl("Error parsing video page");
213 // loadingStreamUrl = false;
218 fmtUrlMap = fmtMapRE.cap(1);
219 fmtUrlMap.replace("\\u0026", "&");
220 // parseFmtUrlMap(fmtUrlMap, true);
224 QString definitionName = settings.value("definition", "360p").toString();
225 if (definitionName == QLatin1String("1080p")) {
226 QRegExp dashManifestRe("\"dashmpd\":\\s*\"([^\"]+)\"");
227 if (dashManifestRe.indexIn(html) != -1) {
228 dashManifestUrl = dashManifestRe.cap(1);
229 dashManifestUrl.remove('\\');
230 qDebug() << "dashManifestUrl" << dashManifestUrl;
232 qWarning() << "DASH manifest not found in webpage";
233 if (dashManifestRe.indexIn(fmtUrlMap) != -1) {
234 dashManifestUrl = dashManifestRe.cap(1);
235 dashManifestUrl.remove('\\');
236 qDebug() << "dashManifestUrl" << dashManifestUrl;
238 qWarning() << "DASH manifest not found in fmtUrlMap" << fmtUrlMap;
243 static const QRegExp jsPlayerRe(JsFunctions::instance()->jsPlayerRE());
244 if (jsPlayerRe.indexIn(html) != -1) {
245 QString jsPlayerUrl = jsPlayerRe.cap(1);
246 jsPlayerUrl.remove('\\');
247 if (jsPlayerUrl.startsWith(QLatin1String("//"))) {
248 jsPlayerUrl = QLatin1String("https:") + jsPlayerUrl;
249 } else if (jsPlayerUrl.startsWith("/")) {
250 jsPlayerUrl = QLatin1String("https://youtube.com") + jsPlayerUrl;
252 // qDebug() << "jsPlayerUrl" << jsPlayerUrl;
254 QRegExp jsPlayerIdRe("-(.+)\\.js");
255 jsPlayerIdRe.indexIn(jsPlayerUrl);
256 QString jsPlayerId = jsPlayerRe.cap(1);
258 QObject *reply = HttpUtils::yt().get(jsPlayerUrl);
259 connect(reply, SIGNAL(data(QByteArray)), SLOT(parseJsPlayer(QByteArray)));
260 connect(reply, SIGNAL(error(QString)), SLOT(errorVideoInfo(QString)));
264 void YTVideo::parseJsPlayer(const QByteArray &bytes) {
265 jsPlayer = QString::fromUtf8(bytes);
266 // qDebug() << "jsPlayer" << jsPlayer;
268 // QRegExp funcNameRe("[\"']signature[\"']\\s*,\\s*([" + jsNameChars + "]+)\\(");
269 static const QRegExp funcNameRe(
270 JsFunctions::instance()->signatureFunctionNameRE().arg(jsNameChars));
272 if (funcNameRe.indexIn(jsPlayer) == -1) {
273 qWarning() << "Cannot capture signature function name" << jsPlayer;
275 sigFuncName = funcNameRe.cap(1);
276 captureFunction(sigFuncName, jsPlayer);
277 // qWarning() << sigFunctions << sigObjects;
281 if (!dashManifestUrl.isEmpty()) {
282 QRegExp sigRe("/s/([\\w\\.]+)");
283 if (sigRe.indexIn(dashManifestUrl) != -1) {
284 qDebug() << "Decrypting signature for dash manifest";
285 QString sig = sigRe.cap(1);
286 sig = decryptSignature(sig);
287 dashManifestUrl.replace(sigRe, "/signature/" + sig);
288 qWarning() << "dash manifest" << dashManifestUrl;
291 // let phonon play the manifest
292 m_streamUrl = dashManifestUrl;
293 this->definitionCode = 37;
294 emit gotStreamUrl(m_streamUrl);
295 loadingStreamUrl = false;
297 // download the manifest
298 QObject *reply = HttpUtils::yt().get(QUrl::fromEncoded(dashManifestUrl.toUtf8()));
299 connect(reply, SIGNAL(data(QByteArray)), SLOT(parseDashManifest(QByteArray)));
300 connect(reply, SIGNAL(error(QString)), SLOT(errorVideoInfo(QString)));
308 parseFmtUrlMap(fmtUrlMap, true);
311 void YTVideo::parseDashManifest(const QByteArray &bytes) {
312 QFile file(Temporary::filename() + ".mpd");
313 if (!file.open(QIODevice::WriteOnly)) qWarning() << file.errorString() << file.fileName();
314 QDataStream stream(&file);
315 stream.writeRawData(bytes.constData(), bytes.size());
317 m_streamUrl = "file://" + file.fileName();
318 this->definitionCode = 37;
319 emit gotStreamUrl(m_streamUrl);
320 loadingStreamUrl = false;
323 void YTVideo::captureFunction(const QString &name, const QString &js) {
324 qDebug() << __PRETTY_FUNCTION__ << name;
325 const QString argsAndBody =
326 QLatin1String("\\s*\\([") + jsNameChars + QLatin1String(",\\s]*\\)\\s*\\{[^\\}]+\\}");
328 QRegExp funcRe(QLatin1String("function\\s+") + QRegExp::escape(name) + argsAndBody);
329 if (funcRe.indexIn(js) != -1) {
330 func = funcRe.cap(0);
332 // try var foo = function(bar) { };
333 funcRe = QRegExp(QLatin1String("var\\s+") + QRegExp::escape(name) +
334 QLatin1String("\\s*=\\s*function") + argsAndBody);
335 if (funcRe.indexIn(js) != -1) {
336 func = funcRe.cap(0);
338 // try ,gr= function(bar) { };
339 funcRe = QRegExp(QLatin1String("[,\\s;}\\.\\)](") + QRegExp::escape(name) +
340 QLatin1String("\\s*=\\s*function") + argsAndBody + ")");
341 if (funcRe.indexIn(js) != -1) {
342 func = funcRe.cap(1);
344 qWarning() << "Cannot capture function" << name;
349 sigFunctions.insert(name, func);
351 // capture inner functions
352 static const QRegExp invokedFuncRe(QLatin1String("[\\s=;\\(]([") + jsNameChars +
353 QLatin1String("]+)\\s*\\([") + jsNameChars +
354 QLatin1String(",\\s]+\\)"));
355 int pos = name.length() + 9;
356 while ((pos = invokedFuncRe.indexIn(func, pos)) != -1) {
357 QString funcName = invokedFuncRe.cap(1);
358 if (!sigFunctions.contains(funcName)) captureFunction(funcName, js);
359 pos += invokedFuncRe.matchedLength();
362 // capture referenced objects
363 static const QRegExp objRe(QLatin1String("[\\s=;\\(]([") + jsNameChars +
364 QLatin1String("]+)\\.[") + jsNameChars + QLatin1String("]+"));
365 pos = name.length() + 9;
366 while ((pos = objRe.indexIn(func, pos)) != -1) {
367 QString objName = objRe.cap(1);
368 if (!sigObjects.contains(objName)) captureObject(objName, js);
369 pos += objRe.matchedLength();
373 void YTVideo::captureObject(const QString &name, const QString &js) {
374 QRegExp re(QLatin1String("var\\s+") + QRegExp::escape(name) +
375 QLatin1String("\\s*=\\s*\\{.*\\}\\s*;"));
377 if (re.indexIn(js) == -1) {
378 qWarning() << "Cannot capture object" << name;
381 QString obj = re.cap(0);
382 sigObjects.insert(name, obj);
385 QString YTVideo::decryptSignature(const QString &s) {
386 qDebug() << "decryptSignature" << sigFuncName << sigFunctions << sigObjects;
387 if (sigFuncName.isEmpty()) return QString();
389 for (const QString &f : sigObjects) {
390 QJSValue value = engine.evaluate(f);
391 if (value.isError()) qWarning() << "Error in" << f << value.toString();
393 for (const QString &f : sigFunctions) {
394 QJSValue value = engine.evaluate(f);
395 if (value.isError()) qWarning() << "Error in" << f << value.toString();
397 QString js = sigFuncName + "('" + s + "');";
398 QJSValue value = engine.evaluate(js);
400 if (value.isUndefined()) {
401 qWarning() << "Undefined result for" << js;
404 if (value.isError()) {
405 qWarning() << "Error in" << js << value.toString();
410 engine2.evaluate(jsPlayer);
411 value = engine2.evaluate(js);
412 if (value.isUndefined()) {
413 qWarning() << "Undefined result for" << js;
416 if (value.isError()) {
417 qWarning() << "Error in" << js << value.toString();
421 if (error) return QString();
422 return value.toString();
425 void YTVideo::saveDefinitionForUrl(const QString &url, const VideoDefinition &definition) {
426 m_streamUrl = QUrl::fromEncoded(url.toUtf8(), QUrl::StrictMode);
427 definitionCode = definition.getCode();
428 emit gotStreamUrl(m_streamUrl);
429 loadingStreamUrl = false;