]> git.sur5r.net Git - minitube/blob - src/video.cpp
29b74b8125731e125886a0de2e4dc9a3beb99c90
[minitube] / src / video.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 "video.h"
22 #include "networkaccess.h"
23 #include <QtNetwork>
24 #include "videodefinition.h"
25 #include "jsfunctions.h"
26 #include "temporary.h"
27
28 namespace The {
29 NetworkAccess* http();
30 }
31
32 namespace {
33 static const QString jsNameChars = "a-zA-Z0-9\\$_";
34 }
35
36 Video::Video() : m_duration(0),
37     m_viewCount(-1),
38     m_license(LicenseYouTube),
39     definitionCode(0),
40     elIndex(0),
41     ageGate(false),
42     loadingStreamUrl(false),
43     loadingThumbnail(false) {
44 }
45
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;
63     return cloneVideo;
64 }
65
66 const QString &Video::webpage() {
67     if (m_webpage.isEmpty() && !videoId.isEmpty())
68         m_webpage.append("https://www.youtube.com/watch?v=").append(videoId);
69     return m_webpage;
70 }
71
72 void Video::setWebpage(const QString &value) {
73     m_webpage = value;
74
75     // Get Video ID
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;
82             return;
83         }
84         videoId = re.cap(1);
85     }
86 }
87
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)));
93 }
94
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);
101     emit gotThumbnail();
102 }
103
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)));
108 }
109
110 void Video::loadStreamUrl() {
111     if (loadingStreamUrl) {
112         qDebug() << "Already loading stream URL for" << this->title();
113         return;
114     }
115     loadingStreamUrl = true;
116     elIndex = 0;
117     ageGate = false;
118
119     getVideoInfo();
120 }
121
122 void  Video::getVideoInfo() {
123     static const QStringList elTypes = QStringList() << "&el=embedded" << "&el=detailpage" << "&el=vevo" << "";
124
125     QUrl url;
126
127     if (elIndex == elTypes.size()) {
128         // qDebug() << "Trying special embedded el param";
129         url = QUrl("https://www.youtube.com/get_video_info");
130
131 #if QT_VERSION >= 0x050000
132         {
133             QUrl &u = url;
134             QUrlQuery url;
135 #endif
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
144             u.setQuery(url);
145         }
146 #endif
147
148     } else if (elIndex > elTypes.size() - 1) {
149         qWarning() << "Cannot get video info";
150         loadingStreamUrl = false;
151         emit errorStreamUrl("Cannot get video info");
152         return;
153     } else {
154         // qDebug() << "Trying el param:" << elTypes.at(elIndex) << elIndex;
155         url = QUrl(QString(
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)));
158     }
159
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*)));
163
164     // see you in gotVideoInfo...
165 }
166
167 void  Video::gotVideoInfo(QByteArray data) {
168     QString videoInfo = QString::fromUtf8(data);
169     // qDebug() << "videoInfo" << videoInfo;
170
171     // get video token
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
176         elIndex++;
177         getVideoInfo();
178         return;
179     }
180
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;
187
188     // get fmt_url_map
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
193         elIndex++;
194         getVideoInfo();
195         return;
196     }
197
198     // qDebug() << "Got token and urlMap" << elIndex;
199
200     QString fmtUrlMap = fmtMapRE.cap(1);
201     // qDebug() << "got fmtUrlMap" << fmtUrlMap;
202     fmtUrlMap = QByteArray::fromPercentEncoding(fmtUrlMap.toUtf8());
203     parseFmtUrlMap(fmtUrlMap);
204 }
205
206 void Video::parseFmtUrlMap(const QString &fmtUrlMap, bool fromWebPage) {
207     QSettings settings;
208     QString definitionName = settings.value("definition", "360p").toString();
209     int definitionCode = VideoDefinition::getDefinitionCode(definitionName);
210
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;
218
219         int format = -1;
220         QString url;
221         QString sig;
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());
240                     if (ageGate)
241                         sig = JsFunctions::instance()->decryptAgeSignature(sig);
242                     else {
243                         sig = decryptSignature(sig);
244                         if (sig.isEmpty())
245                             sig = JsFunctions::instance()->decryptSignature(sig);
246                     }
247                 } else {
248
249                     QUrl url("http://www.youtube.com/watch");
250
251 #if QT_VERSION >= 0x050000
252                     {
253                         QUrl &u = url;
254                         QUrlQuery url;
255 #endif
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
261                         u.setQuery(url);
262                     }
263 #endif
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)
269                     return;
270                 }
271             }
272         }
273         if (format == -1 || url.isNull()) continue;
274
275         url += "&signature=" + sig;
276
277         if (!url.contains("ratebypass"))
278             url += "&ratebypass=yes";
279
280         // qWarning() << url;
281
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;
289             return;
290         }
291
292         urlMap.insert(format, url);
293     }
294
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;
310             return;
311         }
312         currentIndex--;
313     }
314
315     emit errorStreamUrl(tr("Cannot get video stream for %1").arg(m_webpage));
316 }
317
318 void Video::errorVideoInfo(QNetworkReply *reply) {
319     loadingStreamUrl = false;
320     emit errorStreamUrl(tr("Network error: %1 for %2").arg(reply->errorString(), reply->url().toString()));
321 }
322
323 void Video::scrapeWebPage(QByteArray data) {
324     QString html = QString::fromUtf8(data);
325
326     QRegExp ageGateRE(JsFunctions::instance()->ageGateRE());
327     if (ageGateRE.indexIn(html) != -1) {
328         // qDebug() << "Found ageGate";
329         ageGate = true;
330         elIndex = 4;
331         getVideoInfo();
332         return;
333     }
334
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;
340         elIndex++;
341         getVideoInfo();
342         return;
343     }
344     fmtUrlMap = fmtMapRE.cap(1);
345     fmtUrlMap.replace("\\u0026", "&");
346     // parseFmtUrlMap(fmtUrlMap, true);
347
348 #ifdef APP_DASH
349     QSettings settings;
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;
357         }
358     }
359 #endif
360
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;
367         /*
368                     QRegExp jsPlayerIdRe("-(.+)\\.js");
369                     jsPlayerIdRe.indexIn(jsPlayerUrl);
370                     QString jsPlayerId = jsPlayerRe.cap(1);
371                     */
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*)));
375     }
376 }
377
378 void Video::parseJsPlayer(QByteArray bytes) {
379     QString js = QString::fromUtf8(bytes);
380     // qWarning() << "jsPlayer" << js;
381
382     // QRegExp funcNameRe("\"signature\"\\w*,\\w*([" + jsNameChars + "]+)");
383     QRegExp funcNameRe(JsFunctions::instance()->signatureFunctionNameRE().arg(jsNameChars));
384
385     if (funcNameRe.indexIn(js) == -1) {
386         qWarning() << "Cannot capture signature function name" << js;
387     } else {
388         sigFuncName = funcNameRe.cap(1);
389         captureFunction(sigFuncName, js);
390         // qWarning() << sigFunctions;
391     }
392
393 #ifdef APP_DASH
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;
402
403             if (false) {
404                 // let phonon play the manifest
405                 m_streamUrl = dashManifestUrl;
406                 this->definitionCode = 37;
407                 emit gotStreamUrl(m_streamUrl);
408                 loadingStreamUrl = false;
409             } else {
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*)));
414             }
415
416             return;
417         }
418     }
419 #endif
420
421     parseFmtUrlMap(fmtUrlMap, true);
422 }
423
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());
430
431     m_streamUrl = "file://" + file.fileName();
432     this->definitionCode = 37;
433     emit gotStreamUrl(m_streamUrl);
434     loadingStreamUrl = false;
435 }
436
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;
441         return;
442     }
443     QString func = funcRe.cap(0);
444     sigFunctions.insert(name, func);
445
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();
454     }
455
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();
464     }
465 }
466
467 void Video::captureObject(const QString &name, const QString &js) {
468     QRegExp re("var\\s+" + QRegExp::escape(name) + "\\s*=\\s*\\{.+\\}\\s*;");
469     re.setMinimal(true);
470     if (re.indexIn(js) == -1) {
471         qWarning() << "Cannot capture object" << name;
472         return;
473     }
474     QString obj = re.cap(0);
475     sigObjects.insert(name, obj);
476 }
477
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);
483         if (value.isError())
484             qWarning() << "Error in" << f << value.toString();
485     }
486     foreach (QString f, sigFunctions.values()) {
487         QScriptValue value = engine.evaluate(f);
488         if (value.isError())
489             qWarning() << "Error in" << f << value.toString();
490     }
491     QString js = sigFuncName + "('" + s + "');";
492     QScriptValue value = engine.evaluate(js);
493     if (value.isUndefined()) {
494         qWarning() << "Undefined result for" << js;
495         return QString();
496     }
497     if (value.isError()) {
498         qWarning() << "Error in" << js << value.toString();
499         return QString();
500     }
501     return value.toString();
502 }
503
504 QString Video::formattedDuration() const {
505     QString format = m_duration > 3600 ? "h:mm:ss" : "m:ss";
506     return QTime().addSecs(m_duration).toString(format);
507 }