- // ==UserScript==
- // @name BBC iPlayer video download
- // @namespace http://andrealazzarotto.com/
- // @include http://www.bbc.co.uk/*
- // @version 3.6.1
- // @description This script allows to save videos from BBC iPlayer.
- // @copyright 2015+, Andrea Lazzarotto - GPLv3 License
- // @require http://code.jquery.com/jquery-latest.min.js
- // @grant GM_xmlhttpRequest
- // @connect edgesuite.net
- // @connect bbc.co.uk
- // @connect akamaihd.net
- // @license GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html
- // ==/UserScript==
-
- var added = 0;
-
- var get_title = function(name) {
- var title = name || $('meta[property="og:title"]').attr('content') || 'output';
- return title.replace(/\W+/g, '_');
- };
-
- var place_link_box = function(element, id) {
- element.after('<div id="' + id + '"></div>');
- $('#' + id).css({
- 'padding': '.75em',
- 'margin': '25px auto',
- 'width': $('#player-outer-outer').width(),
- 'border': '1px solid #444',
- 'background-color': '#252525',
- 'color': 'white',
- 'font-family': 'sans-serif',
- 'box-sizing': 'border-box',
- 'font-size': '0.85rem'
- });
- };
-
- var appendURL = function(element, url, ext, kind, title) {
- var extension = ext || 'mp4';
- var type = kind || 'video';
- var codec = type == 'video' ? ' -codec copy -qscale 0 -bsf:a aac_adtstoasc ' : ' ';
- var tool = 'ffmpeg';
- var safe_title = get_title(title);
- var id = 'direct-link-' + safe_title + (added++);
- element.after('<div id=' + id + '"></div>');
- place_link_box(element, id);
- $('#' + id).append('<p>To record the ' + type + ', use <code>' + tool + '</code> with the following command line:</p>' +
- '<pre>' + tool + ' -v 16 -stats -i "' + url + '"' + codec + safe_title + '.' + extension + '</pre>' +
- /*'<p>If you get an error about <code>Malformed AAC bitstream</code>, add option ' +
- '<code>-bsf:a aac_adtstoasc</code> before the file name.</p>' +*/
- '<p>Alternatively, you may also try to record the M3U8 stream URL with VLC.</p>');
- $('#' + id + ' pre, #' + id + ' code').css({
- 'white-space': 'normal',
- 'word-break': 'break-word',
- 'font-size': $('#direct-link p').css('font-size'),
- 'margin': '.75em 0',
- 'padding': '.75em',
- 'background-color': '#444',
- 'font-family': 'monospace'
- });
- $('#' + id + ' code').css('padding','.25em');
- $('#' + id + ' p:last-child').css('margin-bottom', '0');
-
- return id;
- };
-
- var append_directURL = function(element, url, ext) {
- place_link_box(element, 'direct-link');
- $('#direct-link').append('<p>Yay! We have a direct link to a file. :D</p>' +
- '<p><a href="' + url + '">Click here to open/download (' + ext + ')</a></p>');
- };
-
- var m3u8_qualities = function(contents, m3u8_url) {
- var lines = contents.split('\n');
- var streams = {};
- for (var i = 0; i < lines.length - 1; i++) {
- var h = lines[i];
- var u = lines[i+1];
- if(h.indexOf('#EXT-X-STREAM-INF') === 0 && u.indexOf('m3u8') > 0) {
- var divider = 'RESOLUTION=';
- if (h.indexOf(divider) < 0)
- divider = 'BANDWIDTH=';
- var q = parseInt(h.split(divider)[1].split('x')[0]);
- if(h.indexOf('audio-') > 0) {
- var audio_value = parseInt(lines[i].split('audio-')[1].split(',')[0].split('"')[0].replace(/[^0-9]*/g, ''));
- if (!isNaN(audio_value))
- q = q*100 + audio_value;
- }
- if (u.indexOf('://') > 0)
- streams[q] = u;
- else
- streams[q] = m3u8_url.split('/').slice(0,-1).join('/') + '/' + u;
- i++;
- }
- }
- return streams;
- };
-
- var get_biggest = function(dict) {
- var s = 0;
- var o = null;
- for(var key in dict) {
- key = parseInt(key) || 0;
- if (key > s) {
- s = key;
- o = dict[key];
- }
- }
- return {'size': s, 'object': o};
- };
-
- var render_piece = function(html) {
- var tree = $(html);
- if (!tree.length)
- return '';
- var output = [];
- var nodes = tree[0].childNodes;
- var hyph = html.toString().indexOf('<span') > 0 ? '- ' : '';
- for (var o = 0; o < nodes.length; o++) {
- if (nodes[o].toString().indexOf('Text') > 0)
- output.push(hyph + nodes[o].textContent);
- else {
- var name = nodes[o].tagName.toLowerCase();
- switch(name) {
- case 'br':
- output.push(' ');
- break;
- case 'span':
- output.push('\n' + hyph);
- output.push(render_piece(nodes[o]));
- output.push('\n');
- break;
- }
- }
- }
- var joined = output.join('');
- joined = joined.replace(/\s+\n/, '\n').replace(/(^\n|\n$)/, '');
- joined = joined.replace(/\n+/, '\n').replace(/\s+/, ' ');
- return joined;
- };
-
- var render_p = function(html, id) {
- var tree = $(html);
- var begin = tree.attr('begin').replace('.', ',');
- var end = tree.attr('end').replace('.', ',');
- return id + '\n' +
- begin + ' --> ' + end + '\n' +
- render_piece(html);
- };
-
- var handle_subtitles = function(subURL, element_id) {
- if (!subURL)
- return;
-
- GM_xmlhttpRequest({
- method: 'GET',
- url: subURL,
- onload: function(responseDetails) {
- var r = responseDetails.responseText;
- var doc = $.parseXML(r);
- var $xml = $(doc);
-
- var srt_list = [];
- $xml.find('p').each(function(index, value){
- srt_list.push(render_p(value.outerHTML, index+1));
- });
-
- $('#' + element_id + ' p:last-child').css('margin-bottom', 'auto');
- $('#' + element_id).append('<ul><li><a id="srt-link">Download converted subtitles (SRT)</a></li>' +
- '<li><a href="' + subURL + '">Download original subtitles (TTML)</a></li></ul>');
- $('#srt-link').attr('href', 'data:text/plain;charset=utf-8,' +
- encodeURIComponent(srt_list.join('\n\n'))).attr('download', get_title() + '.srt');
- $('#' + element_id + ' a').css({
- 'color': 'white',
- 'font-weight': 'bold'
- });
- $('#' + element_id + ' ul').css({
- 'list-style': 'initial',
- 'padding-left': '2em',
- 'margin-top': '.5em'
- });
- }
- });
- };
-
- var handle_pid = function(vpid, selector, video_title){
- var config_url = 'http://www.bbc.co.uk/iplayer/config/windows-phone';
- // figure out the mediaselector URL
- $.getJSON(config_url, function(data) {
- var selector_mobile = data.mediaselector.replace('{vpid}', vpid);
- var selector_pc = selector_mobile.replace(/mobile-.*vpid/, 'pc/vpid');
-
- console.log(selector_mobile);
- console.log(selector_pc);
-
- // get mobile data
- GM_xmlhttpRequest({
- method: 'GET',
- url: selector_mobile,
- onload: function(responseDetails) {
- var r = responseDetails.responseText;
- var doc = $.parseXML(r);
- var $xml = $(doc);
-
- var media = {};
-
- console.log('SELECTOR_MOBILE: ' + selector_mobile);
- var urls = $xml.find('media[kind^="video"]');
- var kind = 'video';
- if (!urls.length) {
- urls = $xml.find('media[kind^="audio"]');
- kind = 'audio';
- }
-
- urls.each(function() {
- var bitrate = $(this).attr('bitrate');
- var href = $(this).find('connection').attr('href');
- media[bitrate] = href;
- });
- var subURL = $xml.find('media[service="captions"] connection').attr('href');
- var m3u8_url = get_biggest(media);
- console.log("M3U8_URL: " + m3u8_url.object);
-
- // get desktop data for higher quality
- GM_xmlhttpRequest({
- method: 'GET',
- url: selector_pc,
- onload: function(responseDetails) {
- var r = responseDetails.responseText;
- var doc = $.parseXML(r);
- var $xml = $(doc);
-
- console.log('SELECTOR_PC: ' + selector_pc);
-
- var media = {};
-
- var urls = $xml.find('media[kind^="video"]');
- if (!urls.length)
- urls = $xml.find('media[kind^="audio"]');
-
- urls.each(function() {
- var bitrate = $(this).attr('bitrate');
- var identifier = $(this).find('connection[application="ondemand"], ' +
- 'connection[application*="/e3"]').attr('identifier');
- if(identifier)
- media[bitrate] = identifier;
- });
- var high_quality = get_biggest(media);
- console.log("HIGH_QUALITY: " + high_quality.object);
-
- // compose the M3U8 stream URL
- GM_xmlhttpRequest({
- method: 'GET',
- url: m3u8_url.object,
- onload: function(responseDetails) {
- var r = responseDetails.responseText;
-
- var urls = r.split('\n').slice(1);
- var final_url = (m3u8_url.object.indexOf('prod_af') < 0 && urls[1].indexOf('_av.') > 0) ? urls[1] : m3u8_url.object;
- console.log('FINAL_URL: ' + final_url);
-
- var ext = kind == 'video' ? 'mp4' : 'mp3';
- // fix the final url
- if ((kind == 'video' && final_url.indexOf(',') > 0) || final_url.indexOf('kbps') > 0) {
- var old_pieces = final_url.split(',');
- var pieces = [old_pieces[0], old_pieces[1], old_pieces[old_pieces.length-1]];
- var p = 1;
-
- var strpiv = 'kbps/';
-
- var template = pieces[p];
- var cutter = template.indexOf(strpiv);
- var pivot = template.substring(0, cutter).lastIndexOf('/');
- template = template.substring(0, pivot+1);
-
- var hq_piece = high_quality.object;
- cutter = hq_piece.indexOf(strpiv);
- pivot = hq_piece.substring(0, cutter).lastIndexOf('/');
- hq_piece = hq_piece.substring(pivot+1, 1000).replace('.mp4', '');
-
- console.log(pivot);
- console.log(hq_piece);
-
- pieces[p] = [template, hq_piece].join('');
-
- console.log(pieces);
-
- final_url = pieces.join(',');
- }
- console.log('KIND: ' + kind);
-
- // output the M3U8 URL
- if(final_url.indexOf('master.m3u8') > 0 && kind == 'video') {
- GM_xmlhttpRequest({
- method: 'GET',
- url: final_url,
- onload: function(responseDetails) {
- var r = responseDetails.responseText;
- var qualities = m3u8_qualities(r, final_url);
- var element_id = appendURL($(selector), (get_biggest(qualities)).object, ext, kind, video_title);
- handle_subtitles(subURL, element_id);
- }
- });
- }
- else {
- var element_id = appendURL($(selector), final_url, ext, kind, video_title);
- console.log("SUBURL: " + subURL);
- handle_subtitles(subURL, element_id);
- }
- }
- });
- }
- });
- }
- });
- }); // getJSON
- };
-
- var handle_embeds = function() {
- var present = $("div.smp-embed[data-vpid]").length;
- var loaded = $("div.smp-embed[data-vpid] > div").length;
-
- if (present != loaded) {
- console.log("Waiting for embeds...");
- setTimeout(handle_embeds, 500);
- }
- else {
- console.log("Working on embeds");
- $("div.smp-embed[data-vpid] > div").each(function() {
- var el = $(this).parent();
- var vpid = el.data('vpid');
- var title = el.data('title');
- handle_pid(vpid, el.parent(), title);
- });
- }
- };
-
- var handle_morph = function() {
- var payloads = unsafeWindow.Morph.payloads;
- if (!payloads) {
- console.log("Waiting for Morph...");
- setTimeout(handle_morph, 1500);
- }
- else {
- console.log("[MORPH] " + payloads);
- for (var key in payloads) {
- if (key.indexOf("assetUri") < 0)
- continue;
- try {
- var identifiers = payloads[key].body.components[0].props.leadMedia.identifiers;
- console.log(identifiers);
- handle_pid(identifiers.vpid, $('#bbcMediaPlayer0').parent());
- }
- catch (err) {}
- }
- }
- };
-
- $(document).ready(function(){
- var isRadio = (unsafeWindow.location.href.indexOf('radio/') > 0) && !!($('#empbox').length);
- var isProgramme = !!unsafeWindow.bbcProgrammes;
- var isMediator = !!$('script:contains("mediator.bind")').length;
- var isMorph = !!unsafeWindow.Morph;
-
- if (isRadio) {
- var playlist = unsafeWindow.clipcontentPlaylist;
- var empconf = unsafeWindow.empConfig;
- var subdir = empconf.split('.co.uk/')[1].split('/')[0];
- if (playlist.indexOf('.xml') > 0) {
- GM_xmlhttpRequest({
- method: 'GET',
- url: playlist,
- onload: function(responseDetails) {
- var r = responseDetails.responseText;
- var doc = $.parseXML(r);
- var $xml = $(doc);
-
- var media_files = $xml.find('media[kind="video"] > connection, media[kind="audio"] > connection');
- if (media_files.length) {
- var link = media_files.attr('href');
- append_directURL($('#empbox'), link, link.split('.co.uk/')[1].split('.')[1].toUpperCase());
- }
- }
- });
- }
- else if (playlist.indexOf('iplayer/playlist') > 0) {
- var parts = playlist.split('iplayer/playlist');
- var playlist_address = parts[0] + subdir + '/iplayer/playlist' + parts[1];
- $.getJSON(playlist_address, function(data) {
- console.log('VPID: ' + data.pid);
- handle_pid(data.pid, '#empbox');
- });
- }
- return;
- }
-
- if (isProgramme) {
- var clipid = location.href.split("/")[4];
- $.getJSON('http://www.bbc.co.uk/programmes/' + clipid + '/playlist.json', function(data) {
- var vpid = data.defaultAvailableVersion.pid;
- console.log("VPID: " + vpid.toString());
- handle_pid(vpid, '.island .cf.component, .episode-playout');
- });
- }
-
- if (isMediator) {
- var spid = $('script:contains("mediator.bind")').html();
- var vpid = spid.split('vpid')[1].split('"')[2];
- handle_pid(vpid, '#player-outer-outer');
- }
-
- if (isMorph)
- handle_morph();
-
- handle_embeds();
-
- }); // $(document).ready