BBC iPlayer video download

This script allows to save videos from BBC iPlayer.

当前为 2016-01-11 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name BBC iPlayer video download
  3. // @namespace http://andrealazzarotto.com/
  4. // @include http://www.bbc.co.uk/iplayer/episode/*
  5. // @include http://www.bbc.co.uk/programmes/*
  6. // @include http://www.bbc.co.uk/*radio/*
  7. // @version 3.3.1
  8. // @description This script allows to save videos from BBC iPlayer.
  9. // @copyright 2015+, Andrea Lazzarotto - GPLv3 License
  10. // @require http://code.jquery.com/jquery-latest.min.js
  11. // @grant GM_xmlhttpRequest
  12. // @license GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html
  13. // ==/UserScript==
  14.  
  15. var get_title = function() {
  16. var title = $('meta[property="og:title"]').attr('content') || 'output';
  17. return title.replace(/\W+/g, '_');
  18. };
  19.  
  20. var place_link_box = function(element) {
  21. element.after('<div id="direct-link"></div>');
  22. $('#direct-link').css({
  23. 'padding': '.75em',
  24. 'margin': '25px auto',
  25. 'width': $('#player-outer-outer').width(),
  26. 'border': '1px solid #444',
  27. 'background-color': '#252525',
  28. 'color': 'white',
  29. 'font-family': 'sans-serif',
  30. 'box-sizing': 'border-box'
  31. });
  32. };
  33.  
  34. var appendURL = function(element, url, ext, kind) {
  35. var extension = ext || 'mp4';
  36. var type = kind || 'video';
  37. var codec = type == 'video' ? ' -codec copy -qscale 0 ' : ' ';
  38. element.after('<div id="direct-link"></div>');
  39. place_link_box(element);
  40. $('#direct-link').append('<p>To record the ' + type + ', use <code>avconv</code> with the following command line:</p>' +
  41. '<pre>avconv -i "' + url + '"' + codec + get_title() + '.' + extension + '</pre>' +
  42. '<p>If you get an error about <code>Malformed AAC bitstream</code>, add option ' +
  43. '<code>-bsf:a aac_adtstoasc</code> before the file name.</p>' +
  44. '<p>Alternatively, you may also try to record the M3U8 stream URL with VLC.</p>');
  45. $('#direct-link pre, #direct-link code').css({
  46. 'white-space': 'normal',
  47. 'word-break': 'break-word',
  48. 'font-size': $('#direct-link p').css('font-size'),
  49. 'margin': '.75em 0',
  50. 'padding': '.75em',
  51. 'background-color': '#444'
  52. });
  53. $('#direct-link code').css('padding','.25em');
  54. $('#direct-link p:last-child').css('margin-bottom', '0');
  55. };
  56.  
  57. var append_directURL = function(element, url, ext) {
  58. place_link_box(element);
  59. $('#direct-link').append('<p>Yay! We have a direct link to a file. :D</p>' +
  60. '<p><a href="' + url + '">Click here to open/download (' + ext + ')</a></p>');
  61. };
  62.  
  63. var get_biggest = function(dict) {
  64. var s = 0;
  65. var o = null;
  66. for(var key in dict) {
  67. key = parseInt(key) || 0;
  68. if (key > s) {
  69. s = key;
  70. o = dict[key];
  71. }
  72. }
  73. return {'size': s, 'object': o};
  74. };
  75.  
  76. var render_piece = function(html) {
  77. var tree = $(html);
  78. if (!tree.length)
  79. return '';
  80. var output = [];
  81. var nodes = tree[0].childNodes;
  82. var hyph = html.toString().indexOf('<span') > 0 ? '- ' : '';
  83. for (var o = 0; o < nodes.length; o++) {
  84. if (nodes[o].toString().indexOf('Text') > 0)
  85. output.push(hyph + nodes[o].textContent);
  86. else {
  87. var name = nodes[o].tagName.toLowerCase();
  88. switch(name) {
  89. case 'br':
  90. output.push(' ');
  91. break;
  92. case 'span':
  93. output.push('\n' + hyph);
  94. output.push(render_piece(nodes[o]));
  95. output.push('\n');
  96. break;
  97. }
  98. }
  99. }
  100. var joined = output.join('');
  101. joined = joined.replace(/\s+\n/, '\n').replace(/(^\n|\n$)/, '');
  102. joined = joined.replace(/\n+/, '\n').replace(/\s+/, ' ');
  103. return joined;
  104. };
  105.  
  106. var render_p = function(html, id) {
  107. var tree = $(html);
  108. var begin = tree.attr('begin').replace('.', ',');
  109. var end = tree.attr('end').replace('.', ',');
  110. return id + '\n' +
  111. begin + ' --> ' + end + '\n' +
  112. render_piece(html);
  113. };
  114.  
  115. var handle_subtitles = function(subURL) {
  116. GM_xmlhttpRequest({
  117. method: 'GET',
  118. url: subURL,
  119. onload: function(responseDetails) {
  120. var r = responseDetails.responseText;
  121. var doc = $.parseXML(r);
  122. var $xml = $(doc);
  123. var srt_list = [];
  124. $xml.find('p').each(function(index, value){
  125. srt_list.push(render_p(value.outerHTML, index+1));
  126. });
  127. $('#direct-link p:last-child').css('margin-bottom', 'auto');
  128. $('#direct-link').append('<ul><li><a id="srt-link">Download converted subtitles (SRT)</a></li>' +
  129. '<li><a href="' + subURL + '">Download original subtitles (TTML)</a></li></ul>');
  130. $('#srt-link').attr('href', 'data:text/plain;charset=utf-8,'
  131. + encodeURIComponent(srt_list.join('\n\n'))).attr('download', get_title() + '.srt');
  132. $('#direct-link a').css({
  133. 'color': 'white',
  134. 'font-weight': 'bold'
  135. });
  136. $('#direct-link ul').css({
  137. 'list-style': 'initial',
  138. 'padding-left': '2em',
  139. 'margin-top': '.5em'
  140. });
  141. }
  142. });
  143. };
  144.  
  145. var handle_pid = function(vpid, selector){
  146. var config_url = 'http://www.bbc.co.uk/iplayer/config/windows-phone';
  147. // figure out the mediaselector URL
  148. $.getJSON(config_url, function(data) {
  149. var selector_mobile = data.mediaselector.replace('{vpid}', vpid);
  150. var selector_pc = selector_mobile.replace(/mobile-.*vpid/, 'pc/vpid');
  151.  
  152. // get mobile data
  153. GM_xmlhttpRequest({
  154. method: 'GET',
  155. url: selector_mobile,
  156. onload: function(responseDetails) {
  157. var r = responseDetails.responseText;
  158. var doc = $.parseXML(r);
  159. var $xml = $(doc);
  160. var media = {};
  161. console.log('SELECTOR_MOBILE: ' + selector_mobile);
  162. var urls = $xml.find('media[kind^="video"]');
  163. var kind = 'video';
  164. if (!urls.length) {
  165. urls = $xml.find('media[kind^="audio"]');
  166. kind = 'audio';
  167. }
  168.  
  169. urls.each(function() {
  170. var bitrate = $(this).attr('bitrate');
  171. var href = $(this).find('connection').attr('href');
  172. media[bitrate] = href;
  173. });
  174. var subURL = $xml.find('media[service="captions"] connection').attr('href');
  175. var m3u8_url = get_biggest(media);
  176. console.log("M3U8_URL: " + m3u8_url.object);
  177. // get desktop data for higher quality
  178. GM_xmlhttpRequest({
  179. method: 'GET',
  180. url: selector_pc,
  181. onload: function(responseDetails) {
  182. var r = responseDetails.responseText;
  183. var doc = $.parseXML(r);
  184. var $xml = $(doc);
  185. console.log('SELECTOR_PC: ' + selector_pc);
  186. var media = {};
  187. var urls = $xml.find('media[kind^="video"]');
  188. if (!urls.length)
  189. urls = $xml.find('media[kind^="audio"]');
  190. urls.each(function() {
  191. var bitrate = $(this).attr('bitrate');
  192. var identifier = $(this).find('connection[application="ondemand"], ' +
  193. 'connection[application*="/e3"]').attr('identifier');
  194. if(identifier)
  195. media[bitrate] = identifier;
  196. });
  197. var high_quality = get_biggest(media);
  198. console.log("HIGH_QUALITY: " + high_quality.object);
  199. // compose the M3U8 stream URL
  200. GM_xmlhttpRequest({
  201. method: 'GET',
  202. url: m3u8_url.object,
  203. onload: function(responseDetails) {
  204. var r = responseDetails.responseText;
  205. var urls = r.split('\n').slice(1);
  206. var final_url = (m3u8_url.object.indexOf('prod_af') < 0 && urls[1].indexOf('_av.') > 0) ? urls[1] : m3u8_url.object;
  207. console.log('FINAL_URL: ' + final_url);
  208. var ext = kind == 'video' ? 'mp4' : 'mp3';
  209. // fix the final url
  210. if (kind == 'video' || final_url.indexOf('kbps') > 0) {
  211. var old_pieces = final_url.split(',');
  212. var pieces = [old_pieces[0], old_pieces[1], old_pieces[old_pieces.length-1]];
  213. var p = 1;
  214. var strpiv = 'kbps/';
  215. var template = pieces[p];
  216. var cutter = template.indexOf(strpiv);
  217. var pivot = template.substring(0, cutter).lastIndexOf('/');
  218. template = template.substring(0, pivot+1);
  219. var hq_piece = high_quality.object;
  220. cutter = hq_piece.indexOf(strpiv);
  221. pivot = hq_piece.substring(0, cutter).lastIndexOf('/');
  222. hq_piece = hq_piece.substring(pivot+1, 1000).replace('.mp4', '');
  223. console.log(pivot);
  224. console.log(hq_piece);
  225.  
  226. pieces[p] = [template, hq_piece].join('');
  227. console.log(pieces);
  228. final_url = pieces.join(',');
  229. }
  230. console.log('KIND: ' + kind);
  231. // output the M3U8 URL
  232. appendURL($(selector), final_url, ext, kind);
  233. console.log("SUBURL: " + subURL);
  234. handle_subtitles(subURL);
  235. }
  236. });
  237. }
  238. });
  239. }
  240. });
  241. }); // getJSON
  242. };
  243.  
  244. $(document).ready(function(){
  245. var isRadio = (unsafeWindow.location.href.indexOf('radio/') > 0) && !!($('#empbox').length);
  246. var isProgramme = !!unsafeWindow.bbcProgrammes;
  247. if (isRadio) {
  248. var playlist = unsafeWindow.clipcontentPlaylist;
  249. var empconf = unsafeWindow.empConfig;
  250. var subdir = empconf.split('.co.uk/')[1].split('/')[0];
  251. if (playlist.indexOf('.xml') > 0) {
  252. GM_xmlhttpRequest({
  253. method: 'GET',
  254. url: playlist,
  255. onload: function(responseDetails) {
  256. var r = responseDetails.responseText;
  257. var doc = $.parseXML(r);
  258. var $xml = $(doc);
  259. var media_files = $xml.find('media[kind="video"] > connection, media[kind="audio"] > connection');
  260. if (media_files.length) {
  261. var link = media_files.attr('href');
  262. append_directURL($('#empbox'), link, link.split('.co.uk/')[1].split('.')[1].toUpperCase());
  263. }
  264. }
  265. });
  266. }
  267. else if (playlist.indexOf('iplayer/playlist') > 0) {
  268. var parts = playlist.split('iplayer/playlist');
  269. var playlist = parts[0] + subdir + '/iplayer/playlist' + parts[1];
  270. $.getJSON(playlist, function(data) {
  271. console.log('VPID: ' + data.pid);
  272. handle_pid(data.pid, '#empbox');
  273. });
  274. }
  275. return;
  276. }
  277. if (isProgramme) {
  278. var clipid = location.href.split("/")[4];
  279. $.getJSON('http://www.bbc.co.uk/programmes/' + clipid + '/playlist.json', function(data) {
  280. var vpid = data.defaultAvailableVersion.pid;
  281. console.log("VPID: " + vpid.toString());
  282. handle_pid(vpid, '.island .cf.component, .episode-playout');
  283. });
  284. }
  285. else {
  286. var spid = $('script:contains("mediator.bind")').html();
  287. var vpid = spid.split('vpid')[1].split('"')[2];
  288. handle_pid(vpid, '#player-outer-outer');
  289. }
  290. }); // $(document).ready