Greasy Fork is available in English.

YouTube视频下载,支持多种分辨率下载,视频搬运必备神器

亲测可用的YouTube下载脚本,支持下载各种分辨率1080P,4K,2K,720P / 在原作者3142 maple 的基础上,集成淘宝领券小助手,一键领券优惠券

// ==UserScript==
// @name YouTube视频下载,支持多种分辨率下载,视频搬运必备神器
// @namespace http://zkq8.com/
// @version 1.0.8
// @description 亲测可用的YouTube下载脚本,支持下载各种分辨率1080P,4K,2K,720P    /    在原作者3142 maple 的基础上,集成淘宝领券小助手,一键领券优惠券
// @include http*://chaoshi.detail.tmall.com/*
// @include http*://detail.tmall.com/*
// @include http*://item.taobao.com/*
// @include http*://list.tmall.com/*
// @include http*://list.tmall.hk/*
// @include http*://www.taobao.com/*
// @include http*://www.tmall.com/*
// @include http*://s.taobao.com/*
// @include http*://detail.tmall.hk/*
// @include http*://chaoshi.tmall.com/*
// @require https://code.jquery.com/jquery-3.4.0.min.js
// @author max su,3142 maple 
// @match https://*.youtube.com/*
// @require https://unpkg.com/vue@2.6.10/dist/vue.js
// @require https://unpkg.com/xfetch-js@0.3.4/xfetch.min.js
// @compatible firefox >=52
// @compatible chrome >=55
// @license MIT
// ==/UserScript==

(function() {
  'use strict'
  const DEBUG = true
  const createLogger = (console, tag) =>
    Object.keys(console)
      .map(k => [k, (...args) => (DEBUG ? console[k](tag + ': ' + args[0], ...args.slice(1)) : void 0)])
      .reduce((acc, [k, fn]) => ((acc[k] = fn), acc), {})
  const logger = createLogger(console, 'YTDL')

  const LANG_FALLBACK = 'en'
  const LOCALE = {
    en: {
      togglelinks: 'Show/Hide Links',
      stream: 'Stream',
      adaptive: 'Adaptive',
      videoid: 'Video Id: ',
      thumbnail: 'Thumbnail',
      inbrowser_adaptive_merger: 'In browser adaptive video & audio merger'
    },
    'zh-tw': {
      togglelinks: '顯示 / 隱藏連結',
      stream: '串流 Stream',
      adaptive: '自適應 Adaptive',
      videoid: '影片 ID: ',
      thumbnail: '影片縮圖',
      inbrowser_adaptive_merger: '瀏覽器版自適應影片及聲音合成器'
    },
    zh: {
      togglelinks: '显示 / 隐藏 下载链接',
      stream: '串流 Stream',
      adaptive: '自适应 Adaptive',
      videoid: '视频 ID: ',
      thumbnail: '视频缩图',
      inbrowser_adaptive_merger: '浏览器版自适应视频及声音合成器'
    }
  }
  const findLang = l => {
    // language resolution logic: zh-tw --(if not exists)--> zh --(if not exists)--> LANG_FALLBACK(en)
    l = l.toLowerCase().replace('_', '-')
    if (l in LOCALE) return l
    else if (l.length > 2) return findLang(l.split('-')[0])
    else return LANG_FALLBACK
  }
  const $ = (s, x = document) => x.querySelector(s)
  const $el = (tag, opts) => {
    const el = document.createElement(tag)
    Object.assign(el, opts)
    return el
  }
  const parseDecsig = data => {
    try {
      if (data.startsWith('var script')) {
        // they inject the script via script tag
        const obj = {}
        const document = { createElement: () => obj, head: { appendChild: () => {} } }
        eval(data)
        data = obj.innerHTML
      }
      const fnnameresult = /yt\.akamaized\.net.*encodeURIComponent\((\w+)/.exec(data)
      const fnname = fnnameresult[1]
      const _argnamefnbodyresult = new RegExp(fnname + '=function\\((.+?)\\){(.+?)}').exec(data)
      const [_, argname, fnbody] = _argnamefnbodyresult
      const helpernameresult = /;(.+?)\..+?\(/.exec(fnbody)
      const helpername = helpernameresult[1]
      const helperresult = new RegExp('var ' + helpername + '={[\\s\\S]+?};').exec(data)
      const helper = helperresult[0]
      logger.log(`parsedecsig result: %s=>{%s\n%s}`, argname, helper, fnbody)
      return new Function([argname], helper + '\n' + fnbody)
    } catch (e) {
      logger.error('parsedecsig error: %o', e)
      logger.info('script content: %s', data)
      logger.info(
        'If you encounter this error, please copy the full "script content" to https://pastebin.com/ for me.'
      )
    }
  }
  const parseQuery = s => [...new URLSearchParams(s).entries()].reduce((acc, [k, v]) => ((acc[k] = v), acc), {})
  const getVideo = async (id, decsig) => {
    return xf
      .get(`https://www.youtube.com/get_video_info?video_id=${id}&el=detailpage`)
      .text()
      .then(async data => {
        const obj = parseQuery(data)
        logger.log(`video %s data: %o`, id, obj)
        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)
          logger.log(`video %s stream: %o`, id, stream)
          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)
          logger.log(`video %s adaptive: %o`, id, adaptive)
          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}` }))
          }
        }
        logger.log(`video %s result: %o`, id, { stream, adaptive })
        return { stream, adaptive, meta: obj }
      })
  }
  const getVideoDetails = id =>
    xf
      .get('https://www.googleapis.com/youtube/v3/videos', {
        qs: {
          key: 'AIzaSyBk6o0igFl-P4Qe4ouVlRTPlqX7kruWdUg',
          part: 'snippet',
          id
        }
      })
      .json(r => r.items[0])
  const workerMessageHandler = async e => {
    const decsig = await xf.get(e.data.path).text(parseDecsig)
    const result = await getVideo(e.data.id, decsig)
    self.postMessage(result)
  }
  const ytdlWorkerCode = `
importScripts('https://unpkg.com/xfetch-js@0.3.4/xfetch.min.js')
const DEBUG=${DEBUG}
const logger=(${createLogger})(console, 'YTDL')
const parseQuery=${parseQuery}
const parseDecsig=${parseDecsig}
const getVideo=${getVideo}
self.onmessage=${workerMessageHandler}`
  const ytdlWorker = new Worker(URL.createObjectURL(new Blob([ytdlWorkerCode])))
  const workerGetVideo = (id, path) => {
    logger.log(`workerGetVideo start: %s %s`, id, path)
    return new Promise((res, rej) => {
      const callback = e => {
        ytdlWorker.removeEventListener('message', callback)
        logger.log('workerGetVideo end: %o', e.data)
        res(e.data)
      }
      ytdlWorker.addEventListener('message', callback)
      ytdlWorker.postMessage({ id, path })
    })
  }

  const template = `
<div class="box" :class="{'dark':dark}">
  <div @click="hide=!hide" class="box-toggle t-center fs-14px" v-text="strings.togglelinks"></div>
  <div :class="{'hide':hide}">
    <div class="t-center fs-14px" v-text="strings.videoid+id"></div>
    <div class="t-center fs-14px">
      <a :href="thumbnail" target="_blank" v-text="strings.thumbnail"></a>
    </div>
    <div class="d-flex">
      <div class="f-1 of-h">
        <div class="t-center fs-14px" v-text="strings.stream"></div>
        <a class="ytdl-link-btn fs-14px" target="_blank" v-for="vid in stream" :href="vid.url" :title="vid.type" v-text="vid.quality||vid.type"></a>
      </div>
      <div class="f-1 of-h">
        <div class="t-center fs-14px" v-text="strings.adaptive"></div>
        <a class="ytdl-link-btn fs-14px" target="_blank" v-for="vid in adaptive" :href="vid.url" :title="vid.type" v-text="[vid.quality_label,vid.type].filter(x=>x).join(':')"></a>
      </div>
    </div>
    <div class="of-h t-center">
      <a href="https://maple3142.github.io/mergemp4/" target="_blank" v-text="strings.inbrowser_adaptive_merger"></a>
    </div>
  </div>
</div>
`.slice(1)
  const app = new Vue({
    data() {
      return {
        hide: true,
        id: '',
        stream: [],
        adaptive: [],
        dark: false,
        thumbnail: null,
        lang: findLang(navigator.language)
      }
    },
    computed: {
      strings() {
        return LOCALE[this.lang.toLowerCase()]
      }
    },
    template
  })
  logger.log(`default language: %s`, app.lang)

  // attach element
  const shadowHost = $el('div')
  const shadow = shadowHost.attachShadow ? shadowHost.attachShadow({ mode: 'closed' }) : shadowHost // no shadow dom
  logger.log('shadowHost: %o', shadowHost)
  const container = $el('div')
  shadow.appendChild(container)
  app.$mount(container)

  if (DEBUG) {
    // expose some functions for debugging
    unsafeWindow.$app = app
    unsafeWindow.parseQuery = parseQuery
    unsafeWindow.parseDecsig = parseDecsig
    unsafeWindow.getVideo = getVideo
  }

  const getLangCode = () => {
    if (typeof ytplayer !== 'undefined') {
      return ytplayer.config.args.host_language
    } else if (typeof yt !== 'undefined') {
      return yt.config_.GAPI_LOCALE
    }
    return null
  }
  const load = async id => {
    const scriptel = $('script[src$="base.js"]')
    try {
      const data = await workerGetVideo(id, scriptel.src)
      const details = await getVideoDetails(id)
      logger.log('video details: %o', details)
      logger.log('video loaded: %s', id)
      app.id = id
      app.stream = data.stream
      app.adaptive = data.adaptive
      app.meta = data.meta

      // find highest quality thumbnail
      const thumbnail = Object.values(details.snippet.thumbnails)
        .map(d => {
          const x = {}
          x.url = d.url
          x.size = d.width * d.height
          return x
        })
        .sort((a, b) => b.size - a.size)[0].url
      app.thumbnail = thumbnail

      const actLang = getLangCode()
      if (actLang !== null) {
        const lang = findLang(actLang)
        logger.log('youtube ui lang: %s', actLang)
        logger.log('ytdl lang:', lang)
        app.lang = lang
      }
    } catch (err) {
      logger.error('load', err)
    }
  }
  let prev = null
  setInterval(() => {
    const el =
      $('#info-contents') ||
      $('#watch-header') ||
      $('.page-container:not([hidden]) ytm-item-section-renderer>lazy-list')
    if (el && !el.contains(shadowHost)) {
      el.appendChild(shadowHost)
    }
    if (location.href !== prev) {
      logger.log(`page change: ${prev} -> ${location.href}`)
      prev = location.href
      if (location.pathname === '/watch') {
        shadowHost.style.display = 'block'
        const id = parseQuery(location.search).v
        logger.log('start loading new video: %s', id)
        app.hide = true // fold it
        load(id)
      } else {
        shadowHost.style.display = 'none'
      }
    }
  }, 1000)

  // listen to dark mode toggle
  const $html = $('html')
  new MutationObserver(() => {
    app.dark = $html.getAttribute('dark') === 'true'
  }).observe($html, { attributes: true })
  app.dark = $html.getAttribute('dark') === 'true'

  const css = `
.hide{
display: none;
}
.t-center{
text-align: center;
}
.d-flex{
display: flex;
}
.f-1{
flex: 1;
}
.fs-14px{
font-size: 14px;
}
.of-h{
overflow: hidden;
}
.box{
border-bottom: 1px solid var(--yt-border-color);
font-family: Arial;
}
.box-toggle{
margin: 3px;
user-select: none;
-moz-user-select: -moz-none;
}
.box-toggle:hover{
color: blue;
}
.ytdl-link-btn{
display: block;
border: 1px solid !important;
border-radius: 3px;
text-decoration: none !important;
outline: 0;
text-align: center;
padding: 2px;
margin: 5px;
color: black;
}
a.ytdl-link-btn{
text-decoration: none;
}
a.ytdl-link-btn:hover{
color: blue;
}
.box.dark{
color: var(--ytd-video-primary-info-renderer-title-color, var(--yt-primary-text-color));
}
.box.dark .ytdl-link-btn{
color: var(--ytd-video-primary-info-renderer-title-color, var(--yt-primary-text-color));
}
.box.dark .ytdl-link-btn:hover{
color: rgba(200, 200, 255, 0.8);
}
.box.dark .box-toggle:hover{
color: rgba(200, 200, 255, 0.8);
}
`
  shadow.appendChild($el('style', { textContent: css }))
  
})();
$(document).ready(function() {
	var idd = '';
	var name = '';//$(document).attr('title');
	var host = window.location.host;
	if($('#max-test').length<=0){
	  if(host == 'detail.tmall.com'){
		idd = $("link[rel=canonical]").attr("href");
		idd = idd.split("id=")[1];
		name = $('meta[name=keywords]').attr('content');
		var tmall = '<div id="max-test"><p> <br/></p></div><div class="tb-action" style="margin-top:0"><a style="display: inline-block;padding: 6px 9px;margin-bottom: 0;font-size: 16px;font-weight: normal;height:16px;line-height:16px;width:100px;text-align: center;white-space: nowrap;vertical-align: middle;-ms-touch-action: manipulation;touch-action: manipulation;cursor: pointer;-webkit-user-select: none;-moz-user-select: none;-ms-user-select: none;user-select: none;background-image: none;border: 1px solid transparent;border-radius:2px;color: #fff;background-color: #2e8b57;#2e8b57;" href="http://zkq8.com/index.php?r=index/classify&kw='+ encodeURI(name) +'&id='+ encodeURI(idd) +'" " target="_blank">优惠购买</a><a style="display: inline-block;padding: 6px 9px;margin-bottom: 0;font-size: 16px;font-weight: normal;height:16px;line-height:16px;width:100px;text-align: center;white-space: nowrap;vertical-align: middle;-ms-touch-action: manipulation;touch-action: manipulation;cursor: pointer;-webkit-user-select: none;-moz-user-select: none;-ms-user-select: none;user-select: none;background-image: none;border: 1px solid transparent;border-radius:5px;color: #fff;background-color: #8600FF;#8600FF;margin-left:5px; " href="http://zkq8.com/index.php?r=p" target="_blank">疯抢榜TOP100</a><a style="display: inline-block;padding: 6px 9px;margin-bottom: 0;font-size: 16px;font-weight: normal;height:16px;line-height:16px;width:100px;text-align: center;white-space: nowrap;vertical-align: middle;-ms-touch-action: manipulation;touch-action: manipulation;cursor: pointer;-webkit-user-select: none;-moz-user-select: none;-ms-user-select: none;user-select: none;background-image: none;border: 1px solid transparent;border-radius:5px;color: #fff;background-color: #2e8b57;#2e8b57;margin-left:5px; " href="http://zkq8.com/index.php?r=nine" target="_blank">9.9元包邮</a></div>';
		$('.tb-sku').append(tmall);
	  }else if (host == 'item.taobao.com') {
		idd = $("link[rel=canonical]").attr("href");
		idd = idd.split("id=")[1];
		name = $('.tb-main-title').attr('data-title');
		var taobao = '<div id="max-test"><p> <br/></p></div><div class="tb-action" style="margin-top:0"><a style="display: inline-block;padding: 6px 9px;margin-bottom: 0;font-size: 16px;font-weight: normal;height:16px;line-height:16px;width:100px;text-align: center;white-space: nowrap;vertical-align: middle;-ms-touch-action: manipulation;touch-action: manipulation;cursor: pointer;-webkit-user-select: none;-moz-user-select: none;-ms-user-select: none;user-select: none;background-image: none;border: 1px solid transparent;border-radius:2px;color: #fff;background-color: #2e8b57;#2e8b57;" href="http://zkq8.com/index.php?r=index/classify&kw='+ encodeURI(name) +'&id='+ encodeURI(idd) +'" " >优惠购买</a><a style="display: inline-block;padding: 6px 9px;margin-bottom: 0;font-size: 16px;font-weight: normal;height:16px;line-height:16px;width:100px;text-align: center;white-space: nowrap;vertical-align: middle;-ms-touch-action: manipulation;touch-action: manipulation;cursor: pointer;-webkit-user-select: none;-moz-user-select: none;-ms-user-select: none;user-select: none;background-image: none;border: 1px solid transparent;border-radius:5px;color: #fff;background-color: #8600FF;#8600FF;margin-left:5px; " href="http://zkq8.com/index.php?r=p" target="_blank">疯抢榜TOP100</a><a style="display: inline-block;padding: 6px 9px;margin-bottom: 0;font-size: 16px;font-weight: normal;height:16px;line-height:16px;width:100px;text-align: center;white-space: nowrap;vertical-align: middle;-ms-touch-action: manipulation;touch-action: manipulation;cursor: pointer;-webkit-user-select: none;-moz-user-select: none;-ms-user-select: none;user-select: none;background-image: none;border: 1px solid transparent;border-radius:5px;color: #fff;background-color: #2e8b57;#2e8b57;margin-left:5px; " href="http://zkq8.com/index.php?r=nine" target="_blank">9.9元包邮</a></div>';
		$('.tb-key').append(taobao);
	  }else if(host == 'detail.tmall.hk'){
		idd = $("link[rel=canonical]").attr("href");
		idd = idd.split("id=")[1];
		name = $('meta[name=keywords]').attr('content');
		var tmall_xg = '<div id="max-test"><p> <br/></p></div><div class="tb-action" style="margin-top:0"><a style="display: inline-block;padding: 6px 9px;margin-bottom: 0;font-size: 16px;font-weight: normal;height:16px;line-height:16px;width:100px;text-align: center;white-space: nowrap;vertical-align: middle;-ms-touch-action: manipulation;touch-action: manipulation;cursor: pointer;-webkit-user-select: none;-moz-user-select: none;-ms-user-select: none;user-select: none;background-image: none;border: 1px solid transparent;border-radius:2px;color: #fff;background-color: #2e8b57;#2e8b57;" href="http://zkq8.com/index.php?r=index/classify&kw='+ encodeURI(name) +'&id='+ encodeURI(idd) +'" " target="_blank">优惠购买</a><a style="display: inline-block;padding: 6px 9px;margin-bottom: 0;font-size: 16px;font-weight: normal;height:16px;line-height:16px;width:100px;text-align: center;white-space: nowrap;vertical-align: middle;-ms-touch-action: manipulation;touch-action: manipulation;cursor: pointer;-webkit-user-select: none;-moz-user-select: none;-ms-user-select: none;user-select: none;background-image: none;border: 1px solid transparent;border-radius:5px;color: #fff;background-color: #8600FF;#8600FF;margin-left:5px; " href="http://zkq8.com/index.php?r=p" target="_blank">疯抢榜TOP100</a><a style="display: inline-block;padding: 6px 9px;margin-bottom: 0;font-size: 16px;font-weight: normal;height:16px;line-height:16px;width:100px;text-align: center;white-space: nowrap;vertical-align: middle;-ms-touch-action: manipulation;touch-action: manipulation;cursor: pointer;-webkit-user-select: none;-moz-user-select: none;-ms-user-select: none;user-select: none;background-image: none;border: 1px solid transparent;border-radius:5px;color: #fff;background-color: #2e8b57;#2e8b57;margin-left:5px; " href="http://zkq8.com/index.php?r=nine" target="_blank">9.9元包邮</a></div>';
		$('.tb-sku').append(tmall_xg);
	  }else if(host == 'chaoshi.detail.tmall.com'){
		idd = $('a[id=J_AddFavorite]').attr('data-aldurl')
		idd = idd.split("idd=")[1]
		name = $('input[name=title]').attr('value');
		var cs_tmall = '<div id="max-test"><p> <br/></p></div><div class="tb-action tb-btn-add tb-btn-sku"><a style="display: inline-block;padding: 6px 9px;margin-bottom: 0;font-size: 16px;font-weight: normal;height:16px;line-height:16px;width:100px;text-align: center;white-space: nowrap;vertical-align: middle;-ms-touch-action: manipulation;touch-action: manipulation;cursor: pointer;-webkit-user-select: none;-moz-user-select: none;-ms-user-select: none;user-select: none;background-image: none;border: 1px solid transparent;border-radius:5px;color: #fff;background-color: #2e8b57;#2e8b57;"href="http://zkq8.com/index.php?r=index/classify&kw='+ encodeURI(name) +'&id='+ encodeURI(idd) +'" " target="_blank">优惠购买</a><a style="display: inline-block;padding: 6px 9px;margin-bottom: 0;font-size: 16px;font-weight: normal;height:16px;line-height:16px;width:100px;text-align: center;white-space: nowrap;vertical-align: middle;-ms-touch-action: manipulation;touch-action: manipulation;cursor: pointer;-webkit-user-select: none;-moz-user-select: none;-ms-user-select: none;user-select: none;background-image: none;border: 1px solid transparent;border-radius:5px;color: #fff;background-color: #8600FF;#8600FF;margin-left:5px; " href="http://zkq8.com/index.php?r=p" target="_blank">疯抢榜TOP100</a><a style="display: inline-block;padding: 6px 9px;margin-bottom: 0;font-size: 16px;font-weight: normal;height:16px;line-height:16px;width:100px;text-align: center;white-space: nowrap;vertical-align: middle;-ms-touch-action: manipulation;touch-action: manipulation;cursor: pointer;-webkit-user-select: none;-moz-user-select: none;-ms-user-select: none;user-select: none;background-image: none;border: 1px solid transparent;border-radius:5px;color: #fff;background-color: #2e8b57;#2e8b57;margin-left:5px; " href="http://zkq8.com/index.php?r=nine" target="_blank">9.9元包邮</a></div>';
		$('.tb-sku').append(cs_tmall);
	  }
	}

	function max_tb_search() {
		var new_search_addr = "http://www.zkq8.com/index.php?input=2&r=l&kw=" + $('.search-combobox-input').val();
		window.open(new_search_addr);
	}

	function max_stb_search() {
		var new_search_addr = "http://www.zkq8.com/index.php?input=2&r=l&kw=" + $('#J_Header_js').val();
		window.open(new_search_addr);
	}
	
	function max_tmall_search() {
		var new_search_addr = "http://www.zkq8.com/index.php?input=2&r=l&kw=" + $('.s-combobox-input').val();
		window.open(new_search_addr);
	}
	if($('#max-search').length<=0){
		if (host == 'www.taobao.com') {
			var tb_search = '<div id="max-search" style="position: absolute;right: -100px;top: 22px;width: 100px;height: 40px;text-align: center;line-height: 40px;border-radius: 9px;color: #ffffff;background: #FF4200;font-size: 16px;font-weight: 800;"><a id="max-search-a" style="color:white;" href="javascript:void(0);">搜优惠券</a></div>'
			$('.search-bd').append(tb_search);
			$('#max-search-a').click(function(){max_tb_search()})
		} else if (host == 's.taobao.com') {
			var stb_search= '<div style="float:right;margin-top:1px;color: #ffffff;background: #FF4200;font-size: 16px;font-weight: 800;width: 130px;height: 30px;text-align: center;line-height: 30px;border-radius: 9px;" id="max-search"><a id="max-search-a" style="color:white;" href="javascript:void(0);">搜优惠券</a></div>';
			$('.search').append(stb_search);
			$('#max-search-a').click(function(){max_tb_search()});
		} else if (host == 'www.tmall.com' || host == 'list.tmall.com') {
			var tmall_search= '<div style="float:right;margin-top:1px;color: #ffffff;background: #FF4200;font-size: 16px;font-weight: 800;width: 130px;height: 30px;text-align: center;line-height: 30px;border-radius: 9px;" id="max-search"><a id="max-search-a" style="color:white;" href="javascript:void(0);">搜优惠券</a></div>';
			$('#mallSearch').append(tmall_search);
			$('#max-search-a').click(function(){max_tmall_search()});
		}
	}

});