BBC iPlayer video download

This script allows you to save videos from BBC iPlayer.

当前为 2018-01-30 提交的版本,查看 最新版本

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