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