Skip to content

Instantly share code, notes, and snippets.

@hizkifw
Last active March 17, 2024 14:13
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save hizkifw/ae229eb0c5ff809fc2a4a88735bfd604 to your computer and use it in GitHub Desktop.
Save hizkifw/ae229eb0c5ff809fc2a4a88735bfd604 to your computer and use it in GitHub Desktop.
Download YouTube videos with Cloudflare Worker
/**
* cloudflare-worker-youtube-dl.js
* Get direct links to YouTube videos using Cloudflare Workers.
*
* Usage:
* GET /?v=dQw4w9WgXcQ
* -> Returns a JSON list of supported formats
*
* GET /?v=dQw4w9WgXcQ&f=251
* -> Returns a stream of the specified format ID
*/
addEventListener("fetch", event => {
event.respondWith(handleRequest(event.request));
});
const parseQueryString = queryString =>
Object.assign(
{},
...queryString.split("&").map(kvp => {
kva = kvp.split("=").map(decodeURIComponent);
return {
[kva[0]]: kva[1]
};
})
);
const getJsPlayer = async videoPage => {
let playerURL = JSON.parse(
/"assets":.+?"js":\s*("[^"]+")/gm.exec(videoPage)[1]
);
if (playerURL.startsWith("//")) playerURL = "https:" + playerURL;
else if (!playerURL.startsWith("http"))
playerURL = "https://www.youtube.com" + playerURL;
const jsPlayerFetch = await fetch(playerURL);
const jsPlayer = await jsPlayerFetch.text();
return jsPlayer;
};
/**
* Respond to the request
* @param {Request} request
*/
async function handleRequest(request) {
try {
const query = parseQueryString(request.url.split("?")[1]);
console.log("Parsed query", query);
const videoId = query["v"];
const videoPageReq = await fetch(
`https://www.youtube.com/watch?v=${encodeURIComponent(
videoId
)}&gl=US&hl=en&has_verified=1&bpctr=9999999999`
);
const videoPage = await videoPageReq.text();
const playerConfigRegex = /;ytplayer\.config\s*=\s*({.+?});ytplayer|;ytplayer\.config\s*=\s*({.+?});/gm;
const playerConfig = JSON.parse(playerConfigRegex.exec(videoPage)[1]);
const playerResponse = JSON.parse(playerConfig.args.player_response);
const jsPlayer = await getJsPlayer(videoPage);
const formatURLs = playerResponse.streamingData.adaptiveFormats.map(
format => {
let url = format.url;
const cipher = format.signatureCipher || format.cipher;
if (!!cipher) {
const components = parseQueryString(cipher);
const sig = applyActions(extractActions(jsPlayer), components.s);
url =
components["url"] +
`&${encodeURIComponent(components.sp)}=${encodeURIComponent(sig)}`;
}
return {
...format,
_decryptedURL: url
};
}
);
if ("f" in query) {
const format = formatURLs.find(
format => format.itag === parseInt(query.f)
);
const stream = await fetch(format._decryptedURL);
const { readable, writable } = new TransformStream();
stream.body.pipeTo(writable);
return new Response(readable, stream);
} else {
return new Response(
JSON.stringify(
formatURLs.map(({ _decryptedURL, ...format }) => format),
null,
2
),
{
status: 200,
headers: {
"Content-Type": "application/json"
}
}
);
}
return new Response(`hello world, ${JSON.stringify(query)}`, {
status: 200
});
} catch (ex) {
return new Response(
JSON.stringify({ ok: false, message: ex.toString(), payload: ex.trace }),
{ status: 500 }
);
}
}
/**
* The following code snippet taken from https://github.com/fent/node-ytdl-core
* https://github.com/fent/node-ytdl-core/blob/5b458b8a2d9016293458330eba466ccaa9d676e2/lib/sig.js
*
* MIT License
*
* Copyright (C) 2012-present by fent
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
const jsVarStr = "[a-zA-Z_\\$][a-zA-Z_0-9]*";
const jsSingleQuoteStr = `'[^'\\\\]*(:?\\\\[\\s\\S][^'\\\\]*)*'`;
const jsDoubleQuoteStr = `"[^"\\\\]*(:?\\\\[\\s\\S][^"\\\\]*)*"`;
const jsQuoteStr = `(?:${jsSingleQuoteStr}|${jsDoubleQuoteStr})`;
const jsKeyStr = `(?:${jsVarStr}|${jsQuoteStr})`;
const jsPropStr = `(?:\\.${jsVarStr}|\\[${jsQuoteStr}\\])`;
const jsEmptyStr = `(?:''|"")`;
const reverseStr =
":function\\(a\\)\\{" + "(?:return )?a\\.reverse\\(\\)" + "\\}";
const sliceStr = ":function\\(a,b\\)\\{" + "return a\\.slice\\(b\\)" + "\\}";
const spliceStr = ":function\\(a,b\\)\\{" + "a\\.splice\\(0,b\\)" + "\\}";
const swapStr =
":function\\(a,b\\)\\{" +
"var c=a\\[0\\];a\\[0\\]=a\\[b(?:%a\\.length)?\\];a\\[b(?:%a\\.length)?\\]=c(?:;return a)?" +
"\\}";
const actionsObjRegexp = new RegExp(
`var (${jsVarStr})=\\{((?:(?:${jsKeyStr}${reverseStr}|${jsKeyStr}${sliceStr}|${jsKeyStr}${spliceStr}|${jsKeyStr}${swapStr}),?\\r?\\n?)+)\\};`
);
const actionsFuncRegexp = new RegExp(
`${`function(?: ${jsVarStr})?\\(a\\)\\{` +
`a=a\\.split\\(${jsEmptyStr}\\);\\s*` +
`((?:(?:a=)?${jsVarStr}`}${jsPropStr}\\(a,\\d+\\);)+)` +
`return a\\.join\\(${jsEmptyStr}\\)` +
`\\}`
);
const reverseRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${reverseStr}`, "m");
const sliceRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${sliceStr}`, "m");
const spliceRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${spliceStr}`, "m");
const swapRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${swapStr}`, "m");
const swapHeadAndPosition = (arr, position) => {
const first = arr[0];
arr[0] = arr[position % arr.length];
arr[position] = first;
return arr;
};
const extractActions = body => {
const objResult = actionsObjRegexp.exec(body);
const funcResult = actionsFuncRegexp.exec(body);
if (!objResult || !funcResult) {
return null;
}
const obj = objResult[1].replace(/\$/g, "\\$");
const objBody = objResult[2].replace(/\$/g, "\\$");
const funcBody = funcResult[1].replace(/\$/g, "\\$");
let result = reverseRegexp.exec(objBody);
const reverseKey =
result && result[1].replace(/\$/g, "\\$").replace(/\$|^'|^"|'$|"$/g, "");
result = sliceRegexp.exec(objBody);
const sliceKey =
result && result[1].replace(/\$/g, "\\$").replace(/\$|^'|^"|'$|"$/g, "");
result = spliceRegexp.exec(objBody);
const spliceKey =
result && result[1].replace(/\$/g, "\\$").replace(/\$|^'|^"|'$|"$/g, "");
result = swapRegexp.exec(objBody);
const swapKey =
result && result[1].replace(/\$/g, "\\$").replace(/\$|^'|^"|'$|"$/g, "");
const keys = `(${[reverseKey, sliceKey, spliceKey, swapKey].join("|")})`;
const myreg =
`(?:a=)?${obj}(?:\\.${keys}|\\['${keys}'\\]|\\["${keys}"\\])` +
`\\(a,(\\d+)\\)`;
const tokenizeRegexp = new RegExp(myreg, "g");
const tokens = [];
while ((result = tokenizeRegexp.exec(funcBody)) !== null) {
let key = result[1] || result[2] || result[3];
switch (key) {
case swapKey:
tokens.push(`w${result[4]}`);
break;
case reverseKey:
tokens.push("r");
break;
case sliceKey:
tokens.push(`s${result[4]}`);
break;
case spliceKey:
tokens.push(`p${result[4]}`);
break;
}
}
return tokens;
};
const applyActions = (tokens, _sig) => {
let sig = _sig.split("");
for (let i = 0, len = tokens.length; i < len; i++) {
let token = tokens[i],
pos;
switch (token[0]) {
case "r":
sig = sig.reverse();
break;
case "w":
pos = ~~token.slice(1);
sig = swapHeadAndPosition(sig, pos);
break;
case "s":
pos = ~~token.slice(1);
sig = sig.slice(pos);
break;
case "p":
pos = ~~token.slice(1);
sig.splice(0, pos);
break;
}
}
return sig.join("");
};
@Bugadder
Copy link

how to use it sir

@hizkifw
Copy link
Author

hizkifw commented Mar 25, 2021

I haven't updated the code in a while, so I don't know if it still works, but if you want to try it, you can deploy this on Cloudflare Workers.

@Bugadder
Copy link

thank you for reply i had hosted it on cloudflare but i dont know how to use it

@Bugadder
Copy link

this is what iam getting when i open site

{"ok":false,"message":"TypeError: Cannot read property 'split' of undefined"}

@hizkifw
Copy link
Author

hizkifw commented Mar 25, 2021

Yeah the code is definitely already broken. I don't plan on updating the script so you'll need to look somewhere else to find a working version, or try to fix it yourself. I used fent/node-ytdl-core as reference when making this one.

Copy link

ghost commented May 11, 2021

can you update the code pls? dont want to use the bloatware ytdl-core

@Bugadder
Copy link

Yes please provide the release sir 🙏🙏

@spyhunter1280
Copy link

@Bugadder Hello
Did you complete youtube video downloader with cloudflare worker?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment