]> git.sur5r.net Git - minitube/blob - src/video.cpp
4b8e247280ee1057654407c657b138584114e7bd
[minitube] / src / video.cpp
1 #include "video.h"
2 #include "networkaccess.h"
3 #include <QtNetwork>
4 #include "videodefinition.h"
5
6 namespace The {
7     NetworkAccess* http();
8 }
9
10 Video::Video() : m_duration(0),
11 m_viewCount(-1),
12 definitionCode(0),
13 elIndex(0),
14 loadingStreamUrl(false)
15 { }
16
17 Video* Video::clone() {
18     Video* cloneVideo = new Video();
19     cloneVideo->m_title = m_title;
20     cloneVideo->m_description = m_description;
21     cloneVideo->m_author = m_author;
22     cloneVideo->m_authorUri = m_authorUri;
23     cloneVideo->m_webpage = m_webpage;
24     cloneVideo->m_streamUrl = m_streamUrl;
25     cloneVideo->m_thumbnail = m_thumbnail;
26     cloneVideo->m_thumbnailUrls = m_thumbnailUrls;
27     cloneVideo->m_duration = m_duration;
28     cloneVideo->m_published = m_published;
29     cloneVideo->m_viewCount = m_viewCount;
30     cloneVideo->videoId = videoId;
31     cloneVideo->videoToken = videoToken;
32     cloneVideo->definitionCode = definitionCode;
33     return cloneVideo;
34 }
35
36 void Video::preloadThumbnail() {
37     if (m_thumbnailUrls.isEmpty()) return;
38     QObject *reply = The::http()->get(m_thumbnailUrls.first());
39     connect(reply, SIGNAL(data(QByteArray)), SLOT(setThumbnail(QByteArray)));
40 }
41
42 void Video::setThumbnail(QByteArray bytes) {
43     m_thumbnail = QImage::fromData(bytes);
44     emit gotThumbnail();
45 }
46
47 const QImage Video::thumbnail() const {
48     return m_thumbnail;
49 }
50
51 void Video::loadStreamUrl() {
52     if (loadingStreamUrl) {
53         qDebug() << "Already loading stream URL for" << this->title();
54         return;
55     }
56     loadingStreamUrl = true;
57
58     // https://develop.participatoryculture.org/trac/democracy/browser/trunk/tv/portable/flashscraper.py
59
60     // Get Video ID
61     // youtube-dl line 428
62     // QRegExp re("^((?:http://)?(?:\\w+\\.)?youtube\\.com/(?:(?:v/)|(?:(?:watch(?:\\.php)?)?\\?(?:.+&)?v=)))?([0-9A-Za-z_-]+)(?(1).+)?$");
63     QRegExp re("^http://www\\.youtube\\.com/watch\\?v=([0-9A-Za-z_-]+).*");
64     bool match = re.exactMatch(m_webpage.toString());
65     if (!match || re.numCaptures() < 1) {
66         emit errorStreamUrl(QString("Cannot get video id for %1").arg(m_webpage.toString()));
67         loadingStreamUrl = false;
68         return;
69     }
70     videoId = re.cap(1);
71
72     getVideoInfo();
73
74 }
75
76 void  Video::getVideoInfo() {
77     static const QStringList elTypes = QStringList() << "&el=embedded" << "&el=vevo" << "&el=detailpage" << "";
78
79     if (elIndex > elTypes.size() - 1) {
80         loadingStreamUrl = false;
81         emit errorStreamUrl("Cannot get video info");
82         /*
83         // Don't panic! We have a plan B.
84         // get the youtube video webpage
85         qDebug() << "Scraping" << webpage().toString();
86         QObject *reply = The::http()->get(webpage().toString());
87         connect(reply, SIGNAL(data(QByteArray)), SLOT(scrapeWebPage(QByteArray)));
88         connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(errorVideoInfo(QNetworkReply*)));
89         // see you in scrapWebPage(QByteArray)
90         */
91         return;
92     }
93
94     // Get Video Token
95     QUrl videoInfoUrl = QUrl(QString(
96             "http://www.youtube.com/get_video_info?video_id=%1%2&ps=default&eurl=&gl=US&hl=en"
97             ).arg(videoId, elTypes.at(elIndex)));
98
99     QObject *reply = The::http()->get(videoInfoUrl);
100     connect(reply, SIGNAL(data(QByteArray)), SLOT(gotVideoInfo(QByteArray)));
101     connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(errorVideoInfo(QNetworkReply*)));
102
103     // see you in gotVideoInfo...
104
105 }
106
107 void  Video::gotVideoInfo(QByteArray data) {
108     QString videoInfo = QString::fromUtf8(data);
109
110     // qDebug() << "videoInfo" << videoInfo;
111
112     // get video token
113     QRegExp re = QRegExp("^.*&token=([^&]+).*$");
114     bool match = re.exactMatch(videoInfo);
115     // handle regexp failure
116     if (!match || re.numCaptures() < 1) {
117         // Don't panic! We're gonna try another magic "el" param
118         elIndex++;
119         getVideoInfo();
120         return;
121     }
122     QString videoToken = re.cap(1);
123     while (videoToken.contains('%'))
124         videoToken = QByteArray::fromPercentEncoding(videoToken.toAscii());
125     // qDebug() << "videoToken" << videoToken;
126     this->videoToken = videoToken;
127
128     // get fmt_url_map
129     re = QRegExp("^.*&url_encoded_fmt_stream_map=([^&]+).*$");
130     match = re.exactMatch(videoInfo);
131     // handle regexp failure
132     if (!match || re.numCaptures() < 1) {
133         // Don't panic! We're gonna try another magic "el" param
134         elIndex++;
135         getVideoInfo();
136         return;
137     }
138
139     QString fmtUrlMap = re.cap(1);
140     fmtUrlMap = QByteArray::fromPercentEncoding(fmtUrlMap.toUtf8());
141
142     QSettings settings;
143     QString definitionName = settings.value("definition").toString();
144     int definitionCode = VideoDefinition::getDefinitionCode(definitionName);
145
146     // qDebug() << "fmtUrlMap" << fmtUrlMap;
147     QStringList formatUrls = fmtUrlMap.split(",", QString::SkipEmptyParts);
148     QHash<int, QString> urlMap;
149     foreach(QString formatUrl, formatUrls) {
150         // qDebug() << "formatUrl" << formatUrl;
151         QStringList urlParams = formatUrl.split("&", QString::SkipEmptyParts);
152         // qDebug() << "urlParams" << urlParams;
153
154         int format = -1;
155         QString url;
156         QString sig;
157         foreach(QString urlParam, urlParams) {
158             if (urlParam.startsWith("itag=")) {
159                 int separator = urlParam.indexOf("=");
160                 format = urlParam.mid(separator + 1).toInt();
161             } else if (urlParam.startsWith("url=")) {
162                 int separator = urlParam.indexOf("=");
163                 url = urlParam.mid(separator + 1);
164                 url = QByteArray::fromPercentEncoding(url.toUtf8());
165             } else if (urlParam.startsWith("sig=")) {
166                 int separator = urlParam.indexOf("=");
167                 sig = urlParam.mid(separator + 1);
168                 sig = QByteArray::fromPercentEncoding(sig.toUtf8());
169             }
170         }
171         if (format == -1 || url.isNull()) continue;
172
173         url += "&signature=" + sig;
174
175         if (format == definitionCode) {
176             qDebug() << "Found format" << definitionCode;
177             QUrl videoUrl = QUrl::fromEncoded(url.toUtf8(), QUrl::StrictMode);
178             m_streamUrl = videoUrl;
179             this->definitionCode = definitionCode;
180             emit gotStreamUrl(videoUrl);
181             loadingStreamUrl = false;
182             return;
183         }
184
185         urlMap.insert(format, url);
186     }
187
188     QList<int> definitionCodes = VideoDefinition::getDefinitionCodes();
189     int currentIndex = definitionCodes.indexOf(definitionCode);
190     int previousIndex = 0;
191     while (currentIndex >= 0) {
192         previousIndex = currentIndex - 1;
193         if (previousIndex < 0) previousIndex = 0;
194         int definitionCode = definitionCodes.at(previousIndex);
195         if (urlMap.contains(definitionCode)) {
196             qDebug() << "Found format" << definitionCode;
197             QString url = urlMap.value(definitionCode);
198             QUrl videoUrl = QUrl::fromEncoded(url.toUtf8(), QUrl::StrictMode);
199             m_streamUrl = videoUrl;
200             this->definitionCode = definitionCode;
201             emit gotStreamUrl(videoUrl);
202             loadingStreamUrl = false;
203             return;
204         }
205         currentIndex--;
206     }
207
208     emit errorStreamUrl(tr("Cannot get video stream for %1").arg(m_webpage.toString()));
209
210 }
211
212 void Video::foundVideoUrl(QString videoToken, int definitionCode) {
213     // qDebug() << "foundVideoUrl" << videoToken << definitionCode;
214
215     QUrl videoUrl = QUrl(QString(
216             "http://www.youtube.com/get_video?video_id=%1&t=%2&eurl=&el=&ps=&asv=&fmt=%3"
217             ).arg(videoId, videoToken, QString::number(definitionCode)));
218
219     m_streamUrl = videoUrl;
220     loadingStreamUrl = false;
221     emit gotStreamUrl(videoUrl);
222 }
223
224 void Video::errorVideoInfo(QNetworkReply *reply) {
225     loadingStreamUrl = false;
226     emit errorStreamUrl(tr("Network error: %1 for %2").arg(reply->errorString(), reply->url().toString()));
227 }
228
229 void Video::scrapeWebPage(QByteArray data) {
230
231     QString videoHTML = QString::fromUtf8(data);
232     QRegExp re(".*, \"t\": \"([^\"]+)\".*");
233     bool match = re.exactMatch(videoHTML);
234
235     // on regexp failure, stop and report error
236     if (!match || re.numCaptures() < 1) {
237         emit errorStreamUrl("Error parsing video page");
238         loadingStreamUrl = false;
239         return;
240     }
241
242     QString videoToken = re.cap(1);
243     // FIXME proper decode
244     videoToken = videoToken.replace("%3D", "=");
245
246     // we'll need this in gotHeadHeaders()
247     this->videoToken = videoToken;
248
249     // qDebug() << "token" << videoToken;
250
251     QSettings settings;
252     QString definitionName = settings.value("definition").toString();
253     int definitionCode = VideoDefinition::getDefinitionCode(definitionName);
254     if (definitionCode == 18) {
255         // This is assumed always available
256         foundVideoUrl(videoToken, 18);
257     } else {
258         findVideoUrl(definitionCode);
259     }
260
261 }
262
263 void Video::gotHeadHeaders(QNetworkReply* reply) {
264     int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
265     // qDebug() << "gotHeaders" << statusCode;
266     if (statusCode == 200) {
267         foundVideoUrl(videoToken, definitionCode);
268     } else {
269
270         // try next (lower quality) definition
271         /*
272         QStringList definitionNames = VideoDefinition::getDefinitionNames();
273         int currentIndex = definitionNames.indexOf(currentDefinition);
274         int previousIndex = 0;
275         if (currentIndex > 0) {
276             previousIndex = currentIndex - 1;
277         }
278         if (previousIndex > 0) {
279             QString nextDefinitionName = definitionNames.at(previousIndex);
280             findVideoUrl(nextDefinitionName);
281         } else {
282             foundVideoUrl(videoToken, 18);
283         }*/
284
285
286         QList<int> definitionCodes = VideoDefinition::getDefinitionCodes();
287         int currentIndex = definitionCodes.indexOf(definitionCode);
288         int previousIndex = 0;
289         if (currentIndex > 0) {
290             previousIndex = currentIndex - 1;
291             int definitionCode = definitionCodes.at(previousIndex);
292             if (definitionCode == 18) {
293                 // This is assumed always available
294                 foundVideoUrl(videoToken, 18);
295             } else {
296                 findVideoUrl(definitionCode);
297             }
298
299         } else {
300             foundVideoUrl(videoToken, 18);
301         }
302
303     }
304 }
305
306 void Video::findVideoUrl(int definitionCode) {
307     this->definitionCode = definitionCode;
308
309     QUrl videoUrl = QUrl(QString(
310             "http://www.youtube.com/get_video?video_id=%1&t=%2&eurl=&el=&ps=&asv=&fmt=%3"
311             ).arg(videoId, videoToken, QString::number(definitionCode)));
312
313     QObject *reply = The::http()->head(videoUrl);
314     connect(reply, SIGNAL(finished(QNetworkReply*)), SLOT(gotHeadHeaders(QNetworkReply*)));
315     // connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(errorVideoInfo(QNetworkReply*)));
316
317     // see you in gotHeadHeaders()
318
319 }