您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
download YouTube video
// ==UserScript== // @name YTdl // @namespace https://tampermonkey.net/ // @version 0.2.1 // @description download YouTube video // @author Shiroikoi // @run-at document-idle // @match https://www.youtube.com/watch* // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.min.js // @require https://unpkg.com/@ffmpeg/[email protected]/dist/ffmpeg.min.js // @grant none // @compatible firefox >=79 // @compatible chrome >=68 // @license MIT // ==/UserScript== (function () { const ffmpeg = FFmpeg.createFFmpeg({ corePath: "https://unpkg.com/@ffmpeg/[email protected]/dist/ffmpeg-core.js", log: true, }); let vidseleVm, audseleVm, infoVm, buttVm, dataArray, controller, fileName, decryptFun, progressText = { totalLength: 0, receivedLength: 0, text: "" }, videoList = { options: [], }, audioList = { options: [], }, style = document.createElement("style"), optionVideo = document.createElement("option"), optionAudio = document.createElement("option"), row1 = document.createElement("div"), row2 = document.createElement("div"), selectVideo = document.createElement("select"), selectAudio = document.createElement("select"), button = document.createElement("button"), spanInfo = document.createElement("span"); style.innerText = "@font-face {\ font-family: 'Quicksand';\ font-style: normal;\ font-weight: 400;\ font-display: swap;\ src: url(https://fonts.gstatic.com/s/quicksand/v21/6xK-dSZaM9iE8KbpRA_LJ3z8mH9BOJvgkP8o58a-wg.woff2) format('woff2');\ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\ }"; document.head.append(style); button.setAttribute("v-on:click", "click"); button.textContent = "{{text}}"; button.style.width = "110px"; button.style.height = "35px"; button.style.fontSize = "20px"; button.style.fontFamily = "Quicksand"; button.style.marginLeft = "20px"; button.style.position = "absolute"; selectVideo.style.fontSize = "20px"; selectVideo.style.fontFamily = "Quicksand"; selectVideo.style.height = "35px"; selectVideo.style.width = "280px"; selectAudio.style.fontSize = "20px"; selectAudio.style.fontFamily = "Quicksand"; selectAudio.style.height = "35px"; selectAudio.style.width = "280px"; optionVideo.setAttribute("v-for", "i in options"); optionVideo.textContent = "{{ i }}"; optionAudio.setAttribute("v-for", "i in options"); optionAudio.textContent = "{{ i }}"; spanInfo.style.fontSize = "20px"; spanInfo.style.fontFamily = "Quicksand"; spanInfo.textContent = "{{text}}"; infoVm = new Vue({ data: progressText, }); vidseleVm = new Vue({ data: videoList, }); audseleVm = new Vue({ data: audioList, }); buttVm = new Vue({ data: { text: "download", signal: false, }, methods: { click: function () { if (this.signal == false) { infoVm.text = " parsing..."; controller = new AbortController(); this.stat2(); vidseleVm.$el.selectedIndex == videoList.options.length - 1 ? this.taskAud() : this.taskVidnAud(); } else { controller.abort(); this.stat1(); infoVm.text = " canceled!"; } }, taskVidnAud: async function () { let indexA = videoList.options.length - 1 + audseleVm.$el.selectedIndex, indexV = vidseleVm.$el.selectedIndex, videoUrl, audioUrl; if (dataArray[indexV].url == undefined) { if (decryptFun == undefined) decryptFun = await fetchCode().then(getDecryptFun); videoUrl = decryptUrls(decryptFun, indexV); audioUrl = decryptUrls(decryptFun, indexA); } else { videoUrl = dataArray[indexV].url; audioUrl = dataArray[indexA].url; } console.log(videoUrl) || console.log(audioUrl); let mediaArray = await Promise.all([ fetchMedia(videoUrl, controller, progressText, this.errcb), fetchMedia(audioUrl, controller, progressText, this.errcb), ]); if (mediaArray[0] == null) { return; } else if (mediaArray[0] == null) { return; } ffmpeg.isLoaded() ? null : await ffmpeg.load(); await blobToUint8Array(mediaArray[0]) .then((result) => { ffmpeg.FS("writeFile", "video", result); }) .catch((error) => { this.errcb(error); }); await blobToUint8Array(mediaArray[1]) .then((result) => { ffmpeg.FS("writeFile", "audio", result); }) .catch((error) => { this.errcb(error); }); await ffmpeg.run("-i", "video", "-i", "audio", "-c", "copy", "output.mp4"); let outPut = ffmpeg.FS("readFile", "output.mp4"); ffmpeg.FS("unlink", "output.mp4"); blobLink(new Blob([outPut]), fileName + ".mp4"); infoVm.text = " merged! total:" + progressText.totalLength + "MiB"; this.stat1(); }, taskAud: async function () { let indexA = videoList.options.length - 1 + audseleVm.$el.selectedIndex, audioUrl, suff; if (dataArray[indexA].url == undefined) { if (decryptFun == undefined) decryptFun = await fetchCode().then(getDecryptFun); audioUrl = decryptUrls(decryptFun, indexA); } else { audioUrl = dataArray[indexA].url; } console.log(audioUrl); let audio = await fetchMedia(audioUrl, controller, progressText, this.errcb); if (audio == null) return; if (dataArray[indexA].mimeType.match("mp4")) { suff = ".m4a"; } else if (dataArray[indexA].mimeType.match("webm")) { suff = ".weba"; } blobLink(audio, fileName + suff); infoVm.text = " merged! total:" + progressText.totalLength + "MiB"; this.stat1(); }, stat1: function () { this.signal = false; this.text = "download"; this.$el.style.backgroundColor = ""; }, stat2: function () { this.signal = true; this.text = "cancel"; this.$el.style.backgroundColor = "#99ccff"; }, errcb: function (error) { console.log(error.name); switch (error.name) { case "AbortError": this.stat1(); break; default: infoVm.text = " error! try refresh the page"; this.stat1(); break; } }, }, }); mountFun(); function mountFun() { if (document.querySelector("#meta") == null) { setTimeout(mountFun, 500); } else { document.querySelector("#meta").append(row1); document.querySelector("#meta").append(row2); row1.append(selectVideo); row1.append(button); row2.append(selectAudio); row2.append(spanInfo); selectVideo.prepend(optionVideo); selectAudio.prepend(optionAudio); buttVm.$mount(button); audseleVm.$mount(selectAudio); vidseleVm.$mount(selectVideo); infoVm.$mount(spanInfo); try { dataArray = ytInitialPlayerResponse.streamingData.adaptiveFormats; fileName = ytInitialPlayerResponse.videoDetails.title; dataArray.forEach((item) => { if (item.mimeType.match(/video/)) { videoList.options.push(item.qualityLabel + "-" + item.mimeType); } else if (item.mimeType.match(/audio/)) { audioList.options.push(item.audioQuality + "-" + item.mimeType); } }); videoList.options.push("none"); } catch (error) { console.log(error.name); infoVm.text = "script currently not available"; buttVm.$el.disabled = true; } } } async function fetchCode(errcb) { let code, script = document.querySelectorAll("script"); try { for (let i = 0; i < script.length; i++) { if (script[i].src.match(/base\.js/)) { const res = await fetch(script[i].src); code = await res.text(); break; } } return code; } catch (error) { errcb(error); console.log("fetchCode failed" + error.name); return null; } } function getDecryptFun(code) { let funName = code.match(/(?<==)[\w]+(?=\(decodeURIC)/)[0], funString = code .match(new RegExp(`(?<=${funName}=)function\\([\\s\\S]+?}`))[0] .replace(/^/, "(") .replace(/$/, ")"), fun = eval(funString), objName = funString.match(/(?<=;).+?./)[0], objSting = `var ${objName} =` + code.match(new RegExp(`(?<=var ${objName}=)[\\s\\S]+?}}`))[0]; eval(objSting); console.log(fun) || console.log(objSting); return fun; } function decryptUrls(fun, index) { let url = dataArray[index].signatureCipher, sig = fun(decodeURIComponent(url.match(/(?<=s=).+?(?=&sp)/))); url = decodeURIComponent(url.match(/https.+/)[0].replace(/\%25/g, "%")); return url + "&sig=" + encodeURIComponent(sig); } async function fetchMedia(url, controller, progressText, errcb) { try { let res = await fetch(url, { signal: controller.signal, }); const reader = res.body.getReader(); const contentLength = res.headers.get("Content-Length"); progressText.totalLength += parseFloat((parseInt(contentLength) / 1024 / 1024).toFixed(2)); let chunks = []; while (true) { const { done, value } = await reader.read(); if (done) { break; } chunks.push(value); progressText.receivedLength += value.length; progressText.text = "downloaded:" + (parseFloat(progressText.receivedLength) / 1024 / 1024).toFixed(2) + "MiB total:" + progressText.totalLength + "MiB"; } return new Blob(chunks); } catch (error) { errcb(error); return null; } } function blobToUint8Array(blob) { return new Promise((rs, rj) => { let fileReader = new FileReader(); fileReader.onload = () => { rs(new Uint8Array(fileReader.result)); }; fileReader.onerror = () => { rj({ name: "b28 failed" }); }; fileReader.readAsArrayBuffer(blob); }); } function blobLink(blob, fileName) { let dl = document.createElement("a"); dl.download = fileName; dl.href = URL.createObjectURL(blob); dl.click(); URL.revokeObjectURL(dl.href); dl.remove(); } })();