]> git.sur5r.net Git - minitube/blob - src/video.cpp
Qt5 fixes
[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
27 namespace The {
28 NetworkAccess* http();
29 }
30
31 namespace {
32 static const QString jsNameChars = "a-zA-Z0-9\\$_";
33 }
34
35 Video::Video() : m_duration(0),
36     m_viewCount(-1),
37     definitionCode(0),
38     elIndex(0),
39     ageGate(false),
40     m_license(LicenseYouTube),
41     loadingStreamUrl(false),
42     loadingThumbnail(false)
43 { }
44
45 Video* Video::clone() {
46     Video* cloneVideo = new Video();
47     cloneVideo->m_title = m_title;
48     cloneVideo->m_description = m_description;
49     cloneVideo->m_author = m_author;
50     cloneVideo->m_userId = m_userId;
51     cloneVideo->m_webpage = m_webpage;
52     cloneVideo->m_streamUrl = m_streamUrl;
53     cloneVideo->m_thumbnail = m_thumbnail;
54     cloneVideo->m_thumbnailUrl = m_thumbnailUrl;
55     cloneVideo->m_mediumThumbnailUrl = m_mediumThumbnailUrl;
56     cloneVideo->m_duration = m_duration;
57     cloneVideo->m_published = m_published;
58     cloneVideo->m_viewCount = m_viewCount;
59     cloneVideo->videoId = videoId;
60     cloneVideo->videoToken = videoToken;
61     cloneVideo->definitionCode = definitionCode;
62     return cloneVideo;
63 }
64
65 void Video::setWebpage(QUrl webpage) {
66     m_webpage = webpage;
67
68     // Get Video ID
69     // youtube-dl line 428
70     // QRegExp re("^((?:http://)?(?:\\w+\\.)?youtube\\.com/(?:(?:v/)|(?:(?:watch(?:\\.php)?)?\\?(?:.+&)?v=)))?([0-9A-Za-z_-]+)(?(1).+)?$");
71     QRegExp re("^https?://www\\.youtube\\.com/watch\\?v=([0-9A-Za-z_-]+).*");
72     bool match = re.exactMatch(m_webpage.toString());
73     if (!match) {
74         qWarning() << QString("Cannot get video id for %1").arg(m_webpage.toString());
75         // emit errorStreamUrl(QString("Cannot get video id for %1").arg(m_webpage.toString()));
76         // loadingStreamUrl = false;
77         return;
78     }
79     videoId = re.cap(1);
80 }
81
82 void Video::loadThumbnail() {
83     if (m_thumbnailUrl.isEmpty() || loadingThumbnail) return;
84     loadingThumbnail = true;
85     QObject *reply = The::http()->get(m_thumbnailUrl);
86     connect(reply, SIGNAL(data(QByteArray)), SLOT(setThumbnail(QByteArray)));
87 }
88
89 void Video::setThumbnail(QByteArray bytes) {
90     loadingThumbnail = false;
91     m_thumbnail.loadFromData(bytes);
92     if (m_thumbnail.width() > 160)
93         m_thumbnail = m_thumbnail.scaledToWidth(160, Qt::SmoothTransformation);
94     emit gotThumbnail();
95 }
96
97 void Video::loadMediumThumbnail() {
98     if (m_mediumThumbnailUrl.isEmpty()) return;
99     QObject *reply = The::http()->get(m_mediumThumbnailUrl);
100     connect(reply, SIGNAL(data(QByteArray)), SIGNAL(gotMediumThumbnail(QByteArray)));
101 }
102
103 void Video::loadStreamUrl() {
104     if (loadingStreamUrl) {
105         qDebug() << "Already loading stream URL for" << this->title();
106         return;
107     }
108     loadingStreamUrl = true;
109     elIndex = 0;
110     ageGate = false;
111
112     getVideoInfo();
113 }
114
115 void  Video::getVideoInfo() {
116     static const QStringList elTypes = QStringList() << "&el=embedded" << "&el=detailpage" << "&el=vevo" << "";
117
118     QUrl url;
119
120     if (elIndex == elTypes.size()) {
121         // qDebug() << "Trying special embedded el param";
122         url = QUrl("http://www.youtube.com/get_video_info");
123
124 #if QT_VERSION >= 0x050000
125         {
126             QUrl &u = url;
127             QUrlQuery url;
128 #endif
129             url.addQueryItem("video_id", videoId);
130             url.addQueryItem("el", "embedded");
131             url.addQueryItem("gl", "US");
132             url.addQueryItem("hl", "en");
133             url.addQueryItem("eurl", "https://youtube.googleapis.com/v/" + videoId);
134             url.addQueryItem("asv", "3");
135             url.addQueryItem("sts", "1588");
136 #if QT_VERSION >= 0x050000
137             u.setQuery(url);
138         }
139 #endif
140
141     } else if (elIndex > elTypes.size() - 1) {
142         qWarning() << "Cannot get video info";
143         loadingStreamUrl = false;
144         emit errorStreamUrl("Cannot get video info");
145         return;
146     } else {
147         // qDebug() << "Trying el param:" << elTypes.at(elIndex) << elIndex;
148         url = QUrl(QString(
149                        "http://www.youtube.com/get_video_info?video_id=%1%2&ps=default&eurl=&gl=US&hl=en"
150                        ).arg(videoId, elTypes.at(elIndex)));
151     }
152
153     QObject *reply = The::http()->get(url);
154     connect(reply, SIGNAL(data(QByteArray)), SLOT(gotVideoInfo(QByteArray)));
155     connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(errorVideoInfo(QNetworkReply*)));
156
157     // see you in gotVideoInfo...
158 }
159
160 void  Video::gotVideoInfo(QByteArray data) {
161     QString videoInfo = QString::fromUtf8(data);
162     // qDebug() << "videoInfo" << videoInfo;
163
164     // get video token
165     QRegExp re = QRegExp("^.*&token=([^&]+).*$");
166     bool match = re.exactMatch(videoInfo);
167     // handle regexp failure
168     if (!match) {
169         // qDebug() << "Cannot get token. Trying next el param";
170         // Don't panic! We're gonna try another magic "el" param
171         elIndex++;
172         getVideoInfo();
173         return;
174     }
175
176     QString videoToken = re.cap(1);
177     while (videoToken.contains('%'))
178         videoToken = QByteArray::fromPercentEncoding(videoToken.toLatin1());
179     // qDebug() << "videoToken" << videoToken;
180     this->videoToken = videoToken;
181
182     // get fmt_url_map
183     re = QRegExp("^.*&url_encoded_fmt_stream_map=([^&]+).*$");
184     match = re.exactMatch(videoInfo);
185     // handle regexp failure
186     if (!match) {
187         // qDebug() << "Cannot get urlMap. Trying next el param";
188         // Don't panic! We're gonna try another magic "el" param
189         elIndex++;
190         getVideoInfo();
191         return;
192     }
193
194     // qDebug() << "Got token and urlMap" << elIndex;
195
196     QString fmtUrlMap = re.cap(1);
197     fmtUrlMap = QByteArray::fromPercentEncoding(fmtUrlMap.toUtf8());
198     parseFmtUrlMap(fmtUrlMap);
199 }
200
201 void Video::parseFmtUrlMap(const QString &fmtUrlMap, bool fromWebPage) {
202     QSettings settings;
203     QString definitionName = settings.value("definition", "360p").toString();
204     int definitionCode = VideoDefinition::getDefinitionCode(definitionName);
205
206     // qDebug() << "fmtUrlMap" << fmtUrlMap;
207     QStringList formatUrls = fmtUrlMap.split(',', QString::SkipEmptyParts);
208     QHash<int, QString> urlMap;
209     foreach(QString formatUrl, formatUrls) {
210         // qDebug() << "formatUrl" << formatUrl;
211         QStringList urlParams = formatUrl.split('&', QString::SkipEmptyParts);
212         // qDebug() << "urlParams" << urlParams;
213
214         int format = -1;
215         QString url;
216         QString sig;
217         foreach(QString urlParam, urlParams) {
218             // qWarning() << urlParam;
219             if (urlParam.startsWith("itag=")) {
220                 int separator = urlParam.indexOf("=");
221                 format = urlParam.mid(separator + 1).toInt();
222             } else if (urlParam.startsWith("url=")) {
223                 int separator = urlParam.indexOf("=");
224                 url = urlParam.mid(separator + 1);
225                 url = QByteArray::fromPercentEncoding(url.toUtf8());
226             } else if (urlParam.startsWith("sig=")) {
227                 int separator = urlParam.indexOf("=");
228                 sig = urlParam.mid(separator + 1);
229                 sig = QByteArray::fromPercentEncoding(sig.toUtf8());
230             } else if (urlParam.startsWith("s=")) {
231                 if (fromWebPage || ageGate) {
232                     int separator = urlParam.indexOf("=");
233                     sig = urlParam.mid(separator + 1);
234                     sig = QByteArray::fromPercentEncoding(sig.toUtf8());
235                     if (ageGate)
236                         sig = JsFunctions::instance()->decryptAgeSignature(sig);
237                     else {
238                         sig = decryptSignature(sig);
239                         if (sig.isEmpty())
240                             sig = JsFunctions::instance()->decryptSignature(sig);
241                     }
242                 } else {
243                     // qDebug() << "Loading webpage";
244                     QUrl url("http://www.youtube.com/watch");
245
246 #if QT_VERSION >= 0x050000
247                     {
248                         QUrl &u = url;
249                         QUrlQuery url;
250 #endif
251                         url.addQueryItem("v", videoId);
252                         url.addQueryItem("gl", "US");
253                         url.addQueryItem("hl", "en");
254                         url.addQueryItem("has_verified", "1");
255 #if QT_VERSION >= 0x050000
256                         u.setQuery(url);
257                     }
258 #endif
259                     QObject *reply = The::http()->get(url);
260                     connect(reply, SIGNAL(data(QByteArray)), SLOT(scrapeWebPage(QByteArray)));
261                     connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(errorVideoInfo(QNetworkReply*)));
262                     // see you in scrapWebPage(QByteArray)
263                     return;
264                 }
265             }
266         }
267         if (format == -1 || url.isNull()) continue;
268
269         url += "&signature=" + sig;
270
271         if (!url.contains("ratebypass"))
272             url += "&ratebypass=yes";
273
274         // qWarning() << url;
275
276         if (format == definitionCode) {
277             qDebug() << "Found format" << definitionCode;
278             QUrl videoUrl = QUrl::fromEncoded(url.toUtf8(), QUrl::StrictMode);
279             m_streamUrl = videoUrl;
280             this->definitionCode = definitionCode;
281             emit gotStreamUrl(videoUrl);
282             loadingStreamUrl = false;
283             return;
284         }
285
286         urlMap.insert(format, url);
287     }
288
289     QList<int> definitionCodes = VideoDefinition::getDefinitionCodes();
290     int currentIndex = definitionCodes.indexOf(definitionCode);
291     int previousIndex = 0;
292     while (currentIndex >= 0) {
293         previousIndex = currentIndex - 1;
294         if (previousIndex < 0) previousIndex = 0;
295         int definitionCode = definitionCodes.at(previousIndex);
296         if (urlMap.contains(definitionCode)) {
297             qDebug() << "Found format" << definitionCode;
298             QString url = urlMap.value(definitionCode);
299             QUrl videoUrl = QUrl::fromEncoded(url.toUtf8(), QUrl::StrictMode);
300             m_streamUrl = videoUrl;
301             this->definitionCode = definitionCode;
302             emit gotStreamUrl(videoUrl);
303             loadingStreamUrl = false;
304             return;
305         }
306         currentIndex--;
307     }
308
309     emit errorStreamUrl(tr("Cannot get video stream for %1").arg(m_webpage.toString()));
310 }
311
312 void Video::foundVideoUrl(QString videoToken, int definitionCode) {
313     // qDebug() << "foundVideoUrl" << videoToken << definitionCode;
314
315     QUrl videoUrl = QUrl(QString(
316                              "http://www.youtube.com/get_video?video_id=%1&t=%2&eurl=&el=&ps=&asv=&fmt=%3"
317                              ).arg(videoId, videoToken, QString::number(definitionCode)));
318
319     m_streamUrl = videoUrl;
320     loadingStreamUrl = false;
321     emit gotStreamUrl(videoUrl);
322 }
323
324 void Video::errorVideoInfo(QNetworkReply *reply) {
325     loadingStreamUrl = false;
326     emit errorStreamUrl(tr("Network error: %1 for %2").arg(reply->errorString(), reply->url().toString()));
327 }
328
329 void Video::scrapeWebPage(QByteArray data) {
330     QString html = QString::fromUtf8(data);
331     // qWarning() << html;
332
333     if (html.contains("player-age-gate-content\"")) {
334         // qDebug() << "Found ageGate";
335         ageGate = true;
336         elIndex = 4;
337         getVideoInfo();
338         return;
339     }
340
341     QRegExp re(".*\"url_encoded_fmt_stream_map\":\\s+\"([^\"]+)\".*");
342     bool match = re.exactMatch(html);
343     // on regexp failure, stop and report error
344     if (!match) {
345         qWarning() << "Error parsing video page";
346         // emit errorStreamUrl("Error parsing video page");
347         // loadingStreamUrl = false;
348         elIndex++;
349         getVideoInfo();
350         return;
351     }
352     fmtUrlMap = re.cap(1);
353     fmtUrlMap.replace("\\u0026", "&");
354     // parseFmtUrlMap(fmtUrlMap, true);
355
356     QRegExp jsPlayerRe("\"assets\":.+\"js\":\\s*\"([^\"]+)\"");
357     if (jsPlayerRe.indexIn(html) != -1) {
358         QString jsPlayerUrl = jsPlayerRe.cap(1);
359         jsPlayerUrl.remove('\\');
360         jsPlayerUrl = "http:" + jsPlayerUrl;
361         // qDebug() << "jsPlayerUrl" << jsPlayerUrl;
362         /*
363         QRegExp jsPlayerIdRe("-(.+)\\.js");
364         jsPlayerIdRe.indexIn(jsPlayerUrl);
365         QString jsPlayerId = jsPlayerRe.cap(1);
366         */
367         QObject *reply = The::http()->get(jsPlayerUrl);
368         connect(reply, SIGNAL(data(QByteArray)), SLOT(parseJsPlayer(QByteArray)));
369         connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(errorVideoInfo(QNetworkReply*)));
370     }
371 }
372
373 void Video::gotHeadHeaders(QNetworkReply* reply) {
374     int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
375     // qDebug() << "gotHeaders" << statusCode;
376     if (statusCode == 200) {
377         foundVideoUrl(videoToken, definitionCode);
378     } else {
379
380         // try next (lower quality) definition
381         /*
382         QStringList definitionNames = VideoDefinition::getDefinitionNames();
383         int currentIndex = definitionNames.indexOf(currentDefinition);
384         int previousIndex = 0;
385         if (currentIndex > 0) {
386             previousIndex = currentIndex - 1;
387         }
388         if (previousIndex > 0) {
389             QString nextDefinitionName = definitionNames.at(previousIndex);
390             findVideoUrl(nextDefinitionName);
391         } else {
392             foundVideoUrl(videoToken, 18);
393         }*/
394
395
396         QList<int> definitionCodes = VideoDefinition::getDefinitionCodes();
397         int currentIndex = definitionCodes.indexOf(definitionCode);
398         int previousIndex = 0;
399         if (currentIndex > 0) {
400             previousIndex = currentIndex - 1;
401             int definitionCode = definitionCodes.at(previousIndex);
402             if (definitionCode == 18) {
403                 // This is assumed always available
404                 foundVideoUrl(videoToken, 18);
405             } else {
406                 findVideoUrl(definitionCode);
407             }
408
409         } else {
410             foundVideoUrl(videoToken, 18);
411         }
412
413     }
414 }
415
416 void Video::parseJsPlayer(QByteArray bytes) {
417     QString js = QString::fromUtf8(bytes);
418     // qWarning() << "jsPlayer" << js;
419     QRegExp funcNameRe("signature=([" + jsNameChars + "]+)");
420     if (funcNameRe.indexIn(js) == -1) {
421         qWarning() << "Cannot capture signature function name";
422     } else {
423         sigFuncName = funcNameRe.cap(1);
424         captureFunction(sigFuncName, js);
425         // qWarning() << sigFunctions;
426     }
427     parseFmtUrlMap(fmtUrlMap, true);
428 }
429
430 void Video::captureFunction(const QString &name, const QString &js) {
431     QRegExp funcRe("function\\s+" + QRegExp::escape(name) + "\\s*\\([" + jsNameChars + ",\\s]*\\)\\s*\\{[^\\}]+\\}");
432     if (funcRe.indexIn(js) == -1) {
433         qWarning() << "Cannot capture function" << name;
434         return;
435     }
436     QString func = funcRe.cap(0);
437     sigFunctions.insert(name, func);
438
439     // capture inner functions
440     QRegExp invokedFuncRe("[\\s=;\\(]([" + jsNameChars + "]+)\\s*\\([" + jsNameChars + ",\\s]+\\)");
441     int pos = name.length() + 9;
442     while ((pos = invokedFuncRe.indexIn(func, pos)) != -1) {
443         QString funcName = invokedFuncRe.cap(1);
444         if (!sigFunctions.contains(funcName))
445             captureFunction(funcName, js);
446         pos += invokedFuncRe.matchedLength();
447     }
448
449     // capture referenced objects
450     QRegExp objRe("[\\s=;\\(]([" + jsNameChars + "]+)\\.[" + jsNameChars + "]+");
451     pos = name.length() + 9;
452     while ((pos = objRe.indexIn(func, pos)) != -1) {
453         QString objName = objRe.cap(1);
454         if (!sigObjects.contains(objName))
455             captureObject(objName, js);
456         pos += objRe.matchedLength();
457     }
458 }
459
460 void Video::captureObject(const QString &name, const QString &js) {
461     QRegExp re("var\\s+" + QRegExp::escape(name) + "\\s*=\\s*\\{.+\\}\\s*;");
462     re.setMinimal(true);
463     if (re.indexIn(js) == -1) {
464         qWarning() << "Cannot capture object" << name;
465         return;
466     }
467     QString obj = re.cap(0);
468     sigObjects.insert(name, obj);
469 }
470
471 QString Video::decryptSignature(const QString &s) {
472     if (sigFuncName.isEmpty()) return QString();
473     QScriptEngine engine;
474     foreach (QString f, sigObjects.values()) {
475         QScriptValue value = engine.evaluate(f);
476         if (value.isError())
477             qWarning() << "Error in" << f << value.toString();
478     }
479     foreach (QString f, sigFunctions.values()) {
480         QScriptValue value = engine.evaluate(f);
481         if (value.isError())
482             qWarning() << "Error in" << f << value.toString();
483     }
484     QString js = sigFuncName + "('" + s + "');";
485     QScriptValue value = engine.evaluate(js);
486     if (value.isUndefined()) {
487         qWarning() << "Undefined result for" << js;
488         return QString();
489     }
490     if (value.isError()) {
491         qWarning() << "Error in" << js << value.toString();
492         return QString();
493     }
494     return value.toString();
495 }
496
497 void Video::findVideoUrl(int definitionCode) {
498     this->definitionCode = definitionCode;
499
500     QUrl videoUrl = QUrl(QString(
501                              "http://www.youtube.com/get_video?video_id=%1&t=%2&eurl=&el=&ps=&asv=&fmt=%3"
502                              ).arg(videoId, videoToken, QString::number(definitionCode)));
503
504     QObject *reply = The::http()->head(videoUrl);
505     connect(reply, SIGNAL(finished(QNetworkReply*)), SLOT(gotHeadHeaders(QNetworkReply*)));
506     // connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(errorVideoInfo(QNetworkReply*)));
507
508     // see you in gotHeadHeaders()
509 }
510
511 QString Video::formattedDuration() const {
512     QString format = m_duration > 3600 ? "h:mm:ss" : "m:ss";
513     return QTime().addSecs(m_duration).toString(format);
514 }