Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save yauri-io/a0ff71c0fbbe3e0cc56e5b77dfc6c80a to your computer and use it in GitHub Desktop.
Save yauri-io/a0ff71c0fbbe3e0cc56e5b77dfc6c80a to your computer and use it in GitHub Desktop.

Revisions

  1. @hizkifw hizkifw revised this gist Aug 14, 2020. 1 changed file with 10 additions and 13 deletions.
    23 changes: 10 additions & 13 deletions cloudflare-worker-youtube-dl.js
    Original file line number Diff line number Diff line change
    @@ -35,7 +35,6 @@ const getJsPlayer = async videoPage => {

    const jsPlayerFetch = await fetch(playerURL);
    const jsPlayer = await jsPlayerFetch.text();
    console.log(playerURL);
    return jsPlayer;
    };

    @@ -45,11 +44,8 @@ const getJsPlayer = async videoPage => {
    */
    async function handleRequest(request) {
    try {
    const route = request.url
    .split("?")[0]
    .replace(/https?:\/\//, "")
    .split("/");
    const query = parseQueryString(request.url.split("?")[1]);
    console.log("Parsed query", query);

    const videoId = query["v"];
    const videoPageReq = await fetch(
    @@ -65,22 +61,23 @@ async function handleRequest(request) {
    const jsPlayer = await getJsPlayer(videoPage);
    const formatURLs = playerResponse.streamingData.adaptiveFormats.map(
    format => {
    let url = format.url;
    const cipher = format.signatureCipher || format.cipher;
    const components = parseQueryString(cipher);
    if (!!cipher) {
    const components = parseQueryString(cipher);

    const sig = applyActions(extractActions(jsPlayer), components.s);
    const url =
    components["url"] +
    `&${encodeURIComponent(components.sp)}=${encodeURIComponent(sig)}`;
    const sig = applyActions(extractActions(jsPlayer), components.s);
    url =
    components["url"] +
    `&${encodeURIComponent(components.sp)}=${encodeURIComponent(sig)}`;
    }

    return {
    ...format,
    _components: components,
    _decryptedURL: url
    };
    }
    );
    console.log(formatURLs);

    if ("f" in query) {
    const format = formatURLs.find(
    @@ -93,7 +90,7 @@ async function handleRequest(request) {
    } else {
    return new Response(
    JSON.stringify(
    formatURLs.map(({ _components, _decryptedURL, ...format }) => format),
    formatURLs.map(({ _decryptedURL, ...format }) => format),
    null,
    2
    ),
  2. @hizkifw hizkifw created this gist Aug 14, 2020.
    258 changes: 258 additions & 0 deletions cloudflare-worker-youtube-dl.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,258 @@
    /**
    * 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();
    console.log(playerURL);
    return jsPlayer;
    };

    /**
    * Respond to the request
    * @param {Request} request
    */
    async function handleRequest(request) {
    try {
    const route = request.url
    .split("?")[0]
    .replace(/https?:\/\//, "")
    .split("/");
    const query = parseQueryString(request.url.split("?")[1]);

    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 => {
    const cipher = format.signatureCipher || format.cipher;
    const components = parseQueryString(cipher);

    const sig = applyActions(extractActions(jsPlayer), components.s);
    const url =
    components["url"] +
    `&${encodeURIComponent(components.sp)}=${encodeURIComponent(sig)}`;

    return {
    ...format,
    _components: components,
    _decryptedURL: url
    };
    }
    );
    console.log(formatURLs);

    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(({ _components, _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("");
    };