BBC iPlayer video download

This script allows you to save videos from BBC iPlayer.

目前为 2017-05-22 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name BBC iPlayer video download
  3. // @namespace http://andrealazzarotto.com/
  4. // @include http://www.bbc.co.uk/*
  5. // @include https://www.bbc.co.uk/*
  6. // @version 4.0.5
  7. // @description This script allows you to save videos from BBC iPlayer.
  8. // @copyright 2015+, Andrea Lazzarotto - GPLv3 License
  9. // @require http://code.jquery.com/jquery-latest.min.js
  10. // @grant GM_xmlhttpRequest
  11. // @connect bbc.co.uk
  12. // @connect akamaized.net
  13. // @connect llnwd.net
  14. // @license GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html
  15. // ==/UserScript==
  16.  
  17. var count = 0;
  18.  
  19. var containers = '.playback-content > button,' +
  20. '.content-item-description__text-container,' +
  21. '.episode-panel__intro,' +
  22. '.vxp-media__summary,' +
  23. '#programme-clip,' +
  24. '[class*="__media-asset"],' +
  25. '.msc-media-player-wrapper';
  26.  
  27. var get_title = function(name) {
  28. var title = name || $('meta[property="og:title"]').attr('content') || 'output';
  29. return title.replace(/\W+/g, '_');
  30. };
  31.  
  32. var get_JSON = function(url, callback) {
  33. GM_xmlhttpRequest({
  34. method: 'GET',
  35. url: url,
  36. onload: function(responseDetails) {
  37. var r = responseDetails.responseText;
  38. var json = $.parseJSON(r);
  39. callback(json);
  40. }
  41. });
  42. };
  43.  
  44. var place_link_box = function(element, id) {
  45. element.after('<div id="' + id + '" />');
  46. $('#' + id).css({
  47. 'padding': '.75em',
  48. 'margin': '25px auto',
  49. //'width': $('#player-outer-outer').width(),
  50. 'border': '1px solid #2C2C2C',
  51. 'background-color': '#0A0C16',
  52. 'color': 'white',
  53. 'font-family': 'sans-serif',
  54. 'box-sizing': 'border-box',
  55. 'font-size': '0.9rem'
  56. });
  57. };
  58.  
  59. var render_piece = function(html) {
  60. var tree = $(html);
  61. if (!tree.length)
  62. return '';
  63. var output = [];
  64. var nodes = tree[0].childNodes;
  65. var hyph = html.toString().indexOf('<span') > 0 ? '- ' : '';
  66. for (var o = 0; o < nodes.length; o++) {
  67. if (nodes[o].toString().indexOf('Text') > 0)
  68. output.push(hyph + nodes[o].textContent);
  69. else {
  70. var name = nodes[o].tagName.toLowerCase();
  71. switch(name) {
  72. case 'br':
  73. output.push(' ');
  74. break;
  75. case 'span':
  76. output.push('\n' + hyph);
  77. output.push(render_piece(nodes[o]));
  78. output.push('\n');
  79. break;
  80. }
  81. }
  82. }
  83. var joined = output.join('');
  84. joined = joined.replace(/\s+\n/, '\n').replace(/(^\n|\n$)/, '');
  85. joined = joined.replace(/\n+/, '\n').replace(/\s+/, ' ');
  86. return joined;
  87. };
  88.  
  89. var render_part = function(html, id) {
  90. var tree = $(html);
  91. var begin = tree.attr('begin').replace('.', ',');
  92. var end = tree.attr('end').replace('.', ',');
  93. return id + '\n' +
  94. begin + ' --> ' + end + '\n' +
  95. render_piece(html);
  96. };
  97.  
  98. var handle_subtitles = function(subURL, element_id, title) {
  99. if (!subURL)
  100. return;
  101.  
  102. GM_xmlhttpRequest({
  103. method: 'GET',
  104. url: subURL,
  105. onload: function(responseDetails) {
  106. var r = responseDetails.responseText;
  107. var doc = $.parseXML(r);
  108. var $xml = $(doc);
  109.  
  110. var srt_list = [];
  111. $xml.find('p').each(function(index, value){
  112. srt_list.push(render_part(value.outerHTML, index+1));
  113. });
  114.  
  115. $('#' + element_id + ' #subtitles').remove();
  116. $('#' + element_id + ' p:last-child').css('margin-bottom', 'auto');
  117. $('#' + element_id).append('<ul id="subtitles"><li><a id="srt-link">Download converted subtitles (SRT)</a></li>' +
  118. '<li><a href="' + subURL + '">Download original subtitles (TTML)</a></li></ul>');
  119. $('#srt-link').attr('href', 'data:text/plain;charset=utf-8,' +
  120. encodeURIComponent(srt_list.join('\n\n'))).attr('download', get_title(title) + '.srt');
  121. $('#' + element_id + ' a').css({
  122. 'color': 'white',
  123. 'font-weight': 'bold'
  124. });
  125. $('#' + element_id + ' ul').css({
  126. 'list-style': 'initial',
  127. 'padding-left': '2em',
  128. 'margin-top': '.5em'
  129. });
  130. }
  131. });
  132. };
  133.  
  134. var append = function(elements, id, parsed, title) {
  135. var type = parsed.kind || 'video';
  136. var tool = 'youtube-dl';
  137. var safe_title = get_title(title);
  138. var element = $(elements.get(0));
  139.  
  140. var objects = parsed[type];
  141. if (objects.length === 0)
  142. return;
  143.  
  144. $("#" + id).remove();
  145. element.after('<div id=' + id + '"></div>');
  146. place_link_box(element, id);
  147.  
  148. if (objects.length > 1)
  149. $('#' + id).append('<h4>Quality level: <select /></h4>');
  150. $('#' + id).append('<p>To record the ' + type + ', use <code>' + tool + '</code> with the following command line:</p>');
  151.  
  152. for (var i in objects) {
  153. var label = parseInt(objects[i].bitrate);
  154. var url = objects[i].connection[0].href;
  155. var format = parsed.kind == 'video' ? 'bestvideo+bestaudio' : 'bestaudio';
  156. $('#' + id).append('<div id="wrapper-' + i + '"><pre>' + tool + ' -f ' + format + ' "' + url + '" -o ' + safe_title + '</pre></div>');
  157. $('#' + id + ' select').append('<option value="' + i + '">' + label + "</option>");
  158. }
  159. $('#' + id + ' div[id*=wrapper]').hide();
  160. $('#' + id + ' #wrapper-0').show();
  161.  
  162. $('#' + id + ' select').css('color', 'black').on('change', function() {
  163. var index = this.value;
  164. $('#' + id + ' div[id*=wrapper]').hide();
  165. $('#' + id + ' #wrapper-' + index).show();
  166. });
  167.  
  168. $('#' + id + ' pre, #' + id + ' code').css({
  169. 'white-space': 'normal',
  170. 'word-break': 'break-all',
  171. 'font-size': $('#direct-link p').css('font-size'),
  172. 'margin': '.75em 0',
  173. 'padding': '.75em',
  174. 'background-color': '#2C2C2C',
  175. 'font-family': '"DejaVu Sans Mono", Menlo, "Andale Mono", monospace'
  176. });
  177. $('#' + id + ' code').css('padding','.25em');
  178. $('#' + id + ' p:last-child').css('margin-bottom', '0');
  179. $('#' + id + ' *').css({
  180. 'color': 'white',
  181. 'line-height': '1em',
  182. 'font-size': '.9rem'
  183. });
  184.  
  185. return id;
  186. };
  187.  
  188. var comparator = function(a,b) { return parseInt(b.bitrate)-parseInt(a.bitrate); };
  189.  
  190. var filter = function(media) {
  191. if (!media.hasOwnProperty('connection'))
  192. return;
  193. if (media.kind != 'video' && media.kind != 'audio')
  194. return;
  195. for (var i = media.connection.length - 1; i >= 0; i--) {
  196. var format = media.connection[i].transferFormat || media.connection[i].format;
  197. if (format !== 'dash')
  198. media.connection.splice(i, 1);
  199. }
  200. };
  201.  
  202. var classify = function(media) {
  203. var results = {
  204. video: [],
  205. audio: [],
  206. captions: []
  207. };
  208. for (var i = 0; i < media.length; i++) {
  209. var element = media[i];
  210. filter(element);
  211. if(results.hasOwnProperty(element.kind) && ((!element.hasOwnProperty('connection') || element.connection.length)))
  212. results[element.kind].push(element);
  213. }
  214. results.video.sort(comparator);
  215. results.audio.sort(comparator);
  216. results.hasCaptions = results.captions.length > 0;
  217. results.kind = results.video.length ? "video" : "audio";
  218. return results;
  219. };
  220.  
  221. var handle_player = function(player) {
  222. var container = player._container;
  223. var title = player.playlist.title;
  224. var vpid, type, kind;
  225.  
  226. for (var i = 0; i < player.playlist.items.length; i++) {
  227. vpid = vpid || player.playlist.items[i].vpid || player.playlist.items[i].identifier;
  228. type = type || player.playlist.items[i].type;
  229. kind = kind || player.playlist.items[i].kind;
  230. if (vpid !== null)
  231. break;
  232. }
  233.  
  234. if (vpid === null)
  235. return false;
  236.  
  237. var elements = $(containers);
  238. if (elements.length === 0)
  239. elements = container.closest('figure');
  240.  
  241. if (kind === 'trailer' || kind === 'ident') {
  242. try {
  243. var contents = $('script:contains(window.mediatorDefer = mediator.bind)').html().split('mediator.bind(')[1].split(', docu')[0];
  244. var data = JSON.parse(contents);
  245. vpid = data.episode.versions[0].id;
  246. }
  247. catch(e) {
  248. var id = 'video-trailer-warning-' + vpid;
  249. if (!$('#' + id).length) {
  250. place_link_box(elements, id);
  251. $('#' + id).append('<p>To download this ' + type + ', press <i>Play</i> and skip the trailer.</p>');
  252. }
  253. return false;
  254. }
  255. }
  256.  
  257. get_JSON('http://open.live.bbc.co.uk/mediaselector/5/select/version/2.0/mediaset/pc/vpid/' + vpid + '/format/json/', function(data) {
  258. var parsed = classify(data.media);
  259. var id = append(elements, 'video-download-' + vpid, parsed, title);
  260. if (parsed.hasCaptions)
  261. handle_subtitles(parsed.captions[0].connection[0].href, id, title);
  262. });
  263.  
  264. return true;
  265. };
  266.  
  267. var monitor = function() {
  268. if(!unsafeWindow.embeddedMedia)
  269. return;
  270.  
  271. var players = unsafeWindow.embeddedMedia.players;
  272. if (players.length != count && players[0].playlist) {
  273. if (count === 0) {
  274. for (var i = 0; i < players.length; i++)
  275. if (handle_player(players[i]))
  276. count++;
  277. }
  278. else
  279. if (handle_player(players[players.length - 1]))
  280. count++;
  281. }
  282. };
  283.  
  284. $(document).ready(function(){
  285. setInterval(monitor, 1000);
  286. });