BBC iPlayer video download

This script allows to save videos from BBC iPlayer.

目前為 2016-08-22 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name BBC iPlayer video download
  3. // @namespace http://andrealazzarotto.com/
  4. // @include http://www.bbc.co.uk/*
  5. // @version 3.6.1
  6. // @description This script allows to save videos from BBC iPlayer.
  7. // @copyright 2015+, Andrea Lazzarotto - GPLv3 License
  8. // @require http://code.jquery.com/jquery-latest.min.js
  9. // @grant GM_xmlhttpRequest
  10. // @connect edgesuite.net
  11. // @connect bbc.co.uk
  12. // @connect akamaihd.net
  13. // @license GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html
  14. // ==/UserScript==
  15.  
  16. var added = 0;
  17.  
  18. var get_title = function(name) {
  19. var title = name || $('meta[property="og:title"]').attr('content') || 'output';
  20. return title.replace(/\W+/g, '_');
  21. };
  22.  
  23. var place_link_box = function(element, id) {
  24. element.after('<div id="' + id + '"></div>');
  25. $('#' + id).css({
  26. 'padding': '.75em',
  27. 'margin': '25px auto',
  28. 'width': $('#player-outer-outer').width(),
  29. 'border': '1px solid #444',
  30. 'background-color': '#252525',
  31. 'color': 'white',
  32. 'font-family': 'sans-serif',
  33. 'box-sizing': 'border-box',
  34. 'font-size': '0.85rem'
  35. });
  36. };
  37.  
  38. var appendURL = function(element, url, ext, kind, title) {
  39. var extension = ext || 'mp4';
  40. var type = kind || 'video';
  41. var codec = type == 'video' ? ' -codec copy -qscale 0 -bsf:a aac_adtstoasc ' : ' ';
  42. var tool = 'ffmpeg';
  43. var safe_title = get_title(title);
  44. var id = 'direct-link-' + safe_title + (added++);
  45. element.after('<div id=' + id + '"></div>');
  46. place_link_box(element, id);
  47. $('#' + id).append('<p>To record the ' + type + ', use <code>' + tool + '</code> with the following command line:</p>' +
  48. '<pre>' + tool + ' -v 16 -stats -i "' + url + '"' + codec + safe_title + '.' + extension + '</pre>' +
  49. /*'<p>If you get an error about <code>Malformed AAC bitstream</code>, add option ' +
  50. '<code>-bsf:a aac_adtstoasc</code> before the file name.</p>' +*/
  51. '<p>Alternatively, you may also try to record the M3U8 stream URL with VLC.</p>');
  52. $('#' + id + ' pre, #' + id + ' code').css({
  53. 'white-space': 'normal',
  54. 'word-break': 'break-word',
  55. 'font-size': $('#direct-link p').css('font-size'),
  56. 'margin': '.75em 0',
  57. 'padding': '.75em',
  58. 'background-color': '#444',
  59. 'font-family': 'monospace'
  60. });
  61. $('#' + id + ' code').css('padding','.25em');
  62. $('#' + id + ' p:last-child').css('margin-bottom', '0');
  63.  
  64. return id;
  65. };
  66.  
  67. var append_directURL = function(element, url, ext) {
  68. place_link_box(element, 'direct-link');
  69. $('#direct-link').append('<p>Yay! We have a direct link to a file. :D</p>' +
  70. '<p><a href="' + url + '">Click here to open/download (' + ext + ')</a></p>');
  71. };
  72.  
  73. var m3u8_qualities = function(contents, m3u8_url) {
  74. var lines = contents.split('\n');
  75. var streams = {};
  76. for (var i = 0; i < lines.length - 1; i++) {
  77. var h = lines[i];
  78. var u = lines[i+1];
  79. if(h.indexOf('#EXT-X-STREAM-INF') === 0 && u.indexOf('m3u8') > 0) {
  80. var divider = 'RESOLUTION=';
  81. if (h.indexOf(divider) < 0)
  82. divider = 'BANDWIDTH=';
  83. var q = parseInt(h.split(divider)[1].split('x')[0]);
  84. if(h.indexOf('audio-') > 0) {
  85. var audio_value = parseInt(lines[i].split('audio-')[1].split(',')[0].split('"')[0].replace(/[^0-9]*/g, ''));
  86. if (!isNaN(audio_value))
  87. q = q*100 + audio_value;
  88. }
  89. if (u.indexOf('://') > 0)
  90. streams[q] = u;
  91. else
  92. streams[q] = m3u8_url.split('/').slice(0,-1).join('/') + '/' + u;
  93. i++;
  94. }
  95. }
  96. return streams;
  97. };
  98.  
  99. var get_biggest = function(dict) {
  100. var s = 0;
  101. var o = null;
  102. for(var key in dict) {
  103. key = parseInt(key) || 0;
  104. if (key > s) {
  105. s = key;
  106. o = dict[key];
  107. }
  108. }
  109. return {'size': s, 'object': o};
  110. };
  111.  
  112. var render_piece = function(html) {
  113. var tree = $(html);
  114. if (!tree.length)
  115. return '';
  116. var output = [];
  117. var nodes = tree[0].childNodes;
  118. var hyph = html.toString().indexOf('<span') > 0 ? '- ' : '';
  119. for (var o = 0; o < nodes.length; o++) {
  120. if (nodes[o].toString().indexOf('Text') > 0)
  121. output.push(hyph + nodes[o].textContent);
  122. else {
  123. var name = nodes[o].tagName.toLowerCase();
  124. switch(name) {
  125. case 'br':
  126. output.push(' ');
  127. break;
  128. case 'span':
  129. output.push('\n' + hyph);
  130. output.push(render_piece(nodes[o]));
  131. output.push('\n');
  132. break;
  133. }
  134. }
  135. }
  136. var joined = output.join('');
  137. joined = joined.replace(/\s+\n/, '\n').replace(/(^\n|\n$)/, '');
  138. joined = joined.replace(/\n+/, '\n').replace(/\s+/, ' ');
  139. return joined;
  140. };
  141.  
  142. var render_p = function(html, id) {
  143. var tree = $(html);
  144. var begin = tree.attr('begin').replace('.', ',');
  145. var end = tree.attr('end').replace('.', ',');
  146. return id + '\n' +
  147. begin + ' --> ' + end + '\n' +
  148. render_piece(html);
  149. };
  150.  
  151. var handle_subtitles = function(subURL, element_id) {
  152. if (!subURL)
  153. return;
  154.  
  155. GM_xmlhttpRequest({
  156. method: 'GET',
  157. url: subURL,
  158. onload: function(responseDetails) {
  159. var r = responseDetails.responseText;
  160. var doc = $.parseXML(r);
  161. var $xml = $(doc);
  162.  
  163. var srt_list = [];
  164. $xml.find('p').each(function(index, value){
  165. srt_list.push(render_p(value.outerHTML, index+1));
  166. });
  167.  
  168. $('#' + element_id + ' p:last-child').css('margin-bottom', 'auto');
  169. $('#' + element_id).append('<ul><li><a id="srt-link">Download converted subtitles (SRT)</a></li>' +
  170. '<li><a href="' + subURL + '">Download original subtitles (TTML)</a></li></ul>');
  171. $('#srt-link').attr('href', 'data:text/plain;charset=utf-8,' +
  172. encodeURIComponent(srt_list.join('\n\n'))).attr('download', get_title() + '.srt');
  173. $('#' + element_id + ' a').css({
  174. 'color': 'white',
  175. 'font-weight': 'bold'
  176. });
  177. $('#' + element_id + ' ul').css({
  178. 'list-style': 'initial',
  179. 'padding-left': '2em',
  180. 'margin-top': '.5em'
  181. });
  182. }
  183. });
  184. };
  185.  
  186. var handle_pid = function(vpid, selector, video_title){
  187. var config_url = 'http://www.bbc.co.uk/iplayer/config/windows-phone';
  188. // figure out the mediaselector URL
  189. $.getJSON(config_url, function(data) {
  190. var selector_mobile = data.mediaselector.replace('{vpid}', vpid);
  191. var selector_pc = selector_mobile.replace(/mobile-.*vpid/, 'pc/vpid');
  192.  
  193. console.log(selector_mobile);
  194. console.log(selector_pc);
  195.  
  196. // get mobile data
  197. GM_xmlhttpRequest({
  198. method: 'GET',
  199. url: selector_mobile,
  200. onload: function(responseDetails) {
  201. var r = responseDetails.responseText;
  202. var doc = $.parseXML(r);
  203. var $xml = $(doc);
  204.  
  205. var media = {};
  206.  
  207. console.log('SELECTOR_MOBILE: ' + selector_mobile);
  208. var urls = $xml.find('media[kind^="video"]');
  209. var kind = 'video';
  210. if (!urls.length) {
  211. urls = $xml.find('media[kind^="audio"]');
  212. kind = 'audio';
  213. }
  214.  
  215. urls.each(function() {
  216. var bitrate = $(this).attr('bitrate');
  217. var href = $(this).find('connection').attr('href');
  218. media[bitrate] = href;
  219. });
  220. var subURL = $xml.find('media[service="captions"] connection').attr('href');
  221. var m3u8_url = get_biggest(media);
  222. console.log("M3U8_URL: " + m3u8_url.object);
  223.  
  224. // get desktop data for higher quality
  225. GM_xmlhttpRequest({
  226. method: 'GET',
  227. url: selector_pc,
  228. onload: function(responseDetails) {
  229. var r = responseDetails.responseText;
  230. var doc = $.parseXML(r);
  231. var $xml = $(doc);
  232.  
  233. console.log('SELECTOR_PC: ' + selector_pc);
  234.  
  235. var media = {};
  236.  
  237. var urls = $xml.find('media[kind^="video"]');
  238. if (!urls.length)
  239. urls = $xml.find('media[kind^="audio"]');
  240.  
  241. urls.each(function() {
  242. var bitrate = $(this).attr('bitrate');
  243. var identifier = $(this).find('connection[application="ondemand"], ' +
  244. 'connection[application*="/e3"]').attr('identifier');
  245. if(identifier)
  246. media[bitrate] = identifier;
  247. });
  248. var high_quality = get_biggest(media);
  249. console.log("HIGH_QUALITY: " + high_quality.object);
  250.  
  251. // compose the M3U8 stream URL
  252. GM_xmlhttpRequest({
  253. method: 'GET',
  254. url: m3u8_url.object,
  255. onload: function(responseDetails) {
  256. var r = responseDetails.responseText;
  257.  
  258. var urls = r.split('\n').slice(1);
  259. var final_url = (m3u8_url.object.indexOf('prod_af') < 0 && urls[1].indexOf('_av.') > 0) ? urls[1] : m3u8_url.object;
  260. console.log('FINAL_URL: ' + final_url);
  261.  
  262. var ext = kind == 'video' ? 'mp4' : 'mp3';
  263. // fix the final url
  264. if ((kind == 'video' && final_url.indexOf(',') > 0) || final_url.indexOf('kbps') > 0) {
  265. var old_pieces = final_url.split(',');
  266. var pieces = [old_pieces[0], old_pieces[1], old_pieces[old_pieces.length-1]];
  267. var p = 1;
  268.  
  269. var strpiv = 'kbps/';
  270.  
  271. var template = pieces[p];
  272. var cutter = template.indexOf(strpiv);
  273. var pivot = template.substring(0, cutter).lastIndexOf('/');
  274. template = template.substring(0, pivot+1);
  275.  
  276. var hq_piece = high_quality.object;
  277. cutter = hq_piece.indexOf(strpiv);
  278. pivot = hq_piece.substring(0, cutter).lastIndexOf('/');
  279. hq_piece = hq_piece.substring(pivot+1, 1000).replace('.mp4', '');
  280.  
  281. console.log(pivot);
  282. console.log(hq_piece);
  283.  
  284. pieces[p] = [template, hq_piece].join('');
  285.  
  286. console.log(pieces);
  287.  
  288. final_url = pieces.join(',');
  289. }
  290. console.log('KIND: ' + kind);
  291.  
  292. // output the M3U8 URL
  293. if(final_url.indexOf('master.m3u8') > 0 && kind == 'video') {
  294. GM_xmlhttpRequest({
  295. method: 'GET',
  296. url: final_url,
  297. onload: function(responseDetails) {
  298. var r = responseDetails.responseText;
  299. var qualities = m3u8_qualities(r, final_url);
  300. var element_id = appendURL($(selector), (get_biggest(qualities)).object, ext, kind, video_title);
  301. handle_subtitles(subURL, element_id);
  302. }
  303. });
  304. }
  305. else {
  306. var element_id = appendURL($(selector), final_url, ext, kind, video_title);
  307. console.log("SUBURL: " + subURL);
  308. handle_subtitles(subURL, element_id);
  309. }
  310. }
  311. });
  312. }
  313. });
  314. }
  315. });
  316. }); // getJSON
  317. };
  318.  
  319. var handle_embeds = function() {
  320. var present = $("div.smp-embed[data-vpid]").length;
  321. var loaded = $("div.smp-embed[data-vpid] > div").length;
  322.  
  323. if (present != loaded) {
  324. console.log("Waiting for embeds...");
  325. setTimeout(handle_embeds, 500);
  326. }
  327. else {
  328. console.log("Working on embeds");
  329. $("div.smp-embed[data-vpid] > div").each(function() {
  330. var el = $(this).parent();
  331. var vpid = el.data('vpid');
  332. var title = el.data('title');
  333. handle_pid(vpid, el.parent(), title);
  334. });
  335. }
  336. };
  337.  
  338. var handle_morph = function() {
  339. var payloads = unsafeWindow.Morph.payloads;
  340. if (!payloads) {
  341. console.log("Waiting for Morph...");
  342. setTimeout(handle_morph, 1500);
  343. }
  344. else {
  345. console.log("[MORPH] " + payloads);
  346. for (var key in payloads) {
  347. if (key.indexOf("assetUri") < 0)
  348. continue;
  349. try {
  350. var identifiers = payloads[key].body.components[0].props.leadMedia.identifiers;
  351. console.log(identifiers);
  352. handle_pid(identifiers.vpid, $('#bbcMediaPlayer0').parent());
  353. }
  354. catch (err) {}
  355. }
  356. }
  357. };
  358.  
  359. $(document).ready(function(){
  360. var isRadio = (unsafeWindow.location.href.indexOf('radio/') > 0) && !!($('#empbox').length);
  361. var isProgramme = !!unsafeWindow.bbcProgrammes;
  362. var isMediator = !!$('script:contains("mediator.bind")').length;
  363. var isMorph = !!unsafeWindow.Morph;
  364.  
  365. if (isRadio) {
  366. var playlist = unsafeWindow.clipcontentPlaylist;
  367. var empconf = unsafeWindow.empConfig;
  368. var subdir = empconf.split('.co.uk/')[1].split('/')[0];
  369. if (playlist.indexOf('.xml') > 0) {
  370. GM_xmlhttpRequest({
  371. method: 'GET',
  372. url: playlist,
  373. onload: function(responseDetails) {
  374. var r = responseDetails.responseText;
  375. var doc = $.parseXML(r);
  376. var $xml = $(doc);
  377.  
  378. var media_files = $xml.find('media[kind="video"] > connection, media[kind="audio"] > connection');
  379. if (media_files.length) {
  380. var link = media_files.attr('href');
  381. append_directURL($('#empbox'), link, link.split('.co.uk/')[1].split('.')[1].toUpperCase());
  382. }
  383. }
  384. });
  385. }
  386. else if (playlist.indexOf('iplayer/playlist') > 0) {
  387. var parts = playlist.split('iplayer/playlist');
  388. var playlist_address = parts[0] + subdir + '/iplayer/playlist' + parts[1];
  389. $.getJSON(playlist_address, function(data) {
  390. console.log('VPID: ' + data.pid);
  391. handle_pid(data.pid, '#empbox');
  392. });
  393. }
  394. return;
  395. }
  396.  
  397. if (isProgramme) {
  398. var clipid = location.href.split("/")[4];
  399. $.getJSON('http://www.bbc.co.uk/programmes/' + clipid + '/playlist.json', function(data) {
  400. var vpid = data.defaultAvailableVersion.pid;
  401. console.log("VPID: " + vpid.toString());
  402. handle_pid(vpid, '.island .cf.component, .episode-playout');
  403. });
  404. }
  405.  
  406. if (isMediator) {
  407. var spid = $('script:contains("mediator.bind")').html();
  408. var vpid = spid.split('vpid')[1].split('"')[2];
  409. handle_pid(vpid, '#player-outer-outer');
  410. }
  411.  
  412. if (isMorph)
  413. handle_morph();
  414.  
  415. handle_embeds();
  416.  
  417. }); // $(document).ready