├── LICENSE ├── README.md ├── index.d.ts ├── index.js ├── package.json └── src ├── classes ├── playlist.js ├── playlistvideo.js ├── searchdata.js ├── stream.js └── ytdata.js ├── convert.js ├── cookieHandler.js ├── genClient.js ├── index.js ├── info.js ├── playlist.js ├── request ├── index.js ├── url.js └── useragent.js ├── search └── index.js ├── stream ├── createstream.js └── decipher.js └── validate.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Luuk 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | ``` 3 | npm install yt-stream 4 | ``` 5 | 6 | ## What is YT-Stream? 7 | YT-Stream is a package which can download and search YouTube video's. YT-Stream is based on the codes of [ytdl-core](https://npmjs.com/ytdl-core) and [play-dl](https://npmjs.com/play-dl) and further extended to create a package which met to my needs. 8 | 9 | ## API or scrape methods 10 | YT-Stream allows both API (youtubei) and scrape methods to be used. By default the api method will always be used. To change this, an api key is required, which can be obtained from the [Google Developer Console](https://console.cloud.google.com/apis/dashboard) (YouTube Data API v3). This api key can be set by using the `setApiKey` function. This function requires one argument, which is the api key. Setting the api key does not automatically make the package use the api instead of the scrape method. This must be set by using the `setPreference` function. This function requires one argument, which is the method, and has an optional argument, which is the client which the package should use when making an api request. YT-Stream allows you to make an api request with a IOS or Android client, which can be manually set inside the second argument of the `setPreference` function. By default, a random client will be choosen. The client which the package uses does have impact on the data it can receive. If you have application restrictions enabled for your api key, you need to set a client manually. 11 | ```js 12 | const ytstream = require('yt-stream'); 13 | 14 | ytstream.setApiKey(`My Secret api key`); // Only sets the api key (not required) 15 | ytstream.setPreference('api', 'ANDROID'); // Tells the package to use the api and use a android client for requests 16 | ... 17 | ytstream.setPreference('scrape'); // Tells the package to use the scrape methods instead of the api, even if an api key has been provided 18 | ``` 19 | 20 | ## Downloading 21 | You can download a video by using the `stream` function. The `stream` function has two parameters; the `info` or `url` parameter and the `options` parameter. The `options` parameter is not required. The first parameter must include the video url or the info that has been received from the `info` function. The `stream` function will return a `Promise`. The `Promise` will be fullfilled if the video was successfully downloaded. If there was an error, the `Promise` will be rejected with the error. Once the `Promise` gets fullfilled it will return the `Stream` class. The most important properties in the `Stream` class are: 22 | * stream: The Readable stream 23 | * url: The url to download the video or song 24 | * video_url: The YouTube video url 25 | 26 | Optional options are: 27 | * type: If your download preference is video or audio. If one of the types does not exists, it will download the other download type. 28 | * quality: The quality of the video (high or low) 29 | * highWaterMark: The highWaterMark for the Readable stream 30 | * download: A boolean which defines whether to automatically download and push the chunks in the Readable stream (`stream` property of the `Stream` class) the video or not (default `true`) 31 | 32 | > Warning: By setting the `download` option to `false`, it is not guaranteed that the provided url contains the requested video/audio. Make sure to check for any possible location headers when making a request to this url. 33 | ```js 34 | const ytstream = require('yt-stream'); 35 | const fs = require('fs'); 36 | 37 | (async () => { 38 | const stream = await ytstream.stream(`https://www.youtube.com/watch?v=dQw4w9WgXcQ`, { 39 | quality: 'high', 40 | type: 'audio', 41 | highWaterMark: 1048576 * 32, 42 | download: true 43 | }); 44 | stream.stream.pipe(fs.createWriteStream('some_song.mp3')); 45 | console.log(stream.video_url); 46 | console.log(stream.url); 47 | })(); 48 | ``` 49 | 50 | ## Searching video's 51 | YT-Stream also has a search function. You can easily search a song by using the `search` function. The `search` function has one parameter which is the `query` to search. The `query` parameter is **required**. The `search` function will return a `Promise` which will be fullfilled if there were no errors while trying to search. The `Promise` will return an `Array` with the amount of video's that were found. The items in the `Array` are the video's with the `Video` class. The most important properties of the `Video` class are: 52 | * url: The video url 53 | * id: The id of the video 54 | * author: The author of the video 55 | * title: The title of the video 56 | ```js 57 | const ytstream = require('yt-stream'); 58 | 59 | (async () => { 60 | const results = await ytstream.search(`Rick Astley Never Gonna Give You Up`); 61 | 62 | console.log(results[0].url); // Output: https://www.youtube.com/watch?v=dQw4w9WgXcQ 63 | console.log(results[0].id); // Output: dQw4w9WgXcQ 64 | console.log(results[0].author); // Output: Rick Astley 65 | console.log(results[0].title); // Output: Rick Astley - Never Gonna Give You Up (Official Music Video) 66 | })(); 67 | ``` 68 | 69 | ## Get video info 70 | You can also get information about a specific video. You can use the `info` function to do this. The `info` function has one parameter which is the `url` and is **required**. The `info` function will return a `Promise` which will be fullfilled when the info successfully was received. The `Promise` returns the `YouTubeData` class. The most important properties of the `YouTubeData` class are: 71 | * url: The video url 72 | * id: The id of the video 73 | * author: The author of the video 74 | * title: The title of the video 75 | * uploaded: When the video was uploaded 76 | * description: An object of descriptions (short or full) 77 | * duration: The duration of the video in seconds 78 | ```js 79 | const ytstream = require('yt-stream'); 80 | 81 | (async () => { 82 | const info = await ytstream.getInfo(`https://www.youtube.com/watch?v=dQw4w9WgXcQ`); 83 | 84 | console.log(info.url); // Output: https://www.youtube.com/watch?v=dQw4w9WgXcQ 85 | console.log(info.id); // Output: dQw4w9WgXcQ 86 | console.log(info.author); // Output: Rick Astley 87 | console.log(info.title); // Output: Rick Astley - Never Gonna Give You Up (Official Music Video) 88 | console.log(info.uploaded); // Output: 2009-10-24 89 | console.log(info.description); // Output: 'The official video for “Never Gonna Give You Up”...' 90 | console.log(info.duration); // Output: 212000 91 | })(); 92 | ``` 93 | 94 | ## Get playlist info 95 | You can get information about a specific playlist. You can use the `getPlaylist` function for this. The `getPlaylist` function requires one parameter which is the `url`. The `getPlaylist` function returns `Promise` which will be fullfilled when the playlist information has successfully been received. The `Promise` returns the `Playlist` class. The most important properties of the `Playlist` class are: 96 | * videos: An `Array` of all the video's (the `PlaylistVideo` class is used for the video's) 97 | * title: The title of the playlist 98 | * author: The author of the playlist 99 | 100 | ```js 101 | const ytstream = require('yt-stream'); 102 | 103 | (async () => { 104 | const info = await ytstream.getPlaylist(`https://www.youtube.com/playlist?list=PLk-dXGDrKWvZMVKjPtGaIqqI5eg3U7KSEA`); 105 | 106 | console.log(info.videos); // Output: [Array] 107 | console.log(info.title); // Output: Some playlist title 108 | console.log(info.author); // Output: Some playlist author 109 | })(); 110 | ``` 111 | 112 | ## Setting a user agent 113 | You can easily set a user agent by changing the `userAgent` property to a user agent. 114 | ```js 115 | const ytstream = require('yt-stream'); 116 | 117 | ytstream.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0"; 118 | ``` 119 | 120 | ## Setting custom headers 121 | It is possible to add custom headers which will be added to each request made. This is possible by using the `setGlobalHeaders` function. 122 | ```js 123 | const ytstream = require('yt-stream'); 124 | 125 | ytstream.setGlobalHeaders({ 126 | 'Accept-Language': 'en-US,en;q=0.5' 127 | }); 128 | ``` 129 | 130 | ## Agent and cookies 131 | By default, the YT-Stream package automatically handles all cookies. This is being handled by a custom agent. The settings of this agent and the cookies can be configured. This can be done by creating a new instance of the `YTStreamAgent` and changing the settings of this. The first argument of the constructor should eiither contain an array of cookies you'd like to add inside the request or a string which represents the file containing the cookies which will be used and synced. If you don't want to add any cookies, you can use an empty array here. The second argument of the constructor should contain an object with the custom settings for the agent. This custom agent can then be set by using the `setGlobalAgent` function. 132 | ```js 133 | const ytstream = require('yt-stream'); 134 | 135 | const agent = new ytstream.YTStreamAgent([{ 136 | key: 'SOCS', 137 | value: 'CAI', 138 | domain: 'youtube.com', 139 | expires: 'Infinity', 140 | sameSite: 'lax', 141 | httpOnly: false, 142 | hostOnly: false, 143 | secure: true, 144 | path: '/' 145 | }], { 146 | localAddress: '127.0.0.1', 147 | keepAlive: true, 148 | keepAliveMsecs: 5e3 149 | }); 150 | 151 | ytstream.setGlobalAgent(agent); 152 | ``` 153 | 154 | The cookies can afterwards be removed by using the `removeCookies` function of the `YTStreamAgent` class. The `removeCookies` function provides one optional argument which should be a boolean. The boolean determines whether all cookies shall be removed (forced) or only the cookies which were cached from previous requests. 155 | ```js 156 | const agent = new ytstream.YTStreamAgent([{ 157 | key: 'SOCS', 158 | value: 'CAI', 159 | domain: 'youtube.com', 160 | expires: 'Infinity', 161 | sameSite: 'lax', 162 | httpOnly: false, 163 | hostOnly: false, 164 | secure: true, 165 | path: '/' 166 | }], { 167 | localAddress: '127.0.0.1', 168 | keepAlive: true, 169 | keepAliveMsecs: 5e3 170 | }); 171 | 172 | agent.removeCookies(false) // Only removes cached cookies 173 | agent.removeCookies(true) // Also removes manually set cookies inside the constructor 174 | ``` 175 | 176 | You can also sync the cookies with a file. The cookies inside this file will be imported and if YouTube sets new cookies or updates existing ones, these will be changed inside this file as well. You can use the `syncFile` function to synchronize a file containing the cookies. This file **must** be a json file and should contain an array with the cookies inside of it. The `syncFile` function requires one argument, which should be the path to the file. The path should either be absolute or relative from the root folder of the process. Otherwise the agent won't be able to find the file. 177 | ```js 178 | const path = require('path'); 179 | 180 | const agent = new ytstream.YTStreamAgent([], { 181 | localAddress: '127.0.0.1', 182 | keepAlive: true, 183 | keepAliveMsecs: 5e3 184 | }); 185 | 186 | agent.syncFile(path.join(__dirname, `./cookies.json`)) // This is an absolute path which will always work 187 | agent.syncFile(`./cookies.json`) // This is a relative path which will only work if the cookies.json file is inside the root folder of the process 188 | ``` 189 | 190 | ## Validate YouTube url 191 | You can validate a YouTube url by using the `validateURL` function. The function requires one parameter which is the string to check whether it is a valid YouTube url or not. 192 | > Important: This also validates playlists, to only validate video's use the `validateVideoURL` function. 193 | ```js 194 | const ytstream = require('yt-stream'); 195 | 196 | console.log(ytstream.validateURL('SomeString')) // Output: false 197 | console.log(ytstream.validateURL('https://www.youtube.com/watch?v=dQw4w9WgXcQ')); // Output: true 198 | ``` 199 | 200 | ## Validate YouTube video url 201 | You can validate a YouTube video url by using the `validateVideoURL` function. The function requires one parameter which is the string to check whether it is a valid YouTube video url or not. 202 | ```js 203 | const ytstream = require('yt-stream'); 204 | 205 | console.log(ytstream.validateVideoURL('SomeString')) // Output: false 206 | console.log(ytstream.validateVideoURL('https://www.youtube.com/watch?v=dQw4w9WgXcQ')); // Output: true 207 | ``` 208 | 209 | ## Validate YouTube video ID 210 | You can validate a YouTube video ID by using the `validateID` function. The function requires one parameter which is the string to check whether it is a valid YouTube video ID or not. 211 | ```js 212 | const ytstream = require('yt-stream'); 213 | 214 | console.log(ytstream.validateID('SomeString')) // Output: false 215 | console.log(ytstream.validateID('dQw4w9WgXcQ')); // Output: true 216 | ``` 217 | 218 | ## Validate YouTube playlist url 219 | You can validate a YouTube playlist url by using the `validatePlaylistURL` function. The function requires one parameter which is the string to check whether it is a valid YouTube playlist url or not. 220 | ```js 221 | const ytstream = require('yt-stream'); 222 | 223 | console.log(ytstream.validatePlaylistURL('SomeString')) // Output: false 224 | console.log(ytstream.validatePlaylistURL('https://www.youtube.com/playlist?list=PLk-dXGDrKWvZMVKjPtGaIqqI5eg3U7KSEA')); // Output: true 225 | ``` 226 | 227 | ## Validate YouTube playlist ID 228 | You can validate a YouTube playlist ID by using the `validatePlaylistID` function. The function requires one parameter which is the string to check whether it is a valid YouTube playlist ID or not. 229 | ```js 230 | const ytstream = require('yt-stream'); 231 | 232 | console.log(ytstream.validatePlaylistID('SomeString')) // Output: false 233 | console.log(ytstream.validatePlaylistID('PLk-dXGDrKWvZMVKjPtGaIqqI5eg3U7KSEA')); // Output: true 234 | ``` 235 | 236 | ## Get YouTube ID 237 | You can easily convert a YouTube url to a YouTube ID by using the `getID` function. The function requires one parameter which is the string to get the YouTube ID from. If the string is an invalid YouTube url, the function will return `undefined`. 238 | ```js 239 | const ytstream = require('yt-stream'); 240 | 241 | console.log(ytstream.getID('https://www.youtube.com/watch?v=dQw4w9WgXcQ')); // Output: dQw4w9WgXcQ 242 | ``` 243 | 244 | ## Get video url 245 | You can easily convert a YouTube video ID to a YouTube video url by using the `getURL` function. The function requires one parameter which is the string to get the YouTube video url from. If the string is an invalid YouTube video ID, the function will return `undefined`. 246 | ```js 247 | const ytstream = require('yt-stream'); 248 | 249 | console.log(ytstream.getURL('dQw4w9WgXcQ')); // Output: https://www.youtube.com/watch?v=dQw4w9WgXcQ 250 | ``` 251 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from "stream"; 2 | import { EventEmitter } from "events"; 3 | import { Cookie as ToughCookie } from "tough-cookie"; 4 | import { HttpsCookieAgent, HttpCookieAgent } from "http-cookie-agent/http"; 5 | import { Agent as HttpsAgent } from "https"; 6 | import { Agent as HttpAgent } from "http"; 7 | 8 | type HttpsCookieAgentOptions = ConstructorParameters[0]; 9 | type HttpCookieAgentOptions = ConstructorParameters[0]; 10 | 11 | type convert = string | boolean; 12 | type download = string | object; 13 | 14 | type CookieType = [Cookie | { 15 | key?: string; 16 | name?: string; 17 | value?: string; 18 | domain: string; 19 | httpOnly?: boolean; 20 | hostOnly?: boolean; 21 | secure?: boolean; 22 | path?: string; 23 | expires?: string; 24 | expirationDate?: string; 25 | sameSite?: string; 26 | }] 27 | 28 | type streamType = "audio" | "video"; 29 | type quality = "high" | "low" | number; 30 | 31 | interface downloadOptions{ 32 | type: streamType; 33 | highWaterMark: number; 34 | quality: quality; 35 | download: boolean; 36 | } 37 | 38 | interface Stream extends EventEmitter{ 39 | stream: Readable; 40 | url: string; 41 | container: string; 42 | video_url: string; 43 | quality: number; 44 | bytes_count: number; 45 | content_length: string; 46 | duration: number; 47 | type: string; 48 | req_type: string; 49 | mimeType: string; 50 | format: { 51 | itag: number; 52 | mimeType: string; 53 | bitrate: number; 54 | width: number; 55 | height: number; 56 | lastModified: string; 57 | contentLength: string; 58 | quality: string; 59 | fps: number; 60 | qualityLabel: string; 61 | projectionType: string; 62 | avarageBitrate: number; 63 | audioQuality: string; 64 | approxDurationMs: string; 65 | audioSampleRate: string; 66 | audioChannels: number; 67 | signatureCipher: string; 68 | codec: string; 69 | container: string; 70 | }; 71 | info: YouTubeData; 72 | 73 | /** 74 | * Destroys the stream and stops downloading the audio if a download was still going on 75 | */ 76 | destroy() : void; 77 | } 78 | 79 | interface YouTubeData{ 80 | id: string; 81 | url: string; 82 | author: string; 83 | title: string; 84 | description: string; 85 | embed_url: string; 86 | family_safe?: boolean; 87 | available_countries?: [string]; 88 | category?: string; 89 | thumbnails: [{url: string, width: number, height: number}]; 90 | default_thumbnail: {url: string, width: number, height: number}; 91 | uploaded?: string; 92 | uploadedTimestamp?: number; 93 | duration: number; 94 | views: number; 95 | views_text: string; 96 | channel: { 97 | author: string; 98 | id: string; 99 | url: string; 100 | }; 101 | formats: [{ 102 | itag?: number; 103 | mimeType?: string; 104 | bitrate?: number; 105 | width?: number; 106 | height?: number; 107 | lastModified?: string; 108 | contentLength?: string; 109 | quality?: string; 110 | fps?: number; 111 | qualityLabel?: string; 112 | projectionType?: string; 113 | avarageBitrate?: number; 114 | audioQuality?: string; 115 | approxDurationMs?: string; 116 | audioSampleRate?: string; 117 | audioChannels?: number; 118 | signatureCipher?: string; 119 | codec?: string; 120 | container?: string; 121 | url?: string; 122 | }]; 123 | html5player?: string; 124 | user_agent: string; 125 | cookie: string; 126 | } 127 | 128 | interface Video{ 129 | id: string; 130 | url: string; 131 | title: string; 132 | author: string; 133 | channel_id?: string; 134 | channel_url?: string; 135 | length_text: string; 136 | length: number; 137 | views_text: string; 138 | views: number; 139 | thumbnail: string; 140 | user_agent: string; 141 | cookie: string | null; 142 | } 143 | 144 | interface PlaylistVideo{ 145 | title: string; 146 | video_url: string; 147 | video_id: string; 148 | position?: number; 149 | length_text?: string; 150 | length: number; 151 | thumbnails: [{url: string, height: number, width: number}]; 152 | default_thumbnail: {url: string, height: number, width: number}; 153 | playlist_id: string; 154 | playlist_url: string; 155 | } 156 | 157 | interface Playlist{ 158 | title: string; 159 | description: string; 160 | author: string; 161 | author_images: [{url: string, height: number, width: number}]; 162 | default_author_images?: {url: string, height: number, width: number}; 163 | author_channel: string; 164 | url: string; 165 | videos: [PlaylistVideo], 166 | videos_amount: number; 167 | cookie: string | null; 168 | user_agent: string; 169 | } 170 | 171 | /** 172 | * Get information about a song 173 | * @param url The YouTube url of the song 174 | * @param boolean A boolean which defines whether the package should force send a request to YouTube to receive the data or whether it can use cached data as well 175 | */ 176 | export declare function getInfo(url: string, force?: boolean) : Promise; 177 | 178 | /** 179 | * Check whether the YouTube video ID is valid or not 180 | * @param id The YouTube video ID 181 | */ 182 | export declare function validateID(id: string) : boolean; 183 | 184 | /** 185 | * Check whether the YouTube URL is valid or not 186 | * @param url The YouTube URL 187 | */ 188 | export declare function validateURL(url: string) : boolean; 189 | 190 | /** 191 | * Check whether the YouTube video URL is valid or not 192 | * @param url The YouTube video URL 193 | */ 194 | export declare function validateVideoURL(url: string) : boolean; 195 | 196 | /** 197 | * Check whether the YouTube playlist URL is valid or not 198 | * @param url The YouTube playlist URL 199 | */ 200 | export declare function validatePlaylistURL(url: string) : boolean; 201 | 202 | /** 203 | * Check whether the YouTube playlist ID is valid or not 204 | * @param id The YouTube playlist ID 205 | */ 206 | export declare function validatePlaylistID(id: string) : boolean; 207 | 208 | /** 209 | * Get the YouTube video ID from the video URL 210 | * @param url The YouTube video URL 211 | */ 212 | export declare function getID(url: string) : convert; 213 | 214 | /** 215 | * Get the YouTube video URL from the video ID 216 | * @param id The YouTube video ID 217 | */ 218 | export declare function getURL(id: string) : convert; 219 | 220 | /** 221 | * Search for YouTube video's 222 | * @param query The search query to search a video 223 | */ 224 | export declare function search(query: string) : Promise<[Video]>; 225 | 226 | /** 227 | * Download the YouTube video and create a readable stream of it 228 | * @param info Either the YouTube url of the video or the received information from the getInfo function 229 | * @param options An object that defines options which the stream function should take into account 230 | */ 231 | export declare function stream(info: download, options: downloadOptions) : Promise; 232 | 233 | /** 234 | * Gets the information of a playlist including the video's inside the playlist 235 | * @param url The url of the playlist 236 | */ 237 | export declare function getPlaylist(url: string) : Promise; 238 | 239 | /** 240 | * Adds custom headers to each request made to YouTube 241 | * @param headers The headers you'd like to add in each request 242 | */ 243 | export declare function setGlobalHeaders(headers: object) : void; 244 | 245 | /** 246 | * Sets a custom agent which is being used to send the requests with 247 | * @param agent An instance of the YTStreamAgent class which represents the HTTP agent 248 | */ 249 | export declare function setGlobalAgent(agent: YTStreamAgent | {https: HttpsAgent | HttpsCookieAgent | any, http: HttpAgent | HttpCookieAgent | any} | any) : void; 250 | 251 | /** 252 | * Allows you to set an api key which can be used to get information instead of scraping pages 253 | * @param apiKey Your api key to use 254 | */ 255 | export declare function setApiKey(apiKey: string) : void; 256 | 257 | /** 258 | * Allows you to choose whether to use the api or scraping methods to get information 259 | * @param preference The method you prefer 260 | */ 261 | export declare function setPreference(preference: 'scrape' | 'api', client?: 'IOS' | 'ANDROID') : void; 262 | 263 | declare var cookie: string; 264 | declare var userAgent: string; 265 | 266 | export declare class Cookie extends ToughCookie{} 267 | 268 | export declare class YTStreamAgent{ 269 | constructor(cookies: CookieType, options: HttpsCookieAgentOptions | HttpCookieAgentOptions); 270 | 271 | /** 272 | * Adds cookies to the cookies headers which is being send in each request to YouTube 273 | * @param cookies An array or an instance of the Cookie class which represents the cookie you want to add 274 | */ 275 | addCookies(cookies: [] | ToughCookie) : void; 276 | 277 | /** 278 | * Removes the cookies which the agent has saved 279 | * @param force True to remove manually set cookies as well, false to only remove cached cookies (default false) 280 | */ 281 | removeCookies(force?: boolean) : void; 282 | 283 | /** 284 | * Import cookies from a json file and sync them with the cookies YouTube provides so they stay up-to-date 285 | * @param filePath The path to the file of the cookies (absolute or from the cwd) 286 | */ 287 | syncFile(filePath: string) : void; 288 | } 289 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license MIT 3 | * 4 | * Written by Luuk Walstra 5 | * Discord: Luuk#8524 6 | * Github: https://github.com/Luuk-Dev 7 | * Replit: https://replit.com/@LuukDev 8 | * Repository: https://github.com/Luuk-Dev/TypeWriter 9 | * 10 | * You're free to use this as long as you keep this statement in this file 11 | */ 12 | module.exports = require('./src/index.js'); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yt-stream", 3 | "version": "1.7.4", 4 | "description": "Create easily readable streams from YouTube video url's", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Luuk", 10 | "license": "MIT", 11 | "readme": "README.md", 12 | "publisher": "Luuk", 13 | "keywords": [ 14 | "youtube", 15 | "downloader", 16 | "download", 17 | "search", 18 | "stream", 19 | "play", 20 | "ytdl", 21 | "youtube-dl", 22 | "yt", 23 | "player", 24 | "info", 25 | "cookie", 26 | "music", 27 | "audio", 28 | "video", 29 | "extractor" 30 | ], 31 | "repository": { 32 | "type": "github", 33 | "url": "https://github.com/Luuk-Dev/yt-stream" 34 | }, 35 | "homepage": "https://github.com/Luuk-Dev/yt-stream#readme", 36 | "dependencies": { 37 | "http-cookie-agent": "^6.0.5", 38 | "tough-cookie": "^4.1.4", 39 | "valuesaver": "^1.9.3" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/classes/playlist.js: -------------------------------------------------------------------------------- 1 | const PlaylistVideo = require('./playlistvideo.js'); 2 | 3 | class Playlist{ 4 | constructor(data, headers, listId){ 5 | if(typeof data.microformat === 'object'){ 6 | this.title = data.microformat.microformatDataRenderer.title; 7 | this.description = data.microformat.microformatDataRenderer.description; 8 | } else if(typeof data.header === 'object') { 9 | this.title = Array.isArray(data.header?.playlistHeaderRenderer?.title?.runs) ? data.header.playlistHeaderRenderer.title.runs[0].text : ''; 10 | this.description = Array.isArray(data.header?.playlistHeaderRenderer?.descriptionText?.runs) ? data.header.playlistHeaderRenderer.descriptionText.runs[0].text : ''; 11 | } 12 | 13 | if(typeof data?.sidebar === 'object'){ 14 | let getAuthorArrayItem = data.sidebar.playlistSidebarRenderer.items.filter(s => typeof s.playlistSidebarSecondaryInfoRenderer !== 'undefined'); 15 | if(!!getAuthorArrayItem.length){ 16 | let authorInfo = getAuthorArrayItem[0]?.playlistSidebarSecondaryInfoRenderer?.videoOwner?.videoOwnerRenderer; 17 | this.author = (authorInfo?.title?.runs[0]?.text ?? ''); 18 | this.author_images = authorInfo?.thumbnail?.thumbnails; 19 | this.default_author_image = authorInfo?.thumbnail?.thumbnails[authorInfo.thumbnail?.thumbnails?.length - 1]; 20 | this.author_channel = `https://www.youtube.com${authorInfo?.navigationEndpoint?.commandMetadata?.webCommandMetadata?.url}`; 21 | } else { 22 | this.author = "YouTube"; 23 | this.author_images = []; 24 | this.default_author_image = undefined; 25 | this.author_channel = `https://www.youtube.com/@YouTube`; 26 | } 27 | } else if(typeof data.header === 'object'){ 28 | this.author = Array.isArray(data.header?.playlistHeaderRenderer?.ownerText?.runs) ? data.header.playlistHeaderRenderer.ownerText.runs[0].text : ''; 29 | this.author_images = []; 30 | this.default_author_image = undefined; 31 | this.author_channel = `https://www.youtube.com/${data.header?.playlistHeaderRenderer?.ownerEndpoint?.browseEndpoint?.browseId}`; 32 | } else { 33 | this.author = "YouTube"; 34 | this.author_images = []; 35 | this.default_author_image = undefined; 36 | this.author_channel = `https://www.youtube.com/@YouTube`; 37 | } 38 | 39 | this.url = `https://www.youtube.com/playlist?list=${listId}`; 40 | 41 | this.videos = []; 42 | let contentTab = data?.contents?.twoColumnBrowseResultsRenderer ?? data?.contents?.singleColumnBrowseResultsRenderer; 43 | let videoInfo = []; 44 | if(contentTab){ 45 | if(Array.isArray(contentTab?.tabs)){ 46 | for(const tab of contentTab.tabs){ 47 | if(Array.isArray(tab?.tabRenderer?.content?.sectionListRenderer?.contents)){ 48 | for(const content of tab.tabRenderer.content.sectionListRenderer.contents){ 49 | if(Array.isArray(content?.playlistVideoListRenderer?.contents)){ 50 | for(const videoContent of content.playlistVideoListRenderer.contents){ 51 | if(typeof videoContent?.playlistVideoRenderer === 'object') videoInfo.push(videoContent); 52 | } 53 | } else if(Array.isArray(content?.itemSectionRenderer?.contents)){ 54 | for(const itemSectionRender of content.itemSectionRenderer.contents){ 55 | if(Array.isArray(itemSectionRender?.playlistVideoListRenderer?.contents)){ 56 | for(const videoContent of itemSectionRender.playlistVideoListRenderer.contents){ 57 | if(typeof videoContent?.playlistVideoRenderer === 'object') videoInfo.push(videoContent); 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | for(let i = 0; i < videoInfo.length; i++){ 68 | if(typeof videoInfo[i]?.playlistVideoRenderer === 'undefined') continue; 69 | this.videos.push(new PlaylistVideo(data, videoInfo[i].playlistVideoRenderer)); 70 | } 71 | this.video_amount = this.videos.length; 72 | 73 | if(typeof headers['cookie'] === 'string'){ 74 | this.cookie = headers['cookie']; 75 | } else { 76 | this.cookie = null; 77 | } 78 | this.user_agent = headers['user-agent']; 79 | } 80 | } 81 | 82 | module.exports = Playlist; 83 | -------------------------------------------------------------------------------- /src/classes/playlistvideo.js: -------------------------------------------------------------------------------- 1 | class PlaylistVideo{ 2 | constructor(data, videodata){ 3 | this.title = Array.isArray(videodata?.title?.runs) ? videodata.title.runs[0].text : ''; 4 | this.video_id = videodata.videoId; 5 | this.video_url = `https://www.youtube.com/watch?v=${videodata.videoId}`; 6 | if(typeof videodata.index?.simpleText === 'string'){ 7 | this.position = parseInt(videodata.index.simpleText); 8 | } else if(Array.isArray(videodata.index?.runs)){ 9 | this.position = parseInt(videodata.index.runs[0].text); 10 | } 11 | if(typeof videodata.lengthText?.simpleText === 'string'){ 12 | this.length_text = videodata.lengthText.simpleText; 13 | } else if(Array.isArray(videodata.lengthText?.runs)){ 14 | this.length_text = videodata.lengthText.runs[0].text; 15 | } 16 | this.length = parseInt(videodata.lengthSeconds) * 1000; 17 | this.thumbnails = videodata.thumbnail.thumbnails; 18 | this.default_thumbnail = videodata.thumbnail.thumbnails[videodata.thumbnail.thumbnails.length - 1]; 19 | this.channel = { 20 | author: Array.isArray(videodata?.shortBylineText?.runs) ? videodata.shortBylineText.runs[0].text : null, 21 | id: Array.isArray(videodata?.shortBylineText?.runs) ? videodata.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId : null, 22 | url: Array.isArray(videodata?.shortBylineText?.runs) ? `https://www.youtube.com/channel/${videodata.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId}` : null 23 | }; 24 | this.playlist_id = videodata.navigationEndpoint.watchEndpoint.playlistId; 25 | this.playlist_url = `https://www.youtube.com/playlist?list=${this.playlist_id}`; 26 | } 27 | } 28 | 29 | module.exports = PlaylistVideo; 30 | -------------------------------------------------------------------------------- /src/classes/searchdata.js: -------------------------------------------------------------------------------- 1 | class Video{ 2 | constructor(data, headers, api){ 3 | this.id = data.videoId; 4 | this.url = `https://www.youtube.com/watch?v=${this.id}`; 5 | this.title = Array.isArray(data.title?.runs) ? data.title.runs[0].text : ''; 6 | let authorObj = (data.ownerText ?? data.longBylineText); 7 | this.author = Array.isArray(authorObj?.runs) ? authorObj.runs[0].text : ''; 8 | let channelObj = (data.ownerText ?? data.longBylineText); 9 | this.channel_id = Array.isArray(channelObj?.runs) ? channelObj.runs[0].navigationEndpoint.browseEndpoint.browseId : null; 10 | this.channel_url = this.channel_id ? `https://www.youtube.com/channel/`+this.channel_id : null; 11 | this.user_agent = headers['user-agent']; 12 | if(typeof headers['cookie'] === 'string'){ 13 | this.cookie = headers['cookie']; 14 | } else { 15 | this.cookie = null; 16 | } 17 | 18 | if(data.lengthText){ 19 | if(typeof data.lengthText.simpleText === 'string'){ 20 | this.length_text = data.lengthText.simpleText; 21 | } else if(Array.isArray(data.lengthText?.runs)){ 22 | this.length_text = data.lengthText.runs[0].text; 23 | } 24 | const length = this.length_text.split(':').reverse(); 25 | var timestampLength = 0; 26 | for(var i = 0; i < length.length; i++){ 27 | const l = length[i]; 28 | if(i < 3) timestampLength += l * Math.pow(60, i) * 1000; 29 | else if(i === 3) timestampLength += l * 24 * Math.pow(60, i) * 1000; 30 | } 31 | this.length = timestampLength; 32 | } else { 33 | this.length_text = 'Unknown length'; 34 | this.length = 0; 35 | } 36 | 37 | if(data.viewCountText){ 38 | if(typeof data.viewCountText.simpleText === 'string'){ 39 | this.views_text = data.viewCountText.simpleText; 40 | } else if(Array.isArray(data.viewCountText?.runs)){ 41 | this.views_text = data.viewCountText.runs[0].text; 42 | } 43 | if(this.views_text) this.views = parseInt(this.views_text.toLowerCase().split(',').join('').split(' ')[0]); 44 | else this.views_text = 'Unknown views'; 45 | } else { 46 | this.view_text = 'Unknown views'; 47 | this.views = 0; 48 | } 49 | 50 | this.thumbnail = data.thumbnail.thumbnails[0].url; 51 | } 52 | } 53 | 54 | module.exports = Video; 55 | -------------------------------------------------------------------------------- /src/classes/stream.js: -------------------------------------------------------------------------------- 1 | const { Readable } = require('stream'); 2 | const { EventEmitter } = require('events'); 3 | const { URL } = require('url'); 4 | const cipher = require('../stream/decipher.js'); 5 | const { requestCallback, request } = require('../request/index.js'); 6 | const { YTStreamAgent } = require('../cookieHandler.js'); 7 | const getInfo = require('../info.js').getInfo; 8 | 9 | function parseAudio(formats){ 10 | const audio = []; 11 | var audioFormats = formats.filter(f => f.mimeType.startsWith('audio')); 12 | for(var i = 0; i < audioFormats.length; i++){ 13 | var format = audioFormats[i]; 14 | const type = format.mimeType; 15 | if(type.startsWith('audio')){ 16 | format.codec = type.split('codecs=')[1].split('"')[0]; 17 | format.container = type.split('audio/')[1].split(';')[0]; 18 | audio.push(format); 19 | } 20 | } 21 | return audio; 22 | } 23 | 24 | function parseVideo(formats){ 25 | const video = []; 26 | var videoFormats = formats.filter(f => f.mimeType.startsWith('video')); 27 | for(var i = 0; i < videoFormats.length; i++){ 28 | var format = videoFormats[i]; 29 | const type = format.mimeType; 30 | if(type.startsWith('video')){ 31 | format.codec = type.split('codecs=')[1].split('"')[0]; 32 | format.container = type.split('video/')[1].split(';')[0]; 33 | video.push(format); 34 | } 35 | } 36 | return video; 37 | } 38 | 39 | class Stream extends EventEmitter{ 40 | constructor(ytstream, url, options, info){ 41 | super(); 42 | this.stream = new Readable({highWaterMark: (options.highWaterMark || 1048576 * 32), read() {}}); 43 | this.container = options.container; 44 | this.ytstream = ytstream; 45 | this.url = url; 46 | this.video_url = options.video_url; 47 | this.quality = options.quality; 48 | this.info = info; 49 | this.mimeType = options.format.mimeType; 50 | 51 | this.bytes_count = 0; 52 | this.content_length = parseInt(options.contentLength); 53 | this.duration = options.duration; 54 | this.type = options.type; 55 | 56 | this.req_type = options.req_type; 57 | 58 | this.per_sec_byte = Math.ceil(this.content_length / this.duration); 59 | this.retryCount = 0; 60 | this.format = options.format; 61 | if(options.download === true) this.loop(); 62 | else { 63 | this.emit('ready'); 64 | this.ready = true; 65 | } 66 | } 67 | async retry(){ 68 | if(this.ytstream.agent instanceof YTStreamAgent) this.ytstream.agent.removeCookies(false); 69 | const info = await getInfo(this.ytstream, this.video_url, true); 70 | 71 | const _ci = await cipher.format_decipher(info.formats, info.cver, info.html5player, this.ytstream.agent); 72 | 73 | info.formats = _ci; 74 | 75 | var audioFormat = this.req_type === 'video' ? parseVideo(info.formats) : parseAudio(info.formats); 76 | 77 | if(audioFormat.length === 0) audioFormat = this.req_type === 'video' ? parseAudio(info.formats) : parseVideo(info.formats); 78 | 79 | this.url = typeof this.quality === 'number' ? (audioFormat[this.quality] ? audioFormat[this.quality].url : audioFormat[audioFormat.length - 1].url) : audioFormat[0].url; 80 | this.loop(); 81 | } 82 | async loop(){ 83 | let parsed = new URL(this.url); 84 | 85 | try{ 86 | await request(parsed.protocol+"//"+parsed.host+"/generate_204", { 87 | method: 'GET' 88 | }, this.ytstream.agent, 0, true); 89 | } catch { 90 | ++this.retryCount; 91 | if(this.retryCount >= 5){ 92 | return this.emit('error', 'Failed to get valid content'); 93 | } else return this.loop(); 94 | } 95 | 96 | requestCallback(this.url, { 97 | headers: { 98 | range: `bytes=0-${this.content_length}` 99 | }, 100 | method: 'GET' 101 | }, this.ytstream.agent, false).then(async ({stream, req}) => { 102 | this.req = req; 103 | if(Number(stream.statusCode) >= 400){ 104 | if(this.retryCount >= 5){ 105 | return this.emit('error', 'No valid download url\'s could be found for the YouTube video'); 106 | } else { 107 | ++this.retryCount; 108 | return await this.retry(); 109 | } 110 | } else if(Number(stream.statusCode) >= 300 && Object.keys(stream.headers).map(h => h.toLowerCase()).indexOf('location') >= 0){ 111 | ++this.retryCount; 112 | const headerKeys = Object.keys(stream.headers).map(h => h.toLowerCase()); 113 | const headerValues = Object.values(stream.headers); 114 | this.url = headerValues[headerKeys.indexOf('location')]; 115 | return this.loop(); 116 | } 117 | var chunkCount = 0; 118 | stream.on('data', chunk => { 119 | this.bytes_count += chunk.length; 120 | this.stream.push(chunk); 121 | ++chunkCount; 122 | if(chunkCount === 3){ 123 | this.emit('ready'); 124 | this.ready = true; 125 | } 126 | }); 127 | 128 | stream.on('end', () => { 129 | if(chunkCount < 3){ 130 | this.emit('ready'); 131 | this.ready = true; 132 | } 133 | this.stream.push(null); 134 | }); 135 | 136 | stream.on('error', err => { 137 | this.emit('error', err); 138 | }); 139 | }).catch(err => { 140 | this.emit('error', err); 141 | }); 142 | } 143 | pause(){ 144 | this.stream.pause(); 145 | } 146 | resume(){ 147 | this.stream.resume(); 148 | } 149 | destroy(){ 150 | this.req.destroy(); 151 | this.stream.destroy(); 152 | } 153 | ready = false; 154 | } 155 | 156 | module.exports = Stream; 157 | -------------------------------------------------------------------------------- /src/classes/ytdata.js: -------------------------------------------------------------------------------- 1 | class YouTubeData{ 2 | constructor(data, cver, html5player, headers, clientInfo){ 3 | let videoDetails = data.videoDetails; 4 | this.id = videoDetails.videoId; 5 | this.url = `https://www.youtube.com/watch?v=${videoDetails.videoId}`; 6 | this.author = videoDetails.author; 7 | this.title = videoDetails.title; 8 | this.description = videoDetails.shortDescription; 9 | this.embed_url = `https://www.youtube.com/embed/${videoDetails.videoId}`; 10 | this.thumbnails = videoDetails.thumbnail.thumbnails; 11 | this.default_thumbnail = videoDetails.thumbnail.thumbnails[videoDetails.thumbnail.thumbnails.length - 1]; 12 | this.duration = parseInt(videoDetails.lengthSeconds) * 1000; 13 | this.views = parseInt(videoDetails.viewCount); 14 | 15 | if(typeof data.microformat === 'object'){ 16 | let microformat = data.microformat.playerMicroformatRenderer; 17 | const uploadedTime = new Date(microformat.publishDate); 18 | this.uploaded = `${uploadedTime.getDate()}-${uploadedTime.getMonth() + 1}-${uploadedTime.getFullYear()}`; 19 | this.uploadedTimestamp = uploadedTime.getTime(); 20 | this.family_safe = microformat.isFamilySafe; 21 | this.available_countries = microformat.availableCountries; 22 | this.category = microformat.category; 23 | } else { 24 | this.uploaded = null; 25 | this.uploadedTimestamp = 0; 26 | this.family_safe = null; 27 | this.available_countries = []; 28 | this.category = null; 29 | } 30 | 31 | let viewsText = videoDetails.viewCount.toString(); 32 | viewsText = viewsText.split('').reverse(); 33 | viewsText = viewsText.reduce((arr, number, index, defaultArray) => { 34 | if((index + 1) % 3 === 0 && index !== 0 && (index + 1) !== defaultArray.length){ 35 | arr.push(number); 36 | arr.push('.'); 37 | } else { 38 | arr.push(number); 39 | } 40 | return arr; 41 | }, []); 42 | viewsText = viewsText.reverse().join(''); 43 | 44 | this.views_text = viewsText; 45 | this.channel = { 46 | author: videoDetails.author, 47 | id: videoDetails.channelId, 48 | url: `https://www.youtube.com/channel/${videoDetails.channelId}` 49 | }; 50 | this.formats = []; 51 | this.html5player = html5player; 52 | this.clientInfo = clientInfo; 53 | 54 | this.formats.push(...(data.streamingData.formats || [])); 55 | this.formats.push(...(data.streamingData.adaptiveFormats || [])); 56 | this.user_agent = headers['user-agent']; 57 | if(typeof headers['cookie'] === 'string'){ 58 | this.cookie = headers['cookie']; 59 | } else { 60 | this.cookie = null; 61 | } 62 | this.cver = cver; 63 | this.streamingURL = data.streamingData.serverAbrStreamingUrl; 64 | } 65 | } 66 | 67 | module.exports = YouTubeData; 68 | -------------------------------------------------------------------------------- /src/convert.js: -------------------------------------------------------------------------------- 1 | const { validateURL, validateID, validatePlaylistID } = require('./validate.js'); 2 | const _getURL = require('./request/url.js'); 3 | 4 | function getID(ytstream, url){ 5 | if(typeof url !== 'string') throw new Error(`URL is not a string`); 6 | if(!validateURL(ytstream, url)) return undefined; 7 | 8 | const parsed = _getURL(url); 9 | 10 | const host = parsed.hostname.toLowerCase().split('www.').join(''); 11 | 12 | let ytid; 13 | if(host === `youtu.be`) ytid = parsed.pathname.split('/').join(''); 14 | else if(host === `youtube.com` || host === 'music.youtube.com'){ 15 | if(parsed.pathname.startsWith('/watch')) ytid = parsed.searchParams.get('v') || null; 16 | else if(parsed.pathname.startsWith('/embed/')) ytid = parsed.pathname.split('/embed/').join(''); 17 | else if(parsed.pathname.startsWith('/v/')) ytid = parsed.pathname.split('/v/').join(''); 18 | else if(parsed.pathname.startsWith('/shorts/')) ytid = parsed.pathname.split('/shorts/').join(''); 19 | else if(parsed.pathname.startsWith('/playlist')) ytid = parsed.searchParams.get('list') || null; 20 | else ytid = null 21 | } else ytid = null; 22 | 23 | if(validateID(ytstream, ytid) || validatePlaylistID(ytstream, ytid)) return ytid; 24 | else return undefined; 25 | } 26 | 27 | function getURL(ytstream, id){ 28 | if(typeof id !== 'string') throw new Error(`ID is not a string`); 29 | 30 | let reg = /^[A-Za-z0-9_-]*$/; 31 | if(!reg.test(id)) return undefined; 32 | 33 | return `https://www.youtube.com/watch?v=${id}`; 34 | } 35 | 36 | module.exports ={ 37 | getID, 38 | getURL 39 | } 40 | -------------------------------------------------------------------------------- /src/cookieHandler.js: -------------------------------------------------------------------------------- 1 | const { Cookie, CookieJar, canonicalDomain } = require('tough-cookie'); 2 | const { HttpCookieAgent, HttpsCookieAgent } = require('http-cookie-agent/http'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | function toDate(cookie){ 7 | if(typeof cookie.expirationDate === 'string'){ 8 | if(cookie.expirationDate.toLowerCase() === 'infinity') return 'Infinity'; 9 | return new Date(cookie.expirationDate); 10 | } else if(typeof cookie.expires === 'string'){ 11 | if(cookie.expires.toLowerCase() === 'infinity') return 'Infinity'; 12 | return new Date(cookie.expires); 13 | } else if(typeof cookie.expirationDate === 'number'){ 14 | return new Date(cookie.expirationDate * 1000); 15 | } else if(typeof cookie.expires === 'number'){ 16 | return new Date(cookie.expires * 1000); 17 | } else return 'Infinity'; 18 | } 19 | 20 | function addCookiesToJar(cookies, jar){ 21 | for(const cookie of cookies){ 22 | if(cookie instanceof Cookie){ 23 | if(cookie.domain !== 'youtube.com' && cookie.domain !== '.youtube.com') continue; 24 | jar.setCookieSync(cookie, 'https://www.youtube.com'); 25 | } 26 | else if(typeof cookie === 'object' && !Array.isArray(cookie) && cookie !== null){ 27 | if(typeof cookie.key !== 'string' && typeof cookie.name !== 'string') throw new Error(`Invalid cookie. A cookie must have a key or name.`); 28 | if(typeof cookie.domain !== 'string') throw new Error(`Invalid cookie. A cookie must have a domain.`); 29 | if(cookie.domain !== 'youtube.com' && cookie.domain !== '.youtube.com') continue; 30 | jar.setCookieSync(new Cookie({ 31 | key: cookie.key ?? cookie.name, 32 | value: typeof cookie.value === 'string' ? cookie.value : "", 33 | domain: canonicalDomain(cookie.domain), 34 | httpOnly: typeof cookie.httpOnly === 'boolean' ? cookie.httpOnly : false, 35 | hostOnly: typeof cookie.hostOnly === 'boolean' ? cookie.hostOnly : false, 36 | secure: typeof cookie.secure === 'boolean' ? cookie.secure : false, 37 | path: typeof cookie.path === 'string' ? cookie.path : '/', 38 | expires: toDate(cookie), 39 | sameSite: ['lax', 'samesite'].indexOf((cookie.sameSite ?? "").toLowerCase()) >= 0 ? cookie.sameSite : 'None' 40 | }), 'https://www.youtube.com'); 41 | } else throw new Error(`Invalid cookie. Cookie must be an instance of Cookie class or an object.`); 42 | } 43 | 44 | return jar; 45 | } 46 | 47 | class YTStreamAgent { 48 | constructor(cookies, options){ 49 | if(typeof options !== 'object' || Array.isArray(options) || options === null) options = {timeout: 5000, keepAlive: true, keepAliveMsecs: (Math.round(Math.random() * 3) + 5)}; 50 | 51 | if(!(options.cookies?.jar instanceof CookieJar)){ 52 | options.cookies = {}; 53 | options.cookies.jar = new CookieJar(); 54 | } 55 | this.jar = options.cookies.jar; 56 | this._options = options; 57 | 58 | if(typeof cookies === 'string') this.syncFile(cookies); 59 | else if(!Array.isArray(cookies)) cookies = []; 60 | if(Array.isArray(cookies)){ 61 | if(!!!cookies.filter(c => c.name === 'SOCS').length){ 62 | const templates = [{ 63 | key: 'SOCS', 64 | value: 'CAI', 65 | sameSite: 'lax', 66 | hostOnly: false, 67 | secure: true, 68 | path: '/', 69 | httpOnly: false, 70 | domain: 'youtube.com' 71 | }, { 72 | key: 'CONSENT', 73 | value: 'PENDING+'+(Math.round(Math.random() * 400) + 1).toString(), 74 | sameSite: 'None', 75 | hostOnly: false, 76 | secure: true, 77 | path: '/', 78 | httpOnly: false, 79 | domain: 'youtube.com', 80 | expires: 'Infinity' 81 | }]; 82 | templates.forEach(t => options.cookies.jar.setCookieSync(new Cookie(t), 'https://www.youtube.com')); 83 | } 84 | options.cookies.jar = addCookiesToJar(cookies, options.cookies.jar); 85 | } 86 | 87 | this.agents = { 88 | https: new HttpsCookieAgent(options), 89 | http: new HttpCookieAgent(options) 90 | }; 91 | this.localAddress = options.localAddress; 92 | this._cookies = this.jar.getCookiesSync('https://www.youtube.com') ?? []; 93 | if(typeof cookies !== 'string') this.syncedFile = ''; 94 | } 95 | addCookies(cookies){ 96 | if(!Array.isArray(cookies)) cookies = []; 97 | if(!(this.jar instanceof CookieJar)) throw new Error(`Jar property is not an instance of CookieJar`); 98 | this.jar = addCookiesToJar(cookies, this.jar); 99 | this._options.cookies.jar = this.jar; 100 | if(this.syncedFile.length > 0){ 101 | fs.writeFileSync(this.syncedFile, JSON.stringify(this.jar.getCookiesSync('https://www.youtube.com'), null, 2)); 102 | } 103 | } 104 | removeCookies(force){ 105 | if(force){ 106 | if(Object.keys(this._options).indexOf('cookies') >= 0) delete this._options.cookies; 107 | this.jar = new CookieJar(); 108 | let options = {..._this._options, cookies: {jar: this.jar}}; 109 | this.agents = { 110 | https: new HttpsCookieAgent(options), 111 | http: new HttpCookieAgent(options) 112 | }; 113 | } else { 114 | if(!(this._options.cookies?.jar instanceof CookieJar)){ 115 | this._options.cookies = {}; 116 | this._options.cookies.jar = new CookieJar(); 117 | } 118 | 119 | if(!!!this._cookies.filter(c => c.name === 'SOCS').length){ 120 | const templates = [{ 121 | key: 'SOCS', 122 | value: 'CAI', 123 | sameSite: 'lax', 124 | hostOnly: false, 125 | secure: true, 126 | path: '/', 127 | httpOnly: false, 128 | domain: 'youtube.com' 129 | }, { 130 | key: 'CONSENT', 131 | value: 'PENDING+'+(Math.round(Math.random() * 400) + 1).toString(), 132 | sameSite: 'None', 133 | hostOnly: false, 134 | secure: true, 135 | path: '/', 136 | httpOnly: false, 137 | domain: 'youtube.com', 138 | expires: 'Infinity' 139 | }]; 140 | templates.forEach(t => this._options.cookies.jar.setCookieSync(new Cookie(t), 'https://www.youtube.com')); 141 | } 142 | this._options.cookies.jar = addCookiesToJar(this._cookies, this._options.cookies.jar); 143 | 144 | this.jar = this._options.cookies.jar; 145 | this.agents = { 146 | https: new HttpsCookieAgent(this._options), 147 | http: new HttpCookieAgent(this._options) 148 | }; 149 | } 150 | } 151 | syncFile(filePath){ 152 | if(typeof filePath !== 'string') throw new Error(`Expected the file path to be a type of string, received ${typeof filePath}`); 153 | if(path.extname(filePath) !== ".json") throw new Error(`File expected to have .json extension name, received ${path.extname(filePath)}`); 154 | if(!path.isAbsolute(filePath)) filePath = path.join(process.cwd(), filePath); 155 | if(!fs.existsSync(filePath)) throw new Error(`Couldn't find a file with the path '${filePath}'. Make sure that the file exists and the path is either absolute or relative to the root of the process`); 156 | let cookies = fs.readFileSync(filePath); 157 | try{ 158 | cookies = JSON.parse(cookies); 159 | } catch { 160 | throw new Error(`Cookies from imported file is not a valid json object`); 161 | } 162 | if(!Array.isArray(cookies)) throw new Error(`Imported cookies expected to be an array, received type of ${typeof cookies}, but no array`); 163 | this.syncedFile = filePath; 164 | this.jar = addCookiesToJar(cookies, this.jar); 165 | this._options.cookies.jar = this.jar; 166 | } 167 | } 168 | 169 | module.exports = { YTStreamAgent }; 170 | -------------------------------------------------------------------------------- /src/genClient.js: -------------------------------------------------------------------------------- 1 | const androidUserAgents = [{ 2 | agent: 'com.google.android.youtube/19.28.35(Linux; U; Android 13; en_US; sdk_gphone64_x86_64 Build/UPB4.230623.005) gzip', 3 | clientVersion: '19.28.35', 4 | deviceModel: 'sdk_gphone64_x86_64', 5 | sdkVersion: 33 6 | }, { 7 | agent: 'com.google.android.youtube/19.28.35(Linux; U; Android 13; en_US; M2103K19G Build/TP1A.220624.014) gzip', 8 | clientVersion: '19.28.35', 9 | deviceModel: 'M2103K19G', 10 | sdkVersion: 33 11 | }, { 12 | agent: 'com.google.android.youtube/19.28.35(Linux; U; Android 14; en_US; 23073RPBFG Build/UKQ1.231003.002) gzip', 13 | clientVersion: '19.28.35', 14 | deviceModel: '23073RPBFG', 15 | sdkVersion: 33 16 | }, { 17 | agent: 'com.google.android.youtube/19.28.35(Linux; U; Android 13; en_US; CPH2557 Build/TP1A.220905.001) gzip', 18 | clientVersion: '19.28.35', 19 | deviceModel: 'CPH2557', 20 | sdkVersion: 33 21 | }]; 22 | const iosUserAgents = [{ 23 | agent: 'com.google.ios.youtube/18.06.35 (iPhone; CPU iPhone OS 14_4 like Mac OS X; en_US)', 24 | clientVersion: '18.06.35', 25 | deviceModel: 'iPhone10,6' 26 | }, { 27 | agent: 'com.google.ios.youtube/19.09.3 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X en_US)', 28 | clientVersion: '19.09.3', 29 | deviceModel: 'iPhone14,3' 30 | }, { 31 | agent: 'com.google.ios.youtube/19.28.1 (iPhone16,2; U; CPU IOS 17_5_1 like Mac OS X; en_US)', 32 | clientVersion: '19.28.1', 33 | deviceModel: 'iPhone16,2' 34 | }]; 35 | 36 | function genNonce(length){ 37 | let chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-'; 38 | let nonce = ""; 39 | while(nonce.length < length){ 40 | nonce += chars[Math.round(Math.random() * (chars.length - 1))]; 41 | } 42 | return nonce; 43 | } 44 | 45 | function genClientInfo(videoId, preferedClient){ 46 | 47 | const clientTypes = ['ANDROID', 'IOS']; 48 | const client = preferedClient ?? clientTypes[Math.round(Math.random() * (clientTypes.length - 1))]; 49 | 50 | let headers = { 51 | 'x-youtube-client-name': '', 52 | 'x-youtube-client-version': '', 53 | 'origin': 'https://www.youtube.com', 54 | 'user-agent': '', 55 | 'content-type': 'application/json' 56 | }; 57 | 58 | let userAgent, clientVersion; 59 | let contextBuilder = { 60 | context: { 61 | client: { 62 | cpn: genNonce(16), 63 | clientName: client, 64 | clientVersion: undefined, 65 | deviceModel: undefined, 66 | userAgent: undefined, 67 | hl: 'en', 68 | timeZone: 'UTC', 69 | utcOffsetMinutes: 0, 70 | acceptLanguage: 'en-US', 71 | acceptRegion: 'US' 72 | } 73 | }, 74 | contentCheckOk: true, 75 | racyCheckOk: true, 76 | attestationRequest: { 77 | omitBotguardData: true 78 | } 79 | }; 80 | if(typeof videoId === 'string'){ 81 | contextBuilder = { 82 | ...contextBuilder, 83 | videoId: videoId, 84 | playbackContext: { 85 | contentPlaybackContext: { 86 | html5Preference: 'HTML5_PREF_WANTS', 87 | vis: 0, 88 | splay: false, 89 | referer: `https://www.youtube.com/watch?v=${videoId}`, 90 | currentUrl: `/watch?v=${videoId}`, 91 | autonavState: 'STATE_ON', 92 | autoCaptionsDefaultOn: false, 93 | lactMilliseconds: '-1' 94 | } 95 | } 96 | }; 97 | } 98 | switch(client){ 99 | case 'ANDROID': 100 | let androidRandomAgent = androidUserAgents[Math.round(Math.random() * (androidUserAgents.length - 1))]; 101 | userAgent = androidRandomAgent.agent; 102 | clientVersion = androidRandomAgent.clientVersion; 103 | contextBuilder.context.client.deviceModel = androidRandomAgent.deviceModel; 104 | contextBuilder.context.client['androidSdkVersion'] = androidRandomAgent.sdkVersion; 105 | headers['x-youtube-client-name'] = '3'; 106 | headers['x-goog-api-format-version'] = '2'; 107 | break; 108 | case 'IOS': 109 | let iosRandomAgent = iosUserAgents[Math.round(Math.random() * (iosUserAgents.length - 1))]; 110 | userAgent = iosRandomAgent.agent; 111 | clientVersion = iosRandomAgent.clientVersion; 112 | contextBuilder.context.client.deviceModel = iosRandomAgent.deviceModel; 113 | headers['x-youtube-client-name'] = '5'; 114 | break; 115 | } 116 | contextBuilder.context.client.userAgent = userAgent; 117 | contextBuilder.context.client.clientVersion = clientVersion; 118 | headers['user-agent'] = userAgent; 119 | headers['x-youtube-client-version'] = clientVersion; 120 | 121 | return { 122 | headers, 123 | body: contextBuilder 124 | }; 125 | } 126 | 127 | module.exports = genClientInfo; 128 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const validate = require('./validate.js'); 2 | const getInfo = require('./info.js').getInfo; 3 | const getPlaylist = require('./playlist.js').getPlaylist; 4 | const convert = require('./convert.js'); 5 | const { Cookie, CookieJar } = require('tough-cookie'); 6 | const { YTStreamAgent } = require('./cookieHandler.js'); 7 | const stream = require('./stream/createstream.js').stream; 8 | const search = require('./search/index.js').search; 9 | 10 | let headers; 11 | let agent = new YTStreamAgent(); 12 | let APIKey = null; 13 | let preference = "api"; 14 | let client = null; 15 | 16 | class YTStream{ 17 | constructor(){ 18 | this.validateURL = (...args) => { 19 | return validate.validateURL(this, ...args) 20 | }; 21 | this.validateID = (...args) => { 22 | return validate.validateID(this, ...args); 23 | }; 24 | this.validateVideoURL = (...args) => { 25 | return validate.validateVideoURL(this, ...args); 26 | }; 27 | this.validatePlaylistURL = (...args) => { 28 | return validate.validatePlaylistURL(this, ...args); 29 | }; 30 | this.validatePlayListID = (...args) => { 31 | return validate.validatePlaylistID(this, ...args); 32 | }; 33 | this.getPlaylist = (...args) => { 34 | return getPlaylist(this, ...args); 35 | }; 36 | this.getInfo = (...args) => { 37 | return getInfo(this, ...args); 38 | }; 39 | this.getID = (...args) => { 40 | return convert.getID(this, ...args); 41 | }; 42 | this.getURL = (...args) => { 43 | return convert.getURL(this, ...args); 44 | }; 45 | this.stream = (...args) => { 46 | return stream(this, ...args); 47 | }; 48 | this.search = (...args) => { 49 | return search(this, ...args); 50 | }; 51 | this.setGlobalAgent = (_agent) => { 52 | if(typeof _agent === 'object'){ 53 | if(_agent instanceof YTStreamAgent){ 54 | agent = _agent; 55 | } else { 56 | agent = { 57 | agents: {}, 58 | jar: new CookieJar() 59 | }; 60 | if(typeof _agent.https === 'object' || typeof _agent.http === 'object'){ 61 | agent.agents['https'] = _agent.https ?? _agent.http; 62 | agent.agents['http'] = _agent.http ?? _agent.https; 63 | } else { 64 | agent.agents['https'] = _agent; 65 | agent.agents['http'] = _agent; 66 | } 67 | } 68 | } else throw new Error(`Agent is not a valid agent`); 69 | }; 70 | this.setGlobalHeaders = (_headers) => { 71 | if(typeof _headers !== 'object' || Array.isArray(_headers) || _headers === null) throw new Error(`Invalid headers. Headers must be a type of object and may not be an Array or null.`); 72 | _headers = {}; 73 | for(const header in _headers){ 74 | headers[header] = _headers[header]; 75 | } 76 | } 77 | this.setApiKey = (apiKey) => { 78 | if(typeof apiKey !== 'string') throw new Error(`API key must be a type of string. Received type of ${typeof apiKey}`); 79 | APIKey = apiKey; 80 | } 81 | this.setPreference = (_preference, _client) => { 82 | if(typeof _preference !== 'string') throw new Error(`Preference must be a type of string. Received type of ${typeof _preference}`); 83 | if(['scrape', 'api'].indexOf(_preference.toLowerCase()) < 0) throw new Error(`Preference must be either 'scrape' or 'api'. Received ${_preference}`); 84 | if(typeof _client === 'string'){ 85 | if(['IOS', 'ANDROID'].indexOf(_client.toUpperCase()) < 0) throw new Error(`Client must be one of IOS or ANDROID. Received ${_client}`); 86 | client = _client.toUpperCase(); 87 | } 88 | preference = _preference.toLowerCase(); 89 | } 90 | this.Cookie = Cookie; 91 | this.YTStreamAgent = YTStreamAgent; 92 | this.userAgent = null; 93 | } 94 | get agent(){ 95 | return agent; 96 | } 97 | get headers(){ 98 | return headers; 99 | } 100 | get apiKey(){ 101 | return APIKey; 102 | } 103 | get preference(){ 104 | return preference; 105 | } 106 | get client(){ 107 | return client; 108 | } 109 | } 110 | 111 | module.exports = new YTStream(); 112 | -------------------------------------------------------------------------------- /src/info.js: -------------------------------------------------------------------------------- 1 | const { validateVideoURL } = require('./validate.js'); 2 | const _url = require('./request/url.js'); 3 | const request = require('./request/index.js').request; 4 | const YouTubeData = require('./classes/ytdata.js'); 5 | const userAgent = require('./request/useragent.js').getRandomUserAgent; 6 | const { getID } = require('./convert.js'); 7 | const genClientInfo = require('./genClient.js'); 8 | 9 | function getHTML5player(response){ 10 | let html5playerRes = 11 | /|"jsUrl":"([^"]+)"/ 12 | .exec(response); 13 | return html5playerRes ? html5playerRes[1] || html5playerRes[2] : null; 14 | }; 15 | 16 | function getCver(response){ 17 | let startCver = response.split(/["|'|`]key["|'|`]:["|'|`]cver["|'|`],["|'|`]value["|'|`]:["|'|`]/); 18 | return startCver[1].split(/["|'|`]/)[0]; 19 | } 20 | 21 | function genNonce(length){ 22 | let chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-'; 23 | let nonce = ""; 24 | while(nonce.length < length){ 25 | nonce += chars[Math.round(Math.random() * (chars.length - 1))]; 26 | } 27 | return nonce; 28 | } 29 | 30 | function getInfo(ytstream, url, force = false){ 31 | return new Promise((resolve, reject) => { 32 | if(typeof url !== 'string') return reject(`URL is not a string`); 33 | 34 | const validation = validateVideoURL(ytstream, url); 35 | if(!validation) return reject(`Invalid YouTube video URL`); 36 | 37 | var ytid = null; 38 | let parsed = _url(url); 39 | if(['youtube.com', 'music.youtube.com'].includes(parsed.hostname.toLowerCase().split('www.').join(''))) ytid = getID(ytstream, url); 40 | else if(parsed.hostname.toLowerCase().split('www.').join('') === `youtu.be`){ 41 | if(parsed.pathname.toLowerCase().startsWith('/watch')){ 42 | const newurl = 'https://www.youtube.com'+parsed.pathname+parsed.search; 43 | ytid = getID(ytstream, newurl); 44 | } else { 45 | const newurl = `https://www.youtube.com/watch?v=`+parsed.pathname.split('/').join(''); 46 | ytid = getID(ytstream, newurl); 47 | } 48 | } 49 | 50 | if(ytid === null) return reject(`Invalid YouTube url`); 51 | 52 | if(ytstream.preference === 'scrape'){ 53 | const yturl = `https://www.youtube.com/watch?v=${ytid}&has_verified=1&cbrd=1`; 54 | 55 | const userA = typeof ytstream.userAgent === 'string' ? ytstream.userAgent : userAgent(); 56 | 57 | let headers = { 58 | 'accept-language': 'en-US,en-IN;q=0.9,en;q=0.8,hi;q=0.7', 59 | 'user-agent' : userA, 60 | }; 61 | 62 | for(const header in ytstream.headers){ 63 | headers[header] = ytstream.headers[header]; 64 | } 65 | headers['cookie'] = ytstream.agent.jar.getCookieStringSync('https://www.youtube.com'); 66 | 67 | request(yturl, { 68 | headers: headers 69 | }, ytstream.agent, 0, force).then(async response => { 70 | let res = response.split('var ytInitialPlayerResponse = ')[1]; 71 | let html5path = getHTML5player(response); 72 | const html5player = typeof html5path === 'string' ? `https://www.youtube.com${html5path}` : null; 73 | let cver = getCver(response); 74 | if(!res){ 75 | return reject(`The YouTube song has no initial player response (1)`); 76 | } 77 | res = res.split(';')[0]; 78 | if(!res){ 79 | return reject(`The YouTube song has no initial player response (2)`); 80 | } 81 | try{ 82 | res = decodeURI(res); 83 | } catch {} 84 | res = res.split(`\\"`).join(""); 85 | let seperate = res.split(/}};[a-z]/); 86 | let jsonObject = (seperate[0] + "}}").split('\\"').join('\"').split("\\'").join("\'").split("\\`").join("\`"); 87 | let splitVars = jsonObject.split(/['|"|`]playerVars['|"|`]:/); 88 | while(splitVars.length > 1){ 89 | jsonObject = splitVars[0] + splitVars.slice(1).join("\"playerVars\":").split(/}}['|"|`],/).slice(1).join("}}\","); 90 | splitVars = jsonObject.split(/['|"|`]playerVars['|"|`]:/); 91 | } 92 | let data; 93 | try{ 94 | data = JSON.parse(jsonObject); 95 | } catch { 96 | return reject(`The YouTube song has no initial player response (3)`); 97 | } 98 | if(data.playabilityStatus.status !== 'OK'){ 99 | let error = data.playabilityStatus.errorScreen.playerErrorMessageRenderer ? data.playabilityStatus.errorScreen.playerErrorMessageRenderer.reason.simpleText : data.playabilityStatus.errorScreen.playerKavRenderer.reason.simpleText; 100 | 101 | reject(`Error while getting video url\n${error}`); 102 | } else resolve(new YouTubeData(data, cver, html5player, headers, null)); 103 | }).catch(err => { 104 | reject(err); 105 | }); 106 | } else if(ytstream.preference === 'api'){ 107 | const clientInfo = genClientInfo(ytid, ytstream.client); 108 | 109 | let endPoint = 'https://www.youtube.com/youtubei/v1/player' + (typeof ytstream.apiKey === 'string' ? '?key='+ytstream.apiKey : '?t='+genNonce(12))+'&prettyPrint=false&id='+ytid; 110 | request(endPoint, { 111 | method: 'POST', 112 | body: JSON.stringify(clientInfo.body), 113 | headers: clientInfo.headers 114 | }, ytstream.agent, 0, false).then(res => { 115 | let data; 116 | try{ 117 | data = JSON.parse(res); 118 | } catch { 119 | reject(`Invalid response from the YouTube API`); 120 | return; 121 | } 122 | if(data.playabilityStatus.status !== 'OK'){ 123 | return reject(data.playabilityStatus.reason); 124 | } 125 | resolve(new YouTubeData(data, clientInfo.body.context.client.clientVersion, null, clientInfo.headers, clientInfo.body)); 126 | }).catch(reject); 127 | } 128 | }); 129 | } 130 | 131 | module.exports = { 132 | getInfo 133 | }; 134 | -------------------------------------------------------------------------------- /src/playlist.js: -------------------------------------------------------------------------------- 1 | const { validatePlaylistURL } = require("./validate.js"); 2 | const _url = require('./request/url.js'); 3 | const { requestCallback, request } = require("./request/index.js"); 4 | const userAgent = require("./request/useragent.js").getRandomUserAgent; 5 | const { getID } = require("./convert.js"); 6 | const Playlist = require("./classes/playlist.js"); 7 | const genClientInfo = require('./genClient.js'); 8 | 9 | function genNonce(length){ 10 | let chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-'; 11 | let nonce = ""; 12 | while(nonce.length < length){ 13 | nonce += chars[Math.round(Math.random() * (chars.length - 1))]; 14 | } 15 | return nonce; 16 | } 17 | 18 | function getPlaylist(ytstream, url){ 19 | return new Promise(async (resolve, reject) => { 20 | if(!validatePlaylistURL(ytstream, url)) return reject(`Invalid YouTube playlist url`); 21 | 22 | let listId = null; 23 | const parsed = _url(url); 24 | if(['youtube.com', 'music.youtube.com'].includes(parsed.hostname.toLowerCase().split('www.').join(''))) listId = getID(ytstream, url); 25 | else if(parsed.hostname.toLowerCase().split('www.').join('') === `youtu.be`){ 26 | if(parsed.pathname.toLowerCase().startsWith('/playlist')){ 27 | const newurl = 'https://www.youtube.com'+parsed.pathname+parsed.search; 28 | listId = getID(ytstream, newurl); 29 | } 30 | } 31 | 32 | if(listId === null) return reject(`Invalid YouTube url`); 33 | 34 | if(ytstream.preference === 'scrape'){ 35 | const userA = typeof ytstream.userAgent === 'string' ? ytstream.userAgent : userAgent(); 36 | 37 | let headers = { 38 | 'accept-language': 'en-US,en-IN;q=0.9,en;q=0.8,hi;q=0.7', 39 | 'user-agent' : userA, 40 | }; 41 | 42 | for(const header in ytstream.headers){ 43 | headers[header] = ytstream.headers[header]; 44 | } 45 | 46 | headers['cookie'] = ytstream.agent.jar.getCookieStringSync('https://www.youtube.com'); 47 | 48 | var request_url = `https://www.youtube.com/playlist?list=${listId}&has_verified=1`; 49 | 50 | var response; 51 | try{ 52 | response = await requestPlayList(request_url, headers, ytstream.agent); 53 | } catch (err){ 54 | reject(err); 55 | } 56 | 57 | response = response.split('var ytInitialData = ')[1]; 58 | if(!response) return reject(`The YouTube playlist has no initial data response`); 59 | 60 | response = response.split(';')[0]; 61 | if(!response) return reject(`The YouTube playlist has no initial data response`); 62 | 63 | var json = response; 64 | try{ 65 | json = JSON.parse(json); 66 | } catch {} 67 | 68 | var alerts = (json.alerts || []); 69 | alerts.push({alertRenderer: {}}); 70 | var errors = alerts.filter(a => { 71 | return (a.alertRenderer || a.alertWithButtonRenderer).type === 'ERROR'; 72 | }); 73 | if(errors.length > 0) return reject(errors[0].alertRenderer.text.runs[0].text); 74 | resolve(new Playlist(json, headers, listId)); 75 | } else if(ytstream.preference === 'api'){ 76 | const clientInfo = genClientInfo(undefined, ytstream.client); 77 | 78 | let endPoint = 'https://www.youtube.com/youtubei/v1/browse' + (typeof ytstream.apiKey === 'string' ? '?key='+ytstream.apiKey : '?t='+genNonce(12))+'&prettyPrint=false'; 79 | request(endPoint, { 80 | method: 'POST', 81 | body: JSON.stringify({...clientInfo.body, browseId: !listId.startsWith("VL") ? `VL${listId}` : listId}), 82 | headers: clientInfo.headers 83 | }, ytstream.agent, 0, false).then(res => { 84 | let data; 85 | try{ 86 | data = JSON.parse(res); 87 | } catch { 88 | reject(`Invalid response from the YouTube API`); 89 | return; 90 | } 91 | resolve(new Playlist(data, clientInfo.headers, listId)); 92 | }).catch(reject); 93 | } 94 | }); 95 | } 96 | 97 | function requestPlayList(url, headers, agent){ 98 | return new Promise(async (resolve, reject) => { 99 | 100 | let options = { 101 | headers: headers, 102 | method: 'GET' 103 | }; 104 | 105 | var res; 106 | try{ 107 | res = await requestCallback(url, options, agent, false); 108 | } catch(err) { 109 | return reject(err); 110 | } 111 | 112 | while(_url((res.stream.headers.location ?? '')) !== false){ 113 | res = await requestCallback(res.stream.headers.location, options, agent, false); 114 | } 115 | 116 | var response = ''; 117 | res.stream.on('data', d => { 118 | response += d; 119 | }); 120 | 121 | res.stream.on('end', () => { 122 | resolve(response); 123 | }); 124 | 125 | res.stream.on('error', err => { 126 | reject(err); 127 | }); 128 | }); 129 | } 130 | 131 | module.exports = { 132 | getPlaylist 133 | }; 134 | -------------------------------------------------------------------------------- /src/request/index.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const https = require('https'); 3 | const { Cookie } = require('tough-cookie'); 4 | const { YTStreamAgent } = require('../cookieHandler.js'); 5 | const { ValueSaver } = require('valuesaver'); 6 | 7 | const cache = new ValueSaver(); 8 | 9 | setInterval(() => { 10 | cache.clear(); 11 | }, 6e4); 12 | 13 | const requestType = {https: https, http: http}; 14 | 15 | const _validate = require('./url.js'); 16 | 17 | function handleCookies(res, agent){ 18 | if(!(agent instanceof YTStreamAgent)) return; 19 | const headerKeys = Object.keys(res.headers).map(h => h.toLowerCase()); 20 | const headerValues = Object.values(res.headers); 21 | 22 | const cookieIndex = headerKeys.indexOf('set-cookie'); 23 | if(cookieIndex >= 0){ 24 | const cookies = headerValues[cookieIndex]; 25 | if(typeof cookies === 'string'){ 26 | agent.addCookies([Cookie.parse(cookies)]); 27 | } else if(Array.isArray(cookies)){ 28 | agent.addCookies(cookies.map(c => Cookie.parse(c))); 29 | } 30 | } 31 | } 32 | 33 | function request(_url, options, agent, retryCount = 0, force = false){ 34 | return new Promise(async (resolve, reject) => { 35 | if(typeof _url !== 'string') return reject(`URL is not a string`); 36 | let response = ''; 37 | 38 | const url = _validate(_url); 39 | if(!url) return reject(`Invalid URL`); 40 | 41 | const cachedPage = cache.get(_url); 42 | if(cachedPage && !force) return resolve(cachedPage); 43 | 44 | const protocol = url.protocol.split(':').join(''); 45 | const prreq = requestType[protocol]; 46 | 47 | const http_options = { 48 | headers: options.headers || {cookie: agent.jar.getCookieStringSync('https://www.youtube.com')}, 49 | path: url.pathname + url.search, 50 | host: url.hostname, 51 | method: options.method || 'GET', 52 | agent: agent instanceof YTStreamAgent ? agent.agents[protocol] : (typeof agent === 'object' ? (typeof agent.agents === 'object' ? agent.agents[protocol] : agent) : undefined), 53 | localAddress: agent.localAddress 54 | }; 55 | 56 | const req = prreq.request(http_options, res => { 57 | if(res.statusCode >= 300 || res.statusCode < 200){ 58 | if(res.statusCode >= 300 && res.statusCode < 400 && retryCount < 3){ 59 | const headersKeys = Object.keys(res.headers).map(h => h.toLowerCase()); 60 | const headerValues = Object.values(res.headers); 61 | const locationIndex = headersKeys.indexOf('location'); 62 | if(locationIndex >= 0){ 63 | request(headerValues[locationIndex], options, agent, ++retryCount, force).then(resolve).catch(reject); 64 | } 65 | } 66 | return reject(`Error while receiving information. Server returned with status code ${res.statusCode}.`); 67 | } 68 | handleCookies(res, agent); 69 | 70 | res.on('data', data => { 71 | response += data; 72 | }); 73 | res.on('end', () => { 74 | cache.set(_url, response); 75 | resolve(response); 76 | }); 77 | res.on('error', error => { 78 | reject(error); 79 | }); 80 | }); 81 | 82 | req.on('error', error => { 83 | reject(error); 84 | }); 85 | 86 | if(typeof options.body === 'string'){ 87 | req.write(options.body); 88 | } 89 | 90 | req.end(); 91 | }); 92 | } 93 | 94 | function requestCallback(_url, options, agent, parsedOnly = false){ 95 | return new Promise(async (resolve, reject) => { 96 | if(typeof _url !== 'string') return reject(`URL is not a string`); 97 | 98 | const url = _validate(_url); 99 | if(!(url instanceof URL)) reject(`Invalid URL`); 100 | 101 | const protocol = url.protocol.split(':').join(''); 102 | const prreq = requestType[protocol]; 103 | 104 | const http_options = { 105 | headers: options.headers || {cookie: agent.jar.getCookieStringSync('https://www.youtube.com')}, 106 | path: url.pathname + url.search, 107 | host: url.hostname, 108 | method: options.method || 'GET', 109 | agent: agent instanceof YTStreamAgent ? agent.agents[protocol] : (typeof agent === 'object' ? (typeof agent.agents === 'object' ? agent.agents[protocol] : agent) : undefined), 110 | localAddress: agent.localAddress 111 | }; 112 | 113 | if(parsedOnly === false){ 114 | const req = prreq.request(http_options, stream => resolve({stream, req: req})); 115 | 116 | req.on('error', error => { 117 | reject(error); 118 | }); 119 | 120 | if(typeof options.body === 'string'){ 121 | req.write(options.body); 122 | } 123 | 124 | req.end(); 125 | } else { 126 | const req = prreq.request(url, stream => resolve({stream, req: req})); 127 | 128 | req.on('error', error => { 129 | reject(error); 130 | }); 131 | 132 | if(typeof options.body === 'string'){ 133 | req.write(options.body); 134 | } 135 | 136 | req.end(); 137 | } 138 | }); 139 | } 140 | 141 | module.exports = {request, requestCallback}; 142 | -------------------------------------------------------------------------------- /src/request/url.js: -------------------------------------------------------------------------------- 1 | function _validate(_url){ 2 | if(typeof _url !== 'string') return false; 3 | try { 4 | return new URL(_url); 5 | } catch { 6 | return false; 7 | } 8 | } 9 | 10 | module.exports = _validate; -------------------------------------------------------------------------------- /src/request/useragent.js: -------------------------------------------------------------------------------- 1 | const userAgent = [ 2 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.3', 3 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36', 4 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0', 5 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.3', 6 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.3', 7 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0', 8 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:127.0) Gecko/20100101 Firefox/127.0', 9 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.', 10 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.', 11 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.3' 12 | ]; 13 | 14 | function getRandomUserAgent(){ 15 | const maxIndex = userAgent.length - 1; 16 | const i = Math.round(Math.random() * maxIndex); 17 | return userAgent[i]; 18 | } 19 | 20 | module.exports = { getRandomUserAgent }; 21 | -------------------------------------------------------------------------------- /src/search/index.js: -------------------------------------------------------------------------------- 1 | const request = require('../request/index.js').request; 2 | const SearchData = require('../classes/searchdata.js'); 3 | const userAgent = require('../request/useragent.js').getRandomUserAgent; 4 | const genClientInfo = require('../genClient.js'); 5 | const { URL } = require('url'); 6 | 7 | function toFullNumber(n){ 8 | let defN = (n.match(/[0-9.]/g) ?? ["0"]).join(""); 9 | let numbers = parseInt(defN); 10 | switch(n.split(defN)[1].toUpperCase()){ 11 | case 'K': 12 | numbers *= 1e3; 13 | break; 14 | case 'M': 15 | numbers *= 1e6; 16 | break; 17 | case 'B': 18 | numbers *= 1e9; 19 | break; 20 | } 21 | return numbers; 22 | } 23 | 24 | function genNonce(length){ 25 | let chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-'; 26 | let nonce = ""; 27 | while(nonce.length < length){ 28 | nonce += chars[Math.round(Math.random() * (chars.length - 1))]; 29 | } 30 | return nonce; 31 | } 32 | 33 | function transformApiData(videoCard){ 34 | let viewsMetaData = videoCard.videoData.metadata.metadataDetails.split(" "); 35 | let filterViews = viewsMetaData.filter(v => /^[0-9.]+[KMB]{0,1}$/.test(v) && v.length > 1); 36 | let views = 0; 37 | if(filterViews.length > 0){ 38 | views = toFullNumber(filterViews[0]); 39 | } 40 | 41 | let videoId = videoCard.videoData.videoId; 42 | if(!videoId){ 43 | if(typeof videoCard.videoData.dragAndDropUrl === 'string'){ 44 | if(!videoCard.videoData.dragAndDropUrl.startsWith("https://")){ 45 | if(videoCard.videoData.dragAndDropUrl.startsWith("//")){ 46 | videoCard.videoData.dragAndDropUrl = "https:"+videoCard.videoData.dragAndDropUrl; 47 | } else if(videoCard.videoData.dragAndDropUrl.startsWith("www.")){ 48 | videoCard.videoData.dragAndDropUrl = "https://"+videoCard.videoData.dragAndDropUrl; 49 | } else if(videoCard.videoData.dragAndDropUrl.dragAndDropUrl.startsWith("youtube")){ 50 | videoCard.videoData.dragAndDropUrl = "https://www."+videoCard.videoData.dragAndDropUrl; 51 | } 52 | } 53 | 54 | try{ 55 | let parseURL = new URL(videoCard.videoData.dragAndDropUrl); 56 | videoId = parseURL.searchParams.get('v'); 57 | if(!videoId){ 58 | if(typeof videoCard?.onTap?.innertubeCommand?.watchEndpoint === 'object'){ 59 | videoId = videoCard.onTap.innertubeCommand.watchEndpoint.videoId; 60 | } 61 | } 62 | } catch { 63 | if(typeof videoCard?.onTap?.innertubeCommand?.watchEndpoint === 'object'){ 64 | videoId = videoCard.onTap.innertubeCommand.watchEndpoint.videoId; 65 | } 66 | } 67 | } else if(typeof videoCard?.onTap?.innertubeCommand?.watchEndpoint === 'object'){ 68 | videoId = videoCard.onTap.innertubeCommand.watchEndpoint.videoId; 69 | } 70 | } 71 | 72 | return { 73 | videoRenderer: { 74 | title: {runs: [{text: videoCard.videoData.metadata.title}]}, 75 | videoId: videoId, 76 | thumbnail: {thumbnails: videoCard.videoData.thumbnail.image.sources}, 77 | lengthText: {simpleText: videoCard.videoData.thumbnail.timestampText}, 78 | viewCountText: {simpleText: views.toString()}, 79 | ownerText: {runs: [{text: videoCard.videoData.metadata.byline, navigationEndpoint: {browseEndpoint: {browseId: null}}}]} 80 | } 81 | }; 82 | } 83 | 84 | function getResults(json, headers, api){ 85 | const videos = []; 86 | 87 | const itemSectionRenders = json?.contents?.twoColumnSearchResultsRenderer?.primaryContents?.sectionListRenderer?.contents ?? json?.contents?.sectionListRenderer?.contents ?? []; 88 | for(const itemSectionRender of itemSectionRenders){ 89 | if(typeof itemSectionRender.itemSectionRenderer !== 'object' && typeof itemSectionRender.shelfRenderer === 'object'){ 90 | if(!Array.isArray(itemSectionRender.shelfRenderer?.content?.verticalListRenderer?.items)) continue; 91 | for(const item of itemSectionRender.shelfRenderer.content.verticalListRenderer.items){ 92 | if(Array.isArray(item?.elementRenderer?.newElement?.type?.componentType?.model?.horizontalTileShelfModel?.videoCards)){ 93 | for(const videoCard of item.elementRenderer.newElement.type.componentType.model.horizontalTileShelfModel.videoCards){ 94 | videos.push(transformApiData(videoCard)); 95 | } 96 | } else if(typeof item?.elementRenderer?.newElement?.type?.componentType?.model?.compactVideoModel?.compactVideoData?.videoData === 'object'){ 97 | const videoCard = item.elementRenderer.newElement.type.componentType.model.compactVideoModel.compactVideoData; 98 | videos.push(transformApiData(videoCard)); 99 | } else if(typeof item?.compactVideoRenderer === 'object'){ 100 | videos.push({ 101 | videoRenderer: item.compactVideoRenderer 102 | }); 103 | } else continue; 104 | } 105 | } else if(typeof itemSectionRender.itemSectionRenderer === 'object') { 106 | if(typeof itemSectionRender.itemSectionRenderer.contents !== 'object') continue; 107 | for(const content of itemSectionRender.itemSectionRenderer.contents){ 108 | const contentKeys = Object.keys(content); 109 | if(contentKeys.indexOf('videoRenderer') >= 0){ 110 | videos.push(content); 111 | } else if(contentKeys.indexOf('compactVideoRenderer') >= 0){ 112 | videos.push({ 113 | videoRenderer: content.compactVideoRenderer 114 | }); 115 | } else if(typeof content?.elementRenderer?.newElement?.type?.componentType?.model?.compactVideoModel?.compactVideoData === 'object'){ 116 | const videoCard = content.elementRenderer.newElement.type.componentType.model.compactVideoModel.compactVideoData; 117 | videos.push(transformApiData(videoCard)); 118 | } else continue; 119 | } 120 | } else continue; 121 | } 122 | 123 | const results = []; 124 | 125 | for(var i = 0; i < videos.length; i++){ 126 | const data = videos[i].videoRenderer; 127 | if(data){ 128 | if(data.videoId){ 129 | const video = new SearchData(data, headers, api); 130 | results.push(video); 131 | } 132 | } 133 | } 134 | 135 | return results; 136 | } 137 | 138 | function defaultExtractor(response, headers){ 139 | var res = response; 140 | res = res.split('var ytInitialData = ')[1]; 141 | if(!res) return reject(`The YouTube page has no initial data response`); 142 | 143 | res = res.split(';')[0]; 144 | 145 | const json = JSON.parse(res); 146 | return getResults(json, headers, false); 147 | } 148 | 149 | function search(ytstream, query, options){ 150 | return new Promise((resolve, reject) => { 151 | if(typeof query !== 'string') return reject(`Query must be a string`); 152 | 153 | if(ytstream.preference === 'scrape'){ 154 | var _options = options || {type: 'video'}; 155 | 156 | var host = 'https://www.youtube.com/results?search_query='; 157 | 158 | let url = host + encodeURIComponent(query) + (_options['type'] !== 'video' ? '&sp=EgIQAQ%253D%253D' : ''); 159 | 160 | let headers = { 161 | 'accept-language': 'en-US,en-IN;q=0.9,en;q=0.8,hi;q=0.7', 162 | }; 163 | 164 | if(typeof ytstream.userAgent === 'string'){ 165 | headers['user-agent'] = ytstream.userAgent; 166 | } else { 167 | headers['user-agent'] = userAgent(); 168 | } 169 | 170 | for(const header in ytstream.headers){ 171 | headers[header] = ytstream.headers[header]; 172 | } 173 | 174 | headers['cookie'] = ytstream.agent.jar.getCookieStringSync('https://www.youtube.com'); 175 | request(url, { 176 | headers: headers 177 | }, ytstream.agent).then(response => { 178 | resolve(defaultExtractor(response, headers)); 179 | }).catch(err => { 180 | reject(err); 181 | }); 182 | } else if(ytstream.preference === 'api'){ 183 | const clientInfo = genClientInfo(undefined, ytstream.client); 184 | 185 | let endPoint = 'https://www.youtube.com/youtubei/v1/search' + (typeof ytstream.apiKey === 'string' ? '?key='+ytstream.apiKey : '?t='+genNonce(12))+'&prettyPrint=false'; 186 | request(endPoint, { 187 | method: 'POST', 188 | body: JSON.stringify({ 189 | ...clientInfo.body, 190 | query: query 191 | }), 192 | headers: clientInfo.headers 193 | }, ytstream.agent, 0, false).then(res => { 194 | let data; 195 | try{ 196 | data = JSON.parse(res); 197 | } catch { 198 | reject(`Invalid response from the YouTube API`); 199 | return; 200 | } 201 | resolve(getResults(data, clientInfo.headers, true)); 202 | }).catch(reject); 203 | } 204 | }); 205 | } 206 | 207 | module.exports = {search}; 208 | -------------------------------------------------------------------------------- /src/stream/createstream.js: -------------------------------------------------------------------------------- 1 | const validate = require('../validate.js').validateVideoURL; 2 | const getInfo = require('../info.js').getInfo; 3 | const Stream = require('../classes/stream.js'); 4 | const Data = require('../classes/ytdata.js'); 5 | const cipher = require('./decipher.js'); 6 | 7 | function parseAudio(formats){ 8 | const audio = []; 9 | var audioFormats = formats.filter(f => f.mimeType.startsWith('audio')); 10 | for(var i = 0; i < audioFormats.length; i++){ 11 | var format = audioFormats[i]; 12 | const type = format.mimeType; 13 | if(type.startsWith('audio')){ 14 | format.codec = type.split('codecs=')[1].split('"')[0]; 15 | format.container = type.split('audio/')[1].split(';')[0]; 16 | audio.push(format); 17 | } 18 | } 19 | return audio; 20 | } 21 | 22 | function parseVideo(formats){ 23 | const video = []; 24 | var videoFormats = formats.filter(f => f.mimeType.startsWith('video')); 25 | for(var i = 0; i < videoFormats.length; i++){ 26 | var format = videoFormats[i]; 27 | const type = format.mimeType; 28 | if(type.startsWith('video')){ 29 | format.codec = type.split('codecs=')[1].split('"')[0]; 30 | format.container = type.split('video/')[1].split(';')[0]; 31 | video.push(format); 32 | } 33 | } 34 | return video; 35 | } 36 | 37 | function getStreamURL(info, options){ 38 | return new Promise((resolve, reject) => { 39 | const formats = info.formats; 40 | var selectedFormat = null; 41 | var _options = options || {}; 42 | var vid = _options['type'] === 'video' ? parseVideo(formats) : parseAudio(formats); 43 | 44 | if(vid.length === 0) vid = _options['type'] === 'video' ? parseAudio(formats) : parseVideo(formats); 45 | 46 | if(vid.length === 0) return reject(`There were no playable formats found`); 47 | 48 | _options['quality'] = typeof _options['quality'] === 'string' ? _options['quality'].toLowerCase() : _options['quality']; 49 | if(typeof _options['quality'] !== 'number'){ 50 | for(var i = 0; i < vid.length; i++){ 51 | let format = vid[i]; 52 | if(!selectedFormat){ 53 | selectedFormat = format; 54 | } else { 55 | if(_options['quality'] === 'high'){ 56 | if(format.bitrate > selectedFormat.bitrate){ 57 | selectedFormat = format; 58 | } 59 | } else { 60 | if(format.bitrate < selectedFormat.bitrate){ 61 | selectedFormat = format; 62 | } 63 | } 64 | } 65 | } 66 | } else { 67 | if(_options['quality'] > vid.length) _options['quality'] = vid.length - 1; 68 | else if(_options['quality'] < vid.length) _options['quality'] = 0; 69 | selectedFormat = vid[_options['quality']]; 70 | } 71 | var { url } = selectedFormat; 72 | let type = selectedFormat.codec === 'opus' && selectedFormat.container === 'webm' ? 'webm/opus' : 'arbitrary'; 73 | resolve({ 74 | url: url, 75 | contentLength: selectedFormat.contentLength, 76 | type: type, 77 | quality: typeof _options['quality'] === 'string' ? (_options['quality'] === 'high' ? vid.length - 1 : 0) : (_options['quality'] || 0), 78 | req_type: (_options['type'] || 'audio'), 79 | container: selectedFormat.container, 80 | format: selectedFormat 81 | }); 82 | }); 83 | } 84 | 85 | function stream(ytstream, info, options){ 86 | if(typeof info !== 'object' && typeof info !== 'string') throw new Error(`Info is a required parameter and must be an object or a string`); 87 | var _options = typeof options === 'object' ? options : {}; 88 | var _info = info; 89 | return new Promise(async (resolve, reject) => { 90 | var stream_res; 91 | if(typeof info === 'string'){ 92 | if(!validate(ytstream, info)) return reject(`URL is not a valid YouTube URL`); 93 | try{ 94 | _info = await getInfo(ytstream, info); 95 | if(typeof _info.clientInfo === 'object' && _info.clientInfo !== null && _info.formats.filter(f => Object.keys(f).indexOf('url') < 0).length === 0) _info.formats.map(f => f.url += "&cver="+_info.clientInfo.context.client.clientVersion) 96 | else { 97 | const _ci = await cipher.format_decipher(_info.formats, _info.cver, _info.html5player, ytstream.agent); 98 | _info.formats = _ci; 99 | } 100 | stream_res = await getStreamURL(_info, _options); 101 | } catch (err) { 102 | return reject(err); 103 | } 104 | } else if(info instanceof Data){ 105 | try{ 106 | if(typeof _info.clientInfo === 'object' && _info.clientInfo !== null) _info.formats.map(f => f.url += "&cver="+_info.clientInfo.context.client.clientVersion) 107 | else { 108 | const _ci = await cipher.format_decipher(_info.formats, _info.cver, _info.html5player, ytstream.agent); 109 | _info.formats = _ci; 110 | } 111 | stream_res = await getStreamURL(_info, _options); 112 | } catch (err) { 113 | return reject(err); 114 | } 115 | } else return reject(`Invalid info has been parsed to the stream function`); 116 | const stream = new Stream(ytstream, stream_res.url, { 117 | highWaterMark: _options['highWaterMark'] || undefined, 118 | duration: _info.duration, 119 | contentLength: stream_res.contentLength, 120 | type: stream_res.type, 121 | quality: stream_res.quality, 122 | video_url: _info.url, 123 | req_type: stream_res.req_type, 124 | container: stream_res.container, 125 | download: typeof _options['download'] === 'boolean' ? _options['download'] : true, 126 | format: stream_res.format, 127 | ytstream: ytstream 128 | }, _info); 129 | if(stream.ready === true){ 130 | resolve(stream); 131 | } else { 132 | stream.once('ready', () => { 133 | resolve(stream); 134 | }); 135 | 136 | stream.once('error', err => { 137 | reject(err); 138 | }); 139 | } 140 | }); 141 | } 142 | 143 | 144 | module.exports = { 145 | stream: stream 146 | }; 147 | -------------------------------------------------------------------------------- /src/stream/decipher.js: -------------------------------------------------------------------------------- 1 | const { URLSearchParams, URL } = require('url'); 2 | const { request } = require('../request/index.js'); 3 | const vm = require('vm'); 4 | 5 | async function getFunctions(html5playerfile, options){ 6 | const body = await request(html5playerfile, options, options.agent, 0, true); 7 | const functions = extractFunctions(body); 8 | return functions; 9 | }; 10 | 11 | const DECIPHER_NAME_REGEXPS = [ 12 | '\\bm=([a-zA-Z0-9$]{2,})\\(decodeURIComponent\\(h\\.s\\)\\)', 13 | '\\bc&&\\(c=([a-zA-Z0-9$]{2,})\\(decodeURIComponent\\(c\\)\\)', 14 | '(?:\\b|[^a-zA-Z0-9$])([a-zA-Z0-9$]{2,})\\s*=\\s*function\\(\\s*a\\s*\\)\\s*\\{\\s*a\\s*=\\s*a\\.split\\(\\s*""\\s*\\)', 15 | '([\\w$]+)\\s*=\\s*function\\((\\w+)\\)\\{\\s*\\2=\\s*\\2\\.split\\(""\\)\\s*;', 16 | ]; 17 | 18 | const VARIABLE_PART = '[a-zA-Z_\\$][a-zA-Z_0-9]*'; 19 | const VARIABLE_PART_DEFINE = `\\"?${VARIABLE_PART}\\"?`; 20 | const BEFORE_ACCESS = '(?:\\[\\"|\\.)'; 21 | const AFTER_ACCESS = '(?:\\"\\]|)'; 22 | const VARIABLE_PART_ACCESS = BEFORE_ACCESS + VARIABLE_PART + AFTER_ACCESS; 23 | const REVERSE_PART = ':function\\(a\\)\\{(?:return )?a\\.reverse\\(\\)\\}'; 24 | const SLICE_PART = ':function\\(a,b\\)\\{return a\\.slice\\(b\\)\\}'; 25 | const SPLICE_PART = ':function\\(a,b\\)\\{a\\.splice\\(0,b\\)\\}'; 26 | const SWAP_PART = ':function\\(a,b\\)\\{' + 27 | 'var c=a\\[0\\];a\\[0\\]=a\\[b%a\\.length\\];a\\[b(?:%a.length|)\\]=c(?:;return a)?\\}'; 28 | 29 | const DECIPHER_REGEXP = `function(?: ${VARIABLE_PART})?\\(a\\)\\{` + 30 | `a=a\\.split\\(""\\);\\s*` + 31 | `((?:(?:a=)?${VARIABLE_PART}${VARIABLE_PART_ACCESS}\\(a,\\d+\\);)+)` + 32 | `return a\\.join\\(""\\)` + 33 | `\\}`; 34 | 35 | const HELPER_REGEXP = `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}),?\\n?)+)\\};`; 40 | 41 | const SCVR = '[a-zA-Z0-9$_]'; 42 | const FNR = `${SCVR}+`; 43 | const AAR = '\\[(\\d+)]'; 44 | const N_TRANSFORM_NAME_REGEXPS = [ 45 | `${SCVR}+="nn"\\[\\+${ 46 | SCVR}+\\.${SCVR}+],${ 47 | SCVR}+=${SCVR 48 | }+\\.get\\(${SCVR}+\\)\\)&&\\(${ 49 | SCVR}+=(${SCVR 50 | }+)\\[(\\d+)]`, 51 | `${SCVR}+="nn"\\[\\+${ 52 | SCVR}+\\.${SCVR}+],${ 53 | SCVR}+=${SCVR}+\\.get\\(${ 54 | SCVR}+\\)\\).+\\|\\|(${SCVR 55 | }+)\\(""\\)`, 56 | `\\(${SCVR}=String\\.fromCharCode\\(110\\),${ 57 | SCVR}=${SCVR}\\.get\\(${ 58 | SCVR}\\)\\)&&\\(${SCVR 59 | }=(${FNR})(?:${AAR})?\\(${ 60 | SCVR}\\)`, 61 | `\\.get\\("n"\\)\\)&&\\(${SCVR 62 | }=(${FNR})(?:${AAR})?\\(${ 63 | SCVR}\\)`, 64 | '(\\w+).length\\|\\|\\w+\\(""\\)', 65 | '\\w+.length\\|\\|(\\w+)\\(""\\)', 66 | ]; 67 | 68 | const N_TRANSFORM_REGEXP = 'function\\(\\s*(\\w+)\\s*\\)\\s*\\{' + 69 | 'var\\s*(\\w+)=(?:\\1\\.split\\(""\\)|String\\.prototype\\.split\\.call\\(\\1,""\\)),' + 70 | '\\s*(\\w+)=(\\[.*?]);\\s*\\3\\[\\d+]' + 71 | '(.*?try)(\\{.*?})catch\\(\\s*(\\w+)\\s*\\)\\s*\\' + 72 | '{\\s*return"enhanced_except_([A-z0-9-]+)"\\s*\\+\\s*\\1\\s*}' + 73 | '\\s*return\\s*(\\2\\.join\\(""\\)|Array\\.prototype\\.join\\.call\\(\\2,""\\))};'; 74 | 75 | function matchRegex(regex, str){ 76 | const match = str.match(new RegExp(regex, 's')); 77 | if (!match) throw new Error(`Could not match ${regex}`); 78 | return match; 79 | }; 80 | 81 | const matchFirst = (regex, str) => matchRegex(regex, str)[0]; 82 | 83 | const matchGroup1 = (regex, str) => matchRegex(regex, str)[1]; 84 | 85 | function getFuncName(body, regexps){ 86 | let fn; 87 | for (const regex of regexps) { 88 | try { 89 | fn = matchGroup1(regex, body); 90 | try { 91 | fn = matchGroup1(`${fn.replace(/\$/g, '\\$')}=\\[([a-zA-Z0-9$\\[\\]]{2,})\\]`, body); 92 | } catch (err) { 93 | } 94 | break; 95 | } catch (err) { 96 | continue; 97 | } 98 | } 99 | if (!fn || fn.includes('[')) throw Error(); 100 | return fn; 101 | }; 102 | 103 | function extractDecipherFunc(body){ 104 | try { 105 | const helperObject = matchFirst(HELPER_REGEXP, body); 106 | const decipherFunc = matchFirst(DECIPHER_REGEXP, body); 107 | const resultFunc = `var decipherFunc=${decipherFunc};`; 108 | const callerFunc = `decipherFunc(sig);`; 109 | return helperObject + resultFunc + callerFunc; 110 | } catch (e) { 111 | return null; 112 | } 113 | }; 114 | 115 | function extractDecipherWithName(body){ 116 | try { 117 | const decipherFuncName = getFuncName(body, DECIPHER_NAME_REGEXPS); 118 | const funcPattern = `(${decipherFuncName.replace(/\$/g, '\\$')}=function\\([a-zA-Z0-9_]+\\)\\{.+?\\})`; 119 | const decipherFunc = `var ${matchGroup1(funcPattern, body)};`; 120 | const helperObjectName = matchGroup1(';([A-Za-z0-9_\\$]{2,})\\.\\w+\\(', decipherFunc); 121 | const helperPattern = `(var ${helperObjectName.replace(/\$/g, '\\$')}=\\{[\\s\\S]+?\\}\\};)`; 122 | const helperObject = matchGroup1(helperPattern, body); 123 | const callerFunc = `${decipherFuncName}(sig);`; 124 | return helperObject + decipherFunc + callerFunc; 125 | } catch (e) { 126 | return null; 127 | } 128 | }; 129 | 130 | function getExtractFunctions(extractFunctions, body){ 131 | for (const extractFunction of extractFunctions) { 132 | try { 133 | const func = extractFunction(body); 134 | if (!func) continue; 135 | return new vm.Script(func); 136 | } catch (err) { 137 | continue; 138 | } 139 | } 140 | return null; 141 | }; 142 | 143 | function extractDecipher(body){ 144 | const decipherFunc = getExtractFunctions([extractDecipherWithName, extractDecipherFunc], body); 145 | if (!decipherFunc) return null; 146 | return decipherFunc; 147 | }; 148 | 149 | function extractNTransformFunc(body){ 150 | try { 151 | const nFunc = matchFirst(N_TRANSFORM_REGEXP, body); 152 | const resultFunc = `var nTransformFunc=${nFunc}`; 153 | const callerFunc = `nTransformFunc(n);`; 154 | return resultFunc + callerFunc; 155 | } catch (e) { 156 | return null; 157 | } 158 | }; 159 | 160 | function extractNTransformWithName(body){ 161 | try { 162 | const nFuncName = getFuncName(body, N_TRANSFORM_NAME_REGEXPS); 163 | const funcPattern = `(${ 164 | nFuncName.replace(/\$/g, '\\$') 165 | }=\\s*function([\\S\\s]*?\\}\\s*return (([\\w$]+?\\.join\\(""\\))|(Array\\.prototype\\.join\\.call\\([\\w$]+?,[\\n\\s]*(("")|(\\("",""\\)))\\)))\\s*\\}))`; 166 | const nTransformFunc = `var ${matchGroup1(funcPattern, body)};`; 167 | const callerFunc = `${nFuncName}(n);`; 168 | return nTransformFunc + callerFunc; 169 | } catch (e) { 170 | return null; 171 | } 172 | }; 173 | 174 | function extractNTransform(body){ 175 | const nTransformFunc = getExtractFunctions([extractNTransformFunc, extractNTransformWithName], body); 176 | if (!nTransformFunc) return null; 177 | return nTransformFunc; 178 | }; 179 | 180 | function extractFunctions(body){ 181 | return [ 182 | extractDecipher(body), 183 | extractNTransform(body), 184 | ]; 185 | } 186 | 187 | function genCPN(length){ 188 | const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890_-'; 189 | let cpn = ""; 190 | while(cpn.length < length){ 191 | cpn += chars[Math.round(Math.random() * (chars.length - 1))]; 192 | } 193 | return cpn; 194 | } 195 | 196 | function setDownloadURL(format, decipherScript, nTransformScript, cver){ 197 | if (!decipherScript) return; 198 | const decipher = url => { 199 | const args = new URLSearchParams(url); 200 | if (!args.has('s')) return args.get('url'); 201 | const components = new URL(decodeURIComponent(args.get('url'))); 202 | components.searchParams.set(args.sp || 'sig', decipherScript.runInNewContext({sig: decodeURIComponent(args.get('s'))})); 203 | return components.toString(); 204 | }; 205 | const nTransform = url => { 206 | const components = new URL(decodeURIComponent(url)); 207 | const n = components.searchParams.get('n'); 208 | if (!n || !nTransformScript) return url; 209 | components.searchParams.set('n', nTransformScript.runInNewContext({n: n})); 210 | return components.toString(); 211 | }; 212 | const cipher = !format.url; 213 | const url = format.url || format.signatureCipher || format.cipher; 214 | 215 | format.url = nTransform(cipher ? decipher(url) : url); 216 | delete format.signatureCipher; 217 | delete format.cipher; 218 | }; 219 | 220 | async function format_decipher(formats, cver, html5player, agent){ 221 | const [decipherScript, nTransformScript] = await getFunctions(html5player, {headers: {cookie: agent.jar.getCookieStringSync('https://www.youtube.com')}, agent: agent}); 222 | for(let format of formats){ 223 | format = setDownloadURL(format, decipherScript, nTransformScript, cver); 224 | } 225 | return formats; 226 | }; 227 | 228 | module.exports = { 229 | format_decipher 230 | }; 231 | -------------------------------------------------------------------------------- /src/validate.js: -------------------------------------------------------------------------------- 1 | const validate = require('./request/url.js'); 2 | 3 | function validateID(ytstream, id) { 4 | if (typeof id !== 'string') return false; 5 | let reg = /^[A-Za-z0-9_-]*$/; 6 | if(!reg.test(id)) return false; 7 | if(id.length > 16 || id.length < 8) return false; 8 | return true; 9 | } 10 | 11 | function validatePlaylistID(ytstream, id){ 12 | if(typeof id !== 'string') return false; 13 | let reg = /^[a-zA-Z0-9_-]*$/; 14 | if(!reg.test(id)) return false; 15 | if(id.length > 50 || id.length < 30) return false; 16 | return true; 17 | } 18 | 19 | function validateURL(ytstream, url) { 20 | if (typeof url !== 'string') return false; 21 | 22 | const _url = validate(url); 23 | if (!_url) return false; 24 | 25 | const hosts = ['music.youtube.com', 'youtube.com', 'youtu.be']; 26 | 27 | let ytid; 28 | 29 | const host = _url.hostname.toLowerCase().split('www.').join(''); 30 | const index = hosts.indexOf(host); 31 | if (index < 0) return false; 32 | 33 | if (!_url.pathname.toLowerCase().startsWith('/watch')) { 34 | if (hosts[index] === `youtu.be` && _url.pathname.length > 0) ytid = _url.pathname.split('/').join(''); 35 | else if (_url.pathname.startsWith('/embed/')) ytid = _url.pathname.split('/embed/').join(''); 36 | else if (_url.pathname.startsWith('/v/')) ytid = _url.pathname.split('/v/').join(''); 37 | else if(_url.pathname.startsWith('/shorts/')) ytid = _url.pathname.split('/shorts/').join(''); 38 | else if(_url.pathname.startsWith('/playlist')){ 39 | if(!_url.searchParams.get('list')) return false; 40 | ytid = _url.searchParams.get('list'); 41 | if(!validatePlaylistID(ytstream, ytid)) return false; 42 | return true; 43 | } 44 | } else { 45 | if (!_url.searchParams.get('v')) return false; 46 | ytid = _url.searchParams.get('v'); 47 | } 48 | 49 | if(!validateID(ytstream, ytid)) return false; 50 | else return true; 51 | } 52 | 53 | function validateVideoURL(ytstream, url) { 54 | if (typeof url !== 'string') return false; 55 | 56 | const _url = validate(url); 57 | if (!_url) return false; 58 | 59 | const hosts = ['music.youtube.com', 'youtube.com', 'youtu.be']; 60 | 61 | let ytid; 62 | 63 | const host = _url.hostname.toLowerCase().split('www.').join(''); 64 | const index = hosts.indexOf(host); 65 | if (index < 0) return false; 66 | 67 | if (!_url.pathname.toLowerCase().startsWith('/watch')) { 68 | if (hosts[index] === `youtu.be` && _url.pathname.length > 0) ytid = _url.pathname.split('/').join(''); 69 | else if (_url.pathname.startsWith('/embed/')) ytid = _url.pathname.split('/embed/').join(''); 70 | else if (_url.pathname.startsWith('/v/')) ytid = _url.pathname.split('/v/').join(''); 71 | else if(_url.pathname.startsWith('/shorts/')) ytid = _url.pathname.split('/shorts/').join(''); 72 | } else { 73 | if (!_url.searchParams.get('v')) return false; 74 | ytid = _url.searchParams.get('v'); 75 | } 76 | 77 | if(!validateID(ytstream, ytid)) return false; 78 | else return true; 79 | } 80 | 81 | function validatePlaylistURL(ytstream, url){ 82 | if (typeof url !== 'string') return false; 83 | 84 | const _url = validate(url); 85 | if (!_url) return false; 86 | 87 | const hosts = ['music.youtube.com', 'youtube.com', 'youtu.be']; 88 | 89 | let ytid; 90 | 91 | const host = _url.hostname.toLowerCase().split('www.').join(''); 92 | const index = hosts.indexOf(host); 93 | if (index < 0) return false; 94 | if(!_url.pathname.startsWith('/playlist')) return false; 95 | if(!_url.searchParams.get('list')) return false; 96 | 97 | ytid = _url.searchParams.get('list'); 98 | if(!validatePlaylistID(ytstream, ytid)) return false; 99 | else return true; 100 | } 101 | 102 | module.exports = { 103 | validateURL, 104 | validateID, 105 | validatePlaylistID, 106 | validatePlaylistURL, 107 | validateVideoURL 108 | }; 109 | --------------------------------------------------------------------------------