source: smplayer/trunk/src/youtube/retrieveyoutubeurl.cpp@ 170

Last change on this file since 170 was 170, checked in by Silvan Scherrer, 11 years ago

SMPlayer: updated trunk to 14.9.0

File size: 17.7 KB
Line 
1/* smplayer, GUI front-end for mplayer.
2 Copyright (C) 2006-2014 Ricardo Villalba <rvm@users.sourceforge.net>
3 Copyright (C) 2010 Ori Rejwan
4
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18*/
19
20#include "retrieveyoutubeurl.h"
21#include <QUrl>
22#include <QRegExp>
23#include <QStringList>
24#include <QFile>
25#include "ytsig.h"
26
27#if QT_VERSION >= 0x050000
28#include <QUrlQuery>
29#endif
30
31#define USE_PLAYER_NAME
32
33QString RetrieveYoutubeUrl::user_agent;
34bool RetrieveYoutubeUrl::use_https_main = false;
35bool RetrieveYoutubeUrl::use_https_vi = false;
36
37RetrieveYoutubeUrl::RetrieveYoutubeUrl( QObject* parent ) : QObject(parent)
38{
39 reply = 0;
40 manager = new QNetworkAccessManager(this);
41
42 preferred_quality = FLV_360p;
43}
44
45RetrieveYoutubeUrl::~RetrieveYoutubeUrl() {
46}
47
48void RetrieveYoutubeUrl::fetchPage(const QString & url) {
49 qDebug("RetrieveYoutubeUrl::fetchPage: url: %s", url.toUtf8().constData());
50 qDebug("RetrieveYoutubeUrl::fetchPage: user agent: '%s'", user_agent.toLatin1().constData());
51
52 QNetworkRequest req(url);
53 req.setRawHeader("User-Agent", user_agent.toLatin1());
54 req.setRawHeader("Accept-Language", "en-us,en;q=0.5");
55 reply = manager->get(req);
56 connect(reply, SIGNAL(finished()), this, SLOT(gotResponse()));
57 orig_url = url;
58
59 emit connecting(QUrl(url).host());
60
61#ifdef YT_GET_VIDEOINFO
62 video_id = getVideoID(url);
63#endif
64}
65
66#ifdef YT_GET_VIDEOINFO
67void RetrieveYoutubeUrl::fetchVideoInfoPage(QString url) {
68 if (url.isEmpty()) {
69 QString scheme = use_https_vi ? "https" : "http";
70 url = QString("%2://www.youtube.com/get_video_info?el=detailpage&ps=default&eurl=&gl=US&hl=en&video_id=%1").arg(video_id).arg(scheme);
71 }
72 qDebug("RetrieveYoutubeUrl::fetchVideoInfoPage: url: %s...", url.left(20).toUtf8().constData());
73
74 qDebug("RetrieveYoutubeUrl::fetchPage: user agent: '%s'", user_agent.toLatin1().constData());
75
76 YTSig::check(url);
77 QNetworkRequest req(url);
78 req.setRawHeader("User-Agent", user_agent.toLatin1());
79 req.setRawHeader("Accept-Language", "en-us,en;q=0.5");
80 reply = manager->get(req);
81 connect(reply, SIGNAL(finished()), this, SLOT(gotVideoInfoResponse()));
82
83 emit connecting(QUrl(url).host());
84}
85#endif
86
87void RetrieveYoutubeUrl::close() {
88 if (reply) reply->abort();
89}
90
91void RetrieveYoutubeUrl::gotResponse() {
92 qDebug("RetrieveYoutubeUrl::gotResponse");
93
94 QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
95
96 if (reply->error() == QNetworkReply::NoError) {
97 int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
98 qDebug("RetrieveYoutubeUrl::gotResponse: status: %d", status);
99 switch (status) {
100 case 301:
101 case 302:
102 case 307:
103 QString r_url = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl().toString();
104 qDebug("RetrieveYoutubeUrl::gotResponse: redirected: %s", r_url.toLatin1().constData());
105 fetchPage(r_url);
106 return;
107 }
108 } else {
109 qDebug("RetrieveYoutubeUrl::gotResponse: error %d: '%s'", (int)reply->error(), reply->errorString().toUtf8().constData());
110 emit errorOcurred((int)reply->error(), reply->errorString());
111 return;
112 }
113 parse(reply->readAll());
114}
115
116#ifdef YT_GET_VIDEOINFO
117void RetrieveYoutubeUrl::gotVideoInfoResponse() {
118 qDebug("RetrieveYoutubeUrl::gotVideoInfoResponse");
119
120 QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
121
122 if (reply->error() == QNetworkReply::NoError) {
123 int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
124 qDebug("RetrieveYoutubeUrl::gotVideoInfoResponse: status: %d", status);
125 switch (status) {
126 case 301:
127 case 302:
128 case 307:
129 QString r_url = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl().toString();
130 //qDebug("RetrieveYoutubeUrl::gotVideoInfoResponse: redirected: %s", r_url.toLatin1().constData());
131 fetchVideoInfoPage(r_url);
132 return;
133 }
134 } else {
135 qDebug("RetrieveYoutubeUrl::gotVideoInfoResponse: error %d: '%s'", (int)reply->error(), reply->errorString().toUtf8().constData());
136 emit errorOcurred((int)reply->error(), reply->errorString());
137 return;
138 }
139 parseVideoInfo(reply->readAll());
140}
141#endif
142
143void RetrieveYoutubeUrl::parse(QByteArray text) {
144 qDebug("RetrieveYoutubeUrl::parse");
145
146 urlMap.clear();
147
148 QString replyString = QString::fromUtf8(text);
149
150 QRegExp rx_title(".*<title>(.*)</title>.*");
151 if (rx_title.indexIn(replyString) != -1) {
152 url_title = rx_title.cap(1).simplified();
153 url_title = QString(url_title).replace("&amp;","&").replace("&gt;", ">").replace("&lt;", "<").replace("&quot;","\"").replace("&#39;","'")/*.replace(" - YouTube", "")*/;
154 qDebug("RetrieveYoutubeUrl::parse: title '%s'", url_title.toUtf8().constData());
155 } else {
156 url_title = "Youtube video";
157 }
158
159 //qDebug("RetrieveYoutubeUrl::parse: replyString: %s",replyString.toLatin1().constData());
160
161 QString player;
162 QRegExp rxplayer("html5player-([\\d,\\w,-]+)\\\\");
163 if (rxplayer.indexIn(replyString) != -1) {
164 player = rxplayer.cap(1);
165 qDebug("RetrieveYoutubeUrl::parse: html5player: %s", player.toLatin1().constData());
166 }
167
168 QString fmtArray;
169 QRegExp regex("\\\"url_encoded_fmt_stream_map\\\"\\s*:\\s*\\\"([^\\\"]*)");
170 if (regex.indexIn(replyString) != -1) {
171 fmtArray = regex.cap(1);
172 }
173
174#ifdef YT_DASH_SUPPORT
175 QRegExp regex2("\\\"adaptive_fmts\\\"\\s*:\\s*\\\"([^\\\"]*)");
176 if (regex2.indexIn(replyString) != -1) {
177 if (!fmtArray.isEmpty()) fmtArray += ",";
178 fmtArray += regex2.cap(1);
179 }
180#endif
181
182 fmtArray = sanitizeForUnicodePoint(fmtArray);
183 fmtArray.replace(QRegExp("\\\\(.)"), "\\1");
184
185#ifndef YT_GET_VIDEOINFO
186 bool failed_to_decrypt_signature = false;
187#endif
188
189 #if QT_VERSION >= 0x050000
190 QUrlQuery * q = new QUrlQuery();
191 #endif
192
193 QList<QByteArray> codeList = fmtArray.toLatin1().split(',');
194 foreach(QByteArray code, codeList) {
195 code = QUrl::fromPercentEncoding(code).toLatin1();
196 //qDebug("code: %s", code.constData());
197
198 QUrl line;
199 #if QT_VERSION >= 0x050000
200 q->setQuery(code);
201 #else
202 QUrl * q = &line;
203 q->setEncodedQuery(code);
204 #endif
205
206 if (q->hasQueryItem("url")) {
207 QUrl url( q->queryItemValue("url") );
208 line.setScheme(url.scheme());
209 line.setHost(url.host());
210 line.setPath(url.path());
211 q->removeQueryItem("url");
212 #if QT_VERSION >= 0x050000
213 q->setQuery( q->query(QUrl::FullyDecoded) + "&" + url.query(QUrl::FullyDecoded) );
214 /*
215 QList < QPair < QString,QString > > l = q->queryItems();
216 for (int n=0; n < l.count(); n++) {
217 qDebug("n: %d, %s = %s", n, l[n].first.toLatin1().constData(), l[n].second.toLatin1().constData());
218 }
219 */
220 #else
221 q->setEncodedQuery( q->encodedQuery() + "&" + url.encodedQuery() );
222 /*
223 QList < QPair < QString,QString > > l = q->queryItems();
224 for (int n=0; n < l.count(); n++) {
225 qDebug("n: %d, %s = %s", n, l[n].first.toLatin1().constData(), l[n].second.toLatin1().constData());
226 }
227 */
228 #endif
229
230 if (q->hasQueryItem("sig")) {
231 q->addQueryItem("signature", q->queryItemValue("sig"));
232 q->removeQueryItem("sig");
233 }
234 else
235 if (q->hasQueryItem("s")) {
236 #ifdef USE_PLAYER_NAME
237 QString signature = YTSig::aclara(q->queryItemValue("s"), player);
238 #else
239 QString signature = YTSig::aclara(q->queryItemValue("s"));
240 #endif
241 if (!signature.isEmpty()) {
242 q->addQueryItem("signature", signature);
243 } else {
244 #ifndef YT_GET_VIDEOINFO
245 failed_to_decrypt_signature = true;
246 #endif
247 }
248 q->removeQueryItem("s");
249 }
250 q->removeAllQueryItems("fallback_host");
251 q->removeAllQueryItems("type");
252
253 if (!q->hasQueryItem("ratebypass")) q->addQueryItem("ratebypass", "yes");
254
255 if ((q->hasQueryItem("itag")) && (q->hasQueryItem("signature"))) {
256 QString itag = q->queryItemValue("itag");
257
258 // Remove duplicated queries
259 QPair <QString,QString> item;
260 QList<QPair<QString, QString> > items = q->queryItems();
261 foreach(item, items) {
262 q->removeAllQueryItems(item.first);
263 q->addQueryItem(item.first, item.second);
264 }
265
266 #if QT_VERSION >= 0x050000
267 line.setQuery(q->query(QUrl::FullyDecoded));
268 #endif
269
270 #ifdef YT_GET_VIDEOINFO
271 if (!line.toString().startsWith("https")) {
272 urlMap[itag.toInt()] = line.toString();
273 }
274 #else
275 urlMap[itag.toInt()] = line.toString();
276 #endif
277
278 //qDebug("itag: %s line: %s", itag.toLatin1().constData(), line.toString().toLatin1().constData());
279 }
280 }
281 }
282
283 #if QT_VERSION >= 0x050000
284 delete q;
285 #endif
286
287 qDebug("RetrieveYoutubeUrl::parse: url count: %d", urlMap.count());
288
289#ifndef YT_GET_VIDEOINFO
290 if ((urlMap.count() == 0) && (failed_to_decrypt_signature)) {
291 qDebug("RetrieveYoutubeUrl::parse: no url found with valid signature");
292 emit signatureNotFound(url_title);
293 return;
294 }
295#else
296 if (urlMap.count() == 0) {
297 qDebug("RetrieveYoutubeUrl::parse: no url found with valid signature");
298 fetchVideoInfoPage();
299 return;
300 }
301#endif
302
303 QString p_url = findPreferredUrl();
304 //qDebug("p_url: '%s'", p_url.toLatin1().constData());
305
306 if (!p_url.isNull()) {
307 emit gotUrls(urlMap);
308 emit gotPreferredUrl(p_url);
309 #ifdef YT_GET_VIDEOINFO
310 emit gotVideoInfo(urlMap, url_title, video_id);
311 #endif
312 } else {
313 emit gotEmptyList();
314 }
315}
316
317QString RetrieveYoutubeUrl::getVideoID(QString video_url) {
318 if (video_url.contains("youtu.be/")) {
319 video_url.replace("youtu.be/", "youtube.com/watch?v=");
320 }
321 else
322 if (video_url.contains("y2u.be/")) {
323 video_url.replace("y2u.be/", "youtube.com/watch?v=");
324 }
325 else
326 if (video_url.contains("m.youtube.com")) {
327 video_url.replace("m.youtube.com", "www.youtube.com");
328 }
329
330 if ((video_url.startsWith("youtube.com")) || (video_url.startsWith("www.youtube.com"))) video_url = "http://" + video_url;
331
332 //qDebug("RetrieveYoutubeUrl::getVideoID: video_url: %s", video_url.toLatin1().constData());
333
334 QUrl url(video_url);
335
336 QString ID;
337
338#if QT_VERSION >= 0x050000
339 QUrlQuery * q = new QUrlQuery(url);
340#else
341 const QUrl * q = &url;
342#endif
343
344 if ((url.host().contains("youtube")) && (url.path().contains("watch_videos"))) {
345 if (q->hasQueryItem("video_ids")) {
346 int index = 0;
347 if (q->hasQueryItem("index")) index = q->queryItemValue("index").toInt();
348 QStringList list = q->queryItemValue("video_ids").split(",");
349 if (index < list.count()) ID = list[index];
350 }
351 }
352 else
353 if ((url.host().contains("youtube")) && (url.path().contains("watch"))) {
354 if (q->hasQueryItem("v")) {
355 ID = q->queryItemValue("v");
356 }
357 }
358
359#if QT_VERSION >= 0x050000
360 delete q;
361#endif
362
363 //qDebug("RetrieveYoutubeUrl::getVideoID: ID: %s", ID.toLatin1().constData());
364
365 return ID;
366}
367
368bool RetrieveYoutubeUrl::isUrlSupported(const QString & url) {
369 return (!getVideoID(url).isEmpty());
370}
371
372QString RetrieveYoutubeUrl::fullUrl(const QString & url) {
373 QString r;
374 QString ID = getVideoID(url);
375 if (!ID.isEmpty()) {
376 QString scheme = use_https_main ? "https" : "http";
377 r = scheme + "://www.youtube.com/watch?v=" + ID;
378 }
379 return r;
380}
381
382#ifdef YT_GET_VIDEOINFO
383void RetrieveYoutubeUrl::parseVideoInfo(QByteArray text) {
384 urlMap.clear();
385
386 #if QT_VERSION >= 0x050000
387 QUrlQuery all;
388 all.setQuery(text);
389 #else
390 QUrl all;
391 all.setEncodedQuery(text);
392 #endif
393
394 QByteArray fmtArray;
395 fmtArray = all.queryItemValue("url_encoded_fmt_stream_map").toLatin1();
396
397#ifdef YT_DASH_SUPPORT
398 if (!fmtArray.isEmpty()) fmtArray += ",";
399 fmtArray += all.queryItemValue("adaptive_fmts").toLatin1();
400#endif
401
402 /*
403 qDebug("fmtArray: %s", fmtArray.constData());
404 return;
405 */
406
407 bool failed_to_decrypt_signature = false;
408
409 #if QT_VERSION >= 0x050000
410 QUrlQuery * q = new QUrlQuery();
411 #endif
412
413 QList<QByteArray> codeList = fmtArray.split(',');
414 foreach(QByteArray code, codeList) {
415 code = QUrl::fromPercentEncoding(code).toLatin1();
416 //qDebug("code: %s", code.constData());
417
418 QUrl line;
419 #if QT_VERSION >= 0x050000
420 q->setQuery(code);
421 #else
422 QUrl * q = &line;
423 q->setEncodedQuery(code);
424 #endif
425
426 if (q->hasQueryItem("url")) {
427 QUrl url( q->queryItemValue("url") );
428 line.setScheme(url.scheme());
429 line.setHost(url.host());
430 line.setPath(url.path());
431 q->removeQueryItem("url");
432 #if QT_VERSION >= 0x050000
433 q->setQuery( q->query(QUrl::FullyDecoded) + "&" + url.query(QUrl::FullyDecoded) );
434 /*
435 QList < QPair < QString,QString > > l = q->queryItems();
436 for (int n=0; n < l.count(); n++) {
437 qDebug("n: %d, %s = %s", n, l[n].first.toLatin1().constData(), l[n].second.toLatin1().constData());
438 }
439 */
440 #else
441 q->setEncodedQuery( q->encodedQuery() + "&" + url.encodedQuery() );
442 /*
443 QList < QPair < QString,QString > > l = q->queryItems();
444 for (int n=0; n < l.count(); n++) {
445 qDebug("n: %d, %s = %s", n, l[n].first.toLatin1().constData(), l[n].second.toLatin1().constData());
446 }
447 */
448 #endif
449
450 if (q->hasQueryItem("sig")) {
451 q->addQueryItem("signature", q->queryItemValue("sig"));
452 q->removeQueryItem("sig");
453 }
454 else
455 if (q->hasQueryItem("s")) {
456 QString signature = YTSig::aclara(q->queryItemValue("s"), "", "aclara_f");
457 if (!signature.isEmpty()) {
458 q->addQueryItem("signature", signature);
459 } else {
460 failed_to_decrypt_signature = true;
461 }
462 q->removeQueryItem("s");
463 }
464 q->removeAllQueryItems("fallback_host");
465 q->removeAllQueryItems("type");
466
467 if (!q->hasQueryItem("ratebypass")) q->addQueryItem("ratebypass", "yes");
468
469 if ((q->hasQueryItem("itag")) && (q->hasQueryItem("signature"))) {
470 QString itag = q->queryItemValue("itag");
471
472 // Remove duplicated queries
473 QPair <QString,QString> item;
474 QList<QPair<QString, QString> > items = q->queryItems();
475 foreach(item, items) {
476 q->removeAllQueryItems(item.first);
477 q->addQueryItem(item.first, item.second);
478 }
479
480 #if QT_VERSION >= 0x050000
481 line.setQuery(q->query(QUrl::FullyDecoded));
482 #endif
483 urlMap[itag.toInt()] = line.toString();
484 //qDebug("itag: %s line: %s", itag.toLatin1().constData(), line.toString().toLatin1().constData());
485 }
486 }
487 }
488
489 #if QT_VERSION >= 0x050000
490 delete q;
491 #endif
492
493 qDebug("RetrieveYoutubeUrl::parseVideoInfo: url count: %d", urlMap.count());
494
495 if ((urlMap.count() == 0) && (failed_to_decrypt_signature)) {
496 qDebug("RetrieveYoutubeUrl::parseVideoInfo: no url found with valid signature");
497 emit signatureNotFound(url_title);
498 return;
499 }
500
501 QString p_url = findPreferredUrl();
502 //qDebug("p_url: '%s'", p_url.toLatin1().constData());
503
504 if (!p_url.isNull()) {
505 emit gotUrls(urlMap);
506 emit gotPreferredUrl(p_url);
507 #ifdef YT_GET_VIDEOINFO
508 emit gotVideoInfo(urlMap, url_title, video_id);
509 #endif
510 } else {
511 emit gotEmptyList();
512 }
513}
514#endif
515
516QString RetrieveYoutubeUrl::findPreferredUrl() {
517 latest_preferred_url = findPreferredUrl(urlMap, preferred_quality);
518 return latest_preferred_url;
519}
520
521QString RetrieveYoutubeUrl::findPreferredUrl(const QMap<int, QString>& urlMap, Quality q) {
522 // Choose a url according to preferred quality
523 QString p_url;
524 //Quality q = preferred_quality;
525
526 if (q==MP4_1080p) {
527 p_url = urlMap.value(MP4_1080p, QString());
528 if (p_url.isNull()) p_url= urlMap.value(WEBM_1080p, QString());
529 if (p_url.isNull()) q = MP4_720p;
530 }
531
532 if (q==WEBM_1080p) {
533 p_url = urlMap.value(WEBM_1080p, QString());
534 if (p_url.isNull()) p_url= urlMap.value(MP4_1080p, QString());
535 if (p_url.isNull()) q = WEBM_720p;
536 }
537
538 if (q==MP4_720p) {
539 p_url = urlMap.value(MP4_720p, QString());
540 if (p_url.isNull()) p_url= urlMap.value(WEBM_720p, QString());
541 if (p_url.isNull()) p_url = urlMap.value(WEBM_480p, QString());
542 if (p_url.isNull()) q = MP4_360p;
543 }
544
545 if (q==WEBM_720p) {
546 p_url = urlMap.value(WEBM_720p, QString());
547 if (p_url.isNull()) p_url= urlMap.value(MP4_720p, QString());
548 if (p_url.isNull()) q = WEBM_480p;
549 }
550
551 if (q==WEBM_480p) {
552 p_url = urlMap.value(WEBM_480p, QString());
553 if (p_url.isNull()) q = WEBM_360p;
554 }
555
556 if (q==MP4_360p) {
557 p_url = urlMap.value(MP4_360p, QString());
558 if (p_url.isNull()) p_url= urlMap.value(WEBM_360p, QString());
559 if (p_url.isNull()) q = FLV_360p;
560 }
561
562 if (q==WEBM_360p) {
563 p_url = urlMap.value(WEBM_360p, QString());
564 if (p_url.isNull()) p_url= urlMap.value(MP4_360p, QString());
565 if (p_url.isNull()) q = FLV_360p;
566 }
567
568 // FLV, low priority
569 if (q==FLV_480p) {
570 p_url = urlMap.value(FLV_480p, QString());
571 if (p_url.isNull()) q = FLV_360p;
572 }
573
574 if (q==FLV_360p) {
575 p_url = urlMap.value(FLV_360p, QString());
576 if (p_url.isNull()) q = FLV_240p;
577 }
578
579 if (q==FLV_240p) {
580 p_url = urlMap.value(q, QString());
581 }
582
583 return p_url;
584}
585
586#ifdef YT_DASH_SUPPORT
587QString RetrieveYoutubeUrl::findBestAudio(const QMap<int, QString>& urlMap) {
588 QString url;
589
590 url = urlMap.value(DASH_AUDIO_MP4_256, QString());
591 if (!url.isEmpty()) return url;
592
593 url = urlMap.value(DASH_AUDIO_WEBM_192, QString());
594 if (!url.isEmpty()) return url;
595
596 url = urlMap.value(DASH_AUDIO_MP4_128, QString());
597 if (!url.isEmpty()) return url;
598
599 url = urlMap.value(DASH_AUDIO_WEBM_128, QString());
600 if (!url.isEmpty()) return url;
601
602 url = urlMap.value(DASH_AUDIO_MP4_48, QString());
603 if (!url.isEmpty()) return url;
604
605 return url;
606}
607#endif
608
609QString RetrieveYoutubeUrl::sanitizeForUnicodePoint(QString string) {
610 QRegExp rx("\\\\u(\\d{4})");
611 while (rx.indexIn(string) != -1) {
612 string.replace(rx.cap(0), QString(QChar(rx.cap(1).toInt(0,16))));
613 }
614 return string;
615}
616
617void RetrieveYoutubeUrl::htmlDecode(QString& string) {
618 string.replace("%3A", ":", Qt::CaseInsensitive);
619 string.replace("%2F", "/", Qt::CaseInsensitive);
620 string.replace("%3F", "?", Qt::CaseInsensitive);
621 string.replace("%3D", "=", Qt::CaseInsensitive);
622 string.replace("%25", "%", Qt::CaseInsensitive);
623 string.replace("%26", "&", Qt::CaseInsensitive);
624 string.replace("%3D", "=", Qt::CaseInsensitive);
625}
626
627#include "moc_retrieveyoutubeurl.cpp"
Note: See TracBrowser for help on using the repository browser.