BBC iPlayer video download

Easily download videos from BBC iPlayer (with youtube-dl)

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