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 "networkaccess.h"
24 #include "videodefinition.h"
25 #include "jsfunctions.h"
26 #include "temporary.h"
29 NetworkAccess* http();
33 static const QString jsNameChars = "a-zA-Z0-9\\$_";
36 Video::Video() : m_duration(0),
38 m_license(LicenseYouTube),
42 loadingStreamUrl(false),
43 loadingThumbnail(false) {
46 Video* Video::clone() {
47 Video* cloneVideo = new Video();
48 cloneVideo->m_title = m_title;
49 cloneVideo->m_description = m_description;
50 cloneVideo->m_channelTitle = m_channelTitle;
51 cloneVideo->m_channelId = m_channelId;
52 cloneVideo->m_webpage = m_webpage;
53 cloneVideo->m_streamUrl = m_streamUrl;
54 cloneVideo->m_thumbnail = m_thumbnail;
55 cloneVideo->m_thumbnailUrl = m_thumbnailUrl;
56 cloneVideo->m_mediumThumbnailUrl = m_mediumThumbnailUrl;
57 cloneVideo->m_duration = m_duration;
58 cloneVideo->m_published = m_published;
59 cloneVideo->m_viewCount = m_viewCount;
60 cloneVideo->videoId = videoId;
61 cloneVideo->videoToken = videoToken;
62 cloneVideo->definitionCode = definitionCode;
66 const QString &Video::webpage() {
67 if (m_webpage.isEmpty() && !videoId.isEmpty())
68 m_webpage.append("https://www.youtube.com/watch?v=").append(videoId);
72 void Video::setWebpage(const QString &value) {
76 if (videoId.isEmpty()) {
77 QRegExp re(JsFunctions::instance()->videoIdRE());
78 if (re.indexIn(m_webpage) == -1) {
79 qWarning() << QString("Cannot get video id for %1").arg(m_webpage);
80 // emit errorStreamUrl(QString("Cannot get video id for %1").arg(m_webpage.toString()));
81 // loadingStreamUrl = false;
88 void Video::loadThumbnail() {
89 if (m_thumbnailUrl.isEmpty() || loadingThumbnail) return;
90 loadingThumbnail = true;
91 QObject *reply = The::http()->get(m_thumbnailUrl);
92 connect(reply, SIGNAL(data(QByteArray)), SLOT(setThumbnail(QByteArray)));
95 void Video::setThumbnail(QByteArray bytes) {
96 loadingThumbnail = false;
97 m_thumbnail = QPixmap();
98 m_thumbnail.loadFromData(bytes);
99 if (m_thumbnail.width() > 160)
100 m_thumbnail = m_thumbnail.scaledToWidth(160, Qt::SmoothTransformation);
104 void Video::loadMediumThumbnail() {
105 if (m_mediumThumbnailUrl.isEmpty()) return;
106 QObject *reply = The::http()->get(m_mediumThumbnailUrl);
107 connect(reply, SIGNAL(data(QByteArray)), SIGNAL(gotMediumThumbnail(QByteArray)));
110 void Video::loadStreamUrl() {
111 if (loadingStreamUrl) {
112 qDebug() << "Already loading stream URL for" << this->title();
115 loadingStreamUrl = true;
122 void Video::getVideoInfo() {
123 static const QStringList elTypes = QStringList() << "&el=embedded" << "&el=detailpage" << "&el=vevo" << "";
127 if (elIndex == elTypes.size()) {
128 // qDebug() << "Trying special embedded el param";
129 url = QUrl("https://www.youtube.com/get_video_info");
131 #if QT_VERSION >= 0x050000
136 url.addQueryItem("video_id", videoId);
137 url.addQueryItem("el", "embedded");
138 url.addQueryItem("gl", "US");
139 url.addQueryItem("hl", "en");
140 url.addQueryItem("eurl", "https://youtube.googleapis.com/v/" + videoId);
141 url.addQueryItem("asv", "3");
142 url.addQueryItem("sts", "1588");
143 #if QT_VERSION >= 0x050000
148 } else if (elIndex > elTypes.size() - 1) {
149 qWarning() << "Cannot get video info";
150 loadingStreamUrl = false;
151 emit errorStreamUrl("Cannot get video info");
154 // qDebug() << "Trying el param:" << elTypes.at(elIndex) << elIndex;
156 "https://www.youtube.com/get_video_info?video_id=%1%2&ps=default&eurl=&gl=US&hl=en"
157 ).arg(videoId, elTypes.at(elIndex)));
160 QObject *reply = The::http()->get(url);
161 connect(reply, SIGNAL(data(QByteArray)), SLOT(gotVideoInfo(QByteArray)));
162 connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(errorVideoInfo(QNetworkReply*)));
164 // see you in gotVideoInfo...
167 void Video::gotVideoInfo(QByteArray data) {
168 QString videoInfo = QString::fromUtf8(data);
169 // qDebug() << "videoInfo" << videoInfo;
172 QRegExp videoTokeRE(JsFunctions::instance()->videoTokenRE());
173 if (videoTokeRE.indexIn(videoInfo) == -1) {
174 qDebug() << "Cannot get token. Trying next el param" << videoInfo << videoTokeRE.pattern();
175 // Don't panic! We're gonna try another magic "el" param
181 QString videoToken = videoTokeRE.cap(1);
182 // qDebug() << "got token" << videoToken;
183 while (videoToken.contains('%'))
184 videoToken = QByteArray::fromPercentEncoding(videoToken.toLatin1());
185 // qDebug() << "videoToken" << videoToken;
186 this->videoToken = videoToken;
189 QRegExp fmtMapRE(JsFunctions::instance()->videoInfoFmtMapRE());
190 if (fmtMapRE.indexIn(videoInfo) == -1) {
191 // qDebug() << "Cannot get urlMap. Trying next el param";
192 // Don't panic! We're gonna try another magic "el" param
198 // qDebug() << "Got token and urlMap" << elIndex;
200 QString fmtUrlMap = fmtMapRE.cap(1);
201 // qDebug() << "got fmtUrlMap" << fmtUrlMap;
202 fmtUrlMap = QByteArray::fromPercentEncoding(fmtUrlMap.toUtf8());
203 parseFmtUrlMap(fmtUrlMap);
206 void Video::parseFmtUrlMap(const QString &fmtUrlMap, bool fromWebPage) {
208 QString definitionName = settings.value("definition", "360p").toString();
209 int definitionCode = VideoDefinition::getDefinitionCode(definitionName);
211 // qDebug() << "fmtUrlMap" << fmtUrlMap;
212 QStringList formatUrls = fmtUrlMap.split(',', QString::SkipEmptyParts);
213 QHash<int, QString> urlMap;
214 foreach(QString formatUrl, formatUrls) {
215 // qDebug() << "formatUrl" << formatUrl;
216 QStringList urlParams = formatUrl.split('&', QString::SkipEmptyParts);
217 // qDebug() << "urlParams" << urlParams;
222 foreach(QString urlParam, urlParams) {
223 // qWarning() << urlParam;
224 if (urlParam.startsWith("itag=")) {
225 int separator = urlParam.indexOf("=");
226 format = urlParam.mid(separator + 1).toInt();
227 } else if (urlParam.startsWith("url=")) {
228 int separator = urlParam.indexOf("=");
229 url = urlParam.mid(separator + 1);
230 url = QByteArray::fromPercentEncoding(url.toUtf8());
231 } else if (urlParam.startsWith("sig=")) {
232 int separator = urlParam.indexOf("=");
233 sig = urlParam.mid(separator + 1);
234 sig = QByteArray::fromPercentEncoding(sig.toUtf8());
235 } else if (urlParam.startsWith("s=")) {
236 if (fromWebPage || ageGate) {
237 int separator = urlParam.indexOf("=");
238 sig = urlParam.mid(separator + 1);
239 sig = QByteArray::fromPercentEncoding(sig.toUtf8());
241 sig = JsFunctions::instance()->decryptAgeSignature(sig);
243 sig = decryptSignature(sig);
245 sig = JsFunctions::instance()->decryptSignature(sig);
249 QUrl url("http://www.youtube.com/watch");
251 #if QT_VERSION >= 0x050000
256 url.addQueryItem("v", videoId);
257 url.addQueryItem("gl", "US");
258 url.addQueryItem("hl", "en");
259 url.addQueryItem("has_verified", "1");
260 #if QT_VERSION >= 0x050000
264 // qDebug() << "Loading webpage" << url;
265 QObject *reply = The::http()->get(url);
266 connect(reply, SIGNAL(data(QByteArray)), SLOT(scrapeWebPage(QByteArray)));
267 connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(errorVideoInfo(QNetworkReply*)));
268 // see you in scrapWebPage(QByteArray)
273 if (format == -1 || url.isNull()) continue;
275 url += "&signature=" + sig;
277 if (!url.contains("ratebypass"))
278 url += "&ratebypass=yes";
280 // qWarning() << url;
282 if (format == definitionCode) {
283 // qDebug() << "Found format" << definitionCode;
284 QUrl videoUrl = QUrl::fromEncoded(url.toUtf8(), QUrl::StrictMode);
285 m_streamUrl = videoUrl;
286 this->definitionCode = definitionCode;
287 emit gotStreamUrl(videoUrl);
288 loadingStreamUrl = false;
292 urlMap.insert(format, url);
295 QList<int> definitionCodes = VideoDefinition::getDefinitionCodes();
296 int currentIndex = definitionCodes.indexOf(definitionCode);
297 int previousIndex = 0;
298 while (currentIndex >= 0) {
299 previousIndex = currentIndex - 1;
300 if (previousIndex < 0) previousIndex = 0;
301 int definitionCode = definitionCodes.at(previousIndex);
302 if (urlMap.contains(definitionCode)) {
303 // qDebug() << "Found format" << definitionCode;
304 QString url = urlMap.value(definitionCode);
305 QUrl videoUrl = QUrl::fromEncoded(url.toUtf8(), QUrl::StrictMode);
306 m_streamUrl = videoUrl;
307 this->definitionCode = definitionCode;
308 emit gotStreamUrl(videoUrl);
309 loadingStreamUrl = false;
315 emit errorStreamUrl(tr("Cannot get video stream for %1").arg(m_webpage));
318 void Video::errorVideoInfo(QNetworkReply *reply) {
319 loadingStreamUrl = false;
320 emit errorStreamUrl(tr("Network error: %1 for %2").arg(reply->errorString(), reply->url().toString()));
323 void Video::scrapeWebPage(QByteArray data) {
324 QString html = QString::fromUtf8(data);
326 QRegExp ageGateRE(JsFunctions::instance()->ageGateRE());
327 if (ageGateRE.indexIn(html) != -1) {
328 // qDebug() << "Found ageGate";
335 QRegExp fmtMapRE(JsFunctions::instance()->webPageFmtMapRE());
336 if (fmtMapRE.indexIn(html) == -1) {
337 qWarning() << "Error parsing video page";
338 // emit errorStreamUrl("Error parsing video page");
339 // loadingStreamUrl = false;
344 fmtUrlMap = fmtMapRE.cap(1);
345 fmtUrlMap.replace("\\u0026", "&");
346 // parseFmtUrlMap(fmtUrlMap, true);
350 QString definitionName = settings.value("definition", "360p").toString();
351 if (definitionName == QLatin1String("1080p")) {
352 QRegExp dashManifestRe("\"dashmpd\":\\s*\"([^\"]+)\"");
353 if (dashManifestRe.indexIn(html) != -1) {
354 dashManifestUrl = dashManifestRe.cap(1);
355 dashManifestUrl.remove('\\');
356 qDebug() << "dashManifestUrl" << dashManifestUrl;
361 QRegExp jsPlayerRe(JsFunctions::instance()->jsPlayerRE());
362 if (jsPlayerRe.indexIn(html) != -1) {
363 QString jsPlayerUrl = jsPlayerRe.cap(1);
364 jsPlayerUrl.remove('\\');
365 jsPlayerUrl = "http:" + jsPlayerUrl;
366 // qDebug() << "jsPlayerUrl" << jsPlayerUrl;
368 QRegExp jsPlayerIdRe("-(.+)\\.js");
369 jsPlayerIdRe.indexIn(jsPlayerUrl);
370 QString jsPlayerId = jsPlayerRe.cap(1);
372 QObject *reply = The::http()->get(jsPlayerUrl);
373 connect(reply, SIGNAL(data(QByteArray)), SLOT(parseJsPlayer(QByteArray)));
374 connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(errorVideoInfo(QNetworkReply*)));
378 void Video::parseJsPlayer(QByteArray bytes) {
379 QString js = QString::fromUtf8(bytes);
380 // qWarning() << "jsPlayer" << js;
382 // QRegExp funcNameRe("\"signature\"\\w*,\\w*([" + jsNameChars + "]+)");
383 QRegExp funcNameRe(JsFunctions::instance()->signatureFunctionNameRE().arg(jsNameChars));
385 if (funcNameRe.indexIn(js) == -1) {
386 qWarning() << "Cannot capture signature function name" << js;
388 sigFuncName = funcNameRe.cap(1);
389 captureFunction(sigFuncName, js);
390 // qWarning() << sigFunctions;
394 if (!dashManifestUrl.isEmpty()) {
395 QRegExp sigRe("/s/([\\w\\.]+)");
396 if (sigRe.indexIn(dashManifestUrl) != -1) {
397 qDebug() << "Decrypting signature for dash manifest";
398 QString sig = sigRe.cap(1);
399 sig = decryptSignature(sig);
400 dashManifestUrl.replace(sigRe, "/signature/" + sig);
401 qDebug() << dashManifestUrl;
404 // let phonon play the manifest
405 m_streamUrl = dashManifestUrl;
406 this->definitionCode = 37;
407 emit gotStreamUrl(m_streamUrl);
408 loadingStreamUrl = false;
410 // download the manifest
411 QObject *reply = The::http()->get(QUrl::fromEncoded(dashManifestUrl.toUtf8()));
412 connect(reply, SIGNAL(data(QByteArray)), SLOT(parseDashManifest(QByteArray)));
413 connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(errorVideoInfo(QNetworkReply*)));
421 parseFmtUrlMap(fmtUrlMap, true);
424 void Video::parseDashManifest(QByteArray bytes) {
425 QFile file(Temporary::filename() + ".mpd");
426 if (!file.open(QIODevice::WriteOnly))
427 qWarning() << file.errorString() << file.fileName();
428 QDataStream stream(&file);
429 stream.writeRawData(bytes.constData(), bytes.size());
431 m_streamUrl = "file://" + file.fileName();
432 this->definitionCode = 37;
433 emit gotStreamUrl(m_streamUrl);
434 loadingStreamUrl = false;
437 void Video::captureFunction(const QString &name, const QString &js) {
438 QRegExp funcRe("function\\s+" + QRegExp::escape(name) + "\\s*\\([" + jsNameChars + ",\\s]*\\)\\s*\\{[^\\}]+\\}");
439 if (funcRe.indexIn(js) == -1) {
440 qWarning() << "Cannot capture function" << name;
443 QString func = funcRe.cap(0);
444 sigFunctions.insert(name, func);
446 // capture inner functions
447 QRegExp invokedFuncRe("[\\s=;\\(]([" + jsNameChars + "]+)\\s*\\([" + jsNameChars + ",\\s]+\\)");
448 int pos = name.length() + 9;
449 while ((pos = invokedFuncRe.indexIn(func, pos)) != -1) {
450 QString funcName = invokedFuncRe.cap(1);
451 if (!sigFunctions.contains(funcName))
452 captureFunction(funcName, js);
453 pos += invokedFuncRe.matchedLength();
456 // capture referenced objects
457 QRegExp objRe("[\\s=;\\(]([" + jsNameChars + "]+)\\.[" + jsNameChars + "]+");
458 pos = name.length() + 9;
459 while ((pos = objRe.indexIn(func, pos)) != -1) {
460 QString objName = objRe.cap(1);
461 if (!sigObjects.contains(objName))
462 captureObject(objName, js);
463 pos += objRe.matchedLength();
467 void Video::captureObject(const QString &name, const QString &js) {
468 QRegExp re("var\\s+" + QRegExp::escape(name) + "\\s*=\\s*\\{.+\\}\\s*;");
470 if (re.indexIn(js) == -1) {
471 qWarning() << "Cannot capture object" << name;
474 QString obj = re.cap(0);
475 sigObjects.insert(name, obj);
478 QString Video::decryptSignature(const QString &s) {
479 if (sigFuncName.isEmpty()) return QString();
480 QScriptEngine engine;
481 foreach (QString f, sigObjects.values()) {
482 QScriptValue value = engine.evaluate(f);
484 qWarning() << "Error in" << f << value.toString();
486 foreach (QString f, sigFunctions.values()) {
487 QScriptValue value = engine.evaluate(f);
489 qWarning() << "Error in" << f << value.toString();
491 QString js = sigFuncName + "('" + s + "');";
492 QScriptValue value = engine.evaluate(js);
493 if (value.isUndefined()) {
494 qWarning() << "Undefined result for" << js;
497 if (value.isError()) {
498 qWarning() << "Error in" << js << value.toString();
501 return value.toString();
504 QString Video::formattedDuration() const {
505 QString format = m_duration > 3600 ? "h:mm:ss" : "m:ss";
506 return QTime().addSecs(m_duration).toString(format);