Local YouTube Downloader

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

Verze ze dne 17. 06. 2018. Zobrazit nejnovější verzi.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==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;
}
`)
})()