Greasy Fork

BBC iPlayer video download

This script allows to save videos from BBC iPlayer.

目前为 2017-04-08 提交的版本。查看 最新版本

// ==UserScript==
// @name        BBC iPlayer video download
// @namespace   http://andrealazzarotto.com/
// @include     http://www.bbc.co.uk/*
// @version     3.8.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
// @connect     llnwd.net
// @license     GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html
// ==/UserScript==

var added = 0;
var config_url = 'http://www.bbc.co.uk/iplayer/config/windows-phone';
var mediaselector;
var vpid_qualities = {};

var get_title = function(name) {
	var title = name || $('meta[property="og:title"]').attr('content') || 'output';
	return title.replace(/\W+/g, '_');
};

var get_JSON = function(url) {
    var data = $.ajax({
        url: url,
        method: 'GET',
        async: false
    });
    console.log(data);
    return $.parseJSON(data.responseText);
};

var get_XML = function(url, callback) {
    GM_xmlhttpRequest({
			method: 'GET',
			url: url,
			onload: function(responseDetails) {
				var r = responseDetails.responseText;
				var doc = $.parseXML(r);
				var $xml = $(doc);
                callback($xml);
            }
    });
};

var get_text = function(url, callback) {
    GM_xmlhttpRequest({
			method: 'GET',
			url: url,
			onload: function(responseDetails) {
				var r = responseDetails.responseText;
                callback(r);
            }
    });
};

var place_link_box = function(element, id) {
	element.after('<div id="' + id + '" />');
	$('#' + 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, id, vpid, 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);
    $("#" + id).remove();
	element.after('<div id=' + id + '"></div>');
	place_link_box(element, id);

    var sizes = [];
    var keys = Object.keys(vpid_qualities[vpid]);
    for (var k in keys)
        sizes.push(parseInt(keys[k]));
    sizes.sort((function(a,b) { return b-a; }));
    console.log(sizes);
    if (sizes.length > 1)
		$('#' + id).append('<h4>Quality level: <select /></h4>');
    $('#' + id).append('<p>To record the ' + type + ', use <code>' + tool + '</code> with the following command line:</p>');

    for (var i in sizes) {
        var label = parseInt(sizes[i]);
        var url = vpid_qualities[vpid][label];
        url = url.replace("http://", "https://");
        $('#' + id).append('<div id="wrapper-' + i + '">' +
                           '<pre>' + tool + ' -v 16 -stats -i "' + url + '"' + codec + safe_title + '.' + extension + '</pre>' +
                           '<p>Alternatively, you may also try to record the <a href="' + url + '">M3U8 stream URL</a> with VLC.</p>' +
                           '</div>');
        $('#' + id + ' select').append('<option value="' + i + '">' + label + "</option>");
    }
    $('#' + id + ' div[id*=wrapper]').hide();
    $('#' + id + ' #wrapper-0').show();

    $('#' + id + ' select').css('color', 'black').on('change', function() {
        var index = this.value;
        $('#' + id + ' div[id*=wrapper]').hide();
        $('#' + id + ' #wrapper-' + index).show();
    });

	$('#' + 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 + ' #subtitles').remove();
			$('#' + element_id + ' p:last-child').css('margin-bottom', 'auto');
			$('#' + element_id).append('<ul id="subtitles"><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 fix_final_url = function(final_url, high_quality) {
    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;
    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);

    return pieces.join(',');
};

var place_and_update = function(vpid, m3u8, high_quality, size, selector, id, kind, video_title, subURL) {
    // compose the M3U8 stream URL
    get_text(m3u8, function(r) {
        var urls = r.split('\n').slice(1);
        console.log(m3u8);
        console.log(r);
        console.log(urls);
        var final_url = (m3u8.indexOf('prod_af') < 0 && urls[1].indexOf('_av.') > 0) ? urls[1] : m3u8;
        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)
            final_url = fix_final_url(final_url, high_quality);
        console.log('KIND: ' + kind);

        // output the M3U8 URL
        if(final_url.indexOf('master.m3u8') > 0) {
            get_text(final_url, function(r) {
                var qualities = m3u8_qualities(r, final_url);
                var keys = Object.keys(qualities);
                for (var index in keys) {
                    var key = parseInt(keys[index]);
                    vpid_qualities[vpid][key] = qualities[key];
                }
                var element_id = appendURL($(selector), id, vpid, ext, kind, video_title);
                handle_subtitles(subURL, element_id);
            }); // get_text final_url
        }
        else {
            vpid_qualities[vpid][size] = final_url;
            var element_id = appendURL($(selector), id, vpid, ext, kind, video_title);
            console.log("SUBURL: " + subURL);
            handle_subtitles(subURL, element_id);
        }
    }); // get_text m3u8
};

var handle_pid = function(vpid, selector, video_title) {
    // figure out the mediaselector URL
    var selector_mobile = mediaselector.replace('{vpid}', vpid);
    var selector_pc = selector_mobile.replace(/mobile-.*vpid/, 'pc/vpid');
    var id = 'direct-link-' + vpid + '-' + (added++);
    vpid_qualities[vpid] = {};

    get_XML(selector_mobile, function($xml) {
        var media = {};

        console.log('SELECTOR_MOBILE: ' + selector_mobile);
        // Exclude fake high-quality streams
        var urls = $xml.find('media[kind^="video"]:not(media[service*="streaming_concrete_combined"])');
        var kind = 'video';
        // Try to find at least low-quality streams
        if (!urls.length)
            urls = $xml.find('media[kind^="video"]');
        // Check for audio-only content
        if (!urls.length) {
            urls = $xml.find('media[kind^="audio"]');
            kind = 'audio';
        }

        urls.each(function() {
            var bitrate = $(this).attr('bitrate');
            var href = $(this).find('connection:not([href*=akamaized])').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
        get_XML(selector_pc, function($xml) {
            console.log('SELECTOR_PC: ' + selector_pc);

            var media = {};
            var urls = $xml.find('media[kind^="' + kind + '"]');

            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 keys = Object.keys(media);
            for (var index in keys) {
                var key = parseInt(keys[index]);
                console.log("Quality: " + key);
                console.log("M3U8: " + media[key]);
                place_and_update(vpid, m3u8_url.object, media[key], key, selector, id, kind, video_title, subURL);
            }
        }); // get_xml selector_pc
    }); // get_xml selector_mobile
};

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;

    console.log("isRadio: " + isRadio);
    console.log("isProgramme: " + isProgramme);
    console.log("isMediator: " + isMediator);
    console.log("isMorph: " + isMorph);

    mediaselector = get_JSON(config_url).mediaselector;

	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];
        var element = $("[data-pid]");
        if (element.length == 1) {
            clipid = element.data("pid");
        }
		$.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, .island');
		});
	}

	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