Greasy Fork is available in English.

ytdl link

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

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

// ==UserScript==
// @name         ytdl link
// @namespace    https://blog.maple3142.net/
// @version      0.3.0
// @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
// ==/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()
    })
    const fallback = a => {
        // 2018/05/04
        const Sy = {
            kJ: function(a, b) {
                var c = a[0]
                a[0] = a[b % a.length]
                a[b % a.length] = c
            },
            ZK: function(a) {
                a.reverse()
            },
            Ug: function(a, b) {
                a.splice(0, b)
            }
        }
        a = a.split('')
        Sy.Ug(a, 2)
        Sy.ZK(a, 72)
        Sy.Ug(a, 2)
        Sy.ZK(a, 10)
        Sy.Ug(a, 3)
        Sy.kJ(a, 60)
        return a.join('')
    }
    const getdecsig= () => {
        let path='/yts/jsbin/player-vfl4qvcOS/zh_TW/base.js'
        if(ytplayer.config && ytplayer.config.assets.js){
            path=ytplayer.config.assets.js
        }
        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)
    }
    const parseQuery = s =>
    Object.assign(
        ...s
        .split('&')
        .map(x => x.split('='))
        .map(p => ({ [p[0]]: decodeURIComponent(p[1]) }))
    )
    const getVideo = id =>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
        }
        const decsig = await getdecsig(id)
        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>').hide()
    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.show()
        else $content.hide()
        hide=!hide
    })
    $content.append($id).append($bbox.append($stream).append($adaptive))
    const load=id=>getVideo(id).then(data=>{
        console.log('load new')
        $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))
    document.addEventListener('transitionend', e=> {
        const q=parseQuery(location.search.slice(1))
        if (e.target.id === 'progress'){
            load(q.v)
        }
        if(!q.v){
            $ul.empty()
        }
    })
    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-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;
}
`)
})()