// ==UserScript== // @name BBC iPlayer video download // @namespace http://andrealazzarotto.com/ // @include http://www.bbc.co.uk/* // @version 3.5.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 // @downloadURL none // ==/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('
'); $('#' + 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(''); place_link_box(element, id); $('#' + id).append('To record the ' + type + ', use ' + tool + '
with the following command line:
' + tool + ' -v 16 -stats -i "' + url + '"' + codec + safe_title + '.' + extension + '' + /*'
If you get an error about Malformed AAC bitstream
, add option ' +
'-bsf:a aac_adtstoasc
before the file name.
Alternatively, you may also try to record the M3U8 stream URL with VLC.
'); $('#' + 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('Yay! We have a direct link to a file. :D
' + 'Click here to open/download (' + ext + ')
'); }; 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(' 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(''); $('#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, title); }); } }; $(document).ready(function(){ var isRadio = (unsafeWindow.location.href.indexOf('radio/') > 0) && !!($('#empbox').length); var isProgramme = !!unsafeWindow.bbcProgrammes; var isMediator = !!$('script:contains("mediator.bind")').length; 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'); } handle_embeds(); }); // $(document).ready