BBC iPlayer video download

This script allows to save videos from BBC iPlayer.

目前为 2015-12-29 提交的版本。查看 最新版本

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

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

appendURL = function(element, url, ext, kind) {
	var extension = ext || 'mp4';
	var type = kind || 'video';
	var codec = type == 'video' ? ' -codec copy -qscale 0 ' : ' ';
	element.after('<div id="direct-link"></div>');
	$('#direct-link').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'
	}).append('<p>To record the ' + type + ', use <code>avconv</code> with the following command line:</p>' + 
			  '<pre>avconv -i "' + url + '"' + codec + get_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>');
	$('#direct-link pre, #direct-link 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'
	});
	$('#direct-link code').css('padding','.25em');
	$('#direct-link p:last-child').css('margin-bottom', '0');
}

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};
}

render_piece = function(html) {
	var tree = $(html);
	if (tree.length == 0)
		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;
}

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);
}

handle_subtitles = function(subURL) {
	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));
			});
			
			$('#direct-link p:last-child').css('margin-bottom', 'auto');
			$('#direct-link').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');
			$('#direct-link a').css({
				'color': 'white',
				'font-weight': 'bold'
			});
			$('#direct-link ul').css({
				'list-style': 'initial',
				'padding-left': '2em',
				'margin-top': '.5em'
			});
		}
	});
}

handle_pid = function(vpid, selector){
	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');

		// 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);
						
						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] : 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('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
								appendURL($(selector), final_url, ext, kind);
								console.log("SUBURL: " + subURL);
								handle_subtitles(subURL);
							}
						});
					}
				});
			}
		});
	}); // getJSON
}

$(document).ready(function(){
	var isRadio = (unsafeWindow.location.href.indexOf('radio/') > 0) && !!($('#empbox').length);
	var isProgramme = !!unsafeWindow.bbcProgrammes;
	
	if (isRadio) {
		var playlist = unsafeWindow.clipcontentPlaylist;
		var empconf = unsafeWindow.empConfig;
		var subdir = empconf.split('.co.uk/')[1].split('/')[0];
		var parts = playlist.split('iplayer/playlist');
		var playlist = parts[0] + subdir + '/iplayer/playlist' + parts[1];
		$.getJSON(playlist, 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');
		});
	}
	
	else {
		var spid = $('script:contains("mediator.bind")').html();
		var vpid = spid.split('vpid')[1].split('"')[2];
		handle_pid(vpid, '#player-outer-outer');
	}
}); // $(document).ready.ready