Greasy Fork is available in English.

Local YouTube Downloader

Get youtube raw link without external service.

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

// ==UserScript==
// @name         Local YouTube Downloader
// @name:zh-TW   本地 YouTube 下載器
// @name:zh-CN   本地 YouTube 下载器
// @namespace    https://blog.maple3142.net/
// @version      0.4.2
// @description  Get youtube raw link without external service.
// @description:zh-TW  不需要透過第三方的服務就能下載 YouTube 影片。
// @description:zh-CN  不需要透过第三方的服务就能下载 YouTube 影片。
// @author       maple3142
// @match        https://www.youtube.com/*
// @require      https://cdnjs.cloudflare.com/ajax/libs/hyperapp/1.2.6/hyperapp.js
// @run-at       document-start
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

;(function() {
	'use strict'
	const $ = (s, x = document) => x.querySelector(s)
	const $$ = (s, x = document) => [...x.querySelectorAll(s)]
	const isobj = o => o && typeof o === 'object' && !Array.isArray(o)
	const deepmerge = (o, o1) => {
		for (const k of Object.keys(o1)) {
			if (isobj(o1[k])) {
				if (!(k in o)) o[k] = o1[k]
				else deepmerge(o[k], o1[k])
			} else o[k] = o1[k]
		}
		return o
	}
	const $el = (tag, { props = {}, events = {}, children = [] } = {}) => {
		const el = document.createElement(tag)
		for (const k of Object.keys(props)) {
			if (k in el && isobj(el[k])) deepmerge(el[k], props[k])
			else if (k in el) el[k] = props[k]
			else el.setAttribute(k, props[k])
		}
		for (const k of Object.keys(events)) {
			el.addEventListener(k, events[k])
		}
		for (const c of children) {
			el.appendChild(c)
		}
		return el
	}
	const xhrhead = 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()
		})
	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 xhrhead('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)
				const fn = new Function([argname], helper + ';' + fnbody)
				fn.meta = { argname, helper, fnbody }
				return fn
			})
			.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, decsig) => {
		return fetch(`https://www.youtube.com/get_video_info?video_id=${id}&el=detailpage`)
			.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 ytdlWorkerCode = `
const parseQuery=${parseQuery.toString()};
const getVideo=${getVideo.toString()};
onmessage=async e=>{
	const {argname,helper,fnbody}=e.data.decsigmeta;
	const decsig=new Function([argname],helper+';'+fnbody);
	const result=await getVideo(e.data.id,decsig);
	postMessage(result)
}`
	const ytdlWorker = new Worker(URL.createObjectURL(new Blob([ytdlWorkerCode])))
	const workerGetVideo = (id, decsigmeta) => {
		return new Promise((res, rej) => {
			const callback = e => {
				ytdlWorker.removeEventListener('message', callback)
				res(e.data)
			}
			ytdlWorker.addEventListener('message', callback)
			ytdlWorker.postMessage({ id, decsigmeta })
		})
	}

	const { app, h } = hyperapp
	const state = {
		hide: true,
		id: '',
		stream: [],
		adaptive: []
	}
	const actions = {
		togglehide: () => state => ({ hide: !state.hide }),
		setstate: newstate => state => newstate
	}
	const view = (state, actions) =>
		h('div', { id: 'ytdl-box', style: { zIndex: 10000 } }, [
			h(
				'div',
				{ onclick: () => actions.togglehide(), id: 'ytdl-box-toggle', className: 't-center' },
				'Toggle Links'
			),
			h('div', { className: state.hide ? 'hide' : '' }, [
				h('div', { className: 't-center' }, state.id),
				h('div', { className: 'd-flex' }, [
					h(
						'div',
						{ className: 'f-1' },
						[h('div', { className: 'ytdl-link-title' }, 'Stream')].concat(
							state.stream.map(x =>
								h(
									'a',
									{ href: x.url, title: x.type, target: '_blank', className: 'ytdl-link-btn' },
									x.quality || x.type
								)
							)
						)
					),
					h(
						'div',
						{ className: 'f-1' },
						[h('div', { className: 'ytdl-link-title' }, 'Adaptive')].concat(
							state.adaptive.map(x =>
								h(
									'a',
									{ href: x.url, title: x.type, target: '_blank', className: 'ytdl-link-btn' },
									(x.quality_label ? x.quality_label + ':' : '') + x.type
								)
							)
						)
					)
				])
			])
		])
	const container = $el('div')
	const $app = app(state, actions, view, container)
	const load = async id => {
		const ytplayer = await getytplayer()
		const decsig = await getdecsig(ytplayer.config.assets.js)
		return workerGetVideo(id, decsig.meta)
			.then(data => {
				console.log('load new: %s', id)
				$app.setstate({
					id,
					stream: data.stream,
					adaptive: data.adaptive
				})
			})
			.catch(err => console.error('load', err))
	}
	let prevurl = null
	setInterval(() => {
		const el = $('#info-contents')
		if (el && !el.contains(container)) el.appendChild(container)
		if (location.href !== prevurl && location.pathname === '/watch') {
			prevurl = location.href
			const q = parseQuery(location.search.slice(1))
			load(q.v)
		}
	}, 1000)
	GM_addStyle(`
.hide{
display: none;
}
.t-center{
text-align: center;
}
.d-flex{
display: flex;
}
.f-1{
flex: 1;
}
#ytdl-box-toggle{
margin: 3px;
user-select: none;
-moz-user-select: -moz-none;
}
#ytdl-box-toggle:hover{
color: blue;
}
.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;
}
`)
})()