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.2
// @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()
    })
    // 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 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)
            .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 = 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>').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')
        $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-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;
}
`)
})()