-
-
Save hizkifw/ae229eb0c5ff809fc2a4a88735bfd604 to your computer and use it in GitHub Desktop.
/** | |
* 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(""); | |
}; |
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.
thank you for reply i had hosted it on cloudflare but i dont know how to use it
this is what iam getting when i open site
{"ok":false,"message":"TypeError: Cannot read property 'split' of undefined"}
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.
can you update the code pls? dont want to use the bloatware ytdl-core
Yes please provide the release sir ๐๐
@Bugadder Hello
Did you complete youtube video downloader with cloudflare worker?
To anyone here about to write your own implementation, there are 2 secrets to not being throttled:
- Decode
r
properly - Send the
Range
header
how to use it sir