├── .gitattributes
├── .github
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── config.yml
└── workflows
│ └── publish.yml
├── .gitignore
├── .prettierrc.json
├── LICENSE
├── README.md
├── lib
├── agent.js
├── cache.js
├── format-utils.js
├── formats.js
├── index.js
├── info-extras.js
├── info.js
├── sig.js
├── url-utils.js
└── utils.js
├── package.json
├── pnpm-lock.yaml
├── tsconfig.json
├── tslint.json
└── typings
└── index.d.ts
/.gitattributes:
--------------------------------------------------------------------------------
1 | test/files/html5player/*.js linguist-detectable=false
2 | test/files/videos/**/* linguist-detectable=false
3 |
--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, caste, color, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | - Demonstrating empathy and kindness toward other people
21 | - Being respectful of differing opinions, viewpoints, and experiences
22 | - Giving and gracefully accepting constructive feedback
23 | - Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | - Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | - The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | - Trolling, insulting or derogatory comments, and personal or political attacks
33 | - Public or private harassment
34 | - Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | - Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | [INSERT CONTACT METHOD].
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
120 |
121 | Community Impact Guidelines were inspired by
122 | [Mozilla's code of conduct enforcement ladder][mozilla coc].
123 |
124 | For answers to common questions about this code of conduct, see the FAQ at
125 | [https://www.contributor-covenant.org/faq][faq]. Translations are available
126 | at [https://www.contributor-covenant.org/translations][translations].
127 |
128 | [homepage]: https://www.contributor-covenant.org
129 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
130 | [mozilla coc]: https://github.com/mozilla/diversity
131 | [faq]: https://www.contributor-covenant.org/faq
132 | [translations]: https://www.contributor-covenant.org/translations
133 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | **The issue tracker is only for bug reports. If you have a question or an enhancement suggestion, please post it in [GitHub Discussions (preferred)](https://github.com/distubejs/ytdl-core/discussions) or the [DisTube Support Server](https://discord.gg/feaDd9h) instead of opening an issue.**
4 |
5 | If you wish to contribute to @distube/ytdl-core, feel free to fork the repository and submit a pull request.
6 |
7 | ## Setup
8 |
9 | 1. Fork & clone the repository, make sure you are on the correct branch
10 | 2. Run `npm ci`
11 | 3. Code your idea
12 | 4. Run `npm run lint` to run ESLint
13 | 5. [Submit a pull request](https://github.com/distubejs/ytdl-core/pulls) (Make sure you follow the [conventional commit format](https://www.conventionalcommits.org/en/v1.0.0/))
14 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | ko_fi: skick
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Report a bug found in @distube/ytdl-core
4 | title: ""
5 | labels: bug
6 | assignees: ""
7 | ---
8 |
9 | ## Describe the bug and how to reproduce it
10 |
11 |
12 |
13 | ## Environment
14 |
15 | - @distube/ytdl-core version:
16 | - Node.js version:
17 | - Operating system:
18 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | # yaml-language-server: $schema=https://json.schemastore.org/github-issue-config.json
2 | blank_issues_enabled: false
3 | contact_links:
4 | - name: "Error: Sign in to confirm you’re not a bot"
5 | url: "https://github.com/distubejs/ytdl-core/issues/21"
6 | about: "Don't create a new issue. Please check how to use cookie with the #21 issue and comment if you have any questions."
7 | - name: "WARNING: Could not parse *** function"
8 | url: "https://github.com/distubejs/ytdl-core/issues/144"
9 | about: "Don't create a new issue. Please upload your file to the #144 issue and comment if you have any questions."
10 | - name: Discord Support Server
11 | url: https://discord.gg/feaDd9h
12 | about: >
13 | Any kind of questions should go onto discord support server.
14 | I recommend using this even if you suspect a bug or have
15 | a feature request for keeping the issue tracker clean.
16 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish @distube/ytdl-core
2 | on:
3 | push:
4 | tags:
5 | - "*"
6 | jobs:
7 | publish:
8 | name: Build & Publish
9 | runs-on: ubuntu-latest
10 | permissions:
11 | id-token: write
12 | steps:
13 | - name: Checkout repository
14 | uses: actions/checkout@v4
15 |
16 | - name: Install pnpm
17 | uses: pnpm/action-setup@v3
18 | with:
19 | version: latest
20 |
21 | - name: Install Node.js
22 | uses: actions/setup-node@v4
23 | with:
24 | node-version: 20
25 | cache: "pnpm"
26 | registry-url: "https://registry.npmjs.org"
27 |
28 | - name: Install dependencies
29 | run: pnpm install --ignore-scripts --frozen-lockfile
30 |
31 | - name: Publish
32 | run: |
33 | pnpm publish --access public --no-git-checks
34 | pnpm deprecate @distube/ytdl-core@"< ${{ github.ref_name }}" "This version is deprecated, please upgrade to the latest version."
35 | env:
36 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
37 | NPM_CONFIG_PROVENANCE: true
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
3 | .nyc_output
4 | .DS_Store
5 | .idea/*
6 | yarn.lock
7 | package-lock.json
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "all",
3 | "printWidth": 120,
4 | "endOfLine": "lf",
5 | "quoteProps": "as-needed",
6 | "arrowParens": "avoid",
7 | "tabWidth": 2
8 | }
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (C) 2012-present by fent
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # @distube/ytdl-core
2 |
3 | > [!IMPORTANT]
4 | > [@distube/youtube](https://github.com/distubejs/extractor-plugins) depends on [youtubei.js](https://github.com/LuanRT/YouTube.js) from now on.\
5 | > This fork will be no longer maintained. Please use alternatives (e.g. [youtubei.js](https://github.com/LuanRT/YouTube.js)) instead.
6 |
7 | DisTube fork of `ytdl-core`. This fork is dedicated to fixing bugs and adding features that are not merged into the original repo as soon as possible.
8 |
9 |
10 |
11 | ## Installation
12 |
13 | ```bash
14 | npm install @distube/ytdl-core@latest
15 | ```
16 |
17 | Make sure you're installing the latest version of `@distube/ytdl-core` to keep up with the latest fixes.
18 |
19 | ## Usage
20 |
21 | ```js
22 | const ytdl = require("@distube/ytdl-core");
23 | // TypeScript: import ytdl from '@distube/ytdl-core'; with --esModuleInterop
24 | // TypeScript: import * as ytdl from '@distube/ytdl-core'; with --allowSyntheticDefaultImports
25 | // TypeScript: import ytdl = require('@distube/ytdl-core'); with neither of the above
26 |
27 | // Download a video
28 | ytdl("http://www.youtube.com/watch?v=aqz-KE-bpKQ").pipe(require("fs").createWriteStream("video.mp4"));
29 |
30 | // Get video info
31 | ytdl.getBasicInfo("http://www.youtube.com/watch?v=aqz-KE-bpKQ").then(info => {
32 | console.log(info.videoDetails.title);
33 | });
34 |
35 | // Get video info with download formats
36 | ytdl.getInfo("http://www.youtube.com/watch?v=aqz-KE-bpKQ").then(info => {
37 | console.log(info.formats);
38 | });
39 | ```
40 |
41 | ### Cookies Support
42 |
43 | ```js
44 | const ytdl = require("@distube/ytdl-core");
45 |
46 | // (Optional) Below are examples, NOT the recommended options
47 | const cookies = [
48 | { name: "cookie1", value: "COOKIE1_HERE" },
49 | { name: "cookie2", value: "COOKIE2_HERE" },
50 | ];
51 |
52 | // (Optional) http-cookie-agent / undici agent options
53 | // Below are examples, NOT the recommended options
54 | const agentOptions = {
55 | pipelining: 5,
56 | maxRedirections: 0,
57 | localAddress: "127.0.0.1",
58 | };
59 |
60 | // agent should be created once if you don't want to change your cookie
61 | const agent = ytdl.createAgent(cookies, agentOptions);
62 |
63 | ytdl.getBasicInfo("http://www.youtube.com/watch?v=aqz-KE-bpKQ", { agent });
64 | ytdl.getInfo("http://www.youtube.com/watch?v=aqz-KE-bpKQ", { agent });
65 | ```
66 |
67 | #### How to get cookies
68 |
69 | - Install [EditThisCookie](http://www.editthiscookie.com/) extension for your browser.
70 | - Go to [YouTube](https://www.youtube.com/).
71 | - Log in to your account. (You should use a new account for this purpose)
72 | - Click on the extension icon and click "Export" icon.
73 | - Your cookies will be added to your clipboard and paste it into your code.
74 |
75 | > [!WARNING]
76 | > Don't logout it by clicking logout button on youtube/google account manager, it will expire your cookies.
77 | > You can delete your browser's cookies to log it out on your browser.
78 | > Or use incognito mode to get your cookies then close it.
79 |
80 | > [!WARNING]
81 | > Paste all the cookies array from clipboard into `createAgent` function. Don't remove/edit any cookies if you don't know what you're doing.
82 |
83 | > [!WARNING]
84 | > Make sure your account, which logged in when you getting your cookies, use 1 IP at the same time only. It will make your cookies alive longer.
85 |
86 | ```js
87 | const ytdl = require("@distube/ytdl-core");
88 | const agent = ytdl.createAgent([
89 | {
90 | domain: ".youtube.com",
91 | expirationDate: 1234567890,
92 | hostOnly: false,
93 | httpOnly: true,
94 | name: "---xxx---",
95 | path: "/",
96 | sameSite: "no_restriction",
97 | secure: true,
98 | session: false,
99 | value: "---xxx---",
100 | },
101 | {
102 | "...": "...",
103 | },
104 | ]);
105 | ```
106 |
107 | - Or you can paste your cookies array into a file and use `fs.readFileSync` to read it.
108 |
109 | ```js
110 | const ytdl = require("@distube/ytdl-core");
111 | const fs = require("fs");
112 | const agent = ytdl.createAgent(JSON.parse(fs.readFileSync("cookies.json")));
113 | ```
114 |
115 | ### Proxy Support
116 |
117 | ```js
118 | const ytdl = require("@distube/ytdl-core");
119 |
120 | const agent = ytdl.createProxyAgent({ uri: "my.proxy.server" });
121 |
122 | ytdl.getBasicInfo("http://www.youtube.com/watch?v=aqz-KE-bpKQ", { agent });
123 | ytdl.getInfo("http://www.youtube.com/watch?v=aqz-KE-bpKQ", { agent });
124 | ```
125 |
126 | Use both proxy and cookies:
127 |
128 | ```js
129 | const ytdl = require("@distube/ytdl-core");
130 |
131 | const agent = ytdl.createProxyAgent({ uri: "my.proxy.server" }, [{ name: "cookie", value: "COOKIE_HERE" }]);
132 |
133 | ytdl.getBasicInfo("http://www.youtube.com/watch?v=aqz-KE-bpKQ", { agent });
134 | ytdl.getInfo("http://www.youtube.com/watch?v=aqz-KE-bpKQ", { agent });
135 | ```
136 |
137 | ### IP Rotation
138 |
139 | _Built-in ip rotation (`getRandomIPv6`) won't be updated and will be removed in the future, create your own ip rotation instead._
140 |
141 | To implement IP rotation, you need to assign the desired IP address to the `localAddress` property within `undici.Agent.Options`.
142 | Therefore, you'll need to use a different `ytdl.Agent` for each IP address you want to use.
143 |
144 | ```js
145 | const ytdl = require("@distube/ytdl-core");
146 | const { getRandomIPv6 } = require("@distube/ytdl-core/lib/utils");
147 |
148 | const agentForARandomIP = ytdl.createAgent(undefined, {
149 | localAddress: getRandomIPv6("2001:2::/48"),
150 | });
151 |
152 | ytdl.getBasicInfo("http://www.youtube.com/watch?v=aqz-KE-bpKQ", { agent: agentForARandomIP });
153 |
154 | const agentForAnotherRandomIP = ytdl.createAgent(undefined, {
155 | localAddress: getRandomIPv6("2001:2::/48"),
156 | });
157 |
158 | ytdl.getInfo("http://www.youtube.com/watch?v=aqz-KE-bpKQ", { agent: agentForAnotherRandomIP });
159 | ```
160 |
161 | ## API
162 |
163 | You can find the API documentation in the [original repo](https://github.com/fent/node-ytdl-core#api). Except a few changes:
164 |
165 | ### `ytdl.getInfoOptions`
166 |
167 | - `requestOptions` is now `undici`'s [`RequestOptions`](https://github.com/nodejs/undici#undicirequesturl-options-promise).
168 | - `agent`: [`ytdl.Agent`](https://github.com/distubejs/ytdl-core/blob/master/typings/index.d.ts#L10-L14)
169 | - `playerClients`: An array of player clients to use. Accepts `WEB`, `WEB_EMBEDDED`, `TV`, `IOS`, and `ANDROID`. Defaults to `["WEB_EMBEDDED", "IOS", "ANDROID","TV"]`.
170 | - `fetch`: Custom fetch implementation. Defaults to `undici`'s request.
171 |
172 | ### `ytdl.createAgent([cookies]): ytdl.Agent`
173 |
174 | `cookies`: an array of json cookies exported with [EditThisCookie](http://www.editthiscookie.com/).
175 |
176 | ### `ytdl.createProxyAgent(proxy[, cookies]): ytdl.Agent`
177 |
178 | `proxy`: [`ProxyAgentOptions`](https://github.com/nodejs/undici/blob/main/docs/api/ProxyAgent.md#parameter-proxyagentoptions) contains your proxy server information.
179 |
180 | #### How to implement `ytdl.Agent` with your own Dispatcher
181 |
182 | You can find the example [here](https://github.com/distubejs/ytdl-core/blob/master/lib/cookie.js#L73-L86)
183 |
184 | ## Limitations
185 |
186 | ytdl cannot download videos that fall into the following
187 |
188 | - Regionally restricted (requires a [proxy](#proxy-support))
189 | - Private (if you have access, requires [cookies](#cookies-support))
190 | - Rentals (if you have access, requires [cookies](#cookies-support))
191 | - YouTube Premium content (if you have access, requires [cookies](#cookies-support))
192 | - Only [HLS Livestreams](https://en.wikipedia.org/wiki/HTTP_Live_Streaming) are currently supported. Other formats will get filtered out in ytdl.chooseFormats
193 |
194 | Generated download links are valid for 6 hours, and may only be downloadable from the same IP address.
195 |
196 | ## Rate Limiting
197 |
198 | When doing too many requests YouTube might block. This will result in your requests getting denied with HTTP-StatusCode 429. The following steps might help you:
199 |
200 | - Update `@distube/ytdl-core` to the latest version
201 | - Use proxies (you can find an example [here](#proxy-support))
202 | - Extend the Proxy Idea by rotating (IPv6-)Addresses
203 | - read [this](https://github.com/fent/node-ytdl-core#how-does-using-an-ipv6-block-help) for more information about this
204 | - Use cookies (you can find an example [here](#cookies-support))
205 | - for this to take effect you have to FIRST wait for the current rate limit to expire
206 | - Wait it out (it usually goes away within a few days)
207 |
208 | ## Update Checks
209 |
210 | The issue of using an outdated version of ytdl-core became so prevalent, that ytdl-core now checks for updates at run time, and every 12 hours. If it finds an update, it will print a warning to the console advising you to update. Due to the nature of this library, it is important to always use the latest version as YouTube continues to update.
211 |
212 | If you'd like to disable this update check, you can do so by providing the `YTDL_NO_UPDATE` env variable.
213 |
214 | ```
215 | env YTDL_NO_UPDATE=1 node myapp.js
216 | ```
217 |
218 | ## Related Projects
219 |
220 | - [DisTube](https://github.com/skick1234/DisTube) - A Discord.js module to simplify your music commands and play songs with audio filters on Discord without any API key.
221 | - [@distube/ytsr](https://github.com/distubejs/ytsr) - DisTube fork of [ytsr](https://github.com/TimeForANinja/node-ytsr).
222 | - [@distube/ytpl](https://github.com/distubejs/ytpl) - DisTube fork of [ytpl](https://github.com/TimeForANinja/node-ytpl).
223 |
--------------------------------------------------------------------------------
/lib/agent.js:
--------------------------------------------------------------------------------
1 | const { ProxyAgent } = require("undici");
2 | const { HttpsProxyAgent } = require("https-proxy-agent");
3 | const { Cookie, CookieJar, canonicalDomain } = require("tough-cookie");
4 | const { CookieAgent, cookie } = require("http-cookie-agent/undici");
5 |
6 | const convertSameSite = sameSite => {
7 | switch (sameSite) {
8 | case "strict":
9 | return "strict";
10 | case "lax":
11 | return "lax";
12 | case "no_restriction":
13 | case "unspecified":
14 | default:
15 | return "none";
16 | }
17 | };
18 |
19 | const convertCookie = cookie =>
20 | cookie instanceof Cookie
21 | ? cookie
22 | : new Cookie({
23 | key: cookie.name,
24 | value: cookie.value,
25 | expires: typeof cookie.expirationDate === "number" ? new Date(cookie.expirationDate * 1000) : "Infinity",
26 | domain: canonicalDomain(cookie.domain),
27 | path: cookie.path,
28 | secure: cookie.secure,
29 | httpOnly: cookie.httpOnly,
30 | sameSite: convertSameSite(cookie.sameSite),
31 | hostOnly: cookie.hostOnly,
32 | });
33 |
34 | const addCookies = (exports.addCookies = (jar, cookies) => {
35 | if (!cookies || !Array.isArray(cookies)) {
36 | throw new Error("cookies must be an array");
37 | }
38 | if (!cookies.some(c => c.name === "SOCS")) {
39 | cookies.push({
40 | domain: ".youtube.com",
41 | hostOnly: false,
42 | httpOnly: false,
43 | name: "SOCS",
44 | path: "/",
45 | sameSite: "lax",
46 | secure: true,
47 | session: false,
48 | value: "CAI",
49 | });
50 | }
51 | for (const cookie of cookies) {
52 | jar.setCookieSync(convertCookie(cookie), "https://www.youtube.com");
53 | }
54 | });
55 |
56 | exports.addCookiesFromString = (jar, cookies) => {
57 | if (!cookies || typeof cookies !== "string") {
58 | throw new Error("cookies must be a string");
59 | }
60 | return addCookies(
61 | jar,
62 | cookies
63 | .split(";")
64 | .map(c => Cookie.parse(c))
65 | .filter(Boolean),
66 | );
67 | };
68 |
69 | const createAgent = (exports.createAgent = (cookies = [], opts = {}) => {
70 | const options = Object.assign({}, opts);
71 | if (!options.cookies) {
72 | const jar = new CookieJar();
73 | addCookies(jar, cookies);
74 | options.cookies = { jar };
75 | }
76 | return {
77 | dispatcher: new CookieAgent(options),
78 | localAddress: options.localAddress,
79 | jar: options.cookies.jar,
80 | };
81 | });
82 |
83 | exports.createProxyAgent = (options, cookies = []) => {
84 | if (!cookies) cookies = [];
85 | if (typeof options === "string") options = { uri: options };
86 | const jar = new CookieJar();
87 | addCookies(jar, cookies);
88 |
89 | // ProxyAgent type that node httplibrary supports
90 | const agent = new HttpsProxyAgent(options.uri);
91 |
92 | // ProxyAgent type that undici supports
93 | const dispatcher = new ProxyAgent(proxyOptions).compose(cookie({ jar }));
94 |
95 | return { dispatcher, agent, jar, localAddress: options.localAddress };
96 | };
97 |
98 | exports.defaultAgent = createAgent();
99 |
--------------------------------------------------------------------------------
/lib/cache.js:
--------------------------------------------------------------------------------
1 | const { setTimeout } = require("timers");
2 |
3 | // A cache that expires.
4 | module.exports = class Cache extends Map {
5 | constructor(timeout = 1000) {
6 | super();
7 | this.timeout = timeout;
8 | }
9 | set(key, value) {
10 | if (this.has(key)) {
11 | clearTimeout(super.get(key).tid);
12 | }
13 | super.set(key, {
14 | tid: setTimeout(this.delete.bind(this, key), this.timeout).unref(),
15 | value,
16 | });
17 | }
18 | get(key) {
19 | let entry = super.get(key);
20 | if (entry) {
21 | return entry.value;
22 | }
23 | return null;
24 | }
25 | getOrSet(key, fn) {
26 | if (this.has(key)) {
27 | return this.get(key);
28 | } else {
29 | let value = fn();
30 | this.set(key, value);
31 | (async () => {
32 | try {
33 | await value;
34 | } catch (err) {
35 | this.delete(key);
36 | }
37 | })();
38 | return value;
39 | }
40 | }
41 | delete(key) {
42 | let entry = super.get(key);
43 | if (entry) {
44 | clearTimeout(entry.tid);
45 | super.delete(key);
46 | }
47 | }
48 | clear() {
49 | for (let entry of this.values()) {
50 | clearTimeout(entry.tid);
51 | }
52 | super.clear();
53 | }
54 | };
55 |
--------------------------------------------------------------------------------
/lib/format-utils.js:
--------------------------------------------------------------------------------
1 | const utils = require("./utils");
2 | const FORMATS = require("./formats");
3 |
4 | // Use these to help sort formats, higher index is better.
5 | const audioEncodingRanks = ["mp4a", "mp3", "vorbis", "aac", "opus", "flac"];
6 | const videoEncodingRanks = ["mp4v", "avc1", "Sorenson H.283", "MPEG-4 Visual", "VP8", "VP9", "H.264"];
7 |
8 | const getVideoBitrate = format => format.bitrate || 0;
9 | const getVideoEncodingRank = format => videoEncodingRanks.findIndex(enc => format.codecs?.includes(enc));
10 | const getAudioBitrate = format => format.audioBitrate || 0;
11 | const getAudioEncodingRank = format => audioEncodingRanks.findIndex(enc => format.codecs?.includes(enc));
12 |
13 | /**
14 | * Sort formats by a list of functions.
15 | *
16 | * @param {Object} a
17 | * @param {Object} b
18 | * @param {Array.} sortBy
19 | * @returns {number}
20 | */
21 | const sortFormatsBy = (a, b, sortBy) => {
22 | let res = 0;
23 | for (let fn of sortBy) {
24 | res = fn(b) - fn(a);
25 | if (res !== 0) {
26 | break;
27 | }
28 | }
29 | return res;
30 | };
31 |
32 | const sortFormatsByVideo = (a, b) =>
33 | sortFormatsBy(a, b, [format => parseInt(format.qualityLabel), getVideoBitrate, getVideoEncodingRank]);
34 |
35 | const sortFormatsByAudio = (a, b) => sortFormatsBy(a, b, [getAudioBitrate, getAudioEncodingRank]);
36 |
37 | /**
38 | * Sort formats from highest quality to lowest.
39 | *
40 | * @param {Object} a
41 | * @param {Object} b
42 | * @returns {number}
43 | */
44 | exports.sortFormats = (a, b) =>
45 | sortFormatsBy(a, b, [
46 | // Formats with both video and audio are ranked highest.
47 | format => +!!format.isHLS,
48 | format => +!!format.isDashMPD,
49 | format => +(format.contentLength > 0),
50 | format => +(format.hasVideo && format.hasAudio),
51 | format => +format.hasVideo,
52 | format => parseInt(format.qualityLabel) || 0,
53 | getVideoBitrate,
54 | getAudioBitrate,
55 | getVideoEncodingRank,
56 | getAudioEncodingRank,
57 | ]);
58 |
59 | /**
60 | * Choose a format depending on the given options.
61 | *
62 | * @param {Array.} formats
63 | * @param {Object} options
64 | * @returns {Object}
65 | * @throws {Error} when no format matches the filter/format rules
66 | */
67 | exports.chooseFormat = (formats, options) => {
68 | if (typeof options.format === "object") {
69 | if (!options.format.url) {
70 | throw Error("Invalid format given, did you use `ytdl.getInfo()`?");
71 | }
72 | return options.format;
73 | }
74 |
75 | if (options.filter) {
76 | formats = exports.filterFormats(formats, options.filter);
77 | }
78 |
79 | // We currently only support HLS-Formats for livestreams
80 | // So we (now) remove all non-HLS streams
81 | if (formats.some(fmt => fmt.isHLS)) {
82 | formats = formats.filter(fmt => fmt.isHLS || !fmt.isLive);
83 | }
84 |
85 | let format;
86 | const quality = options.quality || "highest";
87 | switch (quality) {
88 | case "highest":
89 | format = formats[0];
90 | break;
91 |
92 | case "lowest":
93 | format = formats[formats.length - 1];
94 | break;
95 |
96 | case "highestaudio": {
97 | formats = exports.filterFormats(formats, "audio");
98 | formats.sort(sortFormatsByAudio);
99 | // Filter for only the best audio format
100 | const bestAudioFormat = formats[0];
101 | formats = formats.filter(f => sortFormatsByAudio(bestAudioFormat, f) === 0);
102 | // Check for the worst video quality for the best audio quality and pick according
103 | // This does not loose default sorting of video encoding and bitrate
104 | const worstVideoQuality = formats.map(f => parseInt(f.qualityLabel) || 0).sort((a, b) => a - b)[0];
105 | format = formats.find(f => (parseInt(f.qualityLabel) || 0) === worstVideoQuality);
106 | break;
107 | }
108 |
109 | case "lowestaudio":
110 | formats = exports.filterFormats(formats, "audio");
111 | formats.sort(sortFormatsByAudio);
112 | format = formats[formats.length - 1];
113 | break;
114 |
115 | case "highestvideo": {
116 | formats = exports.filterFormats(formats, "video");
117 | formats.sort(sortFormatsByVideo);
118 | // Filter for only the best video format
119 | const bestVideoFormat = formats[0];
120 | formats = formats.filter(f => sortFormatsByVideo(bestVideoFormat, f) === 0);
121 | // Check for the worst audio quality for the best video quality and pick according
122 | // This does not loose default sorting of audio encoding and bitrate
123 | const worstAudioQuality = formats.map(f => f.audioBitrate || 0).sort((a, b) => a - b)[0];
124 | format = formats.find(f => (f.audioBitrate || 0) === worstAudioQuality);
125 | break;
126 | }
127 |
128 | case "lowestvideo":
129 | formats = exports.filterFormats(formats, "video");
130 | formats.sort(sortFormatsByVideo);
131 | format = formats[formats.length - 1];
132 | break;
133 |
134 | default:
135 | format = getFormatByQuality(quality, formats);
136 | break;
137 | }
138 |
139 | if (!format) {
140 | throw Error(`No such format found: ${quality}`);
141 | }
142 | return format;
143 | };
144 |
145 | /**
146 | * Gets a format based on quality or array of quality's
147 | *
148 | * @param {string|[string]} quality
149 | * @param {[Object]} formats
150 | * @returns {Object}
151 | */
152 | const getFormatByQuality = (quality, formats) => {
153 | let getFormat = itag => formats.find(format => `${format.itag}` === `${itag}`);
154 | if (Array.isArray(quality)) {
155 | return getFormat(quality.find(q => getFormat(q)));
156 | } else {
157 | return getFormat(quality);
158 | }
159 | };
160 |
161 | /**
162 | * @param {Array.} formats
163 | * @param {Function} filter
164 | * @returns {Array.}
165 | */
166 | exports.filterFormats = (formats, filter) => {
167 | let fn;
168 | switch (filter) {
169 | case "videoandaudio":
170 | case "audioandvideo":
171 | fn = format => format.hasVideo && format.hasAudio;
172 | break;
173 |
174 | case "video":
175 | fn = format => format.hasVideo;
176 | break;
177 |
178 | case "videoonly":
179 | fn = format => format.hasVideo && !format.hasAudio;
180 | break;
181 |
182 | case "audio":
183 | fn = format => format.hasAudio;
184 | break;
185 |
186 | case "audioonly":
187 | fn = format => !format.hasVideo && format.hasAudio;
188 | break;
189 |
190 | default:
191 | if (typeof filter === "function") {
192 | fn = filter;
193 | } else {
194 | throw TypeError(`Given filter (${filter}) is not supported`);
195 | }
196 | }
197 | return formats.filter(format => !!format.url && fn(format));
198 | };
199 |
200 | /**
201 | * @param {Object} format
202 | * @returns {Object}
203 | */
204 | exports.addFormatMeta = format => {
205 | format = Object.assign({}, FORMATS[format.itag], format);
206 | format.hasVideo = !!format.qualityLabel;
207 | format.hasAudio = !!format.audioBitrate;
208 | format.container = format.mimeType ? format.mimeType.split(";")[0].split("/")[1] : null;
209 | format.codecs = format.mimeType ? utils.between(format.mimeType, 'codecs="', '"') : null;
210 | format.videoCodec = format.hasVideo && format.codecs ? format.codecs.split(", ")[0] : null;
211 | format.audioCodec = format.hasAudio && format.codecs ? format.codecs.split(", ").slice(-1)[0] : null;
212 | format.isLive = /\bsource[/=]yt_live_broadcast\b/.test(format.url);
213 | format.isHLS = /\/manifest\/hls_(variant|playlist)\//.test(format.url);
214 | format.isDashMPD = /\/manifest\/dash\//.test(format.url);
215 | return format;
216 | };
217 |
--------------------------------------------------------------------------------
/lib/formats.js:
--------------------------------------------------------------------------------
1 | /**
2 | * http://en.wikipedia.org/wiki/YouTube#Quality_and_formats
3 | */
4 | module.exports = {
5 | 5: {
6 | mimeType: 'video/flv; codecs="Sorenson H.283, mp3"',
7 | qualityLabel: "240p",
8 | bitrate: 250000,
9 | audioBitrate: 64,
10 | },
11 |
12 | 6: {
13 | mimeType: 'video/flv; codecs="Sorenson H.263, mp3"',
14 | qualityLabel: "270p",
15 | bitrate: 800000,
16 | audioBitrate: 64,
17 | },
18 |
19 | 13: {
20 | mimeType: 'video/3gp; codecs="MPEG-4 Visual, aac"',
21 | qualityLabel: null,
22 | bitrate: 500000,
23 | audioBitrate: null,
24 | },
25 |
26 | 17: {
27 | mimeType: 'video/3gp; codecs="MPEG-4 Visual, aac"',
28 | qualityLabel: "144p",
29 | bitrate: 50000,
30 | audioBitrate: 24,
31 | },
32 |
33 | 18: {
34 | mimeType: 'video/mp4; codecs="H.264, aac"',
35 | qualityLabel: "360p",
36 | bitrate: 500000,
37 | audioBitrate: 96,
38 | },
39 |
40 | 22: {
41 | mimeType: 'video/mp4; codecs="H.264, aac"',
42 | qualityLabel: "720p",
43 | bitrate: 2000000,
44 | audioBitrate: 192,
45 | },
46 |
47 | 34: {
48 | mimeType: 'video/flv; codecs="H.264, aac"',
49 | qualityLabel: "360p",
50 | bitrate: 500000,
51 | audioBitrate: 128,
52 | },
53 |
54 | 35: {
55 | mimeType: 'video/flv; codecs="H.264, aac"',
56 | qualityLabel: "480p",
57 | bitrate: 800000,
58 | audioBitrate: 128,
59 | },
60 |
61 | 36: {
62 | mimeType: 'video/3gp; codecs="MPEG-4 Visual, aac"',
63 | qualityLabel: "240p",
64 | bitrate: 175000,
65 | audioBitrate: 32,
66 | },
67 |
68 | 37: {
69 | mimeType: 'video/mp4; codecs="H.264, aac"',
70 | qualityLabel: "1080p",
71 | bitrate: 3000000,
72 | audioBitrate: 192,
73 | },
74 |
75 | 38: {
76 | mimeType: 'video/mp4; codecs="H.264, aac"',
77 | qualityLabel: "3072p",
78 | bitrate: 3500000,
79 | audioBitrate: 192,
80 | },
81 |
82 | 43: {
83 | mimeType: 'video/webm; codecs="VP8, vorbis"',
84 | qualityLabel: "360p",
85 | bitrate: 500000,
86 | audioBitrate: 128,
87 | },
88 |
89 | 44: {
90 | mimeType: 'video/webm; codecs="VP8, vorbis"',
91 | qualityLabel: "480p",
92 | bitrate: 1000000,
93 | audioBitrate: 128,
94 | },
95 |
96 | 45: {
97 | mimeType: 'video/webm; codecs="VP8, vorbis"',
98 | qualityLabel: "720p",
99 | bitrate: 2000000,
100 | audioBitrate: 192,
101 | },
102 |
103 | 46: {
104 | mimeType: 'audio/webm; codecs="vp8, vorbis"',
105 | qualityLabel: "1080p",
106 | bitrate: null,
107 | audioBitrate: 192,
108 | },
109 |
110 | 82: {
111 | mimeType: 'video/mp4; codecs="H.264, aac"',
112 | qualityLabel: "360p",
113 | bitrate: 500000,
114 | audioBitrate: 96,
115 | },
116 |
117 | 83: {
118 | mimeType: 'video/mp4; codecs="H.264, aac"',
119 | qualityLabel: "240p",
120 | bitrate: 500000,
121 | audioBitrate: 96,
122 | },
123 |
124 | 84: {
125 | mimeType: 'video/mp4; codecs="H.264, aac"',
126 | qualityLabel: "720p",
127 | bitrate: 2000000,
128 | audioBitrate: 192,
129 | },
130 |
131 | 85: {
132 | mimeType: 'video/mp4; codecs="H.264, aac"',
133 | qualityLabel: "1080p",
134 | bitrate: 3000000,
135 | audioBitrate: 192,
136 | },
137 |
138 | 91: {
139 | mimeType: 'video/ts; codecs="H.264, aac"',
140 | qualityLabel: "144p",
141 | bitrate: 100000,
142 | audioBitrate: 48,
143 | },
144 |
145 | 92: {
146 | mimeType: 'video/ts; codecs="H.264, aac"',
147 | qualityLabel: "240p",
148 | bitrate: 150000,
149 | audioBitrate: 48,
150 | },
151 |
152 | 93: {
153 | mimeType: 'video/ts; codecs="H.264, aac"',
154 | qualityLabel: "360p",
155 | bitrate: 500000,
156 | audioBitrate: 128,
157 | },
158 |
159 | 94: {
160 | mimeType: 'video/ts; codecs="H.264, aac"',
161 | qualityLabel: "480p",
162 | bitrate: 800000,
163 | audioBitrate: 128,
164 | },
165 |
166 | 95: {
167 | mimeType: 'video/ts; codecs="H.264, aac"',
168 | qualityLabel: "720p",
169 | bitrate: 1500000,
170 | audioBitrate: 256,
171 | },
172 |
173 | 96: {
174 | mimeType: 'video/ts; codecs="H.264, aac"',
175 | qualityLabel: "1080p",
176 | bitrate: 2500000,
177 | audioBitrate: 256,
178 | },
179 |
180 | 100: {
181 | mimeType: 'audio/webm; codecs="VP8, vorbis"',
182 | qualityLabel: "360p",
183 | bitrate: null,
184 | audioBitrate: 128,
185 | },
186 |
187 | 101: {
188 | mimeType: 'audio/webm; codecs="VP8, vorbis"',
189 | qualityLabel: "360p",
190 | bitrate: null,
191 | audioBitrate: 192,
192 | },
193 |
194 | 102: {
195 | mimeType: 'audio/webm; codecs="VP8, vorbis"',
196 | qualityLabel: "720p",
197 | bitrate: null,
198 | audioBitrate: 192,
199 | },
200 |
201 | 120: {
202 | mimeType: 'video/flv; codecs="H.264, aac"',
203 | qualityLabel: "720p",
204 | bitrate: 2000000,
205 | audioBitrate: 128,
206 | },
207 |
208 | 127: {
209 | mimeType: 'audio/ts; codecs="aac"',
210 | qualityLabel: null,
211 | bitrate: null,
212 | audioBitrate: 96,
213 | },
214 |
215 | 128: {
216 | mimeType: 'audio/ts; codecs="aac"',
217 | qualityLabel: null,
218 | bitrate: null,
219 | audioBitrate: 96,
220 | },
221 |
222 | 132: {
223 | mimeType: 'video/ts; codecs="H.264, aac"',
224 | qualityLabel: "240p",
225 | bitrate: 150000,
226 | audioBitrate: 48,
227 | },
228 |
229 | 133: {
230 | mimeType: 'video/mp4; codecs="H.264"',
231 | qualityLabel: "240p",
232 | bitrate: 200000,
233 | audioBitrate: null,
234 | },
235 |
236 | 134: {
237 | mimeType: 'video/mp4; codecs="H.264"',
238 | qualityLabel: "360p",
239 | bitrate: 300000,
240 | audioBitrate: null,
241 | },
242 |
243 | 135: {
244 | mimeType: 'video/mp4; codecs="H.264"',
245 | qualityLabel: "480p",
246 | bitrate: 500000,
247 | audioBitrate: null,
248 | },
249 |
250 | 136: {
251 | mimeType: 'video/mp4; codecs="H.264"',
252 | qualityLabel: "720p",
253 | bitrate: 1000000,
254 | audioBitrate: null,
255 | },
256 |
257 | 137: {
258 | mimeType: 'video/mp4; codecs="H.264"',
259 | qualityLabel: "1080p",
260 | bitrate: 2500000,
261 | audioBitrate: null,
262 | },
263 |
264 | 138: {
265 | mimeType: 'video/mp4; codecs="H.264"',
266 | qualityLabel: "4320p",
267 | bitrate: 13500000,
268 | audioBitrate: null,
269 | },
270 |
271 | 139: {
272 | mimeType: 'audio/mp4; codecs="aac"',
273 | qualityLabel: null,
274 | bitrate: null,
275 | audioBitrate: 48,
276 | },
277 |
278 | 140: {
279 | mimeType: 'audio/m4a; codecs="aac"',
280 | qualityLabel: null,
281 | bitrate: null,
282 | audioBitrate: 128,
283 | },
284 |
285 | 141: {
286 | mimeType: 'audio/mp4; codecs="aac"',
287 | qualityLabel: null,
288 | bitrate: null,
289 | audioBitrate: 256,
290 | },
291 |
292 | 151: {
293 | mimeType: 'video/ts; codecs="H.264, aac"',
294 | qualityLabel: "720p",
295 | bitrate: 50000,
296 | audioBitrate: 24,
297 | },
298 |
299 | 160: {
300 | mimeType: 'video/mp4; codecs="H.264"',
301 | qualityLabel: "144p",
302 | bitrate: 100000,
303 | audioBitrate: null,
304 | },
305 |
306 | 171: {
307 | mimeType: 'audio/webm; codecs="vorbis"',
308 | qualityLabel: null,
309 | bitrate: null,
310 | audioBitrate: 128,
311 | },
312 |
313 | 172: {
314 | mimeType: 'audio/webm; codecs="vorbis"',
315 | qualityLabel: null,
316 | bitrate: null,
317 | audioBitrate: 192,
318 | },
319 |
320 | 231: {
321 | mimeType: 'video/ts; codecs="H.264, aac"',
322 | qualityLabel: "480p",
323 | bitrate: 500000,
324 | audioBitrate: null,
325 | },
326 |
327 | 232: {
328 | mimeType: 'video/ts; codecs="H.264, aac"',
329 | qualityLabel: "720p",
330 | bitrate: 800000,
331 | audioBitrate: null,
332 | },
333 |
334 | 242: {
335 | mimeType: 'video/webm; codecs="VP9"',
336 | qualityLabel: "240p",
337 | bitrate: 100000,
338 | audioBitrate: null,
339 | },
340 |
341 | 243: {
342 | mimeType: 'video/webm; codecs="VP9"',
343 | qualityLabel: "360p",
344 | bitrate: 250000,
345 | audioBitrate: null,
346 | },
347 |
348 | 244: {
349 | mimeType: 'video/webm; codecs="VP9"',
350 | qualityLabel: "480p",
351 | bitrate: 500000,
352 | audioBitrate: null,
353 | },
354 |
355 | 247: {
356 | mimeType: 'video/webm; codecs="VP9"',
357 | qualityLabel: "720p",
358 | bitrate: 700000,
359 | audioBitrate: null,
360 | },
361 |
362 | 248: {
363 | mimeType: 'video/webm; codecs="VP9"',
364 | qualityLabel: "1080p",
365 | bitrate: 1500000,
366 | audioBitrate: null,
367 | },
368 |
369 | 249: {
370 | mimeType: 'audio/webm; codecs="opus"',
371 | qualityLabel: null,
372 | bitrate: null,
373 | audioBitrate: 48,
374 | },
375 |
376 | 250: {
377 | mimeType: 'audio/webm; codecs="opus"',
378 | qualityLabel: null,
379 | bitrate: null,
380 | audioBitrate: 64,
381 | },
382 |
383 | 251: {
384 | mimeType: 'audio/webm; codecs="opus"',
385 | qualityLabel: null,
386 | bitrate: null,
387 | audioBitrate: 160,
388 | },
389 |
390 | 264: {
391 | mimeType: 'video/mp4; codecs="H.264"',
392 | qualityLabel: "1440p",
393 | bitrate: 4000000,
394 | audioBitrate: null,
395 | },
396 |
397 | 266: {
398 | mimeType: 'video/mp4; codecs="H.264"',
399 | qualityLabel: "2160p",
400 | bitrate: 12500000,
401 | audioBitrate: null,
402 | },
403 |
404 | 270: {
405 | mimeType: 'video/mp4; codecs="H.264"',
406 | qualityLabel: "1080p",
407 | bitrate: 2500000,
408 | audioBitrate: null,
409 | },
410 |
411 | 271: {
412 | mimeType: 'video/webm; codecs="VP9"',
413 | qualityLabel: "1440p",
414 | bitrate: 9000000,
415 | audioBitrate: null,
416 | },
417 |
418 | 272: {
419 | mimeType: 'video/webm; codecs="VP9"',
420 | qualityLabel: "4320p",
421 | bitrate: 20000000,
422 | audioBitrate: null,
423 | },
424 |
425 | 278: {
426 | mimeType: 'video/webm; codecs="VP9"',
427 | qualityLabel: "144p 30fps",
428 | bitrate: 80000,
429 | audioBitrate: null,
430 | },
431 |
432 | 298: {
433 | mimeType: 'video/mp4; codecs="H.264"',
434 | qualityLabel: "720p",
435 | bitrate: 3000000,
436 | audioBitrate: null,
437 | },
438 |
439 | 299: {
440 | mimeType: 'video/mp4; codecs="H.264"',
441 | qualityLabel: "1080p",
442 | bitrate: 5500000,
443 | audioBitrate: null,
444 | },
445 |
446 | 300: {
447 | mimeType: 'video/ts; codecs="H.264, aac"',
448 | qualityLabel: "720p",
449 | bitrate: 1318000,
450 | audioBitrate: 48,
451 | },
452 |
453 | 301: {
454 | mimeType: 'video/ts; codecs="H.264, aac"',
455 | qualityLabel: "1080p",
456 | bitrate: 3000000,
457 | audioBitrate: 128,
458 | },
459 |
460 | 302: {
461 | mimeType: 'video/webm; codecs="VP9"',
462 | qualityLabel: "720p HFR",
463 | bitrate: 2500000,
464 | audioBitrate: null,
465 | },
466 |
467 | 303: {
468 | mimeType: 'video/webm; codecs="VP9"',
469 | qualityLabel: "1080p HFR",
470 | bitrate: 5000000,
471 | audioBitrate: null,
472 | },
473 |
474 | 308: {
475 | mimeType: 'video/webm; codecs="VP9"',
476 | qualityLabel: "1440p HFR",
477 | bitrate: 10000000,
478 | audioBitrate: null,
479 | },
480 |
481 | 311: {
482 | mimeType: 'video/webm; codecs="VP9"',
483 | qualityLabel: "720p",
484 | bitrate: 1250000,
485 | audioBitrate: null,
486 | },
487 |
488 | 312: {
489 | mimeType: 'video/mp4; codecs="H.264"',
490 | qualityLabel: "1080p",
491 | bitrate: 2500000,
492 | audioBitrate: null,
493 | },
494 |
495 | 313: {
496 | mimeType: 'video/webm; codecs="VP9"',
497 | qualityLabel: "2160p",
498 | bitrate: 13000000,
499 | audioBitrate: null,
500 | },
501 |
502 | 315: {
503 | mimeType: 'video/webm; codecs="VP9"',
504 | qualityLabel: "2160p HFR",
505 | bitrate: 20000000,
506 | audioBitrate: null,
507 | },
508 |
509 | 330: {
510 | mimeType: 'video/webm; codecs="VP9"',
511 | qualityLabel: "144p HDR, HFR",
512 | bitrate: 80000,
513 | audioBitrate: null,
514 | },
515 |
516 | 331: {
517 | mimeType: 'video/webm; codecs="VP9"',
518 | qualityLabel: "240p HDR, HFR",
519 | bitrate: 100000,
520 | audioBitrate: null,
521 | },
522 |
523 | 332: {
524 | mimeType: 'video/webm; codecs="VP9"',
525 | qualityLabel: "360p HDR, HFR",
526 | bitrate: 250000,
527 | audioBitrate: null,
528 | },
529 |
530 | 333: {
531 | mimeType: 'video/webm; codecs="VP9"',
532 | qualityLabel: "240p HDR, HFR",
533 | bitrate: 500000,
534 | audioBitrate: null,
535 | },
536 |
537 | 334: {
538 | mimeType: 'video/webm; codecs="VP9"',
539 | qualityLabel: "720p HDR, HFR",
540 | bitrate: 1000000,
541 | audioBitrate: null,
542 | },
543 |
544 | 335: {
545 | mimeType: 'video/webm; codecs="VP9"',
546 | qualityLabel: "1080p HDR, HFR",
547 | bitrate: 1500000,
548 | audioBitrate: null,
549 | },
550 |
551 | 336: {
552 | mimeType: 'video/webm; codecs="VP9"',
553 | qualityLabel: "1440p HDR, HFR",
554 | bitrate: 5000000,
555 | audioBitrate: null,
556 | },
557 |
558 | 337: {
559 | mimeType: 'video/webm; codecs="VP9"',
560 | qualityLabel: "2160p HDR, HFR",
561 | bitrate: 12000000,
562 | audioBitrate: null,
563 | },
564 | };
565 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | const PassThrough = require("stream").PassThrough;
2 | const getInfo = require("./info");
3 | const utils = require("./utils");
4 | const formatUtils = require("./format-utils");
5 | const urlUtils = require("./url-utils");
6 | const miniget = require("miniget");
7 | const m3u8stream = require("m3u8stream");
8 | const { parseTimestamp } = require("m3u8stream");
9 | const agent = require("./agent");
10 |
11 | /**
12 | * @param {string} link
13 | * @param {!Object} options
14 | * @returns {ReadableStream}
15 | */
16 | const ytdl = (link, options) => {
17 | const stream = createStream(options);
18 | ytdl.getInfo(link, options).then(
19 | info => {
20 | downloadFromInfoCallback(stream, info, options);
21 | },
22 | stream.emit.bind(stream, "error"),
23 | );
24 | return stream;
25 | };
26 | module.exports = ytdl;
27 |
28 | ytdl.getBasicInfo = getInfo.getBasicInfo;
29 | ytdl.getInfo = getInfo.getInfo;
30 | ytdl.chooseFormat = formatUtils.chooseFormat;
31 | ytdl.filterFormats = formatUtils.filterFormats;
32 | ytdl.validateID = urlUtils.validateID;
33 | ytdl.validateURL = urlUtils.validateURL;
34 | ytdl.getURLVideoID = urlUtils.getURLVideoID;
35 | ytdl.getVideoID = urlUtils.getVideoID;
36 | ytdl.createAgent = agent.createAgent;
37 | ytdl.createProxyAgent = agent.createProxyAgent;
38 | ytdl.cache = {
39 | info: getInfo.cache,
40 | watch: getInfo.watchPageCache,
41 | };
42 | ytdl.version = require("../package.json").version;
43 |
44 | const createStream = options => {
45 | const stream = new PassThrough({ highWaterMark: options?.highWaterMark || 1024 * 512 });
46 | stream._destroy = () => {
47 | stream.destroyed = true;
48 | };
49 | return stream;
50 | };
51 |
52 | const pipeAndSetEvents = (req, stream, end) => {
53 | // Forward events from the request to the stream.
54 | ["abort", "request", "response", "error", "redirect", "retry", "reconnect"].forEach(event => {
55 | req.prependListener(event, stream.emit.bind(stream, event));
56 | });
57 | req.pipe(stream, { end });
58 | };
59 |
60 | /**
61 | * Chooses a format to download.
62 | *
63 | * @param {stream.Readable} stream
64 | * @param {Object} info
65 | * @param {Object} options
66 | */
67 | const downloadFromInfoCallback = (stream, info, options) => {
68 | options = options || {};
69 |
70 | let err = utils.playError(info.player_response);
71 | if (err) {
72 | stream.emit("error", err);
73 | return;
74 | }
75 |
76 | if (!info.formats.length) {
77 | stream.emit("error", Error("This video is unavailable"));
78 | return;
79 | }
80 |
81 | let format;
82 | try {
83 | format = formatUtils.chooseFormat(info.formats, options);
84 | } catch (e) {
85 | stream.emit("error", e);
86 | return;
87 | }
88 | stream.emit("info", info, format);
89 | if (stream.destroyed) {
90 | return;
91 | }
92 |
93 | let contentLength,
94 | downloaded = 0;
95 | const ondata = chunk => {
96 | downloaded += chunk.length;
97 | stream.emit("progress", chunk.length, downloaded, contentLength);
98 | };
99 |
100 | utils.applyDefaultHeaders(options);
101 | if (options.IPv6Block) {
102 | options.requestOptions = Object.assign({}, options.requestOptions, {
103 | localAddress: utils.getRandomIPv6(options.IPv6Block),
104 | });
105 | }
106 |
107 | if (options.agent) {
108 | // Set agent on both the miniget and m3u8stream requests
109 | options.requestOptions.agent = options.agent.agent;
110 |
111 | if (options.agent.jar) {
112 | utils.setPropInsensitive(
113 | options.requestOptions.headers,
114 | "cookie",
115 | options.agent.jar.getCookieStringSync("https://www.youtube.com"),
116 | );
117 | }
118 | if (options.agent.localAddress) {
119 | options.requestOptions.localAddress = options.agent.localAddress;
120 | }
121 | }
122 |
123 | // Download the file in chunks, in this case the default is 10MB,
124 | // anything over this will cause youtube to throttle the download
125 | const dlChunkSize = typeof options.dlChunkSize === "number" ? options.dlChunkSize : 1024 * 1024 * 10;
126 | let req;
127 | let shouldEnd = true;
128 |
129 | if (format.isHLS || format.isDashMPD) {
130 | req = m3u8stream(format.url, {
131 | chunkReadahead: +info.live_chunk_readahead,
132 | begin: options.begin || (format.isLive && Date.now()),
133 | liveBuffer: options.liveBuffer,
134 | // Now we have passed not only custom "dispatcher" with undici ProxyAgent, but also "agent" field which is compatible for node http
135 | requestOptions: options.requestOptions,
136 | parser: format.isDashMPD ? "dash-mpd" : "m3u8",
137 | id: format.itag,
138 | });
139 |
140 | req.on("progress", (segment, totalSegments) => {
141 | stream.emit("progress", segment.size, segment.num, totalSegments);
142 | });
143 | pipeAndSetEvents(req, stream, shouldEnd);
144 | } else {
145 | const requestOptions = Object.assign({}, options.requestOptions, {
146 | maxReconnects: 6,
147 | maxRetries: 3,
148 | backoff: { inc: 500, max: 10000 },
149 | });
150 |
151 | let shouldBeChunked = dlChunkSize !== 0 && (!format.hasAudio || !format.hasVideo);
152 |
153 | if (shouldBeChunked) {
154 | let start = options.range?.start || 0;
155 | let end = start + dlChunkSize;
156 | const rangeEnd = options.range?.end;
157 |
158 | contentLength = options.range
159 | ? (rangeEnd ? rangeEnd + 1 : parseInt(format.contentLength)) - start
160 | : parseInt(format.contentLength);
161 |
162 | const getNextChunk = () => {
163 | if (stream.destroyed) return;
164 | if (!rangeEnd && end >= contentLength) end = 0;
165 | if (rangeEnd && end > rangeEnd) end = rangeEnd;
166 | shouldEnd = !end || end === rangeEnd;
167 |
168 | requestOptions.headers = Object.assign({}, requestOptions.headers, {
169 | Range: `bytes=${start}-${end || ""}`,
170 | });
171 | req = miniget(format.url, requestOptions);
172 | req.on("data", ondata);
173 | req.on("end", () => {
174 | if (stream.destroyed) return;
175 | if (end && end !== rangeEnd) {
176 | start = end + 1;
177 | end += dlChunkSize;
178 | getNextChunk();
179 | }
180 | });
181 | pipeAndSetEvents(req, stream, shouldEnd);
182 | };
183 | getNextChunk();
184 | } else {
185 | // Audio only and video only formats don't support begin
186 | if (options.begin) {
187 | format.url += `&begin=${parseTimestamp(options.begin)}`;
188 | }
189 | if (options.range?.start || options.range?.end) {
190 | requestOptions.headers = Object.assign({}, requestOptions.headers, {
191 | Range: `bytes=${options.range.start || "0"}-${options.range.end || ""}`,
192 | });
193 | }
194 | req = miniget(format.url, requestOptions);
195 | req.on("response", res => {
196 | if (stream.destroyed) return;
197 | contentLength = contentLength || parseInt(res.headers["content-length"]);
198 | });
199 | req.on("data", ondata);
200 | pipeAndSetEvents(req, stream, shouldEnd);
201 | }
202 | }
203 |
204 | stream._destroy = () => {
205 | stream.destroyed = true;
206 | if (req) {
207 | req.destroy();
208 | req.end();
209 | }
210 | };
211 | };
212 |
213 | /**
214 | * Can be used to download video after its `info` is gotten through
215 | * `ytdl.getInfo()`. In case the user might want to look at the
216 | * `info` object before deciding to download.
217 | *
218 | * @param {Object} info
219 | * @param {!Object} options
220 | * @returns {ReadableStream}
221 | */
222 | ytdl.downloadFromInfo = (info, options) => {
223 | const stream = createStream(options);
224 | if (!info.full) {
225 | throw Error("Cannot use `ytdl.downloadFromInfo()` when called with info from `ytdl.getBasicInfo()`");
226 | }
227 | setImmediate(() => {
228 | downloadFromInfoCallback(stream, info, options);
229 | });
230 | return stream;
231 | };
232 |
--------------------------------------------------------------------------------
/lib/info-extras.js:
--------------------------------------------------------------------------------
1 | const utils = require("./utils");
2 | const qs = require("querystring");
3 | const { parseTimestamp } = require("m3u8stream");
4 |
5 | const BASE_URL = "https://www.youtube.com/watch?v=";
6 | const TITLE_TO_CATEGORY = {
7 | song: { name: "Music", url: "https://music.youtube.com/" },
8 | };
9 |
10 | const getText = obj => obj?.runs?.[0]?.text ?? obj?.simpleText;
11 |
12 | /**
13 | * Get video media.
14 | *
15 | * @param {Object} info
16 | * @returns {Object}
17 | */
18 | exports.getMedia = info => {
19 | let media = {};
20 | let results = [];
21 | try {
22 | results = info.response.contents.twoColumnWatchNextResults.results.results.contents;
23 | } catch (err) {
24 | // Do nothing
25 | }
26 |
27 | let result = results.find(v => v.videoSecondaryInfoRenderer);
28 | if (!result) {
29 | return {};
30 | }
31 |
32 | try {
33 | let metadataRows = (result.metadataRowContainer || result.videoSecondaryInfoRenderer.metadataRowContainer)
34 | .metadataRowContainerRenderer.rows;
35 | for (let row of metadataRows) {
36 | if (row.metadataRowRenderer) {
37 | let title = getText(row.metadataRowRenderer.title).toLowerCase();
38 | let contents = row.metadataRowRenderer.contents[0];
39 | media[title] = getText(contents);
40 | let runs = contents.runs;
41 | if (runs?.[0]?.navigationEndpoint) {
42 | media[`${title}_url`] = new URL(
43 | runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url,
44 | BASE_URL,
45 | ).toString();
46 | }
47 | if (title in TITLE_TO_CATEGORY) {
48 | media.category = TITLE_TO_CATEGORY[title].name;
49 | media.category_url = TITLE_TO_CATEGORY[title].url;
50 | }
51 | } else if (row.richMetadataRowRenderer) {
52 | let contents = row.richMetadataRowRenderer.contents;
53 | let boxArt = contents.filter(
54 | meta => meta.richMetadataRenderer.style === "RICH_METADATA_RENDERER_STYLE_BOX_ART",
55 | );
56 | for (let { richMetadataRenderer } of boxArt) {
57 | let meta = richMetadataRenderer;
58 | media.year = getText(meta.subtitle);
59 | let type = getText(meta.callToAction).split(" ")[1];
60 | media[type] = getText(meta.title);
61 | media[`${type}_url`] = new URL(meta.endpoint.commandMetadata.webCommandMetadata.url, BASE_URL).toString();
62 | media.thumbnails = meta.thumbnail.thumbnails;
63 | }
64 | let topic = contents.filter(meta => meta.richMetadataRenderer.style === "RICH_METADATA_RENDERER_STYLE_TOPIC");
65 | for (let { richMetadataRenderer } of topic) {
66 | let meta = richMetadataRenderer;
67 | media.category = getText(meta.title);
68 | media.category_url = new URL(meta.endpoint.commandMetadata.webCommandMetadata.url, BASE_URL).toString();
69 | }
70 | }
71 | }
72 | } catch (err) {
73 | // Do nothing.
74 | }
75 |
76 | return media;
77 | };
78 |
79 | const isVerified = badges => !!badges?.find(b => b.metadataBadgeRenderer.tooltip === "Verified");
80 |
81 | /**
82 | * Get video author.
83 | *
84 | * @param {Object} info
85 | * @returns {Object}
86 | */
87 | exports.getAuthor = info => {
88 | let channelId,
89 | thumbnails = [],
90 | subscriberCount,
91 | verified = false;
92 | try {
93 | let results = info.response.contents.twoColumnWatchNextResults.results.results.contents;
94 | let v = results.find(v2 => v2?.videoSecondaryInfoRenderer?.owner?.videoOwnerRenderer);
95 | let videoOwnerRenderer = v.videoSecondaryInfoRenderer.owner.videoOwnerRenderer;
96 | channelId = videoOwnerRenderer.navigationEndpoint.browseEndpoint.browseId;
97 | thumbnails = videoOwnerRenderer.thumbnail.thumbnails.map(thumbnail => {
98 | thumbnail.url = new URL(thumbnail.url, BASE_URL).toString();
99 | return thumbnail;
100 | });
101 | subscriberCount = utils.parseAbbreviatedNumber(getText(videoOwnerRenderer.subscriberCountText));
102 | verified = isVerified(videoOwnerRenderer.badges);
103 | } catch (err) {
104 | // Do nothing.
105 | }
106 | try {
107 | let videoDetails = info.player_response.microformat?.playerMicroformatRenderer;
108 | let id = videoDetails?.channelId || channelId || info.player_response.videoDetails.channelId;
109 | let author = {
110 | id: id,
111 | name: videoDetails?.ownerChannelName ?? info.player_response.videoDetails.author,
112 | user: videoDetails?.ownerProfileUrl.split("/").slice(-1)[0] ?? null,
113 | channel_url: `https://www.youtube.com/channel/${id}`,
114 | external_channel_url: videoDetails ? `https://www.youtube.com/channel/${videoDetails.externalChannelId}` : "",
115 | user_url: videoDetails ? new URL(videoDetails.ownerProfileUrl, BASE_URL).toString() : "",
116 | thumbnails,
117 | verified,
118 | subscriber_count: subscriberCount,
119 | };
120 | if (thumbnails.length) {
121 | utils.deprecate(author, "avatar", author.thumbnails[0].url, "author.avatar", "author.thumbnails[0].url");
122 | }
123 | return author;
124 | } catch (err) {
125 | return {};
126 | }
127 | };
128 |
129 | const parseRelatedVideo = (details, rvsParams) => {
130 | if (!details) return;
131 | try {
132 | let viewCount = getText(details.viewCountText);
133 | let shortViewCount = getText(details.shortViewCountText);
134 | let rvsDetails = rvsParams.find(elem => elem.id === details.videoId);
135 | if (!/^\d/.test(shortViewCount)) {
136 | shortViewCount = rvsDetails?.short_view_count_text || "";
137 | }
138 | viewCount = (/^\d/.test(viewCount) ? viewCount : shortViewCount).split(" ")[0];
139 | let browseEndpoint = details.shortBylineText.runs[0].navigationEndpoint.browseEndpoint;
140 | let channelId = browseEndpoint.browseId;
141 | let name = getText(details.shortBylineText);
142 | let user = (browseEndpoint.canonicalBaseUrl || "").split("/").slice(-1)[0];
143 | let video = {
144 | id: details.videoId,
145 | title: getText(details.title),
146 | published: getText(details.publishedTimeText),
147 | author: {
148 | id: channelId,
149 | name,
150 | user,
151 | channel_url: `https://www.youtube.com/channel/${channelId}`,
152 | user_url: `https://www.youtube.com/user/${user}`,
153 | thumbnails: details.channelThumbnail.thumbnails.map(thumbnail => {
154 | thumbnail.url = new URL(thumbnail.url, BASE_URL).toString();
155 | return thumbnail;
156 | }),
157 | verified: isVerified(details.ownerBadges),
158 |
159 | [Symbol.toPrimitive]() {
160 | console.warn(
161 | `\`relatedVideo.author\` will be removed in a near future release, ` +
162 | `use \`relatedVideo.author.name\` instead.`,
163 | );
164 | return video.author.name;
165 | },
166 | },
167 | short_view_count_text: shortViewCount.split(" ")[0],
168 | view_count: viewCount.replace(/,/g, ""),
169 | length_seconds: details.lengthText
170 | ? Math.floor(parseTimestamp(getText(details.lengthText)) / 1000)
171 | : rvsParams
172 | ? `${rvsParams.length_seconds}`
173 | : undefined,
174 | thumbnails: details.thumbnail.thumbnails,
175 | richThumbnails: details.richThumbnail
176 | ? details.richThumbnail.movingThumbnailRenderer.movingThumbnailDetails.thumbnails
177 | : [],
178 | isLive: !!details.badges?.find(b => b.metadataBadgeRenderer.label === "LIVE NOW"),
179 | };
180 |
181 | utils.deprecate(
182 | video,
183 | "author_thumbnail",
184 | video.author.thumbnails[0].url,
185 | "relatedVideo.author_thumbnail",
186 | "relatedVideo.author.thumbnails[0].url",
187 | );
188 | utils.deprecate(video, "ucid", video.author.id, "relatedVideo.ucid", "relatedVideo.author.id");
189 | utils.deprecate(
190 | video,
191 | "video_thumbnail",
192 | video.thumbnails[0].url,
193 | "relatedVideo.video_thumbnail",
194 | "relatedVideo.thumbnails[0].url",
195 | );
196 | return video;
197 | } catch (err) {
198 | // Skip.
199 | }
200 | };
201 |
202 | /**
203 | * Get related videos.
204 | *
205 | * @param {Object} info
206 | * @returns {Array.}
207 | */
208 | exports.getRelatedVideos = info => {
209 | let rvsParams = [],
210 | secondaryResults = [];
211 | try {
212 | rvsParams = info.response.webWatchNextResponseExtensionData.relatedVideoArgs.split(",").map(e => qs.parse(e));
213 | } catch (err) {
214 | // Do nothing.
215 | }
216 | try {
217 | secondaryResults = info.response.contents.twoColumnWatchNextResults.secondaryResults.secondaryResults.results;
218 | } catch (err) {
219 | return [];
220 | }
221 | let videos = [];
222 | for (let result of secondaryResults || []) {
223 | let details = result.compactVideoRenderer;
224 | if (details) {
225 | let video = parseRelatedVideo(details, rvsParams);
226 | if (video) videos.push(video);
227 | } else {
228 | let autoplay = result.compactAutoplayRenderer || result.itemSectionRenderer;
229 | if (!autoplay || !Array.isArray(autoplay.contents)) continue;
230 | for (let content of autoplay.contents) {
231 | let video = parseRelatedVideo(content.compactVideoRenderer, rvsParams);
232 | if (video) videos.push(video);
233 | }
234 | }
235 | }
236 | return videos;
237 | };
238 |
239 | /**
240 | * Get like count.
241 | *
242 | * @param {Object} info
243 | * @returns {number}
244 | */
245 | exports.getLikes = info => {
246 | try {
247 | let contents = info.response.contents.twoColumnWatchNextResults.results.results.contents;
248 | let video = contents.find(r => r.videoPrimaryInfoRenderer);
249 | let buttons = video.videoPrimaryInfoRenderer.videoActions.menuRenderer.topLevelButtons;
250 | let accessibilityText = buttons.find(b => b.segmentedLikeDislikeButtonViewModel).segmentedLikeDislikeButtonViewModel
251 | .likeButtonViewModel.likeButtonViewModel.toggleButtonViewModel.toggleButtonViewModel.defaultButtonViewModel
252 | .buttonViewModel.accessibilityText;
253 | return parseInt(accessibilityText.match(/[\d,.]+/)[0].replace(/\D+/g, ""));
254 | } catch (err) {
255 | return null;
256 | }
257 | };
258 |
259 | /**
260 | * Cleans up a few fields on `videoDetails`.
261 | *
262 | * @param {Object} videoDetails
263 | * @param {Object} info
264 | * @returns {Object}
265 | */
266 | exports.cleanVideoDetails = (videoDetails, info) => {
267 | videoDetails.thumbnails = videoDetails.thumbnail.thumbnails;
268 | delete videoDetails.thumbnail;
269 | utils.deprecate(
270 | videoDetails,
271 | "thumbnail",
272 | { thumbnails: videoDetails.thumbnails },
273 | "videoDetails.thumbnail.thumbnails",
274 | "videoDetails.thumbnails",
275 | );
276 | videoDetails.description = videoDetails.shortDescription || getText(videoDetails.description);
277 | delete videoDetails.shortDescription;
278 | utils.deprecate(
279 | videoDetails,
280 | "shortDescription",
281 | videoDetails.description,
282 | "videoDetails.shortDescription",
283 | "videoDetails.description",
284 | );
285 |
286 | // Use more reliable `lengthSeconds` from `playerMicroformatRenderer`.
287 | videoDetails.lengthSeconds =
288 | info.player_response.microformat?.playerMicroformatRenderer?.lengthSeconds ||
289 | info.player_response.videoDetails.lengthSeconds;
290 | return videoDetails;
291 | };
292 |
293 | /**
294 | * Get storyboards info.
295 | *
296 | * @param {Object} info
297 | * @returns {Array.}
298 | */
299 | exports.getStoryboards = info => {
300 | const parts = info.player_response?.storyboards?.playerStoryboardSpecRenderer?.spec?.split("|");
301 |
302 | if (!parts) return [];
303 |
304 | const url = new URL(parts.shift());
305 |
306 | return parts.map((part, i) => {
307 | let [thumbnailWidth, thumbnailHeight, thumbnailCount, columns, rows, interval, nameReplacement, sigh] =
308 | part.split("#");
309 |
310 | url.searchParams.set("sigh", sigh);
311 |
312 | thumbnailCount = parseInt(thumbnailCount, 10);
313 | columns = parseInt(columns, 10);
314 | rows = parseInt(rows, 10);
315 |
316 | const storyboardCount = Math.ceil(thumbnailCount / (columns * rows));
317 |
318 | return {
319 | templateUrl: url.toString().replace("$L", i).replace("$N", nameReplacement),
320 | thumbnailWidth: parseInt(thumbnailWidth, 10),
321 | thumbnailHeight: parseInt(thumbnailHeight, 10),
322 | thumbnailCount,
323 | interval: parseInt(interval, 10),
324 | columns,
325 | rows,
326 | storyboardCount,
327 | };
328 | });
329 | };
330 |
331 | /**
332 | * Get chapters info.
333 | *
334 | * @param {Object} info
335 | * @returns {Array.}
336 | */
337 | exports.getChapters = info => {
338 | const playerOverlayRenderer = info.response?.playerOverlays?.playerOverlayRenderer;
339 | const playerBar = playerOverlayRenderer?.decoratedPlayerBarRenderer?.decoratedPlayerBarRenderer?.playerBar;
340 | const markersMap = playerBar?.multiMarkersPlayerBarRenderer?.markersMap;
341 | const marker = Array.isArray(markersMap) && markersMap.find(m => Array.isArray(m.value?.chapters));
342 | if (!marker) return [];
343 | const chapters = marker.value.chapters;
344 |
345 | return chapters.map(chapter => ({
346 | title: getText(chapter.chapterRenderer.title),
347 | start_time: chapter.chapterRenderer.timeRangeStartMillis / 1000,
348 | }));
349 | };
350 |
--------------------------------------------------------------------------------
/lib/info.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | const sax = require("sax");
3 |
4 | const utils = require("./utils");
5 | // Forces Node JS version of setTimeout for Electron based applications
6 | const { setTimeout } = require("timers");
7 | const formatUtils = require("./format-utils");
8 | const urlUtils = require("./url-utils");
9 | const extras = require("./info-extras");
10 | const Cache = require("./cache");
11 | const sig = require("./sig");
12 |
13 | const BASE_URL = "https://www.youtube.com/watch?v=";
14 |
15 | // Cached for storing basic/full info.
16 | exports.cache = new Cache();
17 | exports.watchPageCache = new Cache();
18 |
19 | // List of URLs that show up in `notice_url` for age restricted videos.
20 | const AGE_RESTRICTED_URLS = ["support.google.com/youtube/?p=age_restrictions", "youtube.com/t/community_guidelines"];
21 |
22 | /**
23 | * Gets info from a video without getting additional formats.
24 | *
25 | * @param {string} id
26 | * @param {Object} options
27 | * @returns {Promise}
28 | */
29 | exports.getBasicInfo = async (id, options) => {
30 | utils.applyIPv6Rotations(options);
31 | utils.applyDefaultHeaders(options);
32 | utils.applyDefaultAgent(options);
33 | utils.applyOldLocalAddress(options);
34 | const retryOptions = Object.assign({}, options.requestOptions);
35 | const { jar, dispatcher } = options.agent;
36 | utils.setPropInsensitive(
37 | options.requestOptions.headers,
38 | "cookie",
39 | jar.getCookieStringSync("https://www.youtube.com"),
40 | );
41 | options.requestOptions.dispatcher = dispatcher;
42 | const info = await retryFunc(getWatchHTMLPage, [id, options], retryOptions);
43 |
44 | const playErr = utils.playError(info.player_response);
45 | if (playErr) throw playErr;
46 |
47 | Object.assign(info, {
48 | // Replace with formats from iosPlayerResponse
49 | // formats: parseFormats(info.player_response),
50 | related_videos: extras.getRelatedVideos(info),
51 | });
52 |
53 | // Add additional properties to info.
54 | const media = extras.getMedia(info);
55 | const additional = {
56 | author: extras.getAuthor(info),
57 | media,
58 | likes: extras.getLikes(info),
59 | age_restricted: !!(
60 | media && AGE_RESTRICTED_URLS.some(url => Object.values(media).some(v => typeof v === "string" && v.includes(url)))
61 | ),
62 |
63 | // Give the standard link to the video.
64 | video_url: BASE_URL + id,
65 | storyboards: extras.getStoryboards(info),
66 | chapters: extras.getChapters(info),
67 | };
68 |
69 | info.videoDetails = extras.cleanVideoDetails(
70 | Object.assign(
71 | {},
72 | info.player_response?.microformat?.playerMicroformatRenderer,
73 | info.player_response?.videoDetails,
74 | additional,
75 | ),
76 | info,
77 | );
78 |
79 | return info;
80 | };
81 |
82 | const getWatchHTMLURL = (id, options) =>
83 | `${BASE_URL + id}&hl=${options.lang || "en"}&bpctr=${Math.ceil(Date.now() / 1000)}&has_verified=1`;
84 | const getWatchHTMLPageBody = (id, options) => {
85 | const url = getWatchHTMLURL(id, options);
86 | return exports.watchPageCache.getOrSet(url, () => utils.request(url, options));
87 | };
88 |
89 | const EMBED_URL = "https://www.youtube.com/embed/";
90 | const getEmbedPageBody = (id, options) => {
91 | const embedUrl = `${EMBED_URL + id}?hl=${options.lang || "en"}`;
92 | return utils.request(embedUrl, options);
93 | };
94 |
95 | const getHTML5player = body => {
96 | const html5playerRes =
97 | /") ||
170 | findJSON("watch.html", "player_response", body, /\bytInitialPlayerResponse\s*=\s*\{/i, "", "{");
171 | } catch (_e) {
172 | let args = findJSON("watch.html", "player_response", body, /\bytplayer\.config\s*=\s*{/, "", "{");
173 | info.player_response = findPlayerResponse("watch.html", args);
174 | }
175 |
176 | info.response =
177 | utils.tryParseBetween(body, "var ytInitialData = ", "}};", "", "}}") ||
178 | utils.tryParseBetween(body, "var ytInitialData = ", ";") ||
179 | utils.tryParseBetween(body, 'window["ytInitialData"] = ', "}};", "", "}}") ||
180 | utils.tryParseBetween(body, 'window["ytInitialData"] = ', ";") ||
181 | findJSON("watch.html", "response", body, /\bytInitialData("\])?\s*=\s*\{/i, "", "{");
182 | info.html5player = getHTML5player(body);
183 | } catch (_) {
184 | throw Error(
185 | "Error when parsing watch.html, maybe YouTube made a change.\n" +
186 | `Please report this issue with the "${utils.saveDebugFile(
187 | "watch.html",
188 | body,
189 | )}" file on https://github.com/distubejs/ytdl-core/issues.`,
190 | );
191 | }
192 | return info;
193 | };
194 |
195 | /**
196 | * @param {Object} player_response
197 | * @returns {Array.}
198 | */
199 | const parseFormats = player_response => {
200 | return (player_response?.streamingData?.formats || [])?.concat(player_response?.streamingData?.adaptiveFormats || []);
201 | };
202 |
203 | const parseAdditionalManifests = (player_response, options) => {
204 | const streamingData = player_response?.streamingData,
205 | manifests = [];
206 | if (streamingData) {
207 | if (streamingData.dashManifestUrl) {
208 | manifests.push(getDashManifest(streamingData.dashManifestUrl, options));
209 | }
210 | if (streamingData.hlsManifestUrl) {
211 | manifests.push(getM3U8(streamingData.hlsManifestUrl, options));
212 | }
213 | }
214 | return manifests;
215 | };
216 |
217 | // TODO: Clean up this function for readability and support more clients
218 | /**
219 | * Gets info from a video additional formats and deciphered URLs.
220 | *
221 | * @param {string} id
222 | * @param {Object} options
223 | * @returns {Promise}
224 | */
225 | exports.getInfo = async (id, options) => {
226 | // Initialize request options
227 | utils.applyIPv6Rotations(options);
228 | utils.applyDefaultHeaders(options);
229 | utils.applyDefaultAgent(options);
230 | utils.applyOldLocalAddress(options);
231 | utils.applyPlayerClients(options);
232 |
233 | const info = await exports.getBasicInfo(id, options);
234 |
235 | info.html5player =
236 | info.html5player ||
237 | getHTML5player(await getWatchHTMLPageBody(id, options)) ||
238 | getHTML5player(await getEmbedPageBody(id, options));
239 |
240 | if (!info.html5player) {
241 | throw Error("Unable to find html5player file");
242 | }
243 |
244 | info.html5player = new URL(info.html5player, BASE_URL).toString();
245 |
246 | const formatPromises = [];
247 |
248 | try {
249 | const clientPromises = [];
250 |
251 | if (options.playerClients.includes("WEB_EMBEDDED")) clientPromises.push(fetchWebEmbeddedPlayer(id, info, options));
252 | if (options.playerClients.includes("TV")) clientPromises.push(fetchTvPlayer(id, info, options));
253 | if (options.playerClients.includes("IOS")) clientPromises.push(fetchIosJsonPlayer(id, options));
254 | if (options.playerClients.includes("ANDROID")) clientPromises.push(fetchAndroidJsonPlayer(id, options));
255 |
256 | if (clientPromises.length > 0) {
257 | const responses = await Promise.allSettled(clientPromises);
258 | const successfulResponses = responses
259 | .filter(r => r.status === "fulfilled")
260 | .map(r => r.value)
261 | .filter(r => r);
262 |
263 | for (const response of successfulResponses) {
264 | const formats = parseFormats(response);
265 | if (formats && formats.length > 0) {
266 | formatPromises.push(sig.decipherFormats(formats, info.html5player, options));
267 | }
268 |
269 | const manifestPromises = parseAdditionalManifests(response, options);
270 | formatPromises.push(...manifestPromises);
271 | }
272 | }
273 |
274 | if (options.playerClients.includes("WEB")) {
275 | bestPlayerResponse = info.player_response;
276 |
277 | const formats = parseFormats(info.player_response);
278 | if (formats && formats.length > 0) {
279 | formatPromises.push(sig.decipherFormats(formats, info.html5player, options));
280 | }
281 |
282 | const manifestPromises = parseAdditionalManifests(info.player_response, options);
283 | formatPromises.push(...manifestPromises);
284 | }
285 | } catch (error) {
286 | console.error("Error fetching formats:", error);
287 |
288 | const formats = parseFormats(info.player_response);
289 | if (formats && formats.length > 0) {
290 | formatPromises.push(sig.decipherFormats(formats, info.html5player, options));
291 | }
292 |
293 | const manifestPromises = parseAdditionalManifests(info.player_response, options);
294 | formatPromises.push(...manifestPromises);
295 | }
296 |
297 | if (formatPromises.length === 0) {
298 | throw new Error("Failed to find any playable formats");
299 | }
300 |
301 | const results = await Promise.all(formatPromises);
302 | info.formats = Object.values(Object.assign({}, ...results));
303 |
304 | info.formats = info.formats.filter(format => format && format.url && format.mimeType);
305 |
306 | if (info.formats.length === 0) {
307 | throw new Error("No playable formats found");
308 | }
309 |
310 | info.formats = info.formats.map(format => {
311 | const enhancedFormat = formatUtils.addFormatMeta(format);
312 |
313 | if (!enhancedFormat.audioBitrate && enhancedFormat.hasAudio) {
314 | enhancedFormat.audioBitrate = estimateAudioBitrate(enhancedFormat);
315 | }
316 |
317 | if (
318 | !enhancedFormat.isHLS &&
319 | enhancedFormat.mimeType &&
320 | (enhancedFormat.mimeType.includes("hls") ||
321 | enhancedFormat.mimeType.includes("x-mpegURL") ||
322 | enhancedFormat.mimeType.includes("application/vnd.apple.mpegurl"))
323 | ) {
324 | enhancedFormat.isHLS = true;
325 | }
326 |
327 | return enhancedFormat;
328 | });
329 |
330 | info.formats.sort(formatUtils.sortFormats);
331 |
332 | const bestFormat =
333 | info.formats.find(format => format.hasVideo && format.hasAudio) ||
334 | info.formats.find(format => format.hasVideo) ||
335 | info.formats.find(format => format.hasAudio) ||
336 | info.formats[0];
337 |
338 | info.bestFormat = bestFormat;
339 | info.videoUrl = bestFormat.url;
340 | info.selectedFormat = bestFormat;
341 | info.full = true;
342 |
343 | return info;
344 | };
345 |
346 | const getPlaybackContext = async (html5player, options) => {
347 | const body = await utils.request(html5player, options);
348 | const mo = body.match(/(signatureTimestamp|sts):(\d+)/);
349 | return {
350 | contentPlaybackContext: {
351 | html5Preference: "HTML5_PREF_WANTS",
352 | signatureTimestamp: mo?.[2],
353 | },
354 | };
355 | };
356 |
357 | const getVisitorData = (info, _options) => {
358 | for (const respKey of ["player_response", "response"]) {
359 | try {
360 | return info[respKey].responseContext.serviceTrackingParams
361 | .find(x => x.service === "GFEEDBACK").params
362 | .find(x => x.key === "visitor_data").value;
363 | }
364 | catch { /* not present */ }
365 | }
366 | return undefined;
367 | };
368 |
369 | const LOCALE = { hl: "en", timeZone: "UTC", utcOffsetMinutes: 0 },
370 | CHECK_FLAGS = { contentCheckOk: true, racyCheckOk: true };
371 |
372 | const WEB_EMBEDDED_CONTEXT = {
373 | client: {
374 | clientName: "WEB_EMBEDDED_PLAYER",
375 | clientVersion: "1.20240723.01.00",
376 | ...LOCALE,
377 | },
378 | };
379 |
380 | const TVHTML5_CONTEXT = {
381 | client: {
382 | clientName: "TVHTML5",
383 | clientVersion: "7.20240724.13.00",
384 | ...LOCALE,
385 | },
386 | };
387 |
388 | const fetchWebEmbeddedPlayer = async (videoId, info, options) => {
389 | const payload = {
390 | context: WEB_EMBEDDED_CONTEXT,
391 | videoId,
392 | playbackContext: await getPlaybackContext(info.html5player, options),
393 | ...CHECK_FLAGS,
394 | };
395 | return await playerAPI(videoId, payload, options);
396 | };
397 | const fetchTvPlayer = async (videoId, info, options) => {
398 | const payload = {
399 | context: TVHTML5_CONTEXT,
400 | videoId,
401 | playbackContext: await getPlaybackContext(info.html5player, options),
402 | ...CHECK_FLAGS,
403 | };
404 |
405 | options.visitorId = getVisitorData(info, options);
406 |
407 | return await playerAPI(videoId, payload, options);
408 | };
409 |
410 | const playerAPI = async (videoId, payload, options) => {
411 | const { jar, dispatcher } = options.agent;
412 | const opts = {
413 | requestOptions: {
414 | method: "POST",
415 | dispatcher,
416 | query: {
417 | prettyPrint: false,
418 | t: utils.generateClientPlaybackNonce(12),
419 | id: videoId,
420 | },
421 | headers: {
422 | "Content-Type": "application/json",
423 | Cookie: jar.getCookieStringSync("https://www.youtube.com"),
424 | "X-Goog-Api-Format-Version": "2",
425 | },
426 | body: JSON.stringify(payload),
427 | },
428 | };
429 | if (options.visitorId) opts.requestOptions.headers["X-Goog-Visitor-Id"] = options.visitorId;
430 | const response = await utils.request("https://youtubei.googleapis.com/youtubei/v1/player", opts);
431 | const playErr = utils.playError(response);
432 | if (playErr) throw playErr;
433 | if (!response.videoDetails || videoId !== response.videoDetails.videoId) {
434 | const err = new Error("Malformed response from YouTube");
435 | err.response = response;
436 | throw err;
437 | }
438 | return response;
439 | };
440 |
441 | const IOS_CLIENT_VERSION = "19.45.4",
442 | IOS_DEVICE_MODEL = "iPhone16,2",
443 | IOS_USER_AGENT_VERSION = "17_5_1",
444 | IOS_OS_VERSION = "17.5.1.21F90";
445 |
446 | const fetchIosJsonPlayer = async (videoId, options) => {
447 | const payload = {
448 | videoId,
449 | cpn: utils.generateClientPlaybackNonce(16),
450 | contentCheckOk: true,
451 | racyCheckOk: true,
452 | context: {
453 | client: {
454 | clientName: "IOS",
455 | clientVersion: IOS_CLIENT_VERSION,
456 | deviceMake: "Apple",
457 | deviceModel: IOS_DEVICE_MODEL,
458 | platform: "MOBILE",
459 | osName: "iOS",
460 | osVersion: IOS_OS_VERSION,
461 | hl: "en",
462 | gl: "US",
463 | utcOffsetMinutes: -240,
464 | },
465 | request: {
466 | internalExperimentFlags: [],
467 | useSsl: true,
468 | },
469 | user: {
470 | lockedSafetyMode: false,
471 | },
472 | },
473 | };
474 |
475 | const { jar, dispatcher } = options.agent;
476 | const opts = {
477 | requestOptions: {
478 | method: "POST",
479 | dispatcher,
480 | query: {
481 | prettyPrint: false,
482 | t: utils.generateClientPlaybackNonce(12),
483 | id: videoId,
484 | },
485 | headers: {
486 | "Content-Type": "application/json",
487 | cookie: jar.getCookieStringSync("https://www.youtube.com"),
488 | "User-Agent": `com.google.ios.youtube/${IOS_CLIENT_VERSION}(${
489 | IOS_DEVICE_MODEL
490 | }; U; CPU iOS ${IOS_USER_AGENT_VERSION} like Mac OS X; en_US)`,
491 | "X-Goog-Api-Format-Version": "2",
492 | },
493 | body: JSON.stringify(payload),
494 | },
495 | };
496 | const response = await utils.request("https://youtubei.googleapis.com/youtubei/v1/player", opts);
497 | const playErr = utils.playError(response);
498 | if (playErr) throw playErr;
499 | if (!response.videoDetails || videoId !== response.videoDetails.videoId) {
500 | const err = new Error("Malformed response from YouTube");
501 | err.response = response;
502 | throw err;
503 | }
504 | return response;
505 | };
506 |
507 | const ANDROID_CLIENT_VERSION = "19.44.38",
508 | ANDROID_OS_VERSION = "11",
509 | ANDROID_SDK_VERSION = "30";
510 |
511 | const fetchAndroidJsonPlayer = async (videoId, options) => {
512 | const payload = {
513 | videoId,
514 | cpn: utils.generateClientPlaybackNonce(16),
515 | contentCheckOk: true,
516 | racyCheckOk: true,
517 | context: {
518 | client: {
519 | clientName: "ANDROID",
520 | clientVersion: ANDROID_CLIENT_VERSION,
521 | platform: "MOBILE",
522 | osName: "Android",
523 | osVersion: ANDROID_OS_VERSION,
524 | androidSdkVersion: ANDROID_SDK_VERSION,
525 | hl: "en",
526 | gl: "US",
527 | utcOffsetMinutes: -240,
528 | },
529 | request: {
530 | internalExperimentFlags: [],
531 | useSsl: true,
532 | },
533 | user: {
534 | lockedSafetyMode: false,
535 | },
536 | },
537 | };
538 |
539 | const { jar, dispatcher } = options.agent;
540 | const opts = {
541 | requestOptions: {
542 | method: "POST",
543 | dispatcher,
544 | query: {
545 | prettyPrint: false,
546 | t: utils.generateClientPlaybackNonce(12),
547 | id: videoId,
548 | },
549 | headers: {
550 | "Content-Type": "application/json",
551 | cookie: jar.getCookieStringSync("https://www.youtube.com"),
552 | "User-Agent": `com.google.android.youtube/${
553 | ANDROID_CLIENT_VERSION
554 | } (Linux; U; Android ${ANDROID_OS_VERSION}) gzip`,
555 | "X-Goog-Api-Format-Version": "2",
556 | },
557 | body: JSON.stringify(payload),
558 | },
559 | };
560 | const response = await utils.request("https://youtubei.googleapis.com/youtubei/v1/player", opts);
561 | const playErr = utils.playError(response);
562 | if (playErr) throw playErr;
563 | if (!response.videoDetails || videoId !== response.videoDetails.videoId) {
564 | const err = new Error("Malformed response from YouTube");
565 | err.response = response;
566 | throw err;
567 | }
568 | return response;
569 | };
570 |
571 | /**
572 | * Gets additional DASH formats.
573 | *
574 | * @param {string} url
575 | * @param {Object} options
576 | * @returns {Promise>}
577 | */
578 | const getDashManifest = (url, options) =>
579 | new Promise((resolve, reject) => {
580 | const formats = {};
581 | const parser = sax.parser(false);
582 | parser.onerror = reject;
583 | let adaptationSet;
584 | parser.onopentag = node => {
585 | if (node.name === "ADAPTATIONSET") {
586 | adaptationSet = node.attributes;
587 | } else if (node.name === "REPRESENTATION") {
588 | const itag = parseInt(node.attributes.ID);
589 | if (!isNaN(itag)) {
590 | formats[url] = Object.assign(
591 | {
592 | itag,
593 | url,
594 | bitrate: parseInt(node.attributes.BANDWIDTH),
595 | mimeType: `${adaptationSet.MIMETYPE}; codecs="${node.attributes.CODECS}"`,
596 | },
597 | node.attributes.HEIGHT
598 | ? {
599 | width: parseInt(node.attributes.WIDTH),
600 | height: parseInt(node.attributes.HEIGHT),
601 | fps: parseInt(node.attributes.FRAMERATE),
602 | }
603 | : {
604 | audioSampleRate: node.attributes.AUDIOSAMPLINGRATE,
605 | },
606 | );
607 | }
608 | }
609 | };
610 | parser.onend = () => {
611 | resolve(formats);
612 | };
613 | utils
614 | .request(new URL(url, BASE_URL).toString(), options)
615 | .then(res => {
616 | parser.write(res);
617 | parser.close();
618 | })
619 | .catch(reject);
620 | });
621 |
622 | /**
623 | * Gets additional formats.
624 | *
625 | * @param {string} url
626 | * @param {Object} options
627 | * @returns {Promise>}
628 | */
629 | const getM3U8 = async (url, options) => {
630 | url = new URL(url, BASE_URL);
631 | const body = await utils.request(url.toString(), options);
632 | const formats = {};
633 | body
634 | .split("\n")
635 | .filter(line => /^https?:\/\//.test(line))
636 | .forEach(line => {
637 | const itag = parseInt(line.match(/\/itag\/(\d+)\//)[1]);
638 | formats[line] = { itag, url: line };
639 | });
640 | return formats;
641 | };
642 |
643 | // Cache get info functions.
644 | // In case a user wants to get a video's info before downloading.
645 | for (const funcName of ["getBasicInfo", "getInfo"]) {
646 | /**
647 | * @param {string} link
648 | * @param {Object} options
649 | * @returns {Promise}
650 | */
651 | const func = exports[funcName];
652 | exports[funcName] = async (link, options = {}) => {
653 | utils.checkForUpdates();
654 | const id = await urlUtils.getVideoID(link);
655 | const key = [funcName, id, options.lang].join("-");
656 | return exports.cache.getOrSet(key, () => func(id, options));
657 | };
658 | }
659 |
660 | // Export a few helpers.
661 | exports.validateID = urlUtils.validateID;
662 | exports.validateURL = urlUtils.validateURL;
663 | exports.getURLVideoID = urlUtils.getURLVideoID;
664 | exports.getVideoID = urlUtils.getVideoID;
665 |
--------------------------------------------------------------------------------
/lib/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 = ":function\\(\\w,\\w\\)\\{" +
25 | "var \\w=\\w\\[0\\];\\w\\[0\\]=\\w\\[\\w%\\w\\.length\\];\\w\\[\\w(?:%\\w.length|)\\]=\\w(?:;return \\w)?\\}";
26 |
27 | const DECIPHER_REGEXP =
28 | "function(?: " + VARIABLE_PART + ")?\\(([a-zA-Z])\\)\\{" +
29 | "\\1=\\1\\.split\\(\"\"\\);\\s*" +
30 | "((?:(?:\\1=)?" + VARIABLE_PART + VARIABLE_PART_ACCESS + "\\(\\1,\\d+\\);)+)" +
31 | "return \\1\\.join\\(\"\"\\)" +
32 | "\\}";
33 |
34 | const HELPER_REGEXP =
35 | "var (" + VARIABLE_PART + ")=\\{((?:(?:" +
36 | VARIABLE_PART_DEFINE + REVERSE_PART + "|" +
37 | VARIABLE_PART_DEFINE + SLICE_PART + "|" +
38 | VARIABLE_PART_DEFINE + SPLICE_PART + "|" +
39 | VARIABLE_PART_DEFINE + SWAP_PART +
40 | "),?\\n?)+)\\};";
41 |
42 | const FUNCTION_TCE_REGEXP =
43 | "function(?:\\s+[a-zA-Z_\\$][a-zA-Z0-9_\\$]*)?\\(\\w\\)\\{" +
44 | "\\w=\\w\\.split\\((?:\"\"|[a-zA-Z0-9_$]*\\[\\d+])\\);" +
45 | "\\s*((?:(?:\\w=)?[a-zA-Z_\\$][a-zA-Z0-9_\\$]*(?:\\[\\\"|\\.)[a-zA-Z_\\$][a-zA-Z0-9_\\$]*(?:\\\"\\]|)\\(\\w,\\d+\\);)+)" +
46 | "return \\w\\.join\\((?:\"\"|[a-zA-Z0-9_$]*\\[\\d+])\\)}";
47 |
48 | const N_TRANSFORM_REGEXP =
49 | "function\\(\\s*(\\w+)\\s*\\)\\s*\\{" +
50 | "var\\s*(\\w+)=(?:\\1\\.split\\(.*?\\)|String\\.prototype\\.split\\.call\\(\\1,.*?\\))," +
51 | "\\s*(\\w+)=(\\[.*?]);\\s*\\3\\[\\d+]" +
52 | "(.*?try)(\\{.*?})catch\\(\\s*(\\w+)\\s*\\)\\s*\\{" +
53 | '\\s*return"[\\w-]+([A-z0-9-]+)"\\s*\\+\\s*\\1\\s*}' +
54 | '\\s*return\\s*(\\2\\.join\\(""\\)|Array\\.prototype\\.join\\.call\\(\\2,.*?\\))};';
55 |
56 | const N_TRANSFORM_TCE_REGEXP =
57 | "function\\(\\s*(\\w+)\\s*\\)\\s*\\{" +
58 | "\\s*var\\s*(\\w+)=\\1\\.split\\(\\1\\.slice\\(0,0\\)\\),\\s*(\\w+)=\\[.*?];" +
59 | ".*?catch\\(\\s*(\\w+)\\s*\\)\\s*\\{" +
60 | "\\s*return(?:\"[^\"]+\"|\\s*[a-zA-Z_0-9$]*\\[\\d+])\\s*\\+\\s*\\1\\s*}" +
61 | "\\s*return\\s*\\2\\.join\\((?:\"\"|[a-zA-Z_0-9$]*\\[\\d+])\\)};";
62 |
63 | const TCE_GLOBAL_VARS_REGEXP =
64 | "(?:^|[;,])\\s*(var\\s+([\\w$]+)\\s*=\\s*" +
65 | "(?:" +
66 | "([\"'])(?:\\\\.|[^\\\\])*?\\3" +
67 | "\\s*\\.\\s*split\\((" +
68 | "([\"'])(?:\\\\.|[^\\\\])*?\\5" +
69 | "\\))" +
70 | "|" +
71 | "\\[\\s*(?:([\"'])(?:\\\\.|[^\\\\])*?\\6\\s*,?\\s*)+\\]" +
72 | "))(?=\\s*[,;])";
73 |
74 | const NEW_TCE_GLOBAL_VARS_REGEXP =
75 | "('use\\s*strict';)?" +
76 | "(?var\\s*" +
77 | "(?[a-zA-Z0-9_$]+)\\s*=\\s*" +
78 | "(?" +
79 | "(?:\"[^\"\\\\]*(?:\\\\.[^\"\\\\]*)*\"|'[^'\\\\]*(?:\\\\.[^'\\\\]*)*')" +
80 | "\\.split\\(" +
81 | "(?:\"[^\"\\\\]*(?:\\\\.[^\"\\\\]*)*\"|'[^'\\\\]*(?:\\\\.[^'\\\\]*)*')" +
82 | "\\)" +
83 | "|" +
84 | "\\[" +
85 | "(?:(?:\"[^\"\\\\]*(?:\\\\.[^\"\\\\]*)*\"|'[^'\\\\]*(?:\\\\.[^'\\\\]*)*')" +
86 | "\\s*,?\\s*)*" +
87 | "\\]" +
88 | "|" +
89 | "\"[^\"]*\"\\.split\\(\"[^\"]*\"\\)" +
90 | ")" +
91 | ")";
92 |
93 | const TCE_SIGN_FUNCTION_REGEXP = "function\\(\\s*([a-zA-Z0-9$])\\s*\\)\\s*\\{" +
94 | "\\s*\\1\\s*=\\s*\\1\\[(\\w+)\\[\\d+\\]\\]\\(\\2\\[\\d+\\]\\);" +
95 | "([a-zA-Z0-9$]+)\\[\\2\\[\\d+\\]\\]\\(\\s*\\1\\s*,\\s*\\d+\\s*\\);" +
96 | "\\s*\\3\\[\\2\\[\\d+\\]\\]\\(\\s*\\1\\s*,\\s*\\d+\\s*\\);" +
97 | ".*?return\\s*\\1\\[\\2\\[\\d+\\]\\]\\(\\2\\[\\d+\\]\\)\\};";
98 |
99 | const TCE_SIGN_FUNCTION_ACTION_REGEXP = "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*\\};";
100 |
101 | const TCE_N_FUNCTION_REGEXP = "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*\\;";
102 |
103 | const PATTERN_PREFIX = "(?:^|,)\\\"?(" + VARIABLE_PART + ")\\\"?";
104 | const REVERSE_PATTERN = new RegExp(PATTERN_PREFIX + REVERSE_PART, "m");
105 | const SLICE_PATTERN = new RegExp(PATTERN_PREFIX + SLICE_PART, "m");
106 | const SPLICE_PATTERN = new RegExp(PATTERN_PREFIX + SPLICE_PART, "m");
107 | const SWAP_PATTERN = new RegExp(PATTERN_PREFIX + SWAP_PART, "m");
108 |
109 | const DECIPHER_ARGUMENT = "sig";
110 | const N_ARGUMENT = "ncode";
111 | const DECIPHER_FUNC_NAME = "DisTubeDecipherFunc";
112 | const N_TRANSFORM_FUNC_NAME = "DisTubeNTransformFunc";
113 |
114 | const extractDollarEscapedFirstGroup = (pattern, text) => {
115 | const match = text.match(pattern);
116 | return match ? match[1].replace(/\$/g, "\\$") : null;
117 | };
118 |
119 | const extractTceFunc = (body) => {
120 | try {
121 | const tceVariableMatcher = body.match(new RegExp(NEW_TCE_GLOBAL_VARS_REGEXP, 'm'));
122 |
123 | if (!tceVariableMatcher) return;
124 |
125 | const tceVariableMatcherGroups = tceVariableMatcher.groups;
126 | if (!tceVariableMatcher.groups) return;
127 |
128 | const code = tceVariableMatcherGroups.code;
129 | const varname = tceVariableMatcherGroups.varname;
130 |
131 | return { name: varname, code: code };
132 | } catch (e) {
133 | console.error("Error in extractTceFunc:", e);
134 | return null;
135 | }
136 | }
137 |
138 | const extractDecipherFunc = (body, name, code) => {
139 | try {
140 | const callerFunc = DECIPHER_FUNC_NAME + "(" + DECIPHER_ARGUMENT + ");";
141 | let resultFunc;
142 |
143 | const sigFunctionMatcher = body.match(new RegExp(TCE_SIGN_FUNCTION_REGEXP, 's'));
144 | const sigFunctionActionsMatcher = body.match(new RegExp(TCE_SIGN_FUNCTION_ACTION_REGEXP, 's'));
145 |
146 | if (sigFunctionMatcher && sigFunctionActionsMatcher && code) {
147 | resultFunc = "var " + DECIPHER_FUNC_NAME + "=" + sigFunctionMatcher[0] + sigFunctionActionsMatcher[0] + code + ";\n";
148 | return resultFunc + callerFunc;
149 | }
150 |
151 | const helperMatch = body.match(new RegExp(HELPER_REGEXP, "s"));
152 | if (!helperMatch) return null;
153 |
154 | const helperObject = helperMatch[0];
155 | const actionBody = helperMatch[2];
156 | const helperName = helperMatch[1];
157 |
158 | const reverseKey = extractDollarEscapedFirstGroup(REVERSE_PATTERN, actionBody);
159 | const sliceKey = extractDollarEscapedFirstGroup(SLICE_PATTERN, actionBody);
160 | const spliceKey = extractDollarEscapedFirstGroup(SPLICE_PATTERN, actionBody);
161 | const swapKey = extractDollarEscapedFirstGroup(SWAP_PATTERN, actionBody);
162 |
163 | const quotedFunctions = [reverseKey, sliceKey, spliceKey, swapKey]
164 | .filter(Boolean)
165 | .map(key => key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
166 |
167 | if (quotedFunctions.length === 0) return null;
168 |
169 | let funcMatch = body.match(new RegExp(DECIPHER_REGEXP, "s"));
170 | let isTce = false;
171 | let decipherFunc;
172 |
173 | if (funcMatch) {
174 | decipherFunc = funcMatch[0];
175 | } else {
176 |
177 | const tceFuncMatch = body.match(new RegExp(FUNCTION_TCE_REGEXP, "s"));
178 | if (!tceFuncMatch) return null;
179 |
180 | decipherFunc = tceFuncMatch[0];
181 | isTce = true;
182 | }
183 |
184 | let tceVars = "";
185 | if (isTce) {
186 | const tceVarsMatch = body.match(new RegExp(TCE_GLOBAL_VARS_REGEXP, "m"));
187 | if (tceVarsMatch) {
188 | tceVars = tceVarsMatch[1] + ";\n";
189 | }
190 | }
191 |
192 | resultFunc = tceVars + helperObject + "\nvar " + DECIPHER_FUNC_NAME + "=" + decipherFunc + ";\n";
193 | return resultFunc + callerFunc;
194 | } catch (e) {
195 | console.error("Error in extractDecipherFunc:", e);
196 | return null;
197 | }
198 | };
199 |
200 | const extractNTransformFunc = (body, name, code) => {
201 | try {
202 | const callerFunc = N_TRANSFORM_FUNC_NAME + "(" + N_ARGUMENT + ");";
203 | let resultFunc;
204 | let nFunction;
205 |
206 | const nFunctionMatcher = body.match(new RegExp(TCE_N_FUNCTION_REGEXP, 's'));
207 |
208 | if (nFunctionMatcher && name && code) {
209 | nFunction = nFunctionMatcher[0];
210 |
211 | const tceEscapeName = name.replace("$", "\\$");
212 | const shortCircuitPattern = new RegExp(
213 | `;\\s*if\\s*\\(\\s*typeof\\s+[a-zA-Z0-9_$]+\\s*===?\\s*(?:\"undefined\"|'undefined'|${tceEscapeName}\\[\\d+\\])\\s*\\)\\s*return\\s+\\w+;`
214 | );
215 |
216 | const tceShortCircuitMatcher = nFunction.match(shortCircuitPattern);
217 |
218 | if (tceShortCircuitMatcher) {
219 | nFunction = nFunction.replaceAll(tceShortCircuitMatcher[0], ";");
220 | }
221 |
222 | resultFunc = "var " + N_TRANSFORM_FUNC_NAME + "=" + nFunction + code + ";\n";
223 | return resultFunc + callerFunc;
224 | }
225 |
226 | let nMatch = body.match(new RegExp(N_TRANSFORM_REGEXP, "s"));
227 | let isTce = false;
228 |
229 | if (nMatch) {
230 | nFunction = nMatch[0];
231 | } else {
232 |
233 | const nTceMatch = body.match(new RegExp(N_TRANSFORM_TCE_REGEXP, "s"));
234 | if (!nTceMatch) return null;
235 |
236 | nFunction = nTceMatch[0];
237 | isTce = true;
238 | }
239 |
240 | const paramMatch = nFunction.match(/function\s*\(\s*(\w+)\s*\)/);
241 | if (!paramMatch) return null;
242 |
243 | const paramName = paramMatch[1];
244 |
245 | const cleanedFunction = nFunction.replace(
246 | new RegExp(`if\\s*\\(typeof\\s*[^\\s()]+\\s*===?.*?\\)return ${paramName}\\s*;?`, "g"),
247 | ""
248 | );
249 |
250 | let tceVars = "";
251 | if (isTce) {
252 | const tceVarsMatch = body.match(new RegExp(TCE_GLOBAL_VARS_REGEXP, "m"));
253 | if (tceVarsMatch) {
254 | tceVars = tceVarsMatch[1] + ";\n";
255 | }
256 | }
257 |
258 | resultFunc = tceVars + "var " + N_TRANSFORM_FUNC_NAME + "=" + cleanedFunction + ";\n";
259 | return resultFunc + callerFunc;
260 | } catch (e) {
261 | console.error("Error in extractNTransformFunc:", e);
262 | return null;
263 | }
264 | };
265 |
266 | let decipherWarning = false;
267 | let nTransformWarning = false;
268 |
269 | const getExtractFunction = (extractFunctions, body, name, code, postProcess = null) => {
270 | for (const extractFunction of extractFunctions) {
271 | try {
272 | const func = extractFunction(body, name, code);
273 | if (!func) continue;
274 | return new vm.Script(postProcess ? postProcess(func) : func);
275 | } catch (err) {
276 | console.error("Failed to extract function:", err);
277 | continue;
278 | }
279 | }
280 | return null;
281 | };
282 |
283 | const extractDecipher = (body, name, code) => {
284 | const decipherFunc = getExtractFunction([extractDecipherFunc], body, name, code);
285 | if (!decipherFunc && !decipherWarning) {
286 | console.warn(
287 | "\x1b[33mWARNING:\x1B[0m Could not parse decipher function.\n" +
288 | "Stream URLs will be missing.\n" +
289 | `Please report this issue by uploading the "${utils.saveDebugFile(
290 | "player-script.js",
291 | body,
292 | )}" file on https://github.com/distubejs/ytdl-core/issues/144.`
293 | );
294 | decipherWarning = true;
295 | }
296 | return decipherFunc;
297 | };
298 |
299 | const extractNTransform = (body, name, code) => {
300 | const nTransformFunc = getExtractFunction([extractNTransformFunc], body, name, code);
301 |
302 | if (!nTransformFunc && !nTransformWarning) {
303 | console.warn(
304 | "\x1b[33mWARNING:\x1B[0m Could not parse n transform function.\n" +
305 | `Please report this issue by uploading the "${utils.saveDebugFile(
306 | "player-script.js",
307 | body,
308 | )}" file on https://github.com/distubejs/ytdl-core/issues/144.`
309 | );
310 | nTransformWarning = true;
311 | }
312 |
313 | return nTransformFunc;
314 | };
315 |
316 | exports.extractFunctions = body => {
317 | const { name, code } = extractTceFunc(body);
318 | return [extractDecipher(body, name, code), extractNTransform(body, name, code)];
319 | }
320 |
321 | exports.setDownloadURL = (format, decipherScript, nTransformScript) => {
322 | if (!format) return;
323 |
324 | const decipher = url => {
325 | const args = querystring.parse(url);
326 | if (!args.s || !decipherScript) return args.url;
327 |
328 | try {
329 | const components = new URL(decodeURIComponent(args.url));
330 | const context = {};
331 | context[DECIPHER_ARGUMENT] = decodeURIComponent(args.s);
332 | const decipheredSig = decipherScript.runInNewContext(context);
333 |
334 | components.searchParams.set(args.sp || "sig", decipheredSig);
335 | return components.toString();
336 | } catch (err) {
337 | console.error("Error applying decipher:", err);
338 | return args.url;
339 | }
340 | };
341 |
342 | const nTransform = url => {
343 | try {
344 | const components = new URL(decodeURIComponent(url));
345 | const n = components.searchParams.get("n");
346 |
347 | if (!n || !nTransformScript) return url;
348 |
349 | const context = {};
350 | context[N_ARGUMENT] = n;
351 | const transformedN = nTransformScript.runInNewContext(context);
352 |
353 | if (transformedN) {
354 |
355 | if (n === transformedN) {
356 | console.warn("Transformed n parameter is the same as input, n function possibly short-circuited");
357 | } else if (transformedN.startsWith("enhanced_except_") || transformedN.endsWith("_w8_" + n)) {
358 | console.warn("N function did not complete due to exception");
359 | }
360 |
361 | components.searchParams.set("n", transformedN);
362 | } else {
363 | console.warn("Transformed n parameter is null, n function possibly faulty");
364 | }
365 |
366 | return components.toString();
367 | } catch (err) {
368 | console.error("Error applying n transform:", err);
369 | return url;
370 | }
371 | };
372 |
373 | const cipher = !format.url;
374 | const url = format.url || format.signatureCipher || format.cipher;
375 |
376 | if (!url) return;
377 |
378 | try {
379 | format.url = nTransform(cipher ? decipher(url) : url);
380 |
381 | delete format.signatureCipher;
382 | delete format.cipher;
383 | } catch (err) {
384 | console.error("Error setting download URL:", err);
385 | }
386 | };
387 |
388 | exports.decipherFormats = async (formats, html5player, options) => {
389 | try {
390 | const decipheredFormats = {};
391 | const [decipherScript, nTransformScript] = await exports.getFunctions(html5player, options);
392 |
393 | formats.forEach(format => {
394 | exports.setDownloadURL(format, decipherScript, nTransformScript);
395 | if (format.url) {
396 | decipheredFormats[format.url] = format;
397 | }
398 | });
399 |
400 | return decipheredFormats;
401 | } catch (err) {
402 | console.error("Error deciphering formats:", err);
403 | return {};
404 | }
405 | };
--------------------------------------------------------------------------------
/lib/url-utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Get video ID.
3 | *
4 | * There are a few type of video URL formats.
5 | * - https://www.youtube.com/watch?v=VIDEO_ID
6 | * - https://m.youtube.com/watch?v=VIDEO_ID
7 | * - https://youtu.be/VIDEO_ID
8 | * - https://www.youtube.com/v/VIDEO_ID
9 | * - https://www.youtube.com/embed/VIDEO_ID
10 | * - https://music.youtube.com/watch?v=VIDEO_ID
11 | * - https://gaming.youtube.com/watch?v=VIDEO_ID
12 | *
13 | * @param {string} link
14 | * @return {string}
15 | * @throws {Error} If unable to find a id
16 | * @throws {TypeError} If videoid doesn't match specs
17 | */
18 | const validQueryDomains = new Set([
19 | "youtube.com",
20 | "www.youtube.com",
21 | "m.youtube.com",
22 | "music.youtube.com",
23 | "gaming.youtube.com",
24 | ]);
25 | const validPathDomains = /^https?:\/\/(youtu\.be\/|(www\.)?youtube\.com\/(embed|v|shorts|live)\/)/;
26 | exports.getURLVideoID = link => {
27 | const parsed = new URL(link.trim());
28 | let id = parsed.searchParams.get("v");
29 | if (validPathDomains.test(link.trim()) && !id) {
30 | const paths = parsed.pathname.split("/");
31 | id = parsed.host === "youtu.be" ? paths[1] : paths[2];
32 | } else if (parsed.hostname && !validQueryDomains.has(parsed.hostname)) {
33 | throw Error("Not a YouTube domain");
34 | }
35 | if (!id) {
36 | throw Error(`No video id found: "${link}"`);
37 | }
38 | id = id.substring(0, 11);
39 | if (!exports.validateID(id)) {
40 | throw TypeError(`Video id (${id}) does not match expected ` + `format (${idRegex.toString()})`);
41 | }
42 | return id;
43 | };
44 |
45 | /**
46 | * Gets video ID either from a url or by checking if the given string
47 | * matches the video ID format.
48 | *
49 | * @param {string} str
50 | * @returns {string}
51 | * @throws {Error} If unable to find a id
52 | * @throws {TypeError} If videoid doesn't match specs
53 | */
54 | const urlRegex = /^https?:\/\//;
55 | exports.getVideoID = str => {
56 | if (exports.validateID(str)) {
57 | return str;
58 | } else if (urlRegex.test(str.trim())) {
59 | return exports.getURLVideoID(str);
60 | } else {
61 | throw Error(`No video id found: ${str}`);
62 | }
63 | };
64 |
65 | /**
66 | * Returns true if given id satifies YouTube's id format.
67 | *
68 | * @param {string} id
69 | * @return {boolean}
70 | */
71 | const idRegex = /^[a-zA-Z0-9-_]{11}$/;
72 | exports.validateID = id => idRegex.test(id.trim());
73 |
74 | /**
75 | * Checks wether the input string includes a valid id.
76 | *
77 | * @param {string} string
78 | * @returns {boolean}
79 | */
80 | exports.validateURL = string => {
81 | try {
82 | exports.getURLVideoID(string);
83 | return true;
84 | } catch (e) {
85 | return false;
86 | }
87 | };
88 |
--------------------------------------------------------------------------------
/lib/utils.js:
--------------------------------------------------------------------------------
1 | const { request } = require("undici");
2 | const { writeFileSync } = require("fs");
3 | const AGENT = require("./agent");
4 |
5 | /**
6 | * Extract string inbetween another.
7 | *
8 | * @param {string} haystack
9 | * @param {string} left
10 | * @param {string} right
11 | * @returns {string}
12 | */
13 | const between = (exports.between = (haystack, left, right) => {
14 | let pos;
15 | if (left instanceof RegExp) {
16 | const match = haystack.match(left);
17 | if (!match) {
18 | return "";
19 | }
20 | pos = match.index + match[0].length;
21 | } else {
22 | pos = haystack.indexOf(left);
23 | if (pos === -1) {
24 | return "";
25 | }
26 | pos += left.length;
27 | }
28 | haystack = haystack.slice(pos);
29 | pos = haystack.indexOf(right);
30 | if (pos === -1) {
31 | return "";
32 | }
33 | haystack = haystack.slice(0, pos);
34 | return haystack;
35 | });
36 |
37 | exports.tryParseBetween = (body, left, right, prepend = "", append = "") => {
38 | try {
39 | let data = between(body, left, right);
40 | if (!data) return null;
41 | return JSON.parse(`${prepend}${data}${append}`);
42 | } catch (e) {
43 | return null;
44 | }
45 | };
46 |
47 | /**
48 | * Get a number from an abbreviated number string.
49 | *
50 | * @param {string} string
51 | * @returns {number}
52 | */
53 | exports.parseAbbreviatedNumber = string => {
54 | const match = string
55 | .replace(",", ".")
56 | .replace(" ", "")
57 | .match(/([\d,.]+)([MK]?)/);
58 | if (match) {
59 | let [, num, multi] = match;
60 | num = parseFloat(num);
61 | return Math.round(multi === "M" ? num * 1000000 : multi === "K" ? num * 1000 : num);
62 | }
63 | return null;
64 | };
65 |
66 | /**
67 | * Escape sequences for cutAfterJS
68 | * @param {string} start the character string the escape sequence
69 | * @param {string} end the character string to stop the escape seequence
70 | * @param {undefined|Regex} startPrefix a regex to check against the preceding 10 characters
71 | */
72 | const ESCAPING_SEQUENZES = [
73 | // Strings
74 | { start: '"', end: '"' },
75 | { start: "'", end: "'" },
76 | { start: "`", end: "`" },
77 | // RegeEx
78 | { start: "/", end: "/", startPrefix: /(^|[[{:;,/])\s?$/ },
79 | ];
80 |
81 | /**
82 | * Match begin and end braces of input JS, return only JS
83 | *
84 | * @param {string} mixedJson
85 | * @returns {string}
86 | */
87 | exports.cutAfterJS = mixedJson => {
88 | // Define the general open and closing tag
89 | let open, close;
90 | if (mixedJson[0] === "[") {
91 | open = "[";
92 | close = "]";
93 | } else if (mixedJson[0] === "{") {
94 | open = "{";
95 | close = "}";
96 | }
97 |
98 | if (!open) {
99 | throw new Error(`Can't cut unsupported JSON (need to begin with [ or { ) but got: ${mixedJson[0]}`);
100 | }
101 |
102 | // States if the loop is currently inside an escaped js object
103 | let isEscapedObject = null;
104 |
105 | // States if the current character is treated as escaped or not
106 | let isEscaped = false;
107 |
108 | // Current open brackets to be closed
109 | let counter = 0;
110 |
111 | let i;
112 | // Go through all characters from the start
113 | for (i = 0; i < mixedJson.length; i++) {
114 | // End of current escaped object
115 | if (!isEscaped && isEscapedObject !== null && mixedJson[i] === isEscapedObject.end) {
116 | isEscapedObject = null;
117 | continue;
118 | // Might be the start of a new escaped object
119 | } else if (!isEscaped && isEscapedObject === null) {
120 | for (const escaped of ESCAPING_SEQUENZES) {
121 | if (mixedJson[i] !== escaped.start) continue;
122 | // Test startPrefix against last 10 characters
123 | if (!escaped.startPrefix || mixedJson.substring(i - 10, i).match(escaped.startPrefix)) {
124 | isEscapedObject = escaped;
125 | break;
126 | }
127 | }
128 | // Continue if we found a new escaped object
129 | if (isEscapedObject !== null) {
130 | continue;
131 | }
132 | }
133 |
134 | // Toggle the isEscaped boolean for every backslash
135 | // Reset for every regular character
136 | isEscaped = mixedJson[i] === "\\" && !isEscaped;
137 |
138 | if (isEscapedObject !== null) continue;
139 |
140 | if (mixedJson[i] === open) {
141 | counter++;
142 | } else if (mixedJson[i] === close) {
143 | counter--;
144 | }
145 |
146 | // All brackets have been closed, thus end of JSON is reached
147 | if (counter === 0) {
148 | // Return the cut JSON
149 | return mixedJson.substring(0, i + 1);
150 | }
151 | }
152 |
153 | // We ran through the whole string and ended up with an unclosed bracket
154 | throw Error("Can't cut unsupported JSON (no matching closing bracket found)");
155 | };
156 |
157 | class UnrecoverableError extends Error {}
158 | /**
159 | * Checks if there is a playability error.
160 | *
161 | * @param {Object} player_response
162 | * @returns {!Error}
163 | */
164 | exports.playError = player_response => {
165 | const playability = player_response?.playabilityStatus;
166 | if (!playability) return null;
167 | if (["ERROR", "LOGIN_REQUIRED"].includes(playability.status)) {
168 | return new UnrecoverableError(playability.reason || playability.messages?.[0]);
169 | }
170 | if (playability.status === "LIVE_STREAM_OFFLINE") {
171 | return new UnrecoverableError(playability.reason || "The live stream is offline.");
172 | }
173 | if (playability.status === "UNPLAYABLE") {
174 | return new UnrecoverableError(playability.reason || "This video is unavailable.");
175 | }
176 | return null;
177 | };
178 |
179 | // Undici request
180 | const useFetch = async (fetch, url, requestOptions) => {
181 | // embed query to url
182 | const query = requestOptions.query;
183 | if (query) {
184 | const urlObject = new URL(url);
185 | for (const key in query) {
186 | urlObject.searchParams.append(key, query[key]);
187 | }
188 | url = urlObject.toString();
189 | }
190 |
191 | const response = await fetch(url, requestOptions);
192 |
193 | // convert webstandard response to undici request's response
194 | const statusCode = response.status;
195 | const body = Object.assign(response, response.body || {});
196 | const headers = Object.fromEntries(response.headers.entries());
197 |
198 | return { body, statusCode, headers };
199 | };
200 | exports.request = async (url, options = {}) => {
201 | let { requestOptions, rewriteRequest, fetch } = options;
202 |
203 | if (typeof rewriteRequest === "function") {
204 | const rewritten = rewriteRequest(url, requestOptions);
205 | requestOptions = rewritten.requestOptions || requestOptions;
206 | url = rewritten.url || url;
207 | }
208 |
209 | const req =
210 | typeof fetch === "function" ? await useFetch(fetch, url, requestOptions) : await request(url, requestOptions);
211 | const code = req.statusCode.toString();
212 |
213 | if (code.startsWith("2")) {
214 | if (req.headers["content-type"].includes("application/json")) return req.body.json();
215 | return req.body.text();
216 | }
217 | if (code.startsWith("3")) return exports.request(req.headers.location, options);
218 |
219 | const e = new Error(`Status code: ${code}`);
220 | e.statusCode = req.statusCode;
221 | throw e;
222 | };
223 |
224 | /**
225 | * Temporary helper to help deprecating a few properties.
226 | *
227 | * @param {Object} obj
228 | * @param {string} prop
229 | * @param {Object} value
230 | * @param {string} oldPath
231 | * @param {string} newPath
232 | */
233 | exports.deprecate = (obj, prop, value, oldPath, newPath) => {
234 | Object.defineProperty(obj, prop, {
235 | get: () => {
236 | console.warn(`\`${oldPath}\` will be removed in a near future release, ` + `use \`${newPath}\` instead.`);
237 | return value;
238 | },
239 | });
240 | };
241 |
242 | // Check for updates.
243 | const pkg = require("../package.json");
244 | const UPDATE_INTERVAL = 1000 * 60 * 60 * 12;
245 | let updateWarnTimes = 0;
246 | exports.lastUpdateCheck = 0;
247 | exports.checkForUpdates = () => {
248 | if (
249 | !process.env.YTDL_NO_UPDATE &&
250 | !pkg.version.startsWith("0.0.0-") &&
251 | Date.now() - exports.lastUpdateCheck >= UPDATE_INTERVAL
252 | ) {
253 | exports.lastUpdateCheck = Date.now();
254 | return exports
255 | .request("https://api.github.com/repos/distubejs/ytdl-core/contents/package.json", {
256 | requestOptions: {
257 | headers: {
258 | "User-Agent":
259 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.3",
260 | },
261 | },
262 | })
263 | .then(
264 | response => {
265 | const buf = Buffer.from(response.content, response.encoding);
266 | const pkgFile = JSON.parse(buf.toString("ascii"));
267 | if (pkgFile.version !== pkg.version && updateWarnTimes++ < 5) {
268 | // eslint-disable-next-line max-len
269 | console.warn(
270 | '\x1b[33mWARNING:\x1B[0m @distube/ytdl-core is out of date! Update with "npm install @distube/ytdl-core@latest".',
271 | );
272 | }
273 | },
274 | err => {
275 | console.warn("Error checking for updates:", err.message);
276 | console.warn("You can disable this check by setting the `YTDL_NO_UPDATE` env variable.");
277 | },
278 | );
279 | }
280 | return null;
281 | };
282 |
283 | /**
284 | * Gets random IPv6 Address from a block
285 | *
286 | * @param {string} ip the IPv6 block in CIDR-Notation
287 | * @returns {string}
288 | */
289 | const getRandomIPv6 = ip => {
290 | if (!isIPv6(ip)) {
291 | throw new Error("Invalid IPv6 format");
292 | }
293 |
294 | const [rawAddr, rawMask] = ip.split("/");
295 | const mask = parseInt(rawMask, 10);
296 |
297 | if (isNaN(mask) || mask > 128 || mask < 1) {
298 | throw new Error("Invalid IPv6 subnet mask (must be between 1 and 128)");
299 | }
300 |
301 | const base10addr = normalizeIP(rawAddr);
302 |
303 | const fullMaskGroups = Math.floor(mask / 16);
304 | const remainingBits = mask % 16;
305 |
306 | const result = new Array(8).fill(0);
307 |
308 | for (let i = 0; i < 8; i++) {
309 | if (i < fullMaskGroups) {
310 | result[i] = base10addr[i];
311 | } else if (i === fullMaskGroups && remainingBits > 0) {
312 | const groupMask = 0xffff << (16 - remainingBits);
313 | const randomPart = Math.floor(Math.random() * (1 << (16 - remainingBits)));
314 | result[i] = (base10addr[i] & groupMask) | randomPart;
315 | } else {
316 | result[i] = Math.floor(Math.random() * 0x10000);
317 | }
318 | }
319 |
320 | return result.map(x => x.toString(16).padStart(4, "0")).join(":");
321 | };
322 |
323 | const isIPv6 = ip => {
324 | const IPV6_REGEX =
325 | /^(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(?::[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(?:ffff(?::0{1,4}){0,1}:){0,1}(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])|(?:[0-9a-fA-F]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9]))(?:\/(?:1[0-1][0-9]|12[0-8]|[1-9][0-9]|[1-9]))?$/;
326 | return IPV6_REGEX.test(ip);
327 | };
328 |
329 | /**
330 | * Normalizes an IPv6 address into an array of 8 integers
331 | * @param {string} ip - IPv6 address
332 | * @returns {number[]} - Array of 8 integers representing the address
333 | */
334 | const normalizeIP = ip => {
335 | const parts = ip.split("::");
336 | let start = parts[0] ? parts[0].split(":") : [];
337 | let end = parts[1] ? parts[1].split(":") : [];
338 |
339 | const missing = 8 - (start.length + end.length);
340 | const zeros = new Array(missing).fill("0");
341 |
342 | const full = [...start, ...zeros, ...end];
343 |
344 | return full.map(part => parseInt(part || "0", 16));
345 | };
346 |
347 | exports.saveDebugFile = (name, body) => {
348 | if (process.env.YTDL_NO_DEBUG_FILE) {
349 | console.warn(`\x1b[33mWARNING:\x1b[0m Debug file saving is disabled. "${name}"`);
350 | return body;
351 | }
352 | const filename = `${+new Date()}-${name}`;
353 | const debugPath = process.env.YTDL_DEBUG_PATH || '.';
354 | writeFileSync(`${debugPath}/${filename}`, body);
355 | return filename;
356 | };
357 |
358 | const findPropKeyInsensitive = (obj, prop) =>
359 | Object.keys(obj).find(p => p.toLowerCase() === prop.toLowerCase()) || null;
360 |
361 | exports.getPropInsensitive = (obj, prop) => {
362 | const key = findPropKeyInsensitive(obj, prop);
363 | return key && obj[key];
364 | };
365 |
366 | exports.setPropInsensitive = (obj, prop, value) => {
367 | const key = findPropKeyInsensitive(obj, prop);
368 | obj[key || prop] = value;
369 | return key;
370 | };
371 |
372 | let oldCookieWarning = true;
373 | let oldDispatcherWarning = true;
374 | exports.applyDefaultAgent = options => {
375 | if (!options.agent) {
376 | const { jar } = AGENT.defaultAgent;
377 | const c = exports.getPropInsensitive(options.requestOptions.headers, "cookie");
378 | if (c) {
379 | jar.removeAllCookiesSync();
380 | AGENT.addCookiesFromString(jar, c);
381 | if (oldCookieWarning) {
382 | oldCookieWarning = false;
383 | console.warn(
384 | "\x1b[33mWARNING:\x1B[0m Using old cookie format, " +
385 | "please use the new one instead. (https://github.com/distubejs/ytdl-core#cookies-support)",
386 | );
387 | }
388 | }
389 | if (options.requestOptions.dispatcher && oldDispatcherWarning) {
390 | oldDispatcherWarning = false;
391 | console.warn(
392 | "\x1b[33mWARNING:\x1B[0m Your dispatcher is overridden by `ytdl.Agent`. " +
393 | "To implement your own, check out the documentation. " +
394 | "(https://github.com/distubejs/ytdl-core#how-to-implement-ytdlagent-with-your-own-dispatcher)",
395 | );
396 | }
397 | options.agent = AGENT.defaultAgent;
398 | }
399 | };
400 |
401 | let oldLocalAddressWarning = true;
402 | exports.applyOldLocalAddress = options => {
403 | if (!options?.requestOptions?.localAddress || options.requestOptions.localAddress === options.agent.localAddress)
404 | return;
405 | options.agent = AGENT.createAgent(undefined, { localAddress: options.requestOptions.localAddress });
406 | if (oldLocalAddressWarning) {
407 | oldLocalAddressWarning = false;
408 | console.warn(
409 | "\x1b[33mWARNING:\x1B[0m Using old localAddress option, " +
410 | "please add it to the agent options instead. (https://github.com/distubejs/ytdl-core#ip-rotation)",
411 | );
412 | }
413 | };
414 |
415 | let oldIpRotationsWarning = true;
416 | exports.applyIPv6Rotations = options => {
417 | if (options.IPv6Block) {
418 | options.requestOptions = Object.assign({}, options.requestOptions, {
419 | localAddress: getRandomIPv6(options.IPv6Block),
420 | });
421 | if (oldIpRotationsWarning) {
422 | oldIpRotationsWarning = false;
423 | oldLocalAddressWarning = false;
424 | console.warn(
425 | "\x1b[33mWARNING:\x1B[0m IPv6Block option is deprecated, " +
426 | "please create your own ip rotation instead. (https://github.com/distubejs/ytdl-core#ip-rotation)",
427 | );
428 | }
429 | }
430 | };
431 |
432 | exports.applyDefaultHeaders = options => {
433 | options.requestOptions = Object.assign({}, options.requestOptions);
434 | options.requestOptions.headers = Object.assign(
435 | {},
436 | {
437 | // eslint-disable-next-line max-len
438 | "User-Agent":
439 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.101 Safari/537.36",
440 | },
441 | options.requestOptions.headers,
442 | );
443 | };
444 |
445 | exports.generateClientPlaybackNonce = length => {
446 | const CPN_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
447 | return Array.from({ length }, () => CPN_CHARS[Math.floor(Math.random() * CPN_CHARS.length)]).join("");
448 | };
449 |
450 | exports.applyPlayerClients = options => {
451 | if (!options.playerClients || options.playerClients.length === 0) {
452 | options.playerClients = ["WEB_EMBEDDED", "IOS", "ANDROID", "TV"];
453 | }
454 | };
455 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@distube/ytdl-core",
3 | "description": "DisTube fork of ytdl-core. YouTube video downloader in pure javascript.",
4 | "keywords": [
5 | "youtube",
6 | "video",
7 | "download",
8 | "distube"
9 | ],
10 | "version": "4.16.10",
11 | "repository": {
12 | "type": "git",
13 | "url": "git://github.com/distubejs/ytdl-core.git"
14 | },
15 | "author": "Skick (https://github.com/skick1234)",
16 | "contributors": [
17 | "fent (https://github.com/fent)",
18 | "Tobias Kutscha (https://github.com/TimeForANinja)",
19 | "Andrew Kelley (https://github.com/andrewrk)",
20 | "Mauricio Allende (https://github.com/mallendeo)",
21 | "Rodrigo Altamirano (https://github.com/raltamirano)",
22 | "Jim Buck (https://github.com/JimmyBoh)",
23 | "Pawel Rucinski (https://github.com/Roki100)",
24 | "Alexander Paolini (https://github.com/Million900o)"
25 | ],
26 | "main": "./lib/index.js",
27 | "types": "./typings/index.d.ts",
28 | "files": [
29 | "lib",
30 | "typings"
31 | ],
32 | "scripts": {
33 | "prettier": "prettier --write \"**/*.{js,json,yml,md,ts}\""
34 | },
35 | "dependencies": {
36 | "http-cookie-agent": "^7.0.1",
37 | "https-proxy-agent": "^7.0.6",
38 | "m3u8stream": "^0.8.6",
39 | "miniget": "^4.2.3",
40 | "sax": "^1.4.1",
41 | "tough-cookie": "^5.1.2",
42 | "undici": "^7.8.0"
43 | },
44 | "devDependencies": {
45 | "@types/node": "^22.15.2",
46 | "prettier": "^3.5.3",
47 | "typescript": "^5.8.3"
48 | },
49 | "engines": {
50 | "node": ">=20.18.1"
51 | },
52 | "license": "MIT",
53 | "funding": "https://github.com/distubejs/ytdl-core?sponsor"
54 | }
55 |
--------------------------------------------------------------------------------
/pnpm-lock.yaml:
--------------------------------------------------------------------------------
1 | lockfileVersion: '9.0'
2 |
3 | settings:
4 | autoInstallPeers: true
5 | excludeLinksFromLockfile: false
6 |
7 | importers:
8 |
9 | .:
10 | dependencies:
11 | http-cookie-agent:
12 | specifier: ^7.0.1
13 | version: 7.0.1(tough-cookie@5.1.2)(undici@7.8.0)
14 | https-proxy-agent:
15 | specifier: ^7.0.6
16 | version: 7.0.6
17 | m3u8stream:
18 | specifier: ^0.8.6
19 | version: 0.8.6
20 | miniget:
21 | specifier: ^4.2.3
22 | version: 4.2.3
23 | sax:
24 | specifier: ^1.4.1
25 | version: 1.4.1
26 | tough-cookie:
27 | specifier: ^5.1.2
28 | version: 5.1.2
29 | undici:
30 | specifier: ^7.8.0
31 | version: 7.8.0
32 | devDependencies:
33 | '@types/node':
34 | specifier: ^22.15.2
35 | version: 22.15.2
36 | prettier:
37 | specifier: ^3.5.3
38 | version: 3.5.3
39 | typescript:
40 | specifier: ^5.8.3
41 | version: 5.8.3
42 |
43 | packages:
44 |
45 | '@types/node@22.15.2':
46 | resolution: {integrity: sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==}
47 |
48 | agent-base@7.1.3:
49 | resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==}
50 | engines: {node: '>= 14'}
51 |
52 | debug@4.4.0:
53 | resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
54 | engines: {node: '>=6.0'}
55 | peerDependencies:
56 | supports-color: '*'
57 | peerDependenciesMeta:
58 | supports-color:
59 | optional: true
60 |
61 | http-cookie-agent@7.0.1:
62 | resolution: {integrity: sha512-lZHFZUdPTw64PdksQac5xbUd4NWjUbyDYnvR//2sbLpcC4UqEUW0x/6O+rDntVzJzJ07QvhtL5XZSC+c5EK+IQ==}
63 | engines: {node: '>=20.0.0'}
64 | peerDependencies:
65 | tough-cookie: ^4.0.0 || ^5.0.0
66 | undici: ^7.0.0
67 | peerDependenciesMeta:
68 | undici:
69 | optional: true
70 |
71 | https-proxy-agent@7.0.6:
72 | resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
73 | engines: {node: '>= 14'}
74 |
75 | m3u8stream@0.8.6:
76 | resolution: {integrity: sha512-LZj8kIVf9KCphiHmH7sbFQTVe4tOemb202fWwvJwR9W5ENW/1hxJN6ksAWGhQgSBSa3jyWhnjKU1Fw1GaOdbyA==}
77 | engines: {node: '>=12'}
78 |
79 | miniget@4.2.3:
80 | resolution: {integrity: sha512-SjbDPDICJ1zT+ZvQwK0hUcRY4wxlhhNpHL9nJOB2MEAXRGagTljsO8MEDzQMTFf0Q8g4QNi8P9lEm/g7e+qgzA==}
81 | engines: {node: '>=12'}
82 |
83 | ms@2.1.3:
84 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
85 |
86 | prettier@3.5.3:
87 | resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==}
88 | engines: {node: '>=14'}
89 | hasBin: true
90 |
91 | sax@1.4.1:
92 | resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
93 |
94 | tldts-core@6.1.86:
95 | resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==}
96 |
97 | tldts@6.1.86:
98 | resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==}
99 | hasBin: true
100 |
101 | tough-cookie@5.1.2:
102 | resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
103 | engines: {node: '>=16'}
104 |
105 | typescript@5.8.3:
106 | resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
107 | engines: {node: '>=14.17'}
108 | hasBin: true
109 |
110 | undici-types@6.21.0:
111 | resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
112 |
113 | undici@7.8.0:
114 | resolution: {integrity: sha512-vFv1GA99b7eKO1HG/4RPu2Is3FBTWBrmzqzO0mz+rLxN3yXkE4mqRcb8g8fHxzX4blEysrNZLqg5RbJLqX5buA==}
115 | engines: {node: '>=20.18.1'}
116 |
117 | snapshots:
118 |
119 | '@types/node@22.15.2':
120 | dependencies:
121 | undici-types: 6.21.0
122 |
123 | agent-base@7.1.3: {}
124 |
125 | debug@4.4.0:
126 | dependencies:
127 | ms: 2.1.3
128 |
129 | http-cookie-agent@7.0.1(tough-cookie@5.1.2)(undici@7.8.0):
130 | dependencies:
131 | agent-base: 7.1.3
132 | tough-cookie: 5.1.2
133 | optionalDependencies:
134 | undici: 7.8.0
135 |
136 | https-proxy-agent@7.0.6:
137 | dependencies:
138 | agent-base: 7.1.3
139 | debug: 4.4.0
140 | transitivePeerDependencies:
141 | - supports-color
142 |
143 | m3u8stream@0.8.6:
144 | dependencies:
145 | miniget: 4.2.3
146 | sax: 1.4.1
147 |
148 | miniget@4.2.3: {}
149 |
150 | ms@2.1.3: {}
151 |
152 | prettier@3.5.3: {}
153 |
154 | sax@1.4.1: {}
155 |
156 | tldts-core@6.1.86: {}
157 |
158 | tldts@6.1.86:
159 | dependencies:
160 | tldts-core: 6.1.86
161 |
162 | tough-cookie@5.1.2:
163 | dependencies:
164 | tldts: 6.1.86
165 |
166 | typescript@5.8.3: {}
167 |
168 | undici-types@6.21.0: {}
169 |
170 | undici@7.8.0: {}
171 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es5",
5 | "allowJs": true
6 | },
7 | "include": ["typings/**/*"],
8 | "exclude": ["node_modules"]
9 | }
10 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["dtslint/dtslint.json"],
3 | "rules": {
4 | "prefer-readonly": false,
5 | "await-promise": false,
6 | "no-for-in-array": false,
7 | "no-null-undefined-union": false,
8 | "no-promise-as-boolean": false,
9 | "no-void-expression": false,
10 | "strict-string-expressions": false,
11 | "strict-comparisons": false,
12 | "use-default-type-parameter": false,
13 | "no-boolean-literal-compare": false,
14 | "no-unnecessary-qualifier": false,
15 | "no-unnecessary-type-assertion": false,
16 | "expect": false,
17 | "no-import-default-of-export-equals": false,
18 | "no-relative-import-in-test": false,
19 | "no-unnecessary-generics": false,
20 | "strict-export-declare-modifiers": false,
21 | "no-single-declare-module": false,
22 | "member-access": true,
23 | "no-unnecessary-class": false,
24 | "array-type": [true, "array"],
25 |
26 | "no-any-union": false,
27 | "void-return": false
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/typings/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module "tough-cookie" {
2 | export const version: string;
3 |
4 | export const PrefixSecurityEnum: Readonly<{
5 | DISABLED: string;
6 | SILENT: string;
7 | STRICT: string;
8 | }>;
9 |
10 | /**
11 | * Parse a cookie date string into a Date.
12 | * Parses according to RFC6265 Section 5.1.1, not Date.parse().
13 | */
14 | export function parseDate(string: string): Date;
15 |
16 | /**
17 | * Format a Date into a RFC1123 string (the RFC6265-recommended format).
18 | */
19 | export function formatDate(date: Date): string;
20 |
21 | /**
22 | * Transforms a domain-name into a canonical domain-name.
23 | * The canonical domain-name is a trimmed, lowercased, stripped-of-leading-dot
24 | * and optionally punycode-encoded domain-name (Section 5.1.2 of RFC6265).
25 | * For the most part, this function is idempotent (can be run again on its output without ill effects).
26 | */
27 | export function canonicalDomain(str: string): string;
28 |
29 | /**
30 | * Answers "does this real domain match the domain in a cookie?".
31 | * The str is the "current" domain-name and the domStr is the "cookie" domain-name.
32 | * Matches according to RFC6265 Section 5.1.3, but it helps to think of it as a "suffix match".
33 | *
34 | * The canonicalize parameter will run the other two parameters through canonicalDomain or not.
35 | */
36 | export function domainMatch(str: string, domStr: string, canonicalize?: boolean): boolean;
37 |
38 | /**
39 | * Given a current request/response path, gives the Path apropriate for storing in a cookie.
40 | * This is basically the "directory" of a "file" in the path, but is specified by Section 5.1.4 of the RFC.
41 | *
42 | * The path parameter MUST be only the pathname part of a URI (i.e. excludes the hostname, query, fragment, etc.).
43 | * This is the .pathname property of node's uri.parse() output.
44 | */
45 | export function defaultPath(path: string): string;
46 |
47 | /**
48 | * Answers "does the request-path path-match a given cookie-path?" as per RFC6265 Section 5.1.4.
49 | * Returns a boolean.
50 | *
51 | * This is essentially a prefix-match where cookiePath is a prefix of reqPath.
52 | */
53 | export function pathMatch(reqPath: string, cookiePath: string): boolean;
54 |
55 | /**
56 | * alias for Cookie.parse(cookieString[, options])
57 | */
58 | export function parse(cookieString: string, options?: Cookie.ParseOptions): Cookie | undefined;
59 |
60 | /**
61 | * alias for Cookie.fromJSON(string)
62 | */
63 | export function fromJSON(string: string): Cookie;
64 |
65 | export function getPublicSuffix(hostname: string): string | null;
66 |
67 | export function cookieCompare(a: Cookie, b: Cookie): number;
68 |
69 | export function permuteDomain(domain: string, allowSpecialUseDomain?: boolean): string[];
70 |
71 | export function permutePath(path: string): string[];
72 |
73 | export class Cookie {
74 | static parse(cookieString: string, options?: Cookie.ParseOptions): Cookie | undefined;
75 |
76 | static fromJSON(strOrObj: string | object): Cookie | null;
77 |
78 | constructor(properties?: Cookie.Properties);
79 |
80 | key: string;
81 | value: string;
82 | expires: Date | "Infinity";
83 | maxAge: number | "Infinity" | "-Infinity";
84 | domain: string | null;
85 | path: string | null;
86 | secure: boolean;
87 | httpOnly: boolean;
88 | extensions: string[] | null;
89 | creation: Date | null;
90 | creationIndex: number;
91 |
92 | hostOnly: boolean | null;
93 | pathIsDefault: boolean | null;
94 | lastAccessed: Date | null;
95 | sameSite: string;
96 |
97 | toString(): string;
98 |
99 | cookieString(): string;
100 |
101 | setExpires(exp: Date | string): void;
102 |
103 | setMaxAge(number: number): void;
104 |
105 | expiryTime(now?: number): number;
106 |
107 | expiryDate(now?: number): Date;
108 |
109 | TTL(now?: Date): number | typeof Infinity;
110 |
111 | isPersistent(): boolean;
112 |
113 | canonicalizedDomain(): string | null;
114 |
115 | cdomain(): string | null;
116 |
117 | inspect(): string;
118 |
119 | toJSON(): { [key: string]: any };
120 |
121 | clone(): Cookie;
122 |
123 | validate(): boolean | string;
124 | }
125 |
126 | export namespace Cookie {
127 | interface ParseOptions {
128 | loose?: boolean | undefined;
129 | }
130 |
131 | interface Properties {
132 | key?: string | undefined;
133 | value?: string | undefined;
134 | expires?: Date | "Infinity" | undefined;
135 | maxAge?: number | "Infinity" | "-Infinity" | undefined;
136 | domain?: string | undefined;
137 | path?: string | undefined;
138 | secure?: boolean | undefined;
139 | httpOnly?: boolean | undefined;
140 | extensions?: string[] | undefined;
141 | creation?: Date | undefined;
142 | creationIndex?: number | undefined;
143 |
144 | hostOnly?: boolean | undefined;
145 | pathIsDefault?: boolean | undefined;
146 | lastAccessed?: Date | undefined;
147 | sameSite?: string | undefined;
148 | }
149 |
150 | interface Serialized {
151 | [key: string]: any;
152 | }
153 | }
154 |
155 | export class CookieJar {
156 | static deserialize(serialized: CookieJar.Serialized | string, store?: Store): Promise;
157 | static deserialize(
158 | serialized: CookieJar.Serialized | string,
159 | store: Store,
160 | cb: (err: Error | null, object: CookieJar) => void,
161 | ): void;
162 | static deserialize(
163 | serialized: CookieJar.Serialized | string,
164 | cb: (err: Error | null, object: CookieJar) => void,
165 | ): void;
166 |
167 | static deserializeSync(serialized: CookieJar.Serialized | string, store?: Store): CookieJar;
168 |
169 | static fromJSON(string: string): CookieJar;
170 |
171 | constructor(store?: Store, options?: CookieJar.Options);
172 |
173 | setCookie(
174 | cookieOrString: Cookie | string,
175 | currentUrl: string,
176 | options?: CookieJar.SetCookieOptions,
177 | ): Promise;
178 | setCookie(
179 | cookieOrString: Cookie | string,
180 | currentUrl: string,
181 | options: CookieJar.SetCookieOptions,
182 | cb: (err: Error | null, cookie: Cookie) => void,
183 | ): void;
184 | setCookie(
185 | cookieOrString: Cookie | string,
186 | currentUrl: string,
187 | cb: (err: Error | null, cookie: Cookie) => void,
188 | ): void;
189 |
190 | setCookieSync(cookieOrString: Cookie | string, currentUrl: string, options?: CookieJar.SetCookieOptions): Cookie;
191 |
192 | getCookies(currentUrl: string, options?: CookieJar.GetCookiesOptions): Promise;
193 | getCookies(
194 | currentUrl: string,
195 | options: CookieJar.GetCookiesOptions,
196 | cb: (err: Error | null, cookies: Cookie[]) => void,
197 | ): void;
198 | getCookies(currentUrl: string, cb: (err: Error | null, cookies: Cookie[]) => void): void;
199 |
200 | getCookiesSync(currentUrl: string, options?: CookieJar.GetCookiesOptions): Cookie[];
201 |
202 | getCookieString(currentUrl: string, options?: CookieJar.GetCookiesOptions): Promise;
203 | getCookieString(
204 | currentUrl: string,
205 | options: CookieJar.GetCookiesOptions,
206 | cb: (err: Error | null, cookies: string) => void,
207 | ): void;
208 | getCookieString(currentUrl: string, cb: (err: Error | null, cookies: string) => void): void;
209 |
210 | getCookieStringSync(currentUrl: string, options?: CookieJar.GetCookiesOptions): string;
211 |
212 | getSetCookieStrings(currentUrl: string, options?: CookieJar.GetCookiesOptions): Promise;
213 | getSetCookieStrings(
214 | currentUrl: string,
215 | options: CookieJar.GetCookiesOptions,
216 | cb: (err: Error | null, cookies: string[]) => void,
217 | ): void;
218 | getSetCookieStrings(currentUrl: string, cb: (err: Error | null, cookies: string[]) => void): void;
219 |
220 | getSetCookieStringsSync(currentUrl: string, options?: CookieJar.GetCookiesOptions): string[];
221 |
222 | serialize(): Promise;
223 | serialize(cb: (err: Error | null, serializedObject: CookieJar.Serialized) => void): void;
224 |
225 | serializeSync(): CookieJar.Serialized;
226 |
227 | toJSON(): CookieJar.Serialized;
228 |
229 | clone(store?: Store): Promise;
230 | clone(store: Store, cb: (err: Error | null, newJar: CookieJar) => void): void;
231 | clone(cb: (err: Error | null, newJar: CookieJar) => void): void;
232 |
233 | cloneSync(store?: Store): CookieJar;
234 |
235 | removeAllCookies(): Promise;
236 | removeAllCookies(cb: (err: Error | null) => void): void;
237 |
238 | removeAllCookiesSync(): void;
239 | }
240 |
241 | export namespace CookieJar {
242 | interface Options {
243 | allowSpecialUseDomain?: boolean | undefined;
244 | looseMode?: boolean | undefined;
245 | rejectPublicSuffixes?: boolean | undefined;
246 | prefixSecurity?: string | undefined;
247 | }
248 |
249 | interface SetCookieOptions {
250 | http?: boolean | undefined;
251 | secure?: boolean | undefined;
252 | now?: Date | undefined;
253 | ignoreError?: boolean | undefined;
254 | }
255 |
256 | interface GetCookiesOptions {
257 | http?: boolean | undefined;
258 | secure?: boolean | undefined;
259 | now?: Date | undefined;
260 | expire?: boolean | undefined;
261 | allPaths?: boolean | undefined;
262 | }
263 |
264 | interface Serialized {
265 | version: string;
266 | storeType: string;
267 | rejectPublicSuffixes: boolean;
268 | cookies: Cookie.Serialized[];
269 | }
270 | }
271 |
272 | export abstract class Store {
273 | synchronous: boolean;
274 |
275 | findCookie(domain: string, path: string, key: string, cb: (err: Error | null, cookie: Cookie | null) => void): void;
276 |
277 | findCookies(
278 | domain: string,
279 | path: string,
280 | allowSpecialUseDomain: boolean,
281 | cb: (err: Error | null, cookie: Cookie[]) => void,
282 | ): void;
283 |
284 | putCookie(cookie: Cookie, cb: (err: Error | null) => void): void;
285 |
286 | updateCookie(oldCookie: Cookie, newCookie: Cookie, cb: (err: Error | null) => void): void;
287 |
288 | removeCookie(domain: string, path: string, key: string, cb: (err: Error | null) => void): void;
289 |
290 | removeCookies(domain: string, path: string, cb: (err: Error | null) => void): void;
291 |
292 | getAllCookies(cb: (err: Error | null, cookie: Cookie[]) => void): void;
293 | }
294 |
295 | export class MemoryCookieStore extends Store {
296 | findCookie(domain: string, path: string, key: string, cb: (err: Error | null, cookie: Cookie | null) => void): void;
297 | findCookie(domain: string, path: string, key: string): Promise;
298 |
299 | findCookies(
300 | domain: string,
301 | path: string,
302 | allowSpecialUseDomain: boolean,
303 | cb: (err: Error | null, cookie: Cookie[]) => void,
304 | ): void;
305 | findCookies(domain: string, path: string, cb: (err: Error | null, cookie: Cookie[]) => void): void;
306 | findCookies(domain: string, path: string, allowSpecialUseDomain?: boolean): Promise;
307 |
308 | putCookie(cookie: Cookie, cb: (err: Error | null) => void): void;
309 | putCookie(cookie: Cookie): Promise;
310 |
311 | updateCookie(oldCookie: Cookie, newCookie: Cookie, cb: (err: Error | null) => void): void;
312 | updateCookie(oldCookie: Cookie, newCookie: Cookie): Promise;
313 |
314 | removeCookie(domain: string, path: string, key: string, cb: (err: Error | null) => void): void;
315 | removeCookie(domain: string, path: string, key: string): Promise;
316 |
317 | removeCookies(domain: string, path: string, cb: (err: Error | null) => void): void;
318 | removeCookies(domain: string, path: string): Promise;
319 |
320 | getAllCookies(cb: (err: Error | null, cookie: Cookie[]) => void): void;
321 | getAllCookies(): Promise;
322 | }
323 | }
324 |
325 | declare module "@distube/ytdl-core" {
326 | import { Dispatcher, ProxyAgent, request } from "undici";
327 | import { Cookie as CK, CookieJar } from "tough-cookie";
328 | import { CookieAgent } from "http-cookie-agent/undici";
329 | import { Readable } from "stream";
330 |
331 | namespace ytdl {
332 | type Filter =
333 | | "audioandvideo"
334 | | "videoandaudio"
335 | | "video"
336 | | "videoonly"
337 | | "audio"
338 | | "audioonly"
339 | | ((format: videoFormat) => boolean);
340 |
341 | interface Agent {
342 | dispatcher: Dispatcher;
343 | jar: CookieJar;
344 | localAddress?: string;
345 | }
346 |
347 | interface getInfoOptions {
348 | lang?: string;
349 | requestCallback?: () => {};
350 | rewriteRequest?: (
351 | url: string,
352 | requestOptions: Parameters[1],
353 | ) => { url: string; requestOptions: Parameters[1] };
354 | fetch?: (url: string, requestOptions: Parameters[1]) => Promise;
355 | requestOptions?: Parameters[1];
356 | agent?: Agent;
357 | playerClients?: Array<"WEB_EMBEDDED" | "TV" | "IOS" | "ANDROID" | "WEB">;
358 | }
359 |
360 | interface chooseFormatOptions {
361 | quality?:
362 | | "lowest"
363 | | "highest"
364 | | "highestaudio"
365 | | "lowestaudio"
366 | | "highestvideo"
367 | | "lowestvideo"
368 | | (string & {})
369 | | number
370 | | string[]
371 | | number[];
372 | filter?: Filter;
373 | format?: videoFormat;
374 | }
375 |
376 | interface downloadOptions extends getInfoOptions, chooseFormatOptions {
377 | range?: {
378 | start?: number;
379 | end?: number;
380 | };
381 | begin?: string | number | Date;
382 | liveBuffer?: number;
383 | highWaterMark?: number;
384 | IPv6Block?: string;
385 | dlChunkSize?: number;
386 | }
387 |
388 | interface videoFormat {
389 | itag: number;
390 | url: string;
391 | mimeType?: string;
392 | bitrate?: number;
393 | audioBitrate?: number;
394 | width?: number;
395 | height?: number;
396 | initRange?: { start: string; end: string };
397 | indexRange?: { start: string; end: string };
398 | lastModified: string;
399 | contentLength: string;
400 | quality: "tiny" | "small" | "medium" | "large" | "hd720" | "hd1080" | "hd1440" | "hd2160" | "highres" | string;
401 | qualityLabel:
402 | | "144p"
403 | | "144p 15fps"
404 | | "144p60 HDR"
405 | | "240p"
406 | | "240p60 HDR"
407 | | "270p"
408 | | "360p"
409 | | "360p60 HDR"
410 | | "480p"
411 | | "480p60 HDR"
412 | | "720p"
413 | | "720p60"
414 | | "720p60 HDR"
415 | | "1080p"
416 | | "1080p60"
417 | | "1080p60 HDR"
418 | | "1440p"
419 | | "1440p60"
420 | | "1440p60 HDR"
421 | | "2160p"
422 | | "2160p60"
423 | | "2160p60 HDR"
424 | | "4320p"
425 | | "4320p60";
426 | projectionType?: "RECTANGULAR";
427 | fps?: number;
428 | averageBitrate?: number;
429 | audioQuality?: "AUDIO_QUALITY_LOW" | "AUDIO_QUALITY_MEDIUM";
430 | colorInfo?: {
431 | primaries: string;
432 | transferCharacteristics: string;
433 | matrixCoefficients: string;
434 | };
435 | highReplication?: boolean;
436 | approxDurationMs?: string;
437 | targetDurationSec?: number;
438 | maxDvrDurationSec?: number;
439 | audioSampleRate?: string;
440 | audioChannels?: number;
441 |
442 | // Added by ytdl-core
443 | container: "flv" | "3gp" | "mp4" | "webm" | "ts";
444 | hasVideo: boolean;
445 | hasAudio: boolean;
446 | codecs: string;
447 | videoCodec?: string;
448 | audioCodec?: string;
449 |
450 | isLive: boolean;
451 | isHLS: boolean;
452 | isDashMPD: boolean;
453 | }
454 |
455 | interface thumbnail {
456 | url: string;
457 | width: number;
458 | height: number;
459 | }
460 |
461 | interface captionTrack {
462 | baseUrl: string;
463 | name: {
464 | simpleText:
465 | | "Afrikaans"
466 | | "Albanian"
467 | | "Amharic"
468 | | "Arabic"
469 | | "Armenian"
470 | | "Azerbaijani"
471 | | "Bangla"
472 | | "Basque"
473 | | "Belarusian"
474 | | "Bosnian"
475 | | "Bulgarian"
476 | | "Burmese"
477 | | "Catalan"
478 | | "Cebuano"
479 | | "Chinese (Simplified)"
480 | | "Chinese (Traditional)"
481 | | "Corsican"
482 | | "Croatian"
483 | | "Czech"
484 | | "Danish"
485 | | "Dutch"
486 | | "English"
487 | | "English (auto-generated)"
488 | | "Esperanto"
489 | | "Estonian"
490 | | "Filipino"
491 | | "Finnish"
492 | | "French"
493 | | "Galician"
494 | | "Georgian"
495 | | "German"
496 | | "Greek"
497 | | "Gujarati"
498 | | "Haitian Creole"
499 | | "Hausa"
500 | | "Hawaiian"
501 | | "Hebrew"
502 | | "Hindi"
503 | | "Hmong"
504 | | "Hungarian"
505 | | "Icelandic"
506 | | "Igbo"
507 | | "Indonesian"
508 | | "Irish"
509 | | "Italian"
510 | | "Japanese"
511 | | "Javanese"
512 | | "Kannada"
513 | | "Kazakh"
514 | | "Khmer"
515 | | "Korean"
516 | | "Kurdish"
517 | | "Kyrgyz"
518 | | "Lao"
519 | | "Latin"
520 | | "Latvian"
521 | | "Lithuanian"
522 | | "Luxembourgish"
523 | | "Macedonian"
524 | | "Malagasy"
525 | | "Malay"
526 | | "Malayalam"
527 | | "Maltese"
528 | | "Maori"
529 | | "Marathi"
530 | | "Mongolian"
531 | | "Nepali"
532 | | "Norwegian"
533 | | "Nyanja"
534 | | "Pashto"
535 | | "Persian"
536 | | "Polish"
537 | | "Portuguese"
538 | | "Punjabi"
539 | | "Romanian"
540 | | "Russian"
541 | | "Samoan"
542 | | "Scottish Gaelic"
543 | | "Serbian"
544 | | "Shona"
545 | | "Sindhi"
546 | | "Sinhala"
547 | | "Slovak"
548 | | "Slovenian"
549 | | "Somali"
550 | | "Southern Sotho"
551 | | "Spanish"
552 | | "Spanish (Spain)"
553 | | "Sundanese"
554 | | "Swahili"
555 | | "Swedish"
556 | | "Tajik"
557 | | "Tamil"
558 | | "Telugu"
559 | | "Thai"
560 | | "Turkish"
561 | | "Ukrainian"
562 | | "Urdu"
563 | | "Uzbek"
564 | | "Vietnamese"
565 | | "Welsh"
566 | | "Western Frisian"
567 | | "Xhosa"
568 | | "Yiddish"
569 | | "Yoruba"
570 | | "Zulu"
571 | | string;
572 | };
573 | vssId: string;
574 | languageCode:
575 | | "af"
576 | | "sq"
577 | | "am"
578 | | "ar"
579 | | "hy"
580 | | "az"
581 | | "bn"
582 | | "eu"
583 | | "be"
584 | | "bs"
585 | | "bg"
586 | | "my"
587 | | "ca"
588 | | "ceb"
589 | | "zh-Hans"
590 | | "zh-Hant"
591 | | "co"
592 | | "hr"
593 | | "cs"
594 | | "da"
595 | | "nl"
596 | | "en"
597 | | "eo"
598 | | "et"
599 | | "fil"
600 | | "fi"
601 | | "fr"
602 | | "gl"
603 | | "ka"
604 | | "de"
605 | | "el"
606 | | "gu"
607 | | "ht"
608 | | "ha"
609 | | "haw"
610 | | "iw"
611 | | "hi"
612 | | "hmn"
613 | | "hu"
614 | | "is"
615 | | "ig"
616 | | "id"
617 | | "ga"
618 | | "it"
619 | | "ja"
620 | | "jv"
621 | | "kn"
622 | | "kk"
623 | | "km"
624 | | "ko"
625 | | "ku"
626 | | "ky"
627 | | "lo"
628 | | "la"
629 | | "lv"
630 | | "lt"
631 | | "lb"
632 | | "mk"
633 | | "mg"
634 | | "ms"
635 | | "ml"
636 | | "mt"
637 | | "mi"
638 | | "mr"
639 | | "mn"
640 | | "ne"
641 | | "no"
642 | | "ny"
643 | | "ps"
644 | | "fa"
645 | | "pl"
646 | | "pt"
647 | | "pa"
648 | | "ro"
649 | | "ru"
650 | | "sm"
651 | | "gd"
652 | | "sr"
653 | | "sn"
654 | | "sd"
655 | | "si"
656 | | "sk"
657 | | "sl"
658 | | "so"
659 | | "st"
660 | | "es"
661 | | "su"
662 | | "sw"
663 | | "sv"
664 | | "tg"
665 | | "ta"
666 | | "te"
667 | | "th"
668 | | "tr"
669 | | "uk"
670 | | "ur"
671 | | "uz"
672 | | "vi"
673 | | "cy"
674 | | "fy"
675 | | "xh"
676 | | "yi"
677 | | "yo"
678 | | "zu"
679 | | string;
680 | kind: string;
681 | rtl?: boolean;
682 | isTranslatable: boolean;
683 | }
684 |
685 | interface audioTrack {
686 | captionTrackIndices: number[];
687 | }
688 |
689 | interface translationLanguage {
690 | languageCode: captionTrack["languageCode"];
691 | languageName: captionTrack["name"];
692 | }
693 |
694 | interface VideoDetails {
695 | videoId: string;
696 | title: string;
697 | shortDescription: string;
698 | lengthSeconds: string;
699 | keywords?: string[];
700 | channelId: string;
701 | isOwnerViewing: boolean;
702 | isCrawlable: boolean;
703 | thumbnails: thumbnail[];
704 | averageRating: number;
705 | allowRatings: boolean;
706 | viewCount: string;
707 | author: string;
708 | isPrivate: boolean;
709 | isUnpluggedCorpus: boolean;
710 | isLiveContent: boolean;
711 | isLive: boolean;
712 | }
713 |
714 | interface Media {
715 | category: string;
716 | category_url: string;
717 | game?: string;
718 | game_url?: string;
719 | year?: number;
720 | song?: string;
721 | artist?: string;
722 | artist_url?: string;
723 | writers?: string;
724 | licensed_by?: string;
725 | thumbnails: thumbnail[];
726 | }
727 |
728 | interface Author {
729 | id: string;
730 | name: string;
731 | avatar: string; // to remove later
732 | thumbnails?: thumbnail[];
733 | verified: boolean;
734 | user?: string;
735 | channel_url: string;
736 | external_channel_url?: string;
737 | user_url?: string;
738 | subscriber_count?: number;
739 | }
740 |
741 | interface MicroformatRenderer {
742 | thumbnail: {
743 | thumbnails: thumbnail[];
744 | };
745 | embed: {
746 | iframeUrl: string;
747 | flashUrl: string;
748 | width: number;
749 | height: number;
750 | flashSecureUrl: string;
751 | };
752 | title: {
753 | simpleText: string;
754 | };
755 | description: {
756 | simpleText: string;
757 | };
758 | lengthSeconds: string;
759 | ownerProfileUrl: string;
760 | ownerGplusProfileUrl?: string;
761 | externalChannelId: string;
762 | isFamilySafe: boolean;
763 | availableCountries: string[];
764 | isUnlisted: boolean;
765 | hasYpcMetadata: boolean;
766 | viewCount: string;
767 | category: string;
768 | publishDate: string;
769 | ownerChannelName: string;
770 | liveBroadcastDetails?: {
771 | isLiveNow: boolean;
772 | startTimestamp: string;
773 | endTimestamp?: string;
774 | };
775 | uploadDate: string;
776 | }
777 |
778 | interface storyboard {
779 | templateUrl: string;
780 | thumbnailWidth: number;
781 | thumbnailHeight: number;
782 | thumbnailCount: number;
783 | interval: number;
784 | columns: number;
785 | rows: number;
786 | storyboardCount: number;
787 | }
788 |
789 | interface Chapter {
790 | title: string;
791 | start_time: number;
792 | }
793 |
794 | interface MoreVideoDetails
795 | extends Omit,
796 | Omit {
797 | published: number;
798 | video_url: string;
799 | age_restricted: boolean;
800 | likes: number | null;
801 | media: Media;
802 | author: Author;
803 | thumbnails: thumbnail[];
804 | storyboards: storyboard[];
805 | chapters: Chapter[];
806 | description: string | null;
807 | }
808 |
809 | interface videoInfo {
810 | iv_load_policy?: string;
811 | iv_allow_in_place_switch?: string;
812 | iv_endscreen_url?: string;
813 | iv_invideo_url?: string;
814 | iv3_module?: string;
815 | rmktEnabled?: string;
816 | uid?: string;
817 | vid?: string;
818 | focEnabled?: string;
819 | baseUrl?: string;
820 | storyboard_spec?: string;
821 | serialized_ad_ux_config?: string;
822 | player_error_log_fraction?: string;
823 | sffb?: string;
824 | ldpj?: string;
825 | videostats_playback_base_url?: string;
826 | innertube_context_client_version?: string;
827 | t?: string;
828 | fade_in_start_milliseconds: string;
829 | timestamp: string;
830 | ad3_module: string;
831 | relative_loudness: string;
832 | allow_below_the_player_companion: string;
833 | eventid: string;
834 | token: string;
835 | atc: string;
836 | cr: string;
837 | apply_fade_on_midrolls: string;
838 | cl: string;
839 | fexp: string[];
840 | apiary_host: string;
841 | fade_in_duration_milliseconds: string;
842 | fflags: string;
843 | ssl: string;
844 | pltype: string;
845 | enabled_engage_types: string;
846 | hl: string;
847 | is_listed: string;
848 | gut_tag: string;
849 | apiary_host_firstparty: string;
850 | enablecsi: string;
851 | csn: string;
852 | status: string;
853 | afv_ad_tag: string;
854 | idpj: string;
855 | sfw_player_response: string;
856 | account_playback_token: string;
857 | encoded_ad_safety_reason: string;
858 | tag_for_children_directed: string;
859 | no_get_video_log: string;
860 | ppv_remarketing_url: string;
861 | fmt_list: string[][];
862 | ad_slots: string;
863 | fade_out_duration_milliseconds: string;
864 | instream_long: string;
865 | allow_html5_ads: string;
866 | core_dbp: string;
867 | ad_device: string;
868 | itct: string;
869 | root_ve_type: string;
870 | excluded_ads: string;
871 | aftv: string;
872 | loeid: string;
873 | cver: string;
874 | shortform: string;
875 | dclk: string;
876 | csi_page_type: string;
877 | ismb: string;
878 | gpt_migration: string;
879 | loudness: string;
880 | ad_tag: string;
881 | of: string;
882 | probe_url: string;
883 | vm: string;
884 | afv_ad_tag_restricted_to_instream: string;
885 | gapi_hint_params: string;
886 | cid: string;
887 | c: string;
888 | oid: string;
889 | ptchn: string;
890 | as_launched_in_country: string;
891 | avg_rating: string;
892 | fade_out_start_milliseconds: string;
893 | midroll_prefetch_size: string;
894 | allow_ratings: string;
895 | thumbnail_url: string;
896 | iurlsd: string;
897 | iurlmq: string;
898 | iurlhq: string;
899 | iurlmaxres: string;
900 | ad_preroll: string;
901 | tmi: string;
902 | trueview: string;
903 | host_language: string;
904 | innertube_api_key: string;
905 | show_content_thumbnail: string;
906 | afv_instream_max: string;
907 | innertube_api_version: string;
908 | mpvid: string;
909 | allow_embed: string;
910 | ucid: string;
911 | plid: string;
912 | midroll_freqcap: string;
913 | ad_logging_flag: string;
914 | ptk: string;
915 | vmap: string;
916 | watermark: string[];
917 | dbp: string;
918 | ad_flags: string;
919 | html5player: string;
920 | formats: videoFormat[];
921 | related_videos: relatedVideo[];
922 | no_embed_allowed?: boolean;
923 | player_response: {
924 | playabilityStatus: {
925 | status: string;
926 | playableInEmbed: boolean;
927 | miniplayer: {
928 | miniplayerRenderer: {
929 | playbackMode: string;
930 | };
931 | };
932 | contextParams: string;
933 | };
934 | streamingData: {
935 | expiresInSeconds: string;
936 | formats: {}[];
937 | adaptiveFormats: {}[];
938 | };
939 | captions?: {
940 | playerCaptionsRenderer: {
941 | baseUrl: string;
942 | visibility: string;
943 | };
944 | playerCaptionsTracklistRenderer: {
945 | captionTracks: captionTrack[];
946 | audioTracks: audioTrack[];
947 | translationLanguages: translationLanguage[];
948 | defaultAudioTrackIndex: number;
949 | };
950 | };
951 | microformat: {
952 | playerMicroformatRenderer: MicroformatRenderer;
953 | };
954 | videoDetails: VideoDetails;
955 | playerConfig: {
956 | audioConfig: {
957 | loudnessDb: number;
958 | perceptualLoudnessDb: number;
959 | enablePerFormatLoudness: boolean;
960 | };
961 | streamSelectionConfig: { maxBitrate: string };
962 | mediaCommonConfig: { dynamicReadaheadConfig: {}[] };
963 | webPlayerConfig: { webPlayerActionsPorting: {}[] };
964 | };
965 | };
966 | videoDetails: MoreVideoDetails;
967 | }
968 |
969 | interface relatedVideo {
970 | id?: string;
971 | title?: string;
972 | published?: string;
973 | author: Author | "string"; // to remove the `string` part later
974 | ucid?: string; // to remove later
975 | author_thumbnail?: string; // to remove later
976 | short_view_count_text?: string;
977 | view_count?: string;
978 | length_seconds?: number;
979 | video_thumbnail?: string; // to remove later
980 | thumbnails: thumbnail[];
981 | richThumbnails: thumbnail[];
982 | isLive: boolean;
983 | }
984 |
985 | interface Cookie {
986 | name: string;
987 | value: string;
988 | expirationDate?: number;
989 | domain?: string;
990 | path?: string;
991 | secure?: boolean;
992 | httpOnly?: boolean;
993 | hostOnly?: boolean;
994 | sameSite?: string;
995 | }
996 |
997 | function getBasicInfo(url: string, options?: getInfoOptions): Promise;
998 | function getInfo(url: string, options?: getInfoOptions): Promise;
999 | function downloadFromInfo(info: videoInfo, options?: downloadOptions): Readable;
1000 | function chooseFormat(format: videoFormat | videoFormat[], options?: chooseFormatOptions): videoFormat | never;
1001 | function filterFormats(formats: videoFormat | videoFormat[], filter?: Filter): videoFormat[];
1002 | function validateID(string: string): boolean;
1003 | function validateURL(string: string): boolean;
1004 | function getURLVideoID(string: string): string | never;
1005 | function getVideoID(string: string): string | never;
1006 | function createProxyAgent(options: ProxyAgent.Options | string): Agent;
1007 | function createProxyAgent(options: ProxyAgent.Options | string, cookies?: (Cookie | CK)[]): Agent;
1008 | function createAgent(): Agent;
1009 | function createAgent(cookies?: (Cookie | CK)[]): Agent;
1010 | function createAgent(cookies?: (Cookie | CK)[], opts?: CookieAgent.Options): Agent;
1011 | const version: number;
1012 | }
1013 |
1014 | function ytdl(link: string, options?: ytdl.downloadOptions): Readable;
1015 |
1016 | export = ytdl;
1017 | }
1018 |
--------------------------------------------------------------------------------