Last active
July 31, 2025 21:04
-
-
Save kuylar/bff978e469961819660ed8ec0e4b2883 to your computer and use it in GitHub Desktop.
Small Firefox (and Chrome, maybe, I didn't test it) extension code (content-script) for downloading videos on YouTube.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Small Firefox (and Chrome, maybe, I didn't test it) extension code | |
| * (content-script) for downloading videos on YouTube. Currently only | |
| * shows a default HTML download button on the top left of the page | |
| * and opens up the format 18 (360p muxed MP4) on a new tab. | |
| * | |
| * It "works", has a "working" poToken implementation (untested for | |
| * downloading videos and unused in this version of the script) and | |
| * can decipher playback URLs automatically. | |
| * | |
| * This was mainly a quick challenge for myself and I don't have any | |
| * intention to finish this project as I have a lot of other stuff to | |
| * work on at the moment. If anyone wants, they can pick from where I | |
| * left off and finish this. | |
| * | |
| * P.S.: I didn't license this code in any way because most of it is | |
| * stolen from NewPipeExtractor & yt-dlp. If you decide to use | |
| * this code as a starting point and publish a finished version, | |
| * keep that in mind. | |
| * | |
| * - kuylar, 17/02/2025 | |
| */ | |
| // Taken from NewPipe. Love y'all <3 | |
| const poTokenScript = `function loadBotGuard(n){if(this.vm=this[n.globalName],this.program=n.program,this.vmFunctions={},this.syncSnapshotFunction=null,!this.vm)throw Error("[BotGuardClient]: VM not found in the global object");if(!this.vm.a)throw Error("[BotGuardClient]: Could not load program");let t=function(n,t,o,e){this.vmFunctions={asyncSnapshotFunction:n,shutdownFunction:t,passEventFunction:o,checkCameraFunction:e}};return this.syncSnapshotFunction=this.vm.a(this.program,t,!0,this.userInteractionElement,function(){},[[],[]])[0],new Promise(function(n,t){i=0,refreshIntervalId=setInterval(function(){this.vmFunctions.asyncSnapshotFunction&&(n(this),clearInterval(refreshIntervalId)),i>=1e4&&(t("asyncSnapshotFunction is null even after 10 seconds"),clearInterval(refreshIntervalId)),i+=1},1)})}function snapshot(n){return new Promise(function(t,o){if(!this.vmFunctions.asyncSnapshotFunction)return o(Error("[BotGuardClient]: Async snapshot function not found"));this.vmFunctions.asyncSnapshotFunction(function(n){t(n)},[n.contentBinding,n.signedTimestamp,n.webPoSignalOutput,n.skipPrivacyBuffer])})}function runBotGuard(n){let t=n.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue;if(t)Function(t)();else throw Error("Could not load VM");let o=[];return loadBotGuard({globalName:n.globalName,globalObj:this,program:n.program}).then(function(n){return n.snapshot({webPoSignalOutput:o})}).then(function(n){return{webPoSignalOutput:o,botguardResponse:n}})}function obtainPoToken(n,t,o){let e=n[0];if(!e)throw Error("PMD:Undefined");let a=e(t);if(!(a instanceof Function))throw Error("APF:Failed");let s=a(o);if(!s)throw Error("YNJ:Undefined");if(!(s instanceof Uint8Array))throw Error("ODM:Invalid");return s};function makeBotguardServiceRequest(e,t){return new Promise(async(r,a)=>{await fetch(e,{headers:{Accept:"application/json","Content-Type":"application/json+protobuf","x-goog-api-key":"AIzaSyDyT5W0Jh49F30Pqqtyfdf7pDLFKLJoAnw","x-user-agent":"grpc-web-javascript/0.1"},method:"POST",body:t}).then(e=>{if(200!=e.status)throw Error("BotGuard request failed");r(e)}).catch(e=>{console.error(e),a(e)})})}function descramble(e){return Array.from((byteArray=Uint8Array.fromBase64(e)).map(e=>e+97)).map(e=>String.fromCharCode(e)).join("")}function parseChallengeData(e){var t="";t=e.length>1&&"string"==typeof e[1]?descramble(e[1]):e[1];let r=JSON.parse(t);var a=r[0],n=r[3],o=r[4],i=r[5],p=r[7],s=r[1]?.find(e=>"string"==typeof e),c=r[2]?.find(e=>"string"==typeof e);return{messageId:a,interpreterJavascript:{privateDoNotAccessOrElseSafeScriptWrappedValue:s,privateDoNotAccessOrElseTrustedResourceUrlWrappedValue:c},interpreterHash:n,program:o,globalName:i,clientExperimentsStateBlob:p}}`; | |
| // Template script to inject signatureCipher and nParam deciphering code | |
| const sigTemplate = `const %%helperObjName%%={%%helperObjBody%%};function decipherSignature(%%sigParam%%){%%sigFuncBody%%};function decipherNParam%%nParamCode%%`; | |
| let signatureTimestamp = 0; | |
| /** | |
| * Downloads a file and returns the result as a string | |
| * @param {String} url | |
| * @returns {String} Contents of the given URL | |
| */ | |
| function downloadFile(url) { | |
| return new Promise((res, rej) => { | |
| fetch(url) | |
| .then((x) => x.text()) | |
| .then(res) | |
| .catch(rej); | |
| }); | |
| } | |
| /** | |
| * Tries to match input with every pattern inside the regexes array and returns the first one that matches | |
| * @param {String[]} regexes List of RegEx patterns to match input with | |
| * @param {String} input String to check the regexes with | |
| * @returns {RegExpMatchArray | null} | |
| */ | |
| function regexExtractMultiple(regexes, input) { | |
| for (const pattern of regexes) { | |
| const result = input.match(pattern); | |
| if (result) return result; | |
| } | |
| return null; | |
| } | |
| /** | |
| * Injects the given JS code into the HTML | |
| */ | |
| function injectScript(id, js) { | |
| if (document.getElementById(`injectedScript_${id}`)) { | |
| console.error(`Script with ID '${id}' is already injected`); | |
| return; | |
| } | |
| const scr = document.createElement("script"); | |
| scr.id = `injectedScript_${id}`; | |
| scr.src = URL.createObjectURL(new Blob([js], { type: "text/javascript" })); | |
| document.body.appendChild(scr); | |
| } | |
| /** | |
| * Downloads and extracts the required parameters to decipher URL signatures | |
| */ | |
| async function getDecipheringStuff() { | |
| const playerHash = await downloadFile( | |
| "https://www.youtube.com/iframe_api" | |
| ).then((x) => { | |
| console.log("dlfile", x, typeof x); | |
| return x.match("player\\\\/(.+?)\\\\/")[1]; | |
| }); | |
| const playerUrl = `https://www.youtube.com/s/player/${playerHash}/player_ias.vflset/en_US/base.js`; | |
| const playerScript = await downloadFile(playerUrl); | |
| console.log("Downloaded player"); | |
| signatureTimestamp = parseInt( | |
| playerScript.match("signatureTimestamp[=:] ?(\\d+)")[1] | |
| ); | |
| // finds the signature code idk which one until i see the result | |
| const sigFuncName = regexExtractMultiple( | |
| [ | |
| "\\b(?<var>[a-zA-Z0-9_$]+)&&\\(\\1=(?<sig>[a-zA-Z0-9_$]{2,})\\(decodeURIComponent\\(\\1\\)\\)", | |
| '(?<sig>[a-zA-Z0-9_$]+)\\s*=\\s*function\\(\\s*(?<arg>[a-zA-Z0-9_$]+)\\s*\\)\\s*{\\s*\\1\\s*=\\s*\\1\\.split\\(\\s*""\\s*\\)\\s*;\\s*[^}]+;\\s*return\\s+\\1\\.join\\(\\s*""\\s*\\)', | |
| '(?:\\b|[^a-zA-Z0-9_$])(?<sig>[a-zA-Z0-9_$]{2,})\\s*=\\s*function\\(\\s*a\\s*\\)\\s*{\\s*a\\s*=\\s*a\\.split\\(\\s*""\\s*\\)(?:;[a-zA-Z0-9_$]{2}\\.[a-zA-Z0-9_$]{2}\\(a,\\d+\\))?', | |
| // Old patterns | |
| "\\b[cs]\\s*&&\\s*[adf]\\.set\\([^,]+\\s*,\\s*encodeURIComponent\\s*\\(\\s*(?<sig>[a-zA-Z0-9$]+)\\(", | |
| "\\b[a-zA-Z0-9]+\\s*&&\\s*[a-zA-Z0-9]+\\.set\\([^,]+\\s*,\\s*encodeURIComponent\\s*\\(\\s*(?<sig>[a-zA-Z0-9$]+)\\(", | |
| "\\bm=(?<sig>[a-zA-Z0-9$]{2,})\\(decodeURIComponent\\(h\\.s\\)\\)", | |
| // Obsolete patterns | |
| "(\"|\\')signature\\1\\s*,\\s*(?<sig>[a-zA-Z0-9$]+)\\(", | |
| "\\.sig\\|\\|(?<sig>[a-zA-Z0-9$]+)\\(", | |
| "yt\\.akamaized\\.net/\\)\\s*\\|\\|\\s*.*?\\s*[cs]\\s*&&\\s*[adf]\\.set\\([^,]+\\s*,\\s*(?:encodeURIComponent\\s*\\()?\\s*(?<sig>[a-zA-Z0-9$]+)\\(", | |
| "\\b[cs]\\s*&&\\s*[adf]\\.set\\([^,]+\\s*,\\s*(?<sig>[a-zA-Z0-9$]+)\\(", | |
| "\\bc\\s*&&\\s*[a-zA-Z0-9]+\\.set\\([^,]+\\s*,\\s*\\([^)]*\\)\\s*\\(\\s*(?<sig>[a-zA-Z0-9$]+)\\(", | |
| ], | |
| playerScript | |
| ).groups["sig"]; | |
| const sigFuncBodyMatch = playerScript.match( | |
| sigFuncName.replace("$", "\\$") + | |
| '=function\\((?<param>.+?)\\){(?<body>.+?;return \\1\\.join\\(\\"\\"\\))};' | |
| ); | |
| const sigFuncBody = sigFuncBodyMatch.groups["body"]; | |
| const sigFuncHelperObjectName = sigFuncBody.match(";(.+?)\\.")[1]; | |
| const sigFuncHelperObject = playerScript.match( | |
| new RegExp( | |
| `${sigFuncHelperObjectName.replace( | |
| "$", | |
| "\\$" | |
| )}=\{(?<body>.+?\})\};`, | |
| "s" | |
| ) | |
| ).groups["body"]; | |
| const nParamMatch = playerScript.match( | |
| /(?:(?:"nn"\[.+?\]|String\.fromCharCode\(110\)),c=a\.get\(b\)|\.get\("n"\)|null)\)&&\(?\S=(?:(?<arr>.+?)\[(?<idx>.+?)\]|(?<func>.+))\(\S+?\)/ | |
| ); | |
| const nParamArrName = nParamMatch.groups["arr"]; | |
| const nParamArrIndex = nParamMatch.groups["idx"]; | |
| const nParamRegex = `var ${nParamArrName.replace( | |
| "$", | |
| "\\$" | |
| )}\\s*=\\s*\\[(.+?)][;,]`; | |
| const nParamArr = playerScript.match(nParamRegex)[1].split(","); | |
| const nParamFuncName = nParamArr[parseInt(nParamArrIndex)]; | |
| const nParamFuncCode = playerScript.match( | |
| new RegExp( | |
| `${nParamFuncName}=\\s*function(?<body>\\(\\w\\)\\s*\\{.+_w8_.+?\\}\\s*return\\s*\\w+.join\\(\\"\\"\\)};)`, | |
| "ms" | |
| ) | |
| ); | |
| const sigFunc = sigTemplate | |
| .replace("%%helperObjName%%", sigFuncHelperObjectName) | |
| .replace("%%helperObjBody%%", sigFuncHelperObject) | |
| .replace("%%sigParam%%", sigFuncBodyMatch.groups["param"]) | |
| .replace("%%sigFuncBody%%", sigFuncBody) | |
| .replace( | |
| "%%nParamCode%%", | |
| nParamFuncCode.groups["body"].replace( | |
| /if\s?\(\s?typeof\s+.+?\s?=+\s?(["'])undefined\1\s?\)\s?return\s+.+?;/, | |
| ";" | |
| ) | |
| ); | |
| console.log("Extracted player functions", { | |
| signatureTimestamp, | |
| sigFunc, | |
| }); | |
| injectScript("decipheringScript", sigFunc); | |
| } | |
| /** | |
| * Deciphers a format to have a valid streaming URL | |
| * @param {Object} format Format taken from InnerTube /player request | |
| * @returns Same format, with the URL key set to a valid URL | |
| */ | |
| function getDecipheredFormat(format) { | |
| let url = format.url; | |
| if (format.signatureCipher) { | |
| const params = new URLSearchParams(format.signatureCipher); | |
| const urlParams = new URLSearchParams(params.get("url").split("?")[1]); | |
| urlParams.append( | |
| params.get("sp"), | |
| this.window.eval("decipherSignature")(params.get("s")) | |
| ); | |
| url = params.get("url").split("?")[0] + "?" + urlParams; | |
| } | |
| let urlParams = new URLSearchParams(url.split("?")[1]); | |
| url = url.split("?")[0]; | |
| if (urlParams.has("n")) | |
| urlParams.set( | |
| "n", | |
| this.window.eval("decipherNParam")(urlParams.get("n")) | |
| ); | |
| format.url = url + "?" + urlParams; | |
| return format; | |
| } | |
| async function downloadVideo() { | |
| const videoId = new URLSearchParams(window.location.search.slice(1)).get( | |
| "v" | |
| ); | |
| if (!videoId) { | |
| alert("video id not found D:"); | |
| return; | |
| } | |
| const player = await fetch( | |
| "https://www.youtube.com/youtubei/v1/player?prettyPrint=false", | |
| { | |
| headers: { | |
| "User-Agent": | |
| "Mozilla/5.0 (X11; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0", | |
| Accept: "*/*", | |
| "Accept-Language": "en-US,en;q=0.5", | |
| "Content-Type": "application/json", | |
| "X-Youtube-Client-Name": "1", | |
| "X-Youtube-Client-Version": "2.20250213.05.00", | |
| "X-Origin": "https://www.youtube.com", | |
| }, | |
| referrer: "https://www.youtube.com/watch?v=" + videoId, | |
| body: JSON.stringify({ | |
| context: { | |
| client: { | |
| hl: "en", | |
| gl: "TR", | |
| // TODO: poToken | |
| // visitorData: "", | |
| userAgent: navigator.userAgent, | |
| clientName: "WEB", | |
| clientVersion: "2.20250213.05.00", | |
| originalUrl: | |
| "https://www.youtube.com/watch?v=" + videoId, | |
| platform: "DESKTOP", | |
| clientFormFactor: "UNKNOWN_FORM_FACTOR", | |
| }, | |
| user: { | |
| lockedSafetyMode: false, | |
| }, | |
| }, | |
| videoId: videoId, | |
| playbackContext: { | |
| contentPlaybackContext: { | |
| currentUrl: "/watch?v=" + videoId, | |
| vis: 0, | |
| splay: false, | |
| autoCaptionsDefaultOn: false, | |
| autonavState: "STATE_OFF", | |
| html5Preference: "HTML5_PREF_WANTS", | |
| signatureTimestamp: signatureTimestamp, | |
| lactMilliseconds: "-1", | |
| watchAmbientModeContext: { | |
| hasShownAmbientMode: true, | |
| hasToggledOffAmbientMode: true, | |
| }, | |
| }, | |
| }, | |
| racyCheckOk: true, | |
| contentCheckOk: true, | |
| // TODO: poToken | |
| //serviceIntegrityDimensions: { | |
| // poToken: "" | |
| //}, | |
| }), | |
| method: "POST", | |
| mode: "cors", | |
| } | |
| ).then((x) => x.json()); | |
| const formats = player.streamingData.formats.map(getDecipheredFormat); | |
| const adaptiveFormats = | |
| player.streamingData.adaptiveFormats.map(getDecipheredFormat); | |
| console.log({ | |
| formats, | |
| adaptiveFormats, | |
| }); | |
| console.log("opening " + formats[0].url); | |
| window.open(formats[0].url); | |
| } | |
| injectScript("poTokenScript", poTokenScript); | |
| getDecipheringStuff(); | |
| const dlButton = document.createElement("button"); | |
| dlButton.innerText = "download :3"; | |
| dlButton.addEventListener("click", downloadVideo); | |
| dlButton.style.position = "fixed"; | |
| dlButton.style.top = "0"; | |
| dlButton.style.left = "0"; | |
| dlButton.style.zIndex = "999999"; | |
| this.document.body.appendChild(dlButton); | |
| console.log(this.document); | |
| console.log("init!! yay"); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment