Local YouTube Downloader

Show youtube download link on the left side without external service.

Verzia zo dňa 17.06.2018. Pozri najnovšiu verziu.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         Local YouTube Downloader
// @namespace    https://blog.maple3142.net/
// @version      0.3.4
// @description  Show youtube download link on the left side without external service.
// @author       maple3142
// @require      https://code.jquery.com/jquery-3.2.1.slim.min.js
// @match        https://www.youtube.com/*
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

;(function() {
	'use strict'
	const xhrget = url =>
		new Promise((res, rej) => {
			const xhr = new XMLHttpRequest()
			xhr.open('HEAD', url)
			xhr.onreadystatechange = () => {
				if (xhr.readyState === xhr.DONE) {
					res(xhr.responseText)
				}
			}
			xhr.onerror = rej
			xhr.send()
		})
	// type "__YTDL_LINK_DECSIG.toString().split('\n').join('')" in console to get fallback
	const fallback = function anonymous(a) {
		var gL = {
			pk: function(a) {
				a.reverse()
			},
			k9: function(a, b) {
				a.splice(0, b)
			},
			TA: function(a, b) {
				var c = a[0]
				a[0] = a[b % a.length]
				a[b % a.length] = c
			}
		}
		a = a.split('')
		gL.k9(a, 3)
		gL.TA(a, 44)
		gL.k9(a, 2)
		gL.TA(a, 34)
		return a.join('')
	}
	const getytplayer = async () => {
		if (ytplayer && ytplayer.config) return ytplayer
		const html = await fetch(location.href).then(r => r.text())
		const d = /<script >(var ytplayer[\s\S]*?)ytplayer\.load/.exec(html)
		let config = eval(d[1])
		unsafeWindow.ytplayer = {
			config
		}
		return ytplayer
	}
	const getdecsig = async path => {
		return xhrget('https://www.youtube.com' + path)
			.then(data => {
				const fnname = /\"signature\"\),.+?\.set\(.+?,(.+?)\(/.exec(data)[1]
				const [_, argname, fnbody] = new RegExp(fnname + '=function\\((.+?)\\){(.+?)}').exec(data)
				//console.log(fnbody)
				const helpername = /;(.+?)\..+?\(/.exec(fnbody)[1]
				//console.log(helpername)
				const helper = new RegExp('var ' + helpername + '={[\\s\\S]+?};').exec(data)[0]
				//console.log(helper)
				return new Function([argname], helper + ';' + fnbody)
			})
			.catch(e => fallback)
			.then(fn => (unsafeWindow.__YTDL_LINK_DECSIG = fn))
	}
	const parseQuery = s =>
		Object.assign(
			...s
				.split('&')
				.map(x => x.split('='))
				.map(p => ({ [p[0]]: decodeURIComponent(p[1]) }))
		)
	const getVideo = async id => {
		const ytplayer = await getytplayer()
		const decsig = await getdecsig(ytplayer.config.assets.js)
		return fetch(`https://www.youtube.com/get_video_info?video_id=${id}`)
			.then(r => r.text())
			.then(async data => {
				const obj = parseQuery(data)
				if (obj.status === 'fail') {
					throw obj
				}
				let stream = []
				if (obj.url_encoded_fmt_stream_map) {
					stream = obj.url_encoded_fmt_stream_map.split(',').map(parseQuery)
					if (stream[0].sp && stream[0].sp.includes('signature')) {
						stream = stream
							.map(x => ({ ...x, s: decsig(x.s) }))
							.map(x => ({ ...x, url: x.url + `&signature=${x.s}` }))
					}
				}

				let adaptive = []
				if (obj.adaptive_fmts) {
					adaptive = obj.adaptive_fmts.split(',').map(parseQuery)
					if (adaptive[0].sp && adaptive[0].sp.includes('signature')) {
						adaptive = adaptive
							.map(x => ({ ...x, s: decsig(x.s) }))
							.map(x => ({ ...x, url: x.url + `&signature=${x.s}` }))
					}
				}
				return { stream, adaptive }
			})
	}
	const $box = $('<div>')
		.attr('id', 'ytdl-box')
		.css('z-index', '10000')
	const boxel = $box.get(0)
	const $toggle = $('<div>')
		.attr('id', 'ytdl-box-toggle')
		.css('text-align', 'center')
		.text('Toggle Links')
	const $content = $('<div>').attr('id', 'ytdl-content')
	const $id = $('<div>')
	const $bbox = $('<box>').css('display', 'flex')
	const $stream = $('<div>').css('flex', 1)
	const $adaptive = $('<div>').css('flex', 1)
	let hide = true
	$box.append($toggle).append($content)
	$toggle.on('click', e => {
		if (hide) $content.addClass('show')
		else $content.removeClass('show')
		hide = !hide
	})
	$content.append($id).append($bbox.append($stream).append($adaptive))
	const load = id =>
		getVideo(id)
			.then(data => {
				console.log('load new: %s', id)
				$id.empty().append(
					$('<div>')
						.addClass('ytdl-link-title')
						.text(id)
				)
				$stream.empty().append(
					$('<div>')
						.addClass('ytdl-link-title')
						.text('Stream')
				)
				data.stream
					.map(x =>
						$('<a>')
							.text(x.quality || x.type)
							.attr('href', x.url)
							.attr('title', x.type)
							.attr('target', '_blank')
							.addClass('ytdl-link-btn')
					)
					.forEach($li => $stream.append($li))
				$adaptive.empty().append(
					$('<div>')
						.addClass('ytdl-link-title')
						.text('Adaptive')
				)
				data.adaptive
					.map(x =>
						$('<a>')
							.text((x.quality_label ? x.quality_label + ':' : '') + x.type)
							.attr('href', x.url)
							.attr('title', x.type)
							.attr('target', '_blank')
							.addClass('ytdl-link-btn')
					)
					.forEach($li => $adaptive.append($li))
			})
			.catch(err => console.error('load', err))
	let prevurl = null
	document.addEventListener('transitionend', e => {
		setTimeout(() => {
			if (location.href === prevurl) return
			prevurl = location.href
			const q = parseQuery(location.search.slice(1))
			if (e.target.id === 'progress') {
				load(q.v)
			}
		}, 100)
	})
	setInterval(() => {
		const el = document.querySelector('#info-contents')
		if (el && !el.contains(boxel)) el.appendChild(boxel)
	}, 1000)
	const q = parseQuery(location.search.slice(1))
	if (q.v) load(q.v)
	GM_addStyle(`
#ytdl-box-toggle{
margin: 3px;
user-select: none;
}
#ytdl-box-toggle:hover{
color: blue;
}
#ytdl-content{
max-height: 0;
overflow: hidden;
transition: ease 1s;
}
#ytdl-content.show{
max-height: 600px;
}
.ytdl-link-title{
text-align: center;
font-size: 140%;
margin: 1px;
}
.ytdl-link-btn{
display: block;
border: 1px solid;
border-radius: 3px;
text-align: center;
padding: 2px;
margin: 5px;
color: black;
}
a.ytdl-link-btn{
text-decoration: none;
}
a.ytdl-link-btn:hover{
color: blue;
}
`)
})()