BBC iPlayer video download

This script allows to save videos from BBC iPlayer.

目前为 2015-12-29 提交的版本,查看 最新版本

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