├── .gitignore ├── .dockerignore ├── nixpacks.toml ├── railway.toml ├── .env.sample ├── fly.toml ├── .eslintrc.json ├── tsconfig.json ├── Dockerfile.fly ├── package.json ├── commands └── sample.ts ├── index.ts ├── wrangler.toml └── package_overrides └── sig.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | build 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | build 4 | -------------------------------------------------------------------------------- /nixpacks.toml: -------------------------------------------------------------------------------- 1 | providers = ["node"] 2 | 3 | [phases.setup] 4 | nixPkgs = ["...", "ffmpeg"] 5 | -------------------------------------------------------------------------------- /railway.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | builder = "NIXPACKS" 3 | 4 | [deploy] 5 | restartPolicyType = "ON_FAILURE" 6 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | DISCORD_BOT_TOKEN="" 2 | DISCORD_APP_ID="" 3 | DISCORD_DEV_SERVER_ID="" 4 | DISCORD_PUBLIC_KEY="" 5 | -------------------------------------------------------------------------------- /fly.toml: -------------------------------------------------------------------------------- 1 | # fly.toml app configuration file generated for discord-samplebot on 2024-04-27T16:31:14-04:00 2 | # 3 | # See https://fly.io/docs/reference/configuration/ for information about how to use this file. 4 | # 5 | 6 | app = 'discord-samplebot' 7 | primary_region = 'ewr' 8 | 9 | [build] 10 | 11 | [http_service] 12 | internal_port = 3000 13 | force_https = true 14 | auto_stop_machines = false 15 | auto_start_machines = false 16 | min_machines_running = 0 17 | processes = ['app'] 18 | 19 | [[vm]] 20 | memory = '1gb' 21 | cpu_kind = 'shared' 22 | cpus = 1 23 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "parser": "@typescript-eslint/parser", 6 | "parserOptions": { 7 | "ecmaVersion": 2020, 8 | "sourceType": "module" 9 | }, 10 | "extends": [ 11 | "eslint:recommended", 12 | "plugin:@typescript-eslint/eslint-recommended", 13 | "plugin:@typescript-eslint/recommended", 14 | "plugin:import/errors", 15 | "plugin:import/warnings", 16 | "plugin:import/typescript", 17 | "plugin:prettier/recommended" 18 | ], 19 | "plugins": ["@typescript-eslint", "simple-import-sort"], 20 | "rules": { 21 | "import/first": "error", 22 | "import/newline-after-import": "error", 23 | "import/no-duplicates": "error", 24 | "simple-import-sort/imports": "error", 25 | "simple-import-sort/exports": "error", 26 | "sort-imports": "off" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "ES2021" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 5 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 6 | "outDir": "./build" /* Redirect output structure to the directory. */, 7 | "rootDir": "./" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 8 | "skipLibCheck": true, 9 | 10 | /* Strict Type-Checking Options */ 11 | "strict": true /* Enable all strict type-checking options. */, 12 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 13 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 14 | }, 15 | "exclude": ["node_modules", "build"] 16 | } 17 | -------------------------------------------------------------------------------- /Dockerfile.fly: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:1 2 | 3 | # Adjust NODE_VERSION as desired 4 | ARG NODE_VERSION=21.7.1 5 | FROM node:${NODE_VERSION}-slim as base 6 | 7 | LABEL fly_launch_runtime="Node.js" 8 | 9 | # Node.js app lives here 10 | WORKDIR /app 11 | 12 | # Set production environment 13 | ENV NODE_ENV="production" 14 | 15 | 16 | # Throw-away build stage to reduce size of final image 17 | FROM base as build 18 | 19 | # Install packages needed to build node modules 20 | RUN apt-get update -qq && \ 21 | apt-get install --no-install-recommends -y build-essential node-gyp pkg-config python-is-python3 22 | 23 | # Install node modules 24 | COPY --link package-lock.json package.json ./ 25 | RUN npm ci --include=dev 26 | 27 | # COPY package_overrides/sig.js node_modules/@distube/ytdl-core/lib/sig.js 28 | 29 | # Copy application code 30 | COPY --link . . 31 | 32 | # Build application 33 | RUN npm run build 34 | 35 | # Remove development dependencies 36 | RUN npm prune --omit=dev 37 | 38 | 39 | # Final stage for app image 40 | FROM base 41 | 42 | # Install packages needed for deployment 43 | RUN apt-get update -qq && \ 44 | apt-get install --no-install-recommends -y ffmpeg && \ 45 | rm -rf /var/lib/apt/lists /var/cache/apt/archives 46 | 47 | # Copy built application 48 | COPY --from=build /app /app 49 | 50 | # Start the server by default, this can be overwritten at runtime 51 | EXPOSE 3000 52 | CMD [ "npm", "run", "start" ] 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "discord-samplebot", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "dev": "tsx watch *.ts", 9 | "start": "tsx index.ts", 10 | "deploy-fly": "flyctl deploy --dockerfile Dockerfile.fly", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "type": "module", 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/iheanyi/discord-samplebot.git" 17 | }, 18 | "author": "Iheanyi Ekechukwu", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/iheanyi/discord-samplebot/issues" 22 | }, 23 | "homepage": "https://github.com/iheanyi/discord-samplebot#readme", 24 | "dependencies": { 25 | "@distube/ytdl-core": "^4.16.12", 26 | "discord.js": "^14.12.1", 27 | "dotenv": "^16.4.5", 28 | "fluent-ffmpeg": "^2.1.3", 29 | "node-fetch": "^3.3.2", 30 | "tempy": "^3.1.0", 31 | "ts-node": "^10.9.2", 32 | "tsx": "^4.7.1", 33 | "typescript": "^5.4.3", 34 | "youtubei.js": "^15.0.1" 35 | }, 36 | "devDependencies": { 37 | "@cloudflare/vitest-pool-workers": "^0.1.0", 38 | "@cloudflare/workers-types": "^4.20240329.0", 39 | "@flydotio/dockerfile": "^0.5.6", 40 | "@types/fluent-ffmpeg": "^2.1.27", 41 | "@typescript-eslint/eslint-plugin": "^7.4.0", 42 | "@typescript-eslint/parser": "^7.4.0", 43 | "chai": "^5.1.0", 44 | "eslint": "^8.57.0", 45 | "eslint-config-prettier": "^9.1.0", 46 | "eslint-plugin-import": "^2.29.1", 47 | "eslint-plugin-prettier": "^5.2.1", 48 | "eslint-plugin-simple-import-sort": "^12.0.0", 49 | "mocha": "^10.4.0", 50 | "prettier": "^3.2.5", 51 | "sinon": "^17.0.1", 52 | "vitest": "1.3.0", 53 | "wrangler": "^3.41.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /commands/sample.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | 3 | import ytdl from "@distube/ytdl-core"; 4 | import { 5 | AttachmentBuilder, 6 | ChatInputCommandInteraction, 7 | codeBlock, 8 | SlashCommandBuilder, 9 | } from "discord.js"; 10 | import ffmpeg from "fluent-ffmpeg"; 11 | import { temporaryFileTask } from "tempy"; 12 | 13 | async function youtubeSampleSource(url: string, filepath: string) { 14 | const info = await ytdl.getInfo(url); 15 | await new Promise((resolve, reject) => 16 | ffmpeg( 17 | ytdl(url, { 18 | filter: "audioonly", 19 | quality: "highestaudio", 20 | }), 21 | ) 22 | .format("mp3") 23 | .on("error", (e: Error) => reject(e)) 24 | .on("end", () => resolve()) 25 | .save(filepath), 26 | ); 27 | 28 | const data = await fs.promises.readFile(filepath); 29 | return { 30 | data, 31 | title: `${info.videoDetails.title} - ${info.videoDetails.author.name}`, 32 | }; 33 | } 34 | 35 | async function downloadSample(url: string, interaction: any) { 36 | const allowedHosts = [ 37 | "youtube.com", 38 | "m.youtube.com", 39 | "youtu.be", 40 | "www.youtube.com", 41 | "music.youtube.com", 42 | ]; 43 | const defaultFormat = "mp3"; 44 | 45 | const { hostname } = new URL(url); 46 | if (allowedHosts.includes(hostname.toString())) { 47 | const user = interaction.user; 48 | await temporaryFileTask(async (filename) => { 49 | const { data, title } = await youtubeSampleSource(url, filename); 50 | const file = new AttachmentBuilder(data).setName( 51 | `${title}.${defaultFormat}`, 52 | ); 53 | console.log("Successfully downloaded sample!"); 54 | await interaction.followUp({ 55 | content: `${user} Your sample is ready!`, 56 | files: [file], 57 | }); 58 | }); 59 | } else { 60 | throw new Error(`url "${url}" is not supported`); 61 | } 62 | } 63 | 64 | const command = { 65 | name: "sample", 66 | data: new SlashCommandBuilder() 67 | .setName("sample") 68 | .setDescription("Download a sample from YouTube") 69 | .addStringOption((option) => { 70 | return option 71 | .setName("url") 72 | .setDescription("YouTube URL to download") 73 | .setRequired(true); 74 | }), 75 | async execute(interaction: ChatInputCommandInteraction): Promise { 76 | const url = interaction.options.getString("url", true); 77 | try { 78 | await interaction.reply("Attempting to download sample..."); 79 | await downloadSample(url, interaction); 80 | } catch (err: any) { 81 | console.error(err); 82 | try { 83 | await interaction.followUp({ 84 | content: `There was an error while executing this command. ${codeBlock(err.toString())}`, 85 | }); 86 | } catch (error) { 87 | console.error(error); 88 | } 89 | } 90 | }, 91 | }; 92 | 93 | export default command; 94 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import { Client, Events, GatewayIntentBits, REST, Routes } from "discord.js"; 2 | import dotenv from "dotenv"; 3 | 4 | import SampleCommand from "./commands/sample"; 5 | 6 | dotenv.config(); 7 | 8 | export interface Env { 9 | // Example binding to KV. Learn more at https://developers.cloudflare.com/workers/runtime-apis/kv/ 10 | // MY_KV_NAMESPACE: KVNamespace; 11 | // 12 | // Example binding to Durable Object. Learn more at https://developers.cloudflare.com/workers/runtime-apis/durable-objects/ 13 | // MY_DURABLE_OBJECT: DurableObjectNamespace; 14 | // 15 | // Example binding to R2. Learn more at https://developers.cloudflare.com/workers/runtime-apis/r2/ 16 | // MY_BUCKET: R2Bucket; 17 | // 18 | // Example binding to a Service. Learn more at https://developers.cloudflare.com/workers/runtime-apis/service-bindings/ 19 | // MY_SERVICE: Fetcher; 20 | // 21 | // Example binding to a Queue. Learn more at https://developers.cloudflare.com/queues/javascript-apis/ 22 | // MY_QUEUE: Queue; 23 | } 24 | 25 | class SampleBot { 26 | constructor() {} 27 | 28 | async run() { 29 | const { 30 | DISCORD_APP_ID: clientID, 31 | DISCORD_BOT_TOKEN: token, 32 | DISCORD_DEV_SERVER_ID: guildID, 33 | } = process.env; 34 | 35 | const client: Client = new Client({ 36 | intents: [ 37 | GatewayIntentBits.Guilds, 38 | GatewayIntentBits.GuildMessages, 39 | GatewayIntentBits.MessageContent, 40 | ], 41 | }); 42 | 43 | client.on(Events.InteractionCreate, async (interaction) => { 44 | if (!interaction.isChatInputCommand()) return; 45 | 46 | if (interaction.commandName === "sample") { 47 | try { 48 | // @ts-ignore 49 | await SampleCommand.execute(interaction); 50 | } catch (error) { 51 | console.error(error); 52 | try { 53 | await interaction.reply({ 54 | content: "There was an error while executing this command.", 55 | }); 56 | } catch (err) { 57 | console.error(err); 58 | } 59 | } 60 | } 61 | }); 62 | 63 | // Construct and prepare an instance of the REST module 64 | const rest = new REST().setToken(token as string); 65 | 66 | const commandsToRegister = [SampleCommand]; 67 | 68 | const registerBody = commandsToRegister.map((command) => 69 | command.data.toJSON(), 70 | ); 71 | 72 | (async () => { 73 | try { 74 | console.log(`Started refreshing 1 application (/) commands.`); 75 | /* await rest.put( 76 | Routes.applicationGuildCommands(String(clientID), String(guildID)), 77 | { body: [] }, 78 | ); 79 | console.log("Successfully deleted all guild commands."); 80 | 81 | // for global commands 82 | await rest.put(Routes.applicationCommands(String(clientID)), { body: [] }); 83 | console.log("Successfully deleted all application commands.");*/ 84 | 85 | // The put method is used to fully refresh all commands in the guild with the current set 86 | await rest.put(Routes.applicationCommands(String(clientID)), { 87 | body: registerBody, 88 | }); 89 | console.log(`Successfully reloaded 1 application (/) commands.`); 90 | } catch (error) { 91 | console.error(error); 92 | } 93 | })(); 94 | 95 | client.once(Events.ClientReady, () => { 96 | console.log("Server is ready to start processing things."); 97 | }); 98 | 99 | client.login(token); 100 | } 101 | } 102 | 103 | const bot = new SampleBot(); 104 | bot.run(); 105 | 106 | export default SampleBot; 107 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "discord-sampelpebot" 2 | main = "index.ts" 3 | compatibility_date = "2024-03-29" 4 | compatibility_flags = ["nodejs_compat"] 5 | 6 | # Variable bindings. These are arbitrary, plaintext strings (similar to environment variables) 7 | # Note: Use secrets to store sensitive data. 8 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables 9 | # [vars] 10 | # MY_VARIABLE = "production_value" 11 | 12 | # Bind the Workers AI model catalog. Run machine learning models, powered by serverless GPUs, on Cloudflare’s global network 13 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#workers-ai 14 | # [ai] 15 | # binding = "AI" 16 | 17 | # Bind an Analytics Engine dataset. Use Analytics Engine to write analytics within your Pages Function. 18 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#analytics-engine-datasets 19 | # [[analytics_engine_datasets]] 20 | # binding = "MY_DATASET" 21 | 22 | # Bind a headless browser instance running on Cloudflare's global network. 23 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#browser-rendering 24 | # [browser] 25 | # binding = "MY_BROWSER" 26 | 27 | # Bind a D1 database. D1 is Cloudflare’s native serverless SQL database. 28 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#d1-databases 29 | # [[d1_databases]] 30 | # binding = "MY_DB" 31 | # database_name = "my-database" 32 | # database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 33 | 34 | # Bind a dispatch namespace. Use Workers for Platforms to deploy serverless functions programmatically on behalf of your customers. 35 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#dispatch-namespace-bindings-workers-for-platforms 36 | # [[dispatch_namespaces]] 37 | # binding = "MY_DISPATCHER" 38 | # namespace = "my-namespace" 39 | 40 | # Bind a Durable Object. Durable objects are a scale-to-zero compute primitive based on the actor model. 41 | # Durable Objects can live for as long as needed. Use these when you need a long-running "server", such as in realtime apps. 42 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#durable-objects 43 | # [[durable_objects.bindings]] 44 | # name = "MY_DURABLE_OBJECT" 45 | # class_name = "MyDurableObject" 46 | 47 | # Durable Object migrations. 48 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#migrations 49 | # [[migrations]] 50 | # tag = "v1" 51 | # new_classes = ["MyDurableObject"] 52 | 53 | # Bind a Hyperdrive configuration. Use to accelerate access to your existing databases from Cloudflare Workers. 54 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#hyperdrive 55 | # [[hyperdrive]] 56 | # binding = "MY_HYPERDRIVE" 57 | # id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 58 | 59 | # Bind a KV Namespace. Use KV as persistent storage for small key-value pairs. 60 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#kv-namespaces 61 | # [[kv_namespaces]] 62 | # binding = "MY_KV_NAMESPACE" 63 | # id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 64 | 65 | # Bind an mTLS certificate. Use to present a client certificate when communicating with another service. 66 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#mtls-certificates 67 | # [[mtls_certificates]] 68 | # binding = "MY_CERTIFICATE" 69 | # certificate_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 70 | 71 | # Bind a Queue producer. Use this binding to schedule an arbitrary task that may be processed later by a Queue consumer. 72 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#queues 73 | # [[queues.producers]] 74 | # binding = "MY_QUEUE" 75 | # queue = "my-queue" 76 | 77 | # Bind a Queue consumer. Queue Consumers can retrieve tasks scheduled by Producers to act on them. 78 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#queues 79 | # [[queues.consumers]] 80 | # queue = "my-queue" 81 | 82 | # Bind an R2 Bucket. Use R2 to store arbitrarily large blobs of data, such as files. 83 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#r2-buckets 84 | # [[r2_buckets]] 85 | # binding = "MY_BUCKET" 86 | # bucket_name = "my-bucket" 87 | 88 | # Bind another Worker service. Use this binding to call another Worker without network overhead. 89 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings 90 | # [[services]] 91 | # binding = "MY_SERVICE" 92 | # service = "my-service" 93 | 94 | # Bind a Vectorize index. Use to store and query vector embeddings for semantic search, classification and other vector search use-cases. 95 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#vectorize-indexes 96 | # [[vectorize]] 97 | # binding = "MY_INDEX" 98 | # index_name = "my-index" 99 | -------------------------------------------------------------------------------- /package_overrides/sig.js: -------------------------------------------------------------------------------- 1 | const querystring = require("querystring"); 2 | const Cache = require("./cache"); 3 | const utils = require("./utils"); 4 | const vm = require("vm"); 5 | 6 | exports.cache = new Cache(1); 7 | 8 | exports.getFunctions = (html5playerfile, options) => 9 | exports.cache.getOrSet(html5playerfile, async () => { 10 | const body = await utils.request(html5playerfile, options); 11 | const functions = exports.extractFunctions(body); 12 | exports.cache.set(html5playerfile, functions); 13 | return functions; 14 | }); 15 | 16 | const VARIABLE_PART = "[a-zA-Z_\\$][a-zA-Z_0-9\\$]*"; 17 | const VARIABLE_PART_DEFINE = '\\"?' + VARIABLE_PART + '\\"?'; 18 | const BEFORE_ACCESS = '(?:\\[\\"|\\.)'; 19 | const AFTER_ACCESS = '(?:\\"\\]|)'; 20 | const VARIABLE_PART_ACCESS = BEFORE_ACCESS + VARIABLE_PART + AFTER_ACCESS; 21 | const REVERSE_PART = ":function\\(\\w\\)\\{(?:return )?\\w\\.reverse\\(\\)\\}"; 22 | const SLICE_PART = ":function\\(\\w,\\w\\)\\{return \\w\\.slice\\(\\w\\)\\}"; 23 | const SPLICE_PART = ":function\\(\\w,\\w\\)\\{\\w\\.splice\\(0,\\w\\)\\}"; 24 | const SWAP_PART = 25 | ":function\\(\\w,\\w\\)\\{" + 26 | "var \\w=\\w\\[0\\];\\w\\[0\\]=\\w\\[\\w%\\w\\.length\\];\\w\\[\\w(?:%\\w.length|)\\]=\\w(?:;return \\w)?\\}"; 27 | 28 | const DECIPHER_REGEXP = 29 | "function(?: " + 30 | VARIABLE_PART + 31 | ")?\\(([a-zA-Z])\\)\\{" + 32 | '\\1=\\1\\.split\\(""\\);\\s*' + 33 | "((?:(?:\\1=)?" + 34 | VARIABLE_PART + 35 | VARIABLE_PART_ACCESS + 36 | "\\(\\1,\\d+\\);)+)" + 37 | 'return \\1\\.join\\(""\\)' + 38 | "\\}"; 39 | 40 | const HELPER_REGEXP = 41 | "var (" + 42 | VARIABLE_PART + 43 | ")=\\{((?:(?:" + 44 | VARIABLE_PART_DEFINE + 45 | REVERSE_PART + 46 | "|" + 47 | VARIABLE_PART_DEFINE + 48 | SLICE_PART + 49 | "|" + 50 | VARIABLE_PART_DEFINE + 51 | SPLICE_PART + 52 | "|" + 53 | VARIABLE_PART_DEFINE + 54 | SWAP_PART + 55 | "),?\\n?)+)\\};"; 56 | 57 | const FUNCTION_TCE_REGEXP = 58 | "function(?:\\s+[a-zA-Z_\\$][a-zA-Z0-9_\\$]*)?\\(\\w\\)\\{" + 59 | '\\w=\\w\\.split\\((?:""|[a-zA-Z0-9_$]*\\[\\d+])\\);' + 60 | '\\s*((?:(?:\\w=)?[a-zA-Z_\\$][a-zA-Z0-9_\\$]*(?:\\[\\"|\\.)[a-zA-Z_\\$][a-zA-Z0-9_\\$]*(?:\\"\\]|)\\(\\w,\\d+\\);)+)' + 61 | 'return \\w\\.join\\((?:""|[a-zA-Z0-9_$]*\\[\\d+])\\)}'; 62 | 63 | const N_TRANSFORM_REGEXP = 64 | "function\\(\\s*(\\w+)\\s*\\)\\s*\\{" + 65 | "var\\s*(\\w+)=(?:\\1\\.split\\(.*?\\)|String\\.prototype\\.split\\.call\\(\\1,.*?\\))," + 66 | "\\s*(\\w+)=(\\[.*?]);\\s*\\3\\[\\d+]" + 67 | "(.*?try)(\\{.*?})catch\\(\\s*(\\w+)\\s*\\)\\s*\\{" + 68 | '\\s*return"[\\w-]+([A-z0-9-]+)"\\s*\\+\\s*\\1\\s*}' + 69 | '\\s*return\\s*(\\2\\.join\\(""\\)|Array\\.prototype\\.join\\.call\\(\\2,.*?\\))};'; 70 | 71 | const N_TRANSFORM_TCE_REGEXP = 72 | "function\\(\\s*(\\w+)\\s*\\)\\s*\\{" + 73 | "\\s*var\\s*(\\w+)=\\1\\.split\\(\\1\\.slice\\(0,0\\)\\),\\s*(\\w+)=\\[.*?];" + 74 | ".*?catch\\(\\s*(\\w+)\\s*\\)\\s*\\{" + 75 | '\\s*return(?:"[^"]+"|\\s*[a-zA-Z_0-9$]*\\[\\d+])\\s*\\+\\s*\\1\\s*}' + 76 | '\\s*return\\s*\\2\\.join\\((?:""|[a-zA-Z_0-9$]*\\[\\d+])\\)};'; 77 | 78 | const TCE_GLOBAL_VARS_REGEXP = 79 | "(?:^|[;,])\\s*(var\\s+([\\w$]+)\\s*=\\s*" + 80 | "(?:" + 81 | "([\"'])(?:\\\\.|[^\\\\])*?\\3" + 82 | "\\s*\\.\\s*split\\((" + 83 | "([\"'])(?:\\\\.|[^\\\\])*?\\5" + 84 | "\\))" + 85 | "|" + 86 | "\\[\\s*(?:([\"'])(?:\\\\.|[^\\\\])*?\\6\\s*,?\\s*)+\\]" + 87 | "))(?=\\s*[,;])"; 88 | 89 | const NEW_TCE_GLOBAL_VARS_REGEXP = 90 | "('use\\s*strict';)?" + 91 | "(?var\\s*" + 92 | "(?[a-zA-Z0-9_$]+)\\s*=\\s*" + 93 | "(?" + 94 | "(?:\"[^\"\\\\]*(?:\\\\.[^\"\\\\]*)*\"|'[^'\\\\]*(?:\\\\.[^'\\\\]*)*')" + 95 | "\\.split\\(" + 96 | "(?:\"[^\"\\\\]*(?:\\\\.[^\"\\\\]*)*\"|'[^'\\\\]*(?:\\\\.[^'\\\\]*)*')" + 97 | "\\)" + 98 | "|" + 99 | "\\[" + 100 | "(?:(?:\"[^\"\\\\]*(?:\\\\.[^\"\\\\]*)*\"|'[^'\\\\]*(?:\\\\.[^'\\\\]*)*')" + 101 | "\\s*,?\\s*)*" + 102 | "\\]" + 103 | "|" + 104 | '"[^"]*"\\.split\\("[^"]*"\\)' + 105 | ")" + 106 | ")"; 107 | 108 | const TCE_SIGN_FUNCTION_REGEXP = 109 | "function\\(\\s*([a-zA-Z0-9$])\\s*\\)\\s*\\{" + 110 | "\\s*\\1\\s*=\\s*\\1\\[(\\w+)\\[\\d+\\]\\]\\(\\2\\[\\d+\\]\\);" + 111 | "([a-zA-Z0-9$]+)\\[\\2\\[\\d+\\]\\]\\(\\s*\\1\\s*,\\s*\\d+\\s*\\);" + 112 | "\\s*\\3\\[\\2\\[\\d+\\]\\]\\(\\s*\\1\\s*,\\s*\\d+\\s*\\);" + 113 | ".*?return\\s*\\1\\[\\2\\[\\d+\\]\\]\\(\\2\\[\\d+\\]\\)\\};"; 114 | 115 | const TCE_SIGN_FUNCTION_ACTION_REGEXP = 116 | "var\\s+([$A-Za-z0-9_]+)\\s*=\\s*\\{\\s*[$A-Za-z0-9_]+\\s*:\\s*function\\s*\\([^)]*\\)\\s*\\{[^{}]*(?:\\{[^{}]*}[^{}]*)*}\\s*,\\s*[$A-Za-z0-9_]+\\s*:\\s*function\\s*\\([^)]*\\)\\s*\\{[^{}]*(?:\\{[^{}]*}[^{}]*)*}\\s*,\\s*[$A-Za-z0-9_]+\\s*:\\s*function\\s*\\([^)]*\\)\\s*\\{[^{}]*(?:\\{[^{}]*}[^{}]*)*}\\s*};"; 117 | 118 | const TCE_N_FUNCTION_REGEXP = 119 | "function\\s*\\((\\w+)\\)\\s*\\{var\\s*\\w+\\s*=\\s*\\1\\[\\w+\\[\\d+\\]\\]\\(\\w+\\[\\d+\\]\\)\\s*,\\s*\\w+\\s*=\\s*\\[.*?\\]\\;.*?catch\\s*\\(\\s*(\\w+)\\s*\\)\\s*\\{return\\s*\\w+\\[\\d+\\]\\s*\\+\\s*\\1\\}\\s*return\\s*\\w+\\[\\w+\\[\\d+\\]\\]\\(\\w+\\[\\d+\\]\\)\\}\\s*\\;"; 120 | 121 | const PATTERN_PREFIX = '(?:^|,)\\"?(' + VARIABLE_PART + ')\\"?'; 122 | const REVERSE_PATTERN = new RegExp(PATTERN_PREFIX + REVERSE_PART, "m"); 123 | const SLICE_PATTERN = new RegExp(PATTERN_PREFIX + SLICE_PART, "m"); 124 | const SPLICE_PATTERN = new RegExp(PATTERN_PREFIX + SPLICE_PART, "m"); 125 | const SWAP_PATTERN = new RegExp(PATTERN_PREFIX + SWAP_PART, "m"); 126 | 127 | const DECIPHER_ARGUMENT = "sig"; 128 | const N_ARGUMENT = "ncode"; 129 | const DECIPHER_FUNC_NAME = "DisTubeDecipherFunc"; 130 | const N_TRANSFORM_FUNC_NAME = "DisTubeNTransformFunc"; 131 | 132 | const extractDollarEscapedFirstGroup = (pattern, text) => { 133 | const match = text.match(pattern); 134 | return match ? match[1].replace(/\$/g, "\\$") : null; 135 | }; 136 | 137 | const extractTceFunc = (body) => { 138 | try { 139 | const tceVariableMatcher = body.match( 140 | new RegExp(NEW_TCE_GLOBAL_VARS_REGEXP, "m"), 141 | ); 142 | 143 | if (!tceVariableMatcher) return; 144 | 145 | const tceVariableMatcherGroups = tceVariableMatcher.groups; 146 | if (!tceVariableMatcher.groups) return; 147 | 148 | const code = tceVariableMatcherGroups.code; 149 | const varname = tceVariableMatcherGroups.varname; 150 | 151 | return { name: varname, code: code }; 152 | } catch (e) { 153 | console.error("Error in extractTceFunc:", e); 154 | return null; 155 | } 156 | }; 157 | 158 | const extractDecipherFunc = (body, name, code) => { 159 | try { 160 | const callerFunc = DECIPHER_FUNC_NAME + "(" + DECIPHER_ARGUMENT + ");"; 161 | let resultFunc; 162 | 163 | const sigFunctionMatcher = body.match( 164 | new RegExp(TCE_SIGN_FUNCTION_REGEXP, "s"), 165 | ); 166 | const sigFunctionActionsMatcher = body.match( 167 | new RegExp(TCE_SIGN_FUNCTION_ACTION_REGEXP, "s"), 168 | ); 169 | 170 | if (sigFunctionMatcher && sigFunctionActionsMatcher && code) { 171 | resultFunc = 172 | "var " + 173 | DECIPHER_FUNC_NAME + 174 | "=" + 175 | sigFunctionMatcher[0] + 176 | sigFunctionActionsMatcher[0] + 177 | code + 178 | ";\n"; 179 | return resultFunc + callerFunc; 180 | } 181 | 182 | const helperMatch = body.match(new RegExp(HELPER_REGEXP, "s")); 183 | if (!helperMatch) return null; 184 | 185 | const helperObject = helperMatch[0]; 186 | const actionBody = helperMatch[2]; 187 | const helperName = helperMatch[1]; 188 | 189 | const reverseKey = extractDollarEscapedFirstGroup( 190 | REVERSE_PATTERN, 191 | actionBody, 192 | ); 193 | const sliceKey = extractDollarEscapedFirstGroup(SLICE_PATTERN, actionBody); 194 | const spliceKey = extractDollarEscapedFirstGroup( 195 | SPLICE_PATTERN, 196 | actionBody, 197 | ); 198 | const swapKey = extractDollarEscapedFirstGroup(SWAP_PATTERN, actionBody); 199 | 200 | const quotedFunctions = [reverseKey, sliceKey, spliceKey, swapKey] 201 | .filter(Boolean) 202 | .map((key) => key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); 203 | 204 | if (quotedFunctions.length === 0) return null; 205 | 206 | let funcMatch = body.match(new RegExp(DECIPHER_REGEXP, "s")); 207 | let isTce = false; 208 | let decipherFunc; 209 | 210 | if (funcMatch) { 211 | decipherFunc = funcMatch[0]; 212 | } else { 213 | const tceFuncMatch = body.match(new RegExp(FUNCTION_TCE_REGEXP, "s")); 214 | if (!tceFuncMatch) return null; 215 | 216 | decipherFunc = tceFuncMatch[0]; 217 | isTce = true; 218 | } 219 | 220 | let tceVars = ""; 221 | if (isTce) { 222 | const tceVarsMatch = body.match(new RegExp(TCE_GLOBAL_VARS_REGEXP, "m")); 223 | if (tceVarsMatch) { 224 | tceVars = tceVarsMatch[1] + ";\n"; 225 | } 226 | } 227 | 228 | resultFunc = 229 | tceVars + 230 | helperObject + 231 | "\nvar " + 232 | DECIPHER_FUNC_NAME + 233 | "=" + 234 | decipherFunc + 235 | ";\n"; 236 | return resultFunc + callerFunc; 237 | } catch (e) { 238 | console.error("Error in extractDecipherFunc:", e); 239 | return null; 240 | } 241 | }; 242 | 243 | const extractNTransformFunc = (body, name, code) => { 244 | try { 245 | const callerFunc = N_TRANSFORM_FUNC_NAME + "(" + N_ARGUMENT + ");"; 246 | let resultFunc; 247 | let nFunction; 248 | 249 | const nFunctionMatcher = body.match(new RegExp(TCE_N_FUNCTION_REGEXP, "s")); 250 | 251 | if (nFunctionMatcher && name && code) { 252 | nFunction = nFunctionMatcher[0]; 253 | 254 | const tceEscapeName = name.replace("$", "\\$"); 255 | const shortCircuitPattern = new RegExp( 256 | `;\\s*if\\s*\\(\\s*typeof\\s+[a-zA-Z0-9_$]+\\s*===?\\s*(?:\"undefined\"|'undefined'|${tceEscapeName}\\[\\d+\\])\\s*\\)\\s*return\\s+\\w+;`, 257 | ); 258 | 259 | const tceShortCircuitMatcher = nFunction.match(shortCircuitPattern); 260 | 261 | if (tceShortCircuitMatcher) { 262 | nFunction = nFunction.replaceAll(tceShortCircuitMatcher[0], ";"); 263 | } 264 | 265 | resultFunc = 266 | "var " + N_TRANSFORM_FUNC_NAME + "=" + nFunction + code + ";\n"; 267 | return resultFunc + callerFunc; 268 | } 269 | 270 | let nMatch = body.match(new RegExp(N_TRANSFORM_REGEXP, "s")); 271 | let isTce = false; 272 | 273 | if (nMatch) { 274 | nFunction = nMatch[0]; 275 | } else { 276 | const nTceMatch = body.match(new RegExp(N_TRANSFORM_TCE_REGEXP, "s")); 277 | if (!nTceMatch) return null; 278 | 279 | nFunction = nTceMatch[0]; 280 | isTce = true; 281 | } 282 | 283 | const paramMatch = nFunction.match(/function\s*\(\s*(\w+)\s*\)/); 284 | if (!paramMatch) return null; 285 | 286 | const paramName = paramMatch[1]; 287 | 288 | const cleanedFunction = nFunction.replace( 289 | new RegExp( 290 | `if\\s*\\(typeof\\s*[^\\s()]+\\s*===?.*?\\)return ${paramName}\\s*;?`, 291 | "g", 292 | ), 293 | "", 294 | ); 295 | 296 | let tceVars = ""; 297 | if (isTce) { 298 | const tceVarsMatch = body.match(new RegExp(TCE_GLOBAL_VARS_REGEXP, "m")); 299 | if (tceVarsMatch) { 300 | tceVars = tceVarsMatch[1] + ";\n"; 301 | } 302 | } 303 | 304 | resultFunc = 305 | tceVars + "var " + N_TRANSFORM_FUNC_NAME + "=" + cleanedFunction + ";\n"; 306 | return resultFunc + callerFunc; 307 | } catch (e) { 308 | console.error("Error in extractNTransformFunc:", e); 309 | return null; 310 | } 311 | }; 312 | 313 | let decipherWarning = false; 314 | let nTransformWarning = false; 315 | 316 | const getExtractFunction = ( 317 | extractFunctions, 318 | body, 319 | name, 320 | code, 321 | postProcess = null, 322 | ) => { 323 | for (const extractFunction of extractFunctions) { 324 | try { 325 | const func = extractFunction(body, name, code); 326 | if (!func) continue; 327 | return new vm.Script(postProcess ? postProcess(func) : func); 328 | } catch (err) { 329 | console.error("Failed to extract function:", err); 330 | continue; 331 | } 332 | } 333 | return null; 334 | }; 335 | 336 | const extractDecipher = (body, name, code) => { 337 | const decipherFunc = getExtractFunction( 338 | [extractDecipherFunc], 339 | body, 340 | name, 341 | code, 342 | ); 343 | if (!decipherFunc && !decipherWarning) { 344 | console.warn( 345 | "\x1b[33mWARNING:\x1B[0m Could not parse decipher function.\n" + 346 | "Stream URLs will be missing.\n" + 347 | `Please report this issue by uploading the "${utils.saveDebugFile( 348 | "player-script.js", 349 | body, 350 | )}" file on https://github.com/distubejs/ytdl-core/issues/144.`, 351 | ); 352 | decipherWarning = true; 353 | } 354 | return decipherFunc; 355 | }; 356 | 357 | const extractNTransform = (body, name, code) => { 358 | const nTransformFunc = getExtractFunction( 359 | [extractNTransformFunc], 360 | body, 361 | name, 362 | code, 363 | ); 364 | 365 | if (!nTransformFunc && !nTransformWarning) { 366 | console.warn( 367 | "\x1b[33mWARNING:\x1B[0m Could not parse n transform function.\n" + 368 | `Please report this issue by uploading the "${utils.saveDebugFile( 369 | "player-script.js", 370 | body, 371 | )}" file on https://github.com/distubejs/ytdl-core/issues/144.`, 372 | ); 373 | nTransformWarning = true; 374 | } 375 | 376 | return nTransformFunc; 377 | }; 378 | 379 | exports.extractFunctions = (body) => { 380 | const { name, code } = extractTceFunc(body); 381 | return [ 382 | extractDecipher(body, name, code), 383 | extractNTransform(body, name, code), 384 | ]; 385 | }; 386 | 387 | exports.setDownloadURL = (format, decipherScript, nTransformScript) => { 388 | if (!format) return; 389 | 390 | const decipher = (url) => { 391 | const args = querystring.parse(url); 392 | if (!args.s || !decipherScript) return args.url; 393 | 394 | try { 395 | const components = new URL(decodeURIComponent(args.url)); 396 | const context = {}; 397 | context[DECIPHER_ARGUMENT] = decodeURIComponent(args.s); 398 | const decipheredSig = decipherScript.runInNewContext(context); 399 | 400 | components.searchParams.set(args.sp || "sig", decipheredSig); 401 | return components.toString(); 402 | } catch (err) { 403 | console.error("Error applying decipher:", err); 404 | return args.url; 405 | } 406 | }; 407 | 408 | const nTransform = (url) => { 409 | try { 410 | const components = new URL(decodeURIComponent(url)); 411 | const n = components.searchParams.get("n"); 412 | 413 | if (!n || !nTransformScript) return url; 414 | 415 | const context = {}; 416 | context[N_ARGUMENT] = n; 417 | const transformedN = nTransformScript.runInNewContext(context); 418 | 419 | if (transformedN) { 420 | if (n === transformedN) { 421 | console.warn( 422 | "Transformed n parameter is the same as input, n function possibly short-circuited", 423 | ); 424 | } else if ( 425 | transformedN.startsWith("enhanced_except_") || 426 | transformedN.endsWith("_w8_" + n) 427 | ) { 428 | console.warn("N function did not complete due to exception"); 429 | } 430 | 431 | components.searchParams.set("n", transformedN); 432 | } else { 433 | console.warn( 434 | "Transformed n parameter is null, n function possibly faulty", 435 | ); 436 | } 437 | 438 | return components.toString(); 439 | } catch (err) { 440 | console.error("Error applying n transform:", err); 441 | return url; 442 | } 443 | }; 444 | 445 | const cipher = !format.url; 446 | const url = format.url || format.signatureCipher || format.cipher; 447 | 448 | if (!url) return; 449 | 450 | try { 451 | format.url = nTransform(cipher ? decipher(url) : url); 452 | 453 | delete format.signatureCipher; 454 | delete format.cipher; 455 | } catch (err) { 456 | console.error("Error setting download URL:", err); 457 | } 458 | }; 459 | 460 | exports.decipherFormats = async (formats, html5player, options) => { 461 | try { 462 | const decipheredFormats = {}; 463 | const [decipherScript, nTransformScript] = await exports.getFunctions( 464 | html5player, 465 | options, 466 | ); 467 | 468 | formats.forEach((format) => { 469 | exports.setDownloadURL(format, decipherScript, nTransformScript); 470 | if (format.url) { 471 | decipheredFormats[format.url] = format; 472 | } 473 | }); 474 | 475 | return decipheredFormats; 476 | } catch (err) { 477 | console.error("Error deciphering formats:", err); 478 | return {}; 479 | } 480 | }; 481 | --------------------------------------------------------------------------------