@irithell-js/yt-play 0.2.8 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,6 +7,10 @@ High-performance YouTube audio/video download engine with intelligent caching, b
7
7
  - **Bundled Binaries** - yt-dlp and aria2c included (no system dependencies)
8
8
  - **Auto-Update System** - yt-dlp updates automatically when needed
9
9
  - **Ultra Fast Downloads** - aria2c acceleration (up to 5x faster)
10
+ - **Playlist Support** - Search, extract, and resolve direct media URLs from entire playlists in batches
11
+ - **Shorts Support** - Download and convertion for more compatibility
12
+ - **Metadata Stalking** - Deep extraction of channels, videos, and live streams (real-time viewers, hidden tags, etc).
13
+ - **Native Streaming** - Get raw `Readable` streams for audio or video without saving to disk first, perfect for Discord bots or streaming directly to HTTP responses.
10
14
  - **Intelligent Caching** - TTL-based cache with automatic cleanup
11
15
  - **Smart Quality** - Auto-reduces quality for long videos (>1h)
12
16
  - **Container Ready** - Works in Docker/isolated environments with cookie support
@@ -88,6 +92,86 @@ const metadata2 = await engine.search(
88
92
  console.log(metadata2.title); // "Rick Astley - Never Gonna Give You Up"
89
93
  ```
90
94
 
95
+ ### Playlist Support
96
+
97
+ You can search and extract direct media URLs from playlists with automatic batch processing:
98
+
99
+ ```typescript
100
+ import { YtDlpClient, searchPlaylistBest } from "@irithell-js/yt-play";
101
+
102
+ // 1. Find a playlist (by search term or direct URL)
103
+ const metadata = await searchPlaylistBest("Lofi hip hop");
104
+
105
+ if (metadata) {
106
+ const ytdlp = new YtDlpClient();
107
+
108
+ // 2. Extract and resolve direct streaming URLs
109
+ const playlistInfo = await ytdlp.getPlaylistInfo(metadata.url, {
110
+ limit: 5, // Get exactly 5 valid tracks
111
+ resolveLinks: true, // Automatically resolve direct media URLs
112
+ type: "audio", // "audio", "video", or "both"
113
+ batchSize: 5, // Process 5 tracks concurrently for maximum speed
114
+ });
115
+
116
+ playlistInfo.entries.forEach((track: any) => {
117
+ console.log(`Title: ${track.title}`);
118
+ console.log(`Direct URL: ${track.directUrl}\n`);
119
+ });
120
+ }
121
+ ```
122
+
123
+ **Example Output:**
124
+
125
+ ```json
126
+ {
127
+ "id": "PLjNlQ2vXx1xbt30X8TcUfNzw_akVISXEu",
128
+ "title": "BEST Anime Openings and Endings Songs *Playlist*",
129
+ "uploader": "by - AnimeEnfermos -",
130
+ "entries": [
131
+ {
132
+ "id": "jIfogFtgV-o",
133
+ "title": "Noragami / Opening 2",
134
+ "url": "https://www.youtube.com/watch?v=jIfogFtgV-o",
135
+ "duration": 103,
136
+ "uploader": "reeaaas",
137
+ "directUrl": "https://rr2---sn-103oxu-bg0l.go..."
138
+ },
139
+ {
140
+ "id": "0YF8vecQWYs",
141
+ "title": "Crying for Rain - 美波 (Minami) MV",
142
+ "url": "https://www.youtube.com/watch?v=0YF8vecQWYs",
143
+ "duration": 254,
144
+ "uploader": "Minami",
145
+ "directUrl": "https://rr1---sn-103oxu-bg0l.go..."
146
+ },
147
+ {
148
+ "id": "pmanD_s7G3U",
149
+ "title": "Demon Slayer | OP | Gurenge by LiSA HD",
150
+ "url": "https://www.youtube.com/watch?v=pmanD_s7G3U",
151
+ "duration": 90,
152
+ "uploader": "animelab",
153
+ "directUrl": "https://rr1---sn-103oxu-bg0l.go"
154
+ },
155
+ {
156
+ "id": "atxYe-nOa9w",
157
+ "title": "One Punch Man - Official Opening - The Hero!! Set Fire to the Furious Fist",
158
+ "url": "https://www.youtube.com/watch?v=atxYe-nOa9w",
159
+ "duration": 90,
160
+ "uploader": "animelab",
161
+ "directUrl": "https://rr1---sn-103oxu-bg0l.go..."
162
+ },
163
+ {
164
+ "id": "792vg0amsuQ",
165
+ "title": "Steins;Gate 0 - op pt-br legendado",
166
+ "url": "https://www.youtube.com/watch?v=792vg0amsuQ",
167
+ "duration": 243,
168
+ "uploader": "Animes Nii-san",
169
+ "directUrl": "https://rr1---sn-103oxu-bg0l.go..."
170
+ }
171
+ ]
172
+ }
173
+ ```
174
+
91
175
  ## Configuration
92
176
 
93
177
  ### Constructor Options
@@ -181,6 +265,16 @@ const lq = new PlayEngine({
181
265
 
182
266
  ## API Reference
183
267
 
268
+ ### Exported Functions
269
+
270
+ #### `searchBest(query: string): Promise<PlayMetadata | null>`
271
+
272
+ Search for a video on YouTube or get metadata from a URL.
273
+
274
+ #### `searchPlaylistBest(query: string): Promise<PlayMetadata | null>`
275
+
276
+ Search for a playlist on YouTube or get metadata from a direct playlist URL. Returns the list ID and base URL.
277
+
184
278
  ### PlayEngine Methods
185
279
 
186
280
  #### `search(query: string): Promise<PlayMetadata | null>`
@@ -264,6 +358,101 @@ if (entry) {
264
358
  }
265
359
  ```
266
360
 
361
+ ### YtDlpClient Methods
362
+
363
+ #### `getPlaylistInfo(url: string, opts?: YtDlpPlaylistOptions): Promise<YtDlpPlaylistInfo>`
364
+
365
+ Fetches all entries from a playlist efficiently. If `resolveLinks: true` is provided in options, it will batch-resolve the direct playable streaming URLs for each valid track up to the provided `limit`.
366
+
367
+ ### StalkerEngine Methods
368
+
369
+ The `StalkerEngine` allows you to deeply inspect channels, videos, and live streams to extract valuable metadata like hidden tags, exact subscriber counts, and real-time live viewers.
370
+
371
+ ```typescript
372
+ import { YtDlpClient, StalkerEngine } from "@irithell-js/yt-play";
373
+
374
+ const client = new YtDlpClient({ cookiesFromBrowser: "firefox" });
375
+ const stalker = new StalkerEngine(client);
376
+ ```
377
+
378
+ #### `stalkVideoOrLive(url: string): Promise<YtDlpVideoMetadata>`
379
+
380
+ Extracts complete metadata from a single video or live stream.
381
+
382
+ ```typescript
383
+ const live = await stalker.stalkVideoOrLive(
384
+ "https://youtube.com/watch?v=LIVE_ID",
385
+ );
386
+
387
+ console.log(live.title);
388
+ console.log(live.view_count);
389
+
390
+ if (live.is_live) {
391
+ console.log(`Live right now with ${live.concurrent_view_count} viewers!`);
392
+ }
393
+ ```
394
+
395
+ #### `stalkChannel(url: string, opts?: StalkChannelOptions): Promise<YtDlpChannelMetadata>`
396
+
397
+ Scrapes channel data. You can target specific tabs (videos, shorts, streams, popular) and set specific ranges.
398
+
399
+ ```typescript
400
+ // Get the 5 most recent videos from a channel
401
+ const recentVideos = await stalker.stalkChannel(
402
+ "https://youtube.com/@MrBeast",
403
+ {
404
+ flat: true, // Faster extraction (basic info only)
405
+ tab: "videos", // "videos" | "shorts" | "streams" | "popular" | "featured"
406
+ endItem: 5, // Get items 1 to 5
407
+ },
408
+ );
409
+
410
+ // Get videos from index 500 to 510
411
+ const olderVideos = await stalker.stalkChannel("https://youtube.com/@MrBeast", {
412
+ flat: true,
413
+ tab: "videos",
414
+ startItem: 500,
415
+ endItem: 510,
416
+ });
417
+ ```
418
+
419
+ ### StreamEngine Methods
420
+
421
+ The `StreamEngine` bypasses disk caching completely and returns a Node.js `Readable` stream. This is ideal for Discord music bots (passing the stream directly to the voice connection) or proxying the download via an HTTP API.
422
+
423
+ It is automatically available inside the `PlayEngine` or can be initialized separately.
424
+
425
+ #### `getAudioStream(url: string, qualityKbps?: number): Promise<StreamInfo>`
426
+
427
+ Returns a readable audio stream (defaults to 128kbps M4A).
428
+
429
+ ```typescript
430
+ import fs from "node:fs";
431
+
432
+ const { stream, quality, format } = await engine.stream.getAudioStream(
433
+ "https://youtube.com/watch?v=VIDEO_ID",
434
+ 128, // preferred kbps
435
+ );
436
+
437
+ console.log(`Streaming ${quality} in ${format} format...`);
438
+
439
+ // Pipe directly to a file, an HTTP response, or Discord voice
440
+ stream.pipe(fs.createWriteStream("output.m4a"));
441
+ ```
442
+
443
+ #### `getVideoStream(url: string, qualityP?: number): Promise<StreamInfo>`
444
+
445
+ Returns a readable video stream (defaults to 720p MP4).
446
+
447
+ ```typescript
448
+ const { stream, quality, format } = await engine.stream.getVideoStream(
449
+ "https://youtube.com/watch?v=VIDEO_ID",
450
+ 1080, // preferred height (1080p, 720p, etc)
451
+ );
452
+
453
+ stream.pipe(fs.createWriteStream("output.mp4"));
454
+ ```
455
+
267
456
  ## Auto-Update System
268
457
 
269
458
  The engine includes an intelligent auto-update system for yt-dlp:
@@ -467,7 +656,23 @@ Issues and PRs welcome!
467
656
 
468
657
  Deprecated versions have been removed to prevent errors during use.
469
658
 
470
- ### 0.2.8 (Latest)
659
+ ### 0.3.3 (Latest)
660
+
661
+ - Added `StalkerEngine` for deep metadata extraction from videos, live streams, and entire channels.
662
+ - Added advanced channel scraping with support for specific tabs (`videos`, `shorts`, `streams`, `popular`) and precise item ranges.
663
+ - Added real-time live stream metadata detection (`is_live`, `concurrent_view_count`, etc.).
664
+ - Added `StreamEngine` to return raw Node.js `Readable` streams for audio and video, bypassing disk storage entirely (ideal for Discord bots and HTTP streaming).
665
+ - Improved error handling to gracefully return empty arrays when a requested channel tab does not exist instead of crashing.
666
+
667
+ ### 0.3.2
668
+
669
+ - Added full support for YouTube Playlists (search by name or URL via `searchPlaylistBest`)
670
+ - Added fast batch processing to resolve direct media URLs from playlists (`resolveLinks`) with auto-replenishment
671
+ - Fixed race conditions in the `PlayEngine` auto-update system during concurrent downloads
672
+ - Fixed binary setup scripts to use atomic downloads (temp files), preventing missing or corrupted binaries on connection drops
673
+ - Added automatic cleanup of leftover `.part` and `.ytdl` temporary files on download failure or timeout
674
+
675
+ ### 0.2.8
471
676
 
472
677
  - Fixed Codec for shorts to work in all plataforms
473
678
 
package/dist/index.cjs CHANGED
@@ -1,6 +1,7 @@
1
- "use strict";var L=Object.create;var b=Object.defineProperty;var R=Object.getOwnPropertyDescriptor;var K=Object.getOwnPropertyNames;var q=Object.getPrototypeOf,N=Object.prototype.hasOwnProperty;var W=(o,t)=>{for(var r in t)b(o,r,{get:t[r],enumerable:!0})},z=(o,t,r,e)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of K(t))!N.call(o,i)&&i!==r&&b(o,i,{get:()=>t[i],enumerable:!(e=R(t,i))||e.enumerable});return o};var u=(o,t,r)=>(r=o!=null?L(q(o)):{},z(t||!o||!o.__esModule?b(r,"default",{value:o,enumerable:!0}):r,o)),J=o=>z(b({},"__esModule",{value:!0}),o);var tt={};W(tt,{PlayEngine:()=>S,YtDlpClient:()=>w,getYouTubeVideoId:()=>x,normalizeYoutubeUrl:()=>f,searchBest:()=>D});module.exports=J(tt);var p=u(require("fs"),1),h=u(require("path"),1),V=require("child_process");var $=u(require("fs"),1),P=class{constructor(t){this.opts=t}store=new Map;cleanupTimer;get(t){return this.store.get(t)}set(t,r){this.store.set(t,r)}has(t){return this.store.has(t)}delete(t){this.cleanupEntry(t),this.store.delete(t)}markLoading(t,r){let e=this.store.get(t);e&&(e.loading=r)}setFile(t,r,e){let i=this.store.get(t);i&&(i[r]=e)}cleanupExpired(t=Date.now()){let r=0;for(let[e,i]of this.store.entries())t>i.expiresAt&&(this.delete(e),r++);return r}start(){this.cleanupTimer||(this.cleanupTimer=setInterval(()=>{this.cleanupExpired(Date.now())},this.opts.cleanupIntervalMs),this.cleanupTimer.unref())}stop(){this.cleanupTimer&&(clearInterval(this.cleanupTimer),this.cleanupTimer=void 0)}cleanupEntry(t){let r=this.store.get(t);r&&["audio","video"].forEach(e=>{let i=r[e];if(i?.path&&$.default.existsSync(i.path))try{$.default.unlinkSync(i.path)}catch{}})}};var m=u(require("fs"),1),k=u(require("path"),1),_=u(require("os"),1);function j(o){m.default.mkdirSync(o,{recursive:!0,mode:511});try{m.default.chmodSync(o,511)}catch{}m.default.accessSync(o,m.default.constants.R_OK|m.default.constants.W_OK)}function B(o){let t=o?.trim()?o:k.default.join(_.default.tmpdir(),"yt-play"),r=k.default.resolve(t),e=k.default.join(r);return j(r),j(e),{baseDir:r,cacheDir:e}}var O=require("child_process"),l=u(require("path"),1),g=u(require("fs"),1),Q={},y;try{y=l.default.dirname(new URL(Q.url).pathname)}catch{y=typeof y<"u"?y:process.cwd()}var w=class{binaryPath;ffmpegPath;aria2cPath;timeoutMs;useAria2c;concurrentFragments;cookiesPath;cookiesFromBrowser;constructor(t={}){this.binaryPath=t.binaryPath||this.detectYtDlp(),this.ffmpegPath=t.ffmpegPath,this.timeoutMs=t.timeoutMs??3e5,this.concurrentFragments=t.concurrentFragments??5,this.cookiesPath=t.cookiesPath,this.cookiesFromBrowser=t.cookiesFromBrowser,this.aria2cPath=t.aria2cPath||this.detectAria2c(),this.useAria2c=t.useAria2c??!!this.aria2cPath}detectYtDlp(){let t=l.default.resolve(y,"../.."),r=[l.default.join(t,"bin","yt-dlp"),l.default.join(t,"bin","yt-dlp.exe")];for(let e of r)if(g.default.existsSync(e))return e;try{let{execSync:e}=require("child_process"),i=process.platform==="win32"?"where yt-dlp":"which yt-dlp",n=e(i,{encoding:"utf-8"}).trim();if(n)return n.split(`
2
- `)[0]}catch{}return"yt-dlp"}detectAria2c(){let t=l.default.resolve(y,"../.."),r=[l.default.join(t,"bin","aria2c"),l.default.join(t,"bin","aria2c.exe")];for(let e of r)if(g.default.existsSync(e))return e;try{let{execSync:e}=require("child_process"),i=process.platform==="win32"?"where aria2c":"which aria2c",n=e(i,{encoding:"utf-8"}).trim();if(n)return n.split(`
3
- `)[0]}catch{}}async exec(t){return new Promise((r,e)=>{let i=[...t];this.ffmpegPath&&(i=["--ffmpeg-location",this.ffmpegPath,...i]),this.cookiesPath&&g.default.existsSync(this.cookiesPath)&&(i=["--cookies",this.cookiesPath,...i]),this.cookiesFromBrowser&&(i=["--cookies-from-browser",this.cookiesFromBrowser,...i]);let n=(0,O.spawn)(this.binaryPath,i,{stdio:["ignore","pipe","pipe"]}),a="",s="";n.stdout.on("data",d=>{a+=d.toString()}),n.stderr.on("data",d=>{s+=d.toString()});let c=setTimeout(()=>{n.kill("SIGKILL"),e(new Error(`yt-dlp timeout after ${this.timeoutMs}ms`))},this.timeoutMs);n.on("close",d=>{clearTimeout(c),d===0?r(a):e(new Error(`yt-dlp exited with code ${d}. stderr: ${s.slice(0,500)}`))}),n.on("error",d=>{clearTimeout(c),e(d)})})}async getInfo(t){let r=await this.exec(["-J","--no-warnings","--no-playlist",t]);return JSON.parse(r)}buildOptimizationArgs(){let t=["--no-warnings","--no-playlist","--no-check-certificates","--concurrent-fragments",String(this.concurrentFragments)];return this.useAria2c&&this.aria2cPath&&(t.push("--downloader",this.aria2cPath),t.push("--downloader-args","aria2c:-x 16 -s 16 -k 1M")),t}async getAudio(t,r,e){let i=await this.getInfo(t),a=["-f","bestaudio[ext=m4a]/bestaudio/best","-o",e,...this.buildOptimizationArgs(),t];if(await this.exec(a),!g.default.existsSync(e))throw new Error(`yt-dlp failed to create audio file: ${e}`);let s=this.formatDuration(i.duration);return{title:i.title,author:i.uploader,duration:s,quality:`${r}kbps m4a`,filename:l.default.basename(e),downloadUrl:e}}async getVideo(t,r,e){let i=await this.getInfo(t),a=["-f",`bestvideo[height<=${r}][ext=mp4][vcodec^=avc]+bestaudio[ext=m4a]/bestvideo[height<=${r}][vcodec!=av1][vcodec!=vp9]+bestaudio/best[height<=${r}]`,"--merge-output-format","mp4","-o",e,...this.buildOptimizationArgs(),t];if(await this.exec(a),!g.default.existsSync(e))throw new Error(`yt-dlp failed to create video file: ${e}`);let s=this.formatDuration(i.duration);return{title:i.title,author:i.uploader,duration:s,quality:`${r}p H.264`,filename:l.default.basename(e),downloadUrl:e}}formatDuration(t){if(!t)return"0:00";let r=Math.floor(t/3600),e=Math.floor(t%3600/60),i=Math.floor(t%60);return r>0?`${r}:${e.toString().padStart(2,"0")}:${i.toString().padStart(2,"0")}`:`${e}:${i.toString().padStart(2,"0")}`}};var M=u(require("yt-search"),1);function H(o){let t=(o||"").trim(),r=[...t.matchAll(/\[[^\]]*\]\((https?:\/\/[^)\s]+)\)/gi)];return r.length>0?r[0][1].trim():(t=t.replace(/^<([^>]+)>$/,"$1").trim(),t=t.replace(/^["'`](.*)["'`]$/,"$1").trim(),t)}function x(o){let t=/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})(?:[?&]|$)/i,r=(o||"").match(t);return r?r[1]:null}function f(o){let t=H(o),r=t.match(/https?:\/\/[^\s)]+/i)?.[0]??t,e=x(r);return e?`https://www.youtube.com/watch?v=${e}`:null}async function D(o){let t=x(o);if(t){let a=await(0,M.default)({videoId:t});if(!a)return null;let s=a.duration?.seconds??0,c=f(a.url)??a.url;return{title:a.title||"Untitled",author:a.author?.name||void 0,duration:a.duration?.timestamp||void 0,thumb:a.image||a.thumbnail||void 0,videoId:a.videoId,url:c,durationSeconds:s}}let e=(await(0,M.default)(o))?.videos?.[0];if(!e)return null;let i=e.duration?.seconds??0,n=f(e.url)??e.url;return{title:e.title||"Untitled",author:e.author?.name||void 0,duration:e.duration?.timestamp||void 0,thumb:e.image||e.thumbnail||void 0,videoId:e.videoId,url:n,durationSeconds:i}}var C={},A=[320,256,192,128,96,64],T=[1080,720,480,360],G=36e5;function v(o,t){return t.includes(o)?o:t[0]}function I(o){return(o||"").replace(/[\\/:*?"<>|]/g,"").replace(/[^\w\s-]/gi,"").trim().replace(/\s+/g," ").substring(0,100)}function Z(){try{return(0,V.execSync)("which node",{encoding:"utf-8"}).trim()||null}catch{return process.execPath||null}}function X(o,t,r){try{let e;if(o)e=h.default.dirname(o);else try{let s=new URL(C.url);e=h.default.join(h.default.dirname(s.pathname),"..","bin")}catch{e=h.default.join(__dirname,"..","bin")}if(!p.default.existsSync(e))return;let i=h.default.join(e,"yt-dlp.conf"),n=[],a=Z();a&&n.push(`--js-runtimes node:${a}`),n.push("--remote-components ejs:npm"),t&&p.default.existsSync(t)?n.push(`--cookies ${t}`):r&&n.push(`--cookies-from-browser ${r}`),p.default.writeFileSync(i,n.join(`
1
+ "use strict";var W=Object.create;var I=Object.defineProperty;var Q=Object.getOwnPropertyDescriptor;var q=Object.getOwnPropertyNames;var H=Object.getPrototypeOf,Z=Object.prototype.hasOwnProperty;var G=(n,t)=>{for(var e in t)I(n,e,{get:t[e],enumerable:!0})},O=(n,t,e,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of q(t))!Z.call(n,i)&&i!==e&&I(n,i,{get:()=>t[i],enumerable:!(r=Q(t,i))||r.enumerable});return n};var h=(n,t,e)=>(e=n!=null?W(H(n)):{},O(t||!n||!n.__esModule?I(e,"default",{value:n,enumerable:!0}):e,n)),X=n=>O(I({},"__esModule",{value:!0}),n);var ot={};G(ot,{PlayEngine:()=>T,StalkerEngine:()=>A,StreamEngine:()=>v,YtDlpClient:()=>P,getYouTubeVideoId:()=>$,normalizeYoutubeUrl:()=>y,searchBest:()=>C,searchPlaylistBest:()=>z});module.exports=X(ot);var m=h(require("fs"),1),g=h(require("path"),1),J=require("child_process");var U=h(require("fs"),1),k=class{constructor(t){this.opts=t}store=new Map;cleanupTimer;get(t){return this.store.get(t)}set(t,e){this.store.set(t,e)}has(t){return this.store.has(t)}delete(t){this.cleanupEntry(t),this.store.delete(t)}markLoading(t,e){let r=this.store.get(t);r&&(r.loading=e)}setFile(t,e,r){let i=this.store.get(t);i&&(i[e]=r)}cleanupExpired(t=Date.now()){let e=0;for(let[r,i]of this.store.entries())t>i.expiresAt&&(this.delete(r),e++);return e}start(){this.cleanupTimer||(this.cleanupTimer=setInterval(()=>{this.cleanupExpired(Date.now())},this.opts.cleanupIntervalMs),this.cleanupTimer.unref())}stop(){this.cleanupTimer&&(clearInterval(this.cleanupTimer),this.cleanupTimer=void 0)}cleanupEntry(t){let e=this.store.get(t);e&&["audio","video"].forEach(r=>{let i=e[r];if(i?.path&&U.default.existsSync(i.path))try{U.default.unlinkSync(i.path)}catch{}})}};var w=h(require("fs"),1),M=h(require("path"),1),_=h(require("os"),1);function B(n){w.default.mkdirSync(n,{recursive:!0,mode:511});try{w.default.chmodSync(n,511)}catch{}w.default.accessSync(n,w.default.constants.R_OK|w.default.constants.W_OK)}function j(n){let t=n?.trim()?n:M.default.join(_.default.tmpdir(),"yt-play"),e=M.default.resolve(t),r=M.default.join(e);return B(e),B(r),{baseDir:e,cacheDir:r}}var V=require("child_process"),p=h(require("path"),1),u=h(require("fs"),1),tt={},b;try{b=p.default.dirname(new URL(tt.url).pathname)}catch{b=typeof b<"u"?b:process.cwd()}var P=class{binaryPath;ffmpegPath;aria2cPath;timeoutMs;useAria2c;concurrentFragments;cookiesPath;cookiesFromBrowser;constructor(t={}){this.binaryPath=t.binaryPath||this.detectYtDlp(),this.ffmpegPath=t.ffmpegPath,this.timeoutMs=t.timeoutMs??3e5,this.concurrentFragments=t.concurrentFragments??5,this.cookiesPath=t.cookiesPath,this.cookiesFromBrowser=t.cookiesFromBrowser,this.aria2cPath=t.aria2cPath||this.detectAria2c(),this.useAria2c=t.useAria2c??!!this.aria2cPath}detectYtDlp(){let t=p.default.resolve(b,"../.."),e=[p.default.join(t,"bin","yt-dlp"),p.default.join(t,"bin","yt-dlp.exe")];for(let r of e)if(u.default.existsSync(r))return r;try{let{execSync:r}=require("child_process"),i=process.platform==="win32"?"where yt-dlp":"which yt-dlp",a=r(i,{encoding:"utf-8"}).trim();if(a)return a.split(`
2
+ `)[0]}catch{}return"yt-dlp"}detectAria2c(){let t=p.default.resolve(b,"../.."),e=[p.default.join(t,"bin","aria2c"),p.default.join(t,"bin","aria2c.exe")];for(let r of e)if(u.default.existsSync(r))return r;try{let{execSync:r}=require("child_process"),i=process.platform==="win32"?"where aria2c":"which aria2c",a=r(i,{encoding:"utf-8"}).trim();if(a)return a.split(`
3
+ `)[0]}catch{}}async exec(t){return new Promise((e,r)=>{let i=[...t];this.ffmpegPath&&(i=["--ffmpeg-location",this.ffmpegPath,...i]),this.cookiesPath&&u.default.existsSync(this.cookiesPath)&&(i=["--cookies",this.cookiesPath,...i]),this.cookiesFromBrowser&&(i=["--cookies-from-browser",this.cookiesFromBrowser,...i]);let a=(0,V.spawn)(this.binaryPath,i,{stdio:["ignore","pipe","pipe"]}),o="",s="";a.stdout.on("data",l=>{o+=l.toString()}),a.stderr.on("data",l=>{s+=l.toString()});let c=setTimeout(()=>{a.kill("SIGKILL"),r(new Error(`yt-dlp timeout after ${this.timeoutMs}ms`))},this.timeoutMs);a.on("close",l=>{clearTimeout(c),l===0?e(o):r(new Error(`yt-dlp exited with code ${l}. stderr: ${s.slice(0,500)}`))}),a.on("error",l=>{clearTimeout(c),r(l)})})}async getInfo(t){let e=await this.exec(["-J","--no-warnings","--no-playlist",t]);return JSON.parse(e)}async getPlaylistInfo(t,e={}){let r=await this.exec(["-J","--flat-playlist","--no-warnings",t]),i=JSON.parse(r),a=(i.entries||[]).filter(o=>{let s=o.title||"";return o.id&&s&&!s.includes("[Deleted video]")&&!s.includes("[Private video]")}).map(o=>({id:o.id,title:o.title,url:`https://www.youtube.com/watch?v=${o.id}`,duration:o.duration,uploader:o.uploader||i.uploader}));return e.resolveLinks?a=await this.resolvePlaylistItems(a,e.limit&&e.limit>0?e.limit:a.length,e.type||"audio",e.batchSize||5):e.limit&&e.limit>0&&(a=a.slice(0,e.limit)),{id:i.id,title:i.title,uploader:i.uploader,entries:a}}async getDirectUrl(t,e="audio"){let r=e==="audio"?"bestaudio[ext=m4a]/bestaudio/best":"best[ext=mp4]/best";return(await this.exec(["-f",r,"-g","--no-warnings",t])).trim().split(`
4
+ `)[0]}async resolvePlaylistItems(t,e,r="audio",i=5){let a=[],o=0;for(;a.length<e&&o<t.length;){let s=e-a.length,c=Math.min(i,s,t.length-o),l=t.slice(o,o+c);o+=c;let f=l.map(async d=>{try{let D=await this.getDirectUrl(d.url,r);return{...d,directUrl:D}}catch{return null}}),S=await Promise.all(f);for(let d of S)d&&a.length<e&&a.push(d)}return a}buildOptimizationArgs(){let t=["--no-warnings","--no-playlist","--no-check-certificates","--concurrent-fragments",String(this.concurrentFragments)];return this.useAria2c&&this.aria2cPath&&(t.push("--downloader",this.aria2cPath),t.push("--downloader-args","aria2c:-x 16 -s 16 -k 1M")),t}async getAudio(t,e,r){let i=await this.getInfo(t),o=["-f","bestaudio[ext=m4a]/bestaudio/best","-o",r,...this.buildOptimizationArgs(),t];try{await this.exec(o)}catch(c){throw[r,`${r}.part`,`${r}.ytdl`].forEach(l=>{if(u.default.existsSync(l))try{u.default.unlinkSync(l)}catch{}}),c}if(!u.default.existsSync(r))throw new Error(`yt-dlp failed to create audio file: ${r}`);let s=this.formatDuration(i.duration);return{title:i.title,author:i.uploader,duration:s,quality:`${e}kbps m4a`,filename:p.default.basename(r),downloadUrl:r}}async getVideo(t,e,r){let i=await this.getInfo(t),o=["-f",`bestvideo[height<=${e}][ext=mp4][vcodec^=avc]+bestaudio[ext=m4a]/bestvideo[height<=${e}][vcodec!=av1][vcodec!=vp9]+bestaudio/best[height<=${e}]`,"--merge-output-format","mp4","-o",r,...this.buildOptimizationArgs(),t];try{await this.exec(o)}catch(c){throw[r,`${r}.part`,`${r}.ytdl`].forEach(l=>{if(u.default.existsSync(l))try{u.default.unlinkSync(l)}catch{}}),c}if(!u.default.existsSync(r))throw new Error(`yt-dlp failed to create video file: ${r}`);let s=this.formatDuration(i.duration);return{title:i.title,author:i.uploader,duration:s,quality:`${e}p H.264`,filename:p.default.basename(r),downloadUrl:r}}formatDuration(t){if(!t)return"0:00";let e=Math.floor(t/3600),r=Math.floor(t%3600/60),i=Math.floor(t%60);return e>0?`${e}:${r.toString().padStart(2,"0")}:${i.toString().padStart(2,"0")}`:`${r}:${i.toString().padStart(2,"0")}`}};var x=h(require("yt-search"),1);function et(n){let t=(n||"").trim(),e=[...t.matchAll(/\[[^\]]*\]\((https?:\/\/[^)\s]+)\)/gi)];return e.length>0?e[0][1].trim():(t=t.replace(/^<([^>]+)>$/,"$1").trim(),t=t.replace(/^["'`](.*)["'`]$/,"$1").trim(),t)}function $(n){let t=/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})(?:[?&]|$)/i,e=(n||"").match(t);return e?e[1]:null}function rt(n){let t=/[?&]list=([a-zA-Z0-9_-]+)/i,e=(n||"").match(t);return e?e[1]:null}function y(n){let t=et(n),e=t.match(/https?:\/\/[^\s)]+/i)?.[0]??t,r=$(e);return r?`https://www.youtube.com/watch?v=${r}`:null}async function C(n){let t=$(n);if(t){let o=await(0,x.default)({videoId:t});if(!o)return null;let s=o.duration?.seconds??0,c=y(o.url)??o.url;return{title:o.title||"Untitled",author:o.author?.name||void 0,duration:o.duration?.timestamp||void 0,thumb:o.image||o.thumbnail||void 0,videoId:o.videoId,url:c,durationSeconds:s}}let r=(await(0,x.default)(n))?.videos?.[0];if(!r)return null;let i=r.duration?.seconds??0,a=y(r.url)??r.url;return{title:r.title||"Untitled",author:r.author?.name||void 0,duration:r.duration?.timestamp||void 0,thumb:r.image||r.thumbnail||void 0,videoId:r.videoId,url:a,durationSeconds:i}}async function z(n){let t=rt(n);if(t)try{let i=await(0,x.default)({listId:t});return i?{title:i.title||"Untitled",author:i.author?.name||void 0,duration:void 0,durationSeconds:0,thumb:i.image||i.thumbnail||void 0,videoId:t,url:i.url||`https://www.youtube.com/playlist?list=${t}`}:null}catch{}let r=(await(0,x.default)(n))?.playlists?.[0];return r?{title:r.title||"Untitled",author:r.author?.name||void 0,duration:void 0,durationSeconds:0,thumb:r.image||r.thumbnail||void 0,videoId:r.listId,url:r.url||`https://www.youtube.com/playlist?list=${r.listId}`}:null}var E=require("child_process"),v=class{constructor(t){this.ytdlpClient=t}getBinaryPath(){return this.ytdlpClient.binaryPath}async getAudioStream(t,e=128){let i=["-f","bestaudio[ext=m4a]/bestaudio/best","-o","-","--no-warnings","--no-playlist",t],a=(0,E.spawn)(this.getBinaryPath(),i,{stdio:["ignore","pipe","pipe"]});return a.stderr.on("data",()=>{}),{stream:a.stdout,quality:`${e}kbps`,format:"m4a"}}async getVideoStream(t,e=720){let i=["-f",`best[height<=${e}][ext=mp4]/best`,"-o","-","--no-warnings","--no-playlist",t],a=(0,E.spawn)(this.getBinaryPath(),i,{stdio:["ignore","pipe","pipe"]});return a.stderr.on("data",()=>{}),{stream:a.stdout,quality:`${e}p`,format:"mp4"}}};var F={},R=[320,256,192,128,96,64],L=[1080,720,480,360],it=36e5;function Y(n,t){return t.includes(n)?n:t[0]}function K(n){return(n||"").replace(/[\\/:*?"<>|]/g,"").replace(/[^\w\s-]/gi,"").trim().replace(/\s+/g," ").substring(0,100)}function nt(){try{return(0,J.execSync)("which node",{encoding:"utf-8"}).trim()||null}catch{return process.execPath||null}}function at(n,t,e){try{let r;if(n)r=g.default.dirname(n);else try{let s=new URL(F.url);r=g.default.join(g.default.dirname(s.pathname),"..","bin")}catch{r=g.default.join(__dirname,"..","bin")}if(!m.default.existsSync(r))return;let i=g.default.join(r,"yt-dlp.conf"),a=[],o=nt();o&&a.push(`--js-runtimes node:${o}`),a.push("--remote-components ejs:npm"),t&&m.default.existsSync(t)?a.push(`--cookies ${t}`):e&&a.push(`--cookies-from-browser ${e}`),m.default.writeFileSync(i,a.join(`
4
5
  `)+`
5
- `,"utf-8")}catch(e){console.warn("Failed to create yt-dlp.conf:",e)}}var S=class o{opts;paths;cache;ytdlp;static lastUpdateCheck=0;static isUpdating=!1;constructor(t={}){this.opts={ttlMs:t.ttlMs??3*6e4,maxPreloadDurationSeconds:t.maxPreloadDurationSeconds??1200,preferredAudioKbps:t.preferredAudioKbps??128,preferredVideoP:t.preferredVideoP??720,preloadBuffer:t.preloadBuffer??!0,cleanupIntervalMs:t.cleanupIntervalMs??3e4,concurrentFragments:t.concurrentFragments??5,useAria2c:t.useAria2c,logger:t.logger},this.paths=B(t.cacheDir),this.cache=new P({cleanupIntervalMs:this.opts.cleanupIntervalMs}),this.cache.start(),X(t.ytdlpBinaryPath,t.cookiesPath,t.cookiesFromBrowser),this.ytdlp=new w({binaryPath:t.ytdlpBinaryPath,ffmpegPath:t.ffmpegPath,aria2cPath:t.aria2cPath,useAria2c:this.opts.useAria2c,concurrentFragments:this.opts.concurrentFragments,timeoutMs:t.ytdlpTimeoutMs??3e5,cookiesPath:t.cookiesPath,cookiesFromBrowser:t.cookiesFromBrowser}),this.backgroundUpdateCheck()}backgroundUpdateCheck(){let t=Date.now();t-o.lastUpdateCheck<G||(async()=>{try{o.lastUpdateCheck=t;let r=new URL("../scripts/check-ytdlp-update.mjs",C.url),{checkAndUpdate:e}=await import(r.href);await e()&&this.opts.logger?.info?.("\u2713 yt-dlp updated to latest version")}catch{this.opts.logger?.debug?.("Update check failed (will retry later)")}})()}async forceUpdateCheck(){if(o.isUpdating){this.opts.logger?.info?.("Update already in progress, skipping...");return}try{o.isUpdating=!0,this.opts.logger?.warn?.("<!> Download failed. Forcing yt-dlp update check...");let t=new URL("../scripts/check-ytdlp-update.mjs",C.url),{checkAndUpdate:r}=await import(t.href);await r()?(this.opts.logger?.info?.("\u2713 yt-dlp updated successfully"),o.lastUpdateCheck=Date.now()):this.opts.logger?.info?.("yt-dlp is already up to date")}catch(t){this.opts.logger?.error?.("Failed to update yt-dlp:",t)}finally{o.isUpdating=!1}}generateRequestId(t="play"){return`${t}_${Date.now()}_${Math.random().toString(36).slice(2,8)}`}async search(t){return D(t)}getFromCache(t){return this.cache.get(t)}async preload(t,r){let e=f(t.url);if(!e)throw new Error("Invalid YouTube URL.");let i=t.durationSeconds>3600;t.durationSeconds>this.opts.maxPreloadDurationSeconds&&this.opts.logger?.warn?.(`Video too long for preload (${Math.floor(t.durationSeconds/60)}min). Will use direct download with reduced quality.`);let n={...t,url:e};this.cache.set(r,{metadata:n,audio:null,video:null,expiresAt:Date.now()+this.opts.ttlMs,loading:!0});let a=i?96:v(this.opts.preferredAudioKbps,A),s=this.preloadOne(r,"audio",e,a),c=i?[s]:[s,this.preloadOne(r,"video",e,v(this.opts.preferredVideoP,T))];i&&this.opts.logger?.info?.(`Long video detected (${Math.floor(t.durationSeconds/60)}min). Audio only mode (96kbps).`),await Promise.allSettled(c),this.cache.markLoading(r,!1)}async getOrDownload(t,r){let e=this.cache.get(t);if(!e)throw new Error("Request not found (cache miss).");let i=e[r];if(i?.path&&p.default.existsSync(i.path)&&i.size>0)return{metadata:e.metadata,file:i,direct:!1};let n=f(e.metadata.url);if(!n)throw new Error("Invalid YouTube URL.");let a=await this.downloadDirect(r,n);return{metadata:e.metadata,file:a,direct:!0}}async waitCache(t,r,e=8e3,i=500){let n=Date.now();for(;Date.now()-n<e;){let s=this.cache.get(t)?.[r];if(s?.path&&p.default.existsSync(s.path)&&s.size>0)return s;await new Promise(c=>setTimeout(c,i))}return null}cleanup(t){this.cache.delete(t)}async preloadOne(t,r,e,i){try{let n=I(`temp_${Date.now()}`),s=`${r}_${t}_${n}.${r==="audio"?"m4a":"mp4"}`,c=h.default.join(this.paths.cacheDir,s),d=r==="audio"?await this.ytdlp.getAudio(e,i,c):await this.ytdlp.getVideo(e,i,c),U=p.default.statSync(c).size,E;this.opts.preloadBuffer&&(E=await p.default.promises.readFile(c));let Y={path:c,size:U,info:{quality:d.quality},buffer:E};this.cache.setFile(t,r,Y),this.opts.logger?.debug?.(`preloaded ${r} ${U} bytes: ${s}`)}catch(n){throw this.opts.logger?.error?.(`preload ${r} failed`,n),await this.forceUpdateCheck(),n}}async downloadDirect(t,r){try{let e=v(this.opts.preferredAudioKbps,A),i=v(this.opts.preferredVideoP,T),n=t==="audio"?"m4a":"mp4",a=I(`direct_${Date.now()}`),s=h.default.join(this.paths.cacheDir,`${t}_${a}.${n}`),c=t==="audio"?await this.ytdlp.getAudio(r,e,s):await this.ytdlp.getVideo(r,i,s),d=p.default.statSync(s);return{path:s,size:d.size,info:{quality:c.quality}}}catch(e){this.opts.logger?.error?.("Direct download failed:",e),await this.forceUpdateCheck(),this.opts.logger?.info?.(">> Retrying download after update...");let i=v(this.opts.preferredAudioKbps,A),n=v(this.opts.preferredVideoP,T),a=t==="audio"?"m4a":"mp4",s=I(`direct_retry_${Date.now()}`),c=h.default.join(this.paths.cacheDir,`${t}_${s}.${a}`),d=t==="audio"?await this.ytdlp.getAudio(r,i,c):await this.ytdlp.getVideo(r,n,c),F=p.default.statSync(c);return{path:c,size:F.size,info:{quality:d.quality}}}}};0&&(module.exports={PlayEngine,YtDlpClient,getYouTubeVideoId,normalizeYoutubeUrl,searchBest});
6
+ `,"utf-8")}catch{}}var T=class n{opts;paths;cache;ytdlp;static lastUpdateCheck=0;static updatePromise=null;constructor(t={}){this.opts={ttlMs:t.ttlMs??3*6e4,maxPreloadDurationSeconds:t.maxPreloadDurationSeconds??1200,preferredAudioKbps:t.preferredAudioKbps??128,preferredVideoP:t.preferredVideoP??720,preloadBuffer:t.preloadBuffer??!0,cleanupIntervalMs:t.cleanupIntervalMs??3e4,concurrentFragments:t.concurrentFragments??5,useAria2c:t.useAria2c,logger:t.logger},this.paths=j(t.cacheDir),this.cache=new k({cleanupIntervalMs:this.opts.cleanupIntervalMs}),this.cache.start(),at(t.ytdlpBinaryPath,t.cookiesPath,t.cookiesFromBrowser),this.ytdlp=new P({binaryPath:t.ytdlpBinaryPath,ffmpegPath:t.ffmpegPath,aria2cPath:t.aria2cPath,useAria2c:this.opts.useAria2c,concurrentFragments:this.opts.concurrentFragments,timeoutMs:t.ytdlpTimeoutMs??3e5,cookiesPath:t.cookiesPath,cookiesFromBrowser:t.cookiesFromBrowser}),this.stream=new v(this.ytdlp),this.backgroundUpdateCheck()}stream;backgroundUpdateCheck(){let t=Date.now();t-n.lastUpdateCheck<it||n.updatePromise||(n.updatePromise=(async()=>{try{n.lastUpdateCheck=t;let e=new URL("../scripts/check-ytdlp-update.mjs",F.url),{checkAndUpdate:r}=await import(e.href);await r()&&this.opts.logger?.info?.("\u2713 yt-dlp updated to latest version")}catch{this.opts.logger?.debug?.("Update check failed (will retry later)")}finally{n.updatePromise=null}})())}async forceUpdateCheck(){return n.updatePromise?(this.opts.logger?.info?.("Update already in progress, waiting..."),n.updatePromise):(n.updatePromise=(async()=>{try{this.opts.logger?.warn?.("<!> Download failed. Forcing yt-dlp update check...");let t=new URL("../scripts/check-ytdlp-update.mjs",F.url),{checkAndUpdate:e}=await import(t.href);await e()&&(this.opts.logger?.info?.("\u2713 yt-dlp updated successfully"),n.lastUpdateCheck=Date.now())}catch(t){this.opts.logger?.error?.("Failed to update yt-dlp:",t)}finally{n.updatePromise=null}})(),n.updatePromise)}generateRequestId(t="play"){return`${t}_${Date.now()}_${Math.random().toString(36).slice(2,8)}`}async search(t){return C(t)}getFromCache(t){return this.cache.get(t)}async preload(t,e){let r=y(t.url);if(!r)throw new Error("Invalid YouTube URL.");let i=t.durationSeconds>3600;t.durationSeconds>this.opts.maxPreloadDurationSeconds&&this.opts.logger?.warn?.(`Video too long for preload (${Math.floor(t.durationSeconds/60)}min). Will use direct download with reduced quality.`);let a={...t,url:r};this.cache.set(e,{metadata:a,audio:null,video:null,expiresAt:Date.now()+this.opts.ttlMs,loading:!0});let o=i?96:Y(this.opts.preferredAudioKbps,R),s=this.preloadOne(e,"audio",r,o),c=i?[s]:[s,this.preloadOne(e,"video",r,Y(this.opts.preferredVideoP,L))];i&&this.opts.logger?.info?.(`Long video detected (${Math.floor(t.durationSeconds/60)}min). Audio only mode (96kbps).`),await Promise.allSettled(c),this.cache.markLoading(e,!1)}async getOrDownload(t,e){let r=this.cache.get(t);if(!r)throw new Error("Request not found (cache miss).");let i=r[e];if(i?.path&&m.default.existsSync(i.path)&&i.size>0)return{metadata:r.metadata,file:i,direct:!1};let a=y(r.metadata.url);if(!a)throw new Error("Invalid YouTube URL.");let o=await this.downloadDirect(e,a);return{metadata:r.metadata,file:o,direct:!0}}async waitCache(t,e,r=8e3,i=500){let a=Date.now();for(;Date.now()-a<r;){let s=this.cache.get(t)?.[e];if(s?.path&&m.default.existsSync(s.path)&&s.size>0)return s;await new Promise(c=>setTimeout(c,i))}return null}cleanup(t){this.cache.delete(t)}async preloadOne(t,e,r,i){try{await this._executePreload(t,e,r,i)}catch(a){this.opts.logger?.error?.(`preload ${e} failed, forcing update...`,a),await this.forceUpdateCheck(),this.opts.logger?.info?.(">> Retrying preload after update..."),await this._executePreload(t,e,r,i)}}async _executePreload(t,e,r,i){let a=Math.random().toString(36).slice(2,8),o=K(`temp_${Date.now()}_${a}`),c=`${e}_${t}_${o}.${e==="audio"?"m4a":"mp4"}`,l=g.default.join(this.paths.cacheDir,c),f=e==="audio"?await this.ytdlp.getAudio(r,i,l):await this.ytdlp.getVideo(r,i,l),d=m.default.statSync(l).size,D;this.opts.preloadBuffer&&(D=await m.default.promises.readFile(l));let N={path:l,size:d,info:{quality:f.quality},buffer:D};this.cache.setFile(t,e,N),this.opts.logger?.debug?.(`preloaded ${e} ${d} bytes: ${c}`)}async downloadDirect(t,e){try{return await this._executeDownloadDirect(t,e,!1)}catch(r){return this.opts.logger?.error?.("Direct download failed:",r),await this.forceUpdateCheck(),this.opts.logger?.info?.(">> Retrying download after update..."),await this._executeDownloadDirect(t,e,!0)}}async _executeDownloadDirect(t,e,r){let i=Y(this.opts.preferredAudioKbps,R),a=Y(this.opts.preferredVideoP,L),o=t==="audio"?"m4a":"mp4",s=r?"direct_retry":"direct",c=Math.random().toString(36).slice(2,8),l=K(`${s}_${Date.now()}_${c}`),f=g.default.join(this.paths.cacheDir,`${t}_${l}.${o}`),S=t==="audio"?await this.ytdlp.getAudio(e,i,f):await this.ytdlp.getVideo(e,a,f),d=m.default.statSync(f);return{path:f,size:d.size,info:{quality:S.quality}}}};var A=class{constructor(t){this.ytdlpClient=t}async stalkVideoOrLive(t){let e=["-J","--no-warnings","--no-playlist",t],r=await this.ytdlpClient.exec(e);return JSON.parse(r)}async stalkChannel(t,e={}){let r=["-J","--no-warnings"];e.flat&&r.push("--flat-playlist"),e.playlistItems?r.push("--playlist-items",e.playlistItems):(e.startItem&&e.startItem>0&&r.push("--playlist-start",e.startItem.toString()),e.endItem&&e.endItem>0&&r.push("--playlist-end",e.endItem.toString()));let i=t.trim().replace(/\/$/,"");e.tab&&(i=i.replace(/\/(videos|shorts|streams|popular|featured)$/i,""),e.tab!=="featured"&&(i=`${i}/${e.tab}`)),r.push(i);try{let a=await this.ytdlpClient.exec(r);return JSON.parse(a)}catch(a){let o=a.message||"";if(o.includes("This channel does not have a")||o.includes("404")||o.includes("does not exist"))return{_type:"playlist",id:i,title:"Tab Not Found",channel:i.split("/").pop()||"Unknown",channel_id:"Unknown",entries:[]};throw a}}};0&&(module.exports={PlayEngine,StalkerEngine,StreamEngine,YtDlpClient,getYouTubeVideoId,normalizeYoutubeUrl,searchBest,searchPlaylistBest});
6
7
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/core/play-engine.ts","../src/core/cache.ts","../src/core/paths.ts","../src/core/ytdlp-client.ts","../src/core/youtube.ts"],"sourcesContent":["export type {\n PlayMetadata,\n CachedFile,\n PlayEngineOptions,\n MediaType,\n DownloadInfo,\n CacheEntry,\n} from \"./core/types.js\";\n\nexport { PlayEngine } from \"./core/play-engine.js\";\nexport {\n searchBest,\n normalizeYoutubeUrl,\n getYouTubeVideoId,\n} from \"./core/youtube.js\";\nexport { YtDlpClient } from \"./core/ytdlp-client.js\";\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { execSync } from \"node:child_process\";\nimport type {\n CacheEntry,\n CachedFile,\n DownloadInfo,\n MediaType,\n PlayEngineOptions,\n PlayMetadata,\n} from \"./types.js\";\nimport { CacheStore } from \"./cache.js\";\nimport { resolvePaths } from \"./paths.js\";\nimport { YtDlpClient } from \"./ytdlp-client.js\";\nimport { normalizeYoutubeUrl, searchBest } from \"./youtube.js\";\n\nconst AUDIO_QUALITIES = [320, 256, 192, 128, 96, 64] as const;\nconst VIDEO_QUALITIES = [1080, 720, 480, 360] as const;\nconst UPDATE_CHECK_INTERVAL = 3600000;\n\nfunction pickQuality<T extends number>(\n requested: T,\n available: readonly T[],\n): T {\n return (available as readonly number[]).includes(requested)\n ? requested\n : available[0];\n}\n\nfunction sanitizeFilename(filename: string): string {\n return (filename || \"\")\n .replace(/[\\\\/:*?\"<>|]/g, \"\")\n .replace(/[^\\w\\s-]/gi, \"\")\n .trim()\n .replace(/\\s+/g, \" \")\n .substring(0, 100);\n}\n\nfunction getNodeBinaryPath(): string | null {\n try {\n const nodePath = execSync(\"which node\", { encoding: \"utf-8\" }).trim();\n return nodePath || null;\n } catch {\n return process.execPath || null;\n }\n}\n\nfunction setupYtDlpConfig(\n ytdlpBinaryPath: string | undefined,\n cookiesPath: string | undefined,\n cookiesFromBrowser: string | undefined,\n): void {\n try {\n let binaryDir: string;\n\n if (ytdlpBinaryPath) {\n binaryDir = path.dirname(ytdlpBinaryPath);\n } else {\n try {\n const moduleUrl = new URL(import.meta.url);\n binaryDir = path.join(path.dirname(moduleUrl.pathname), \"..\", \"bin\");\n } catch {\n binaryDir = path.join(__dirname, \"..\", \"bin\");\n }\n }\n\n if (!fs.existsSync(binaryDir)) {\n return;\n }\n\n const configPath = path.join(binaryDir, \"yt-dlp.conf\");\n\n const configLines: string[] = [];\n\n const nodePath = getNodeBinaryPath();\n if (nodePath) {\n configLines.push(`--js-runtimes node:${nodePath}`);\n }\n\n configLines.push(\"--remote-components ejs:npm\");\n\n if (cookiesPath && fs.existsSync(cookiesPath)) {\n configLines.push(`--cookies ${cookiesPath}`);\n } else if (cookiesFromBrowser) {\n configLines.push(`--cookies-from-browser ${cookiesFromBrowser}`);\n }\n\n fs.writeFileSync(configPath, configLines.join(\"\\n\") + \"\\n\", \"utf-8\");\n } catch (error) {\n console.warn(\"Failed to create yt-dlp.conf:\", error);\n }\n}\n\nexport class PlayEngine {\n private readonly opts: Required<\n Pick<\n PlayEngineOptions,\n | \"ttlMs\"\n | \"maxPreloadDurationSeconds\"\n | \"preferredAudioKbps\"\n | \"preferredVideoP\"\n | \"preloadBuffer\"\n | \"cleanupIntervalMs\"\n | \"concurrentFragments\"\n >\n > &\n Pick<PlayEngineOptions, \"useAria2c\" | \"logger\">;\n\n private readonly paths: { baseDir: string; cacheDir: string };\n readonly cache: CacheStore;\n private readonly ytdlp: YtDlpClient;\n\n private static lastUpdateCheck: number = 0;\n private static isUpdating: boolean = false;\n\n constructor(options: PlayEngineOptions = {}) {\n this.opts = {\n ttlMs: options.ttlMs ?? 3 * 60_000,\n maxPreloadDurationSeconds: options.maxPreloadDurationSeconds ?? 20 * 60,\n preferredAudioKbps: options.preferredAudioKbps ?? 128,\n preferredVideoP: options.preferredVideoP ?? 720,\n preloadBuffer: options.preloadBuffer ?? true,\n cleanupIntervalMs: options.cleanupIntervalMs ?? 30_000,\n concurrentFragments: options.concurrentFragments ?? 5,\n useAria2c: options.useAria2c,\n logger: options.logger,\n };\n\n this.paths = resolvePaths(options.cacheDir);\n this.cache = new CacheStore({\n cleanupIntervalMs: this.opts.cleanupIntervalMs,\n });\n this.cache.start();\n\n setupYtDlpConfig(\n options.ytdlpBinaryPath,\n options.cookiesPath,\n options.cookiesFromBrowser,\n );\n\n this.ytdlp = new YtDlpClient({\n binaryPath: options.ytdlpBinaryPath,\n ffmpegPath: options.ffmpegPath,\n aria2cPath: options.aria2cPath,\n useAria2c: this.opts.useAria2c,\n concurrentFragments: this.opts.concurrentFragments,\n timeoutMs: options.ytdlpTimeoutMs ?? 300_000,\n cookiesPath: options.cookiesPath,\n cookiesFromBrowser: options.cookiesFromBrowser,\n });\n\n this.backgroundUpdateCheck();\n }\n\n private backgroundUpdateCheck(): void {\n const now = Date.now();\n\n if (now - PlayEngine.lastUpdateCheck < UPDATE_CHECK_INTERVAL) {\n return;\n }\n\n (async () => {\n try {\n PlayEngine.lastUpdateCheck = now;\n const scriptPath = new URL(\n \"../scripts/check-ytdlp-update.mjs\",\n import.meta.url,\n );\n const { checkAndUpdate } = await import(scriptPath.href);\n const updated = await checkAndUpdate();\n\n if (updated) {\n this.opts.logger?.info?.(\"✓ yt-dlp updated to latest version\");\n }\n } catch (error) {\n this.opts.logger?.debug?.(\"Update check failed (will retry later)\");\n }\n })();\n }\n\n private async forceUpdateCheck(): Promise<void> {\n if (PlayEngine.isUpdating) {\n this.opts.logger?.info?.(\"Update already in progress, skipping...\");\n return;\n }\n\n try {\n PlayEngine.isUpdating = true;\n this.opts.logger?.warn?.(\n \"<!> Download failed. Forcing yt-dlp update check...\",\n );\n\n const scriptPath = new URL(\n \"../scripts/check-ytdlp-update.mjs\",\n import.meta.url,\n );\n const { checkAndUpdate } = await import(scriptPath.href);\n const updated = await checkAndUpdate();\n\n if (updated) {\n this.opts.logger?.info?.(\"✓ yt-dlp updated successfully\");\n PlayEngine.lastUpdateCheck = Date.now();\n } else {\n this.opts.logger?.info?.(\"yt-dlp is already up to date\");\n }\n } catch (error) {\n this.opts.logger?.error?.(\"Failed to update yt-dlp:\", error);\n } finally {\n PlayEngine.isUpdating = false;\n }\n }\n\n generateRequestId(prefix = \"play\"): string {\n return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;\n }\n\n async search(query: string): Promise<PlayMetadata | null> {\n return searchBest(query);\n }\n\n getFromCache(requestId: string): CacheEntry | undefined {\n return this.cache.get(requestId);\n }\n\n async preload(metadata: PlayMetadata, requestId: string): Promise<void> {\n const normalized = normalizeYoutubeUrl(metadata.url);\n if (!normalized) throw new Error(\"Invalid YouTube URL.\");\n\n const isLongVideo = metadata.durationSeconds > 3600;\n\n if (metadata.durationSeconds > this.opts.maxPreloadDurationSeconds) {\n this.opts.logger?.warn?.(\n `Video too long for preload (${Math.floor(metadata.durationSeconds / 60)}min). Will use direct download with reduced quality.`,\n );\n }\n\n const normalizedMeta: PlayMetadata = { ...metadata, url: normalized };\n\n this.cache.set(requestId, {\n metadata: normalizedMeta,\n audio: null,\n video: null,\n expiresAt: Date.now() + this.opts.ttlMs,\n loading: true,\n });\n\n const audioKbps = isLongVideo\n ? 96\n : pickQuality(this.opts.preferredAudioKbps, AUDIO_QUALITIES);\n\n const audioTask = this.preloadOne(\n requestId,\n \"audio\",\n normalized,\n audioKbps,\n );\n\n const tasks = isLongVideo\n ? [audioTask]\n : [\n audioTask,\n this.preloadOne(\n requestId,\n \"video\",\n normalized,\n pickQuality(this.opts.preferredVideoP, VIDEO_QUALITIES),\n ),\n ];\n\n if (isLongVideo) {\n this.opts.logger?.info?.(\n `Long video detected (${Math.floor(metadata.durationSeconds / 60)}min). Audio only mode (96kbps).`,\n );\n }\n\n await Promise.allSettled(tasks);\n this.cache.markLoading(requestId, false);\n }\n\n async getOrDownload(\n requestId: string,\n type: MediaType,\n ): Promise<{ metadata: PlayMetadata; file: CachedFile; direct: boolean }> {\n const entry = this.cache.get(requestId);\n if (!entry) throw new Error(\"Request not found (cache miss).\");\n\n const cached = entry[type];\n if (cached?.path && fs.existsSync(cached.path) && cached.size > 0) {\n return { metadata: entry.metadata, file: cached, direct: false };\n }\n\n const normalized = normalizeYoutubeUrl(entry.metadata.url);\n if (!normalized) throw new Error(\"Invalid YouTube URL.\");\n\n const directFile = await this.downloadDirect(type, normalized);\n return { metadata: entry.metadata, file: directFile, direct: true };\n }\n\n async waitCache(\n requestId: string,\n type: MediaType,\n timeoutMs = 8_000,\n intervalMs = 500,\n ): Promise<CachedFile | null> {\n const started = Date.now();\n while (Date.now() - started < timeoutMs) {\n const entry = this.cache.get(requestId);\n const f = entry?.[type];\n if (f?.path && fs.existsSync(f.path) && f.size > 0) return f;\n await new Promise((r) => setTimeout(r, intervalMs));\n }\n return null;\n }\n\n cleanup(requestId: string): void {\n this.cache.delete(requestId);\n }\n\n private async preloadOne(\n requestId: string,\n type: MediaType,\n youtubeUrl: string,\n quality: number,\n ): Promise<void> {\n try {\n const safeTitle = sanitizeFilename(`temp_${Date.now()}`);\n const ext = type === \"audio\" ? \"m4a\" : \"mp4\";\n const filename = `${type}_${requestId}_${safeTitle}.${ext}`;\n const filePath = path.join(this.paths.cacheDir, filename);\n\n const info: DownloadInfo =\n type === \"audio\"\n ? await this.ytdlp.getAudio(youtubeUrl, quality, filePath)\n : await this.ytdlp.getVideo(youtubeUrl, quality, filePath);\n\n const stats = fs.statSync(filePath);\n const size = stats.size;\n\n let buffer: Buffer | undefined;\n if (this.opts.preloadBuffer) {\n buffer = await fs.promises.readFile(filePath);\n }\n\n const cached: CachedFile = {\n path: filePath,\n size,\n info: { quality: info.quality },\n buffer,\n };\n\n this.cache.setFile(requestId, type, cached);\n this.opts.logger?.debug?.(`preloaded ${type} ${size} bytes: ${filename}`);\n } catch (err) {\n this.opts.logger?.error?.(`preload ${type} failed`, err);\n await this.forceUpdateCheck();\n throw err;\n }\n }\n\n private async downloadDirect(\n type: MediaType,\n youtubeUrl: string,\n ): Promise<CachedFile> {\n try {\n const audioKbps = pickQuality(\n this.opts.preferredAudioKbps,\n AUDIO_QUALITIES,\n );\n const videoP = pickQuality(this.opts.preferredVideoP, VIDEO_QUALITIES);\n const ext = type === \"audio\" ? \"m4a\" : \"mp4\";\n const safeTitle = sanitizeFilename(`direct_${Date.now()}`);\n const filePath = path.join(\n this.paths.cacheDir,\n `${type}_${safeTitle}.${ext}`,\n );\n\n const info =\n type === \"audio\"\n ? await this.ytdlp.getAudio(youtubeUrl, audioKbps, filePath)\n : await this.ytdlp.getVideo(youtubeUrl, videoP, filePath);\n\n const stats = fs.statSync(filePath);\n return {\n path: filePath,\n size: stats.size,\n info: { quality: info.quality },\n };\n } catch (err) {\n this.opts.logger?.error?.(\"Direct download failed:\", err);\n await this.forceUpdateCheck();\n\n this.opts.logger?.info?.(\">> Retrying download after update...\");\n\n const audioKbps = pickQuality(\n this.opts.preferredAudioKbps,\n AUDIO_QUALITIES,\n );\n const videoP = pickQuality(this.opts.preferredVideoP, VIDEO_QUALITIES);\n const ext = type === \"audio\" ? \"m4a\" : \"mp4\";\n const safeTitle = sanitizeFilename(`direct_retry_${Date.now()}`);\n const filePath = path.join(\n this.paths.cacheDir,\n `${type}_${safeTitle}.${ext}`,\n );\n\n const info =\n type === \"audio\"\n ? await this.ytdlp.getAudio(youtubeUrl, audioKbps, filePath)\n : await this.ytdlp.getVideo(youtubeUrl, videoP, filePath);\n\n const stats = fs.statSync(filePath);\n return {\n path: filePath,\n size: stats.size,\n info: { quality: info.quality },\n };\n }\n }\n}\n","import fs from \"node:fs\";\n\nimport type { CacheEntry, MediaType } from \"./types.js\";\n\nexport class CacheStore {\n private readonly store = new Map<string, CacheEntry>();\n private cleanupTimer?: NodeJS.Timeout;\n\n constructor(\n private readonly opts: {\n cleanupIntervalMs: number;\n }\n ) {}\n\n get(requestId: string): CacheEntry | undefined {\n return this.store.get(requestId);\n }\n\n set(requestId: string, entry: CacheEntry): void {\n this.store.set(requestId, entry);\n }\n\n has(requestId: string): boolean {\n return this.store.has(requestId);\n }\n\n delete(requestId: string): void {\n this.cleanupEntry(requestId);\n this.store.delete(requestId);\n }\n\n markLoading(requestId: string, loading: boolean): void {\n const e = this.store.get(requestId);\n if (e) e.loading = loading;\n }\n\n setFile(\n requestId: string,\n type: MediaType,\n file: CacheEntry[MediaType]\n ): void {\n const e = this.store.get(requestId);\n if (!e) return;\n e[type] = file as any;\n }\n\n cleanupExpired(now = Date.now()): number {\n let removed = 0;\n for (const [requestId, entry] of this.store.entries()) {\n if (now > entry.expiresAt) {\n this.delete(requestId);\n removed++;\n }\n }\n return removed;\n }\n\n start(): void {\n if (this.cleanupTimer) return;\n\n this.cleanupTimer = setInterval(() => {\n this.cleanupExpired(Date.now());\n }, this.opts.cleanupIntervalMs);\n\n this.cleanupTimer.unref();\n }\n\n stop(): void {\n if (!this.cleanupTimer) return;\n clearInterval(this.cleanupTimer);\n this.cleanupTimer = undefined;\n }\n\n private cleanupEntry(requestId: string) {\n const entry = this.store.get(requestId);\n if (!entry) return;\n\n ([\"audio\", \"video\"] as const).forEach((type) => {\n const f = entry[type];\n if (f?.path && fs.existsSync(f.path)) {\n try {\n fs.unlinkSync(f.path);\n } catch {\n // ignore\n }\n }\n });\n }\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport os from \"node:os\";\n\nexport interface ResolvedPaths {\n baseDir: string;\n cacheDir: string;\n}\n\nexport function ensureDirSync(dirPath: string) {\n fs.mkdirSync(dirPath, { recursive: true, mode: 0o777 });\n\n try {\n fs.chmodSync(dirPath, 0o777);\n } catch {\n // ignore\n }\n\n fs.accessSync(dirPath, fs.constants.R_OK | fs.constants.W_OK);\n}\n\nexport function resolvePaths(cacheDir?: string): ResolvedPaths {\n const baseDir = cacheDir?.trim()\n ? cacheDir\n : path.join(os.tmpdir(), \"yt-play\");\n const resolvedBase = path.resolve(baseDir);\n\n const resolvedCache = path.join(resolvedBase);\n\n ensureDirSync(resolvedBase);\n ensureDirSync(resolvedCache);\n\n return {\n baseDir: resolvedBase,\n cacheDir: resolvedCache,\n };\n}\n","import { spawn } from \"node:child_process\";\nimport path from \"node:path\";\nimport fs from \"node:fs\";\nimport type { DownloadInfo } from \"./types.js\";\n\nlet __dirname: string;\ntry {\n // @ts-ignore\n __dirname = path.dirname(new URL(import.meta.url).pathname);\n} catch {\n // @ts-ignore\n __dirname = typeof __dirname !== \"undefined\" ? __dirname : process.cwd();\n}\n\nexport interface YtDlpClientOptions {\n binaryPath?: string;\n ffmpegPath?: string;\n aria2cPath?: string;\n timeoutMs?: number;\n useAria2c?: boolean;\n concurrentFragments?: number;\n cookiesPath?: string;\n cookiesFromBrowser?: string;\n}\n\ninterface YtDlpVideoInfo {\n id: string;\n title: string;\n uploader?: string;\n duration: number;\n thumbnail?: string;\n}\n\nexport class YtDlpClient {\n private readonly binaryPath: string;\n private readonly ffmpegPath?: string;\n private readonly aria2cPath?: string;\n private readonly timeoutMs: number;\n private readonly useAria2c: boolean;\n private readonly concurrentFragments: number;\n private readonly cookiesPath?: string;\n private readonly cookiesFromBrowser?: string;\n\n constructor(opts: YtDlpClientOptions = {}) {\n this.binaryPath = opts.binaryPath || this.detectYtDlp();\n this.ffmpegPath = opts.ffmpegPath;\n this.timeoutMs = opts.timeoutMs ?? 300_000;\n this.concurrentFragments = opts.concurrentFragments ?? 5;\n this.cookiesPath = opts.cookiesPath;\n this.cookiesFromBrowser = opts.cookiesFromBrowser;\n\n this.aria2cPath = opts.aria2cPath || this.detectAria2c();\n this.useAria2c = opts.useAria2c ?? !!this.aria2cPath;\n }\n\n private detectYtDlp(): string {\n const packageRoot = path.resolve(__dirname, \"../..\");\n const bundledPaths = [\n path.join(packageRoot, \"bin\", \"yt-dlp\"),\n path.join(packageRoot, \"bin\", \"yt-dlp.exe\"),\n ];\n\n for (const p of bundledPaths) {\n if (fs.existsSync(p)) {\n return p;\n }\n }\n\n try {\n const { execSync } = require(\"node:child_process\");\n const cmd =\n process.platform === \"win32\" ? \"where yt-dlp\" : \"which yt-dlp\";\n const result = execSync(cmd, { encoding: \"utf-8\" }).trim();\n if (result) return result.split(\"\\n\")[0];\n } catch {}\n\n return \"yt-dlp\";\n }\n\n private detectAria2c(): string | undefined {\n const packageRoot = path.resolve(__dirname, \"../..\");\n const bundledPaths = [\n path.join(packageRoot, \"bin\", \"aria2c\"),\n path.join(packageRoot, \"bin\", \"aria2c.exe\"),\n ];\n\n for (const p of bundledPaths) {\n if (fs.existsSync(p)) {\n return p;\n }\n }\n\n try {\n const { execSync } = require(\"node:child_process\");\n const cmd =\n process.platform === \"win32\" ? \"where aria2c\" : \"which aria2c\";\n const result = execSync(cmd, { encoding: \"utf-8\" }).trim();\n if (result) return result.split(\"\\n\")[0];\n } catch {}\n\n return undefined;\n }\n\n private async exec(args: string[]): Promise<string> {\n return new Promise((resolve, reject) => {\n let allArgs = [...args];\n\n if (this.ffmpegPath) {\n allArgs = [\"--ffmpeg-location\", this.ffmpegPath, ...allArgs];\n }\n\n if (this.cookiesPath && fs.existsSync(this.cookiesPath)) {\n allArgs = [\"--cookies\", this.cookiesPath, ...allArgs];\n }\n\n if (this.cookiesFromBrowser) {\n allArgs = [\n \"--cookies-from-browser\",\n this.cookiesFromBrowser,\n ...allArgs,\n ];\n }\n\n const proc = spawn(this.binaryPath, allArgs, {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n let stdout = \"\";\n let stderr = \"\";\n\n proc.stdout.on(\"data\", (chunk) => {\n stdout += chunk.toString();\n });\n\n proc.stderr.on(\"data\", (chunk) => {\n stderr += chunk.toString();\n });\n\n const timer = setTimeout(() => {\n proc.kill(\"SIGKILL\");\n reject(new Error(`yt-dlp timeout after ${this.timeoutMs}ms`));\n }, this.timeoutMs);\n\n proc.on(\"close\", (code) => {\n clearTimeout(timer);\n if (code === 0) {\n resolve(stdout);\n } else {\n reject(\n new Error(\n `yt-dlp exited with code ${code}. stderr: ${stderr.slice(0, 500)}`,\n ),\n );\n }\n });\n\n proc.on(\"error\", (err) => {\n clearTimeout(timer);\n reject(err);\n });\n });\n }\n\n async getInfo(youtubeUrl: string): Promise<YtDlpVideoInfo> {\n const stdout = await this.exec([\n \"-J\",\n \"--no-warnings\",\n \"--no-playlist\",\n youtubeUrl,\n ]);\n const info = JSON.parse(stdout) as YtDlpVideoInfo;\n return info;\n }\n\n private buildOptimizationArgs(): string[] {\n const args: string[] = [\n \"--no-warnings\",\n \"--no-playlist\",\n \"--no-check-certificates\",\n \"--concurrent-fragments\",\n String(this.concurrentFragments),\n ];\n\n if (this.useAria2c && this.aria2cPath) {\n args.push(\"--downloader\", this.aria2cPath);\n args.push(\"--downloader-args\", \"aria2c:-x 16 -s 16 -k 1M\");\n }\n\n return args;\n }\n\n async getAudio(\n youtubeUrl: string,\n qualityKbps: number,\n outputPath: string,\n ): Promise<DownloadInfo> {\n const info = await this.getInfo(youtubeUrl);\n const format = \"bestaudio[ext=m4a]/bestaudio/best\";\n\n const args = [\n \"-f\",\n format,\n \"-o\",\n outputPath,\n ...this.buildOptimizationArgs(),\n youtubeUrl,\n ];\n\n await this.exec(args);\n\n if (!fs.existsSync(outputPath)) {\n throw new Error(`yt-dlp failed to create audio file: ${outputPath}`);\n }\n\n const duration = this.formatDuration(info.duration);\n\n return {\n title: info.title,\n author: info.uploader,\n duration,\n quality: `${qualityKbps}kbps m4a`,\n filename: path.basename(outputPath),\n downloadUrl: outputPath,\n };\n }\n\n async getVideo(\n youtubeUrl: string,\n qualityP: number,\n outputPath: string,\n ): Promise<DownloadInfo> {\n const info = await this.getInfo(youtubeUrl);\n\n // Força H.264 (avc) e evita AV1/VP9 que WhatsApp não suporta\n const format = `bestvideo[height<=${qualityP}][ext=mp4][vcodec^=avc]+bestaudio[ext=m4a]/bestvideo[height<=${qualityP}][vcodec!=av1][vcodec!=vp9]+bestaudio/best[height<=${qualityP}]`;\n\n const args = [\n \"-f\",\n format,\n \"--merge-output-format\",\n \"mp4\",\n \"-o\",\n outputPath,\n ...this.buildOptimizationArgs(),\n youtubeUrl,\n ];\n\n await this.exec(args);\n\n if (!fs.existsSync(outputPath)) {\n throw new Error(`yt-dlp failed to create video file: ${outputPath}`);\n }\n\n const duration = this.formatDuration(info.duration);\n\n return {\n title: info.title,\n author: info.uploader,\n duration,\n quality: `${qualityP}p H.264`,\n filename: path.basename(outputPath),\n downloadUrl: outputPath,\n };\n }\n\n private formatDuration(seconds: number): string {\n if (!seconds) return \"0:00\";\n const h = Math.floor(seconds / 3600);\n const m = Math.floor((seconds % 3600) / 60);\n const s = Math.floor(seconds % 60);\n if (h > 0) {\n return `${h}:${m.toString().padStart(2, \"0\")}:${s.toString().padStart(2, \"0\")}`;\n }\n return `${m}:${s.toString().padStart(2, \"0\")}`;\n }\n}\n","import yts from \"yt-search\";\n\nimport type { PlayMetadata } from \"./types.js\";\n\nexport function stripWeirdUrlWrappers(input: string): string {\n let s = (input || \"\").trim();\n const mdAll = [...s.matchAll(/\\[[^\\]]*\\]\\((https?:\\/\\/[^)\\s]+)\\)/gi)];\n if (mdAll.length > 0) return mdAll[0][1].trim();\n s = s.replace(/^<([^>]+)>$/, \"$1\").trim();\n s = s.replace(/^[\"'`](.*)[\"'`]$/, \"$1\").trim();\n\n return s;\n}\n\nexport function getYouTubeVideoId(input: string): string | null {\n const regex =\n /(?:https?:\\/\\/)?(?:www\\.)?(?:youtube\\.com\\/(?:[^\\/]+\\/.+\\/|(?:v|e(?:mbed)?)\\/|.*[?&]v=|shorts\\/)|youtu\\.be\\/)([a-zA-Z0-9_-]{11})(?:[?&]|$)/i;\n\n const match = (input || \"\").match(regex);\n return match ? match[1] : null;\n}\n\nexport function normalizeYoutubeUrl(input: string): string | null {\n const cleaned0 = stripWeirdUrlWrappers(input);\n\n const firstUrl = cleaned0.match(/https?:\\/\\/[^\\s)]+/i)?.[0] ?? cleaned0;\n\n const id = getYouTubeVideoId(firstUrl);\n if (!id) return null;\n\n return `https://www.youtube.com/watch?v=${id}`;\n}\n\nexport async function searchBest(query: string): Promise<PlayMetadata | null> {\n // Se já é uma URL válida, extrair o ID e buscar direto\n const videoId = getYouTubeVideoId(query);\n\n if (videoId) {\n // É URL - buscar pelo videoId específico (retorna VideoMetadataResult)\n const video = await yts({ videoId });\n\n if (!video) return null;\n\n const durationSeconds = video.duration?.seconds ?? 0;\n const normalizedUrl = normalizeYoutubeUrl(video.url) ?? video.url;\n\n return {\n title: video.title || \"Untitled\",\n author: video.author?.name || undefined,\n duration: video.duration?.timestamp || undefined,\n thumb: video.image || video.thumbnail || undefined,\n videoId: video.videoId,\n url: normalizedUrl,\n durationSeconds,\n };\n }\n\n // Não é URL - buscar normalmente (retorna SearchResult)\n const result = await yts(query);\n const v = result?.videos?.[0];\n if (!v) return null;\n\n const durationSeconds = v.duration?.seconds ?? 0;\n const normalizedUrl = normalizeYoutubeUrl(v.url) ?? v.url;\n\n return {\n title: v.title || \"Untitled\",\n author: v.author?.name || undefined,\n duration: v.duration?.timestamp || undefined,\n thumb: v.image || v.thumbnail || undefined,\n videoId: v.videoId,\n url: normalizedUrl,\n durationSeconds,\n };\n}\n"],"mappings":"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","names":["index_exports","__export","PlayEngine","YtDlpClient","getYouTubeVideoId","normalizeYoutubeUrl","searchBest","__toCommonJS","import_node_fs","import_node_path","import_node_child_process","import_node_fs","CacheStore","opts","requestId","entry","loading","type","file","e","now","removed","f","fs","import_node_fs","import_node_path","import_node_os","ensureDirSync","dirPath","fs","resolvePaths","cacheDir","baseDir","path","os","resolvedBase","resolvedCache","import_node_child_process","import_node_path","import_node_fs","import_meta","__dirname","path","YtDlpClient","opts","packageRoot","bundledPaths","p","fs","execSync","cmd","result","args","resolve","reject","allArgs","proc","stdout","stderr","chunk","timer","code","err","youtubeUrl","qualityKbps","outputPath","info","duration","qualityP","seconds","h","m","s","import_yt_search","stripWeirdUrlWrappers","input","s","mdAll","getYouTubeVideoId","regex","match","normalizeYoutubeUrl","cleaned0","firstUrl","id","searchBest","query","videoId","video","yts","durationSeconds","normalizedUrl","v","import_meta","AUDIO_QUALITIES","VIDEO_QUALITIES","UPDATE_CHECK_INTERVAL","pickQuality","requested","available","sanitizeFilename","filename","getNodeBinaryPath","setupYtDlpConfig","ytdlpBinaryPath","cookiesPath","cookiesFromBrowser","binaryDir","path","moduleUrl","fs","configPath","configLines","nodePath","error","PlayEngine","_PlayEngine","options","resolvePaths","CacheStore","YtDlpClient","now","scriptPath","checkAndUpdate","prefix","query","searchBest","requestId","metadata","normalized","normalizeYoutubeUrl","isLongVideo","normalizedMeta","audioKbps","audioTask","tasks","type","entry","cached","directFile","timeoutMs","intervalMs","started","f","r","youtubeUrl","quality","safeTitle","filePath","info","size","buffer","err","videoP","ext","stats"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/core/play-engine.ts","../src/core/cache.ts","../src/core/paths.ts","../src/core/ytdlp-client.ts","../src/core/youtube.ts","../src/core/stream.ts","../src/core/stalker.ts"],"sourcesContent":["export type {\n PlayMetadata,\n CachedFile,\n PlayEngineOptions,\n MediaType,\n DownloadInfo,\n CacheEntry,\n StreamInfo,\n YtDlpVideoMetadata,\n YtDlpChannelMetadata,\n YtDlpFlatEntry,\n StalkChannelOptions,\n} from \"./core/types.js\";\n\nexport { PlayEngine } from \"./core/play-engine.js\";\nexport { StalkerEngine } from \"./core/stalker.js\";\nexport { StreamEngine } from \"./core/stream.js\";\nexport {\n searchBest,\n searchPlaylistBest,\n normalizeYoutubeUrl,\n getYouTubeVideoId,\n} from \"./core/youtube.js\";\nexport { YtDlpClient } from \"./core/ytdlp-client.js\";\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { execSync } from \"node:child_process\";\nimport type {\n CacheEntry,\n CachedFile,\n DownloadInfo,\n MediaType,\n PlayEngineOptions,\n PlayMetadata,\n} from \"./types.js\";\nimport { CacheStore } from \"./cache.js\";\nimport { resolvePaths } from \"./paths.js\";\nimport { YtDlpClient } from \"./ytdlp-client.js\";\nimport { normalizeYoutubeUrl, searchBest } from \"./youtube.js\";\nimport { StreamEngine } from \"./stream.js\";\n\nconst AUDIO_QUALITIES = [320, 256, 192, 128, 96, 64] as const;\nconst VIDEO_QUALITIES = [1080, 720, 480, 360] as const;\nconst UPDATE_CHECK_INTERVAL = 3600000;\n\nfunction pickQuality<T extends number>(\n requested: T,\n available: readonly T[],\n): T {\n return (available as readonly number[]).includes(requested)\n ? requested\n : available[0];\n}\n\nfunction sanitizeFilename(filename: string): string {\n return (filename || \"\")\n .replace(/[\\\\/:*?\"<>|]/g, \"\")\n .replace(/[^\\w\\s-]/gi, \"\")\n .trim()\n .replace(/\\s+/g, \" \")\n .substring(0, 100);\n}\n\nfunction getNodeBinaryPath(): string | null {\n try {\n const nodePath = execSync(\"which node\", { encoding: \"utf-8\" }).trim();\n return nodePath || null;\n } catch {\n return process.execPath || null;\n }\n}\n\nfunction setupYtDlpConfig(\n ytdlpBinaryPath: string | undefined,\n cookiesPath: string | undefined,\n cookiesFromBrowser: string | undefined,\n): void {\n try {\n let binaryDir: string;\n\n if (ytdlpBinaryPath) {\n binaryDir = path.dirname(ytdlpBinaryPath);\n } else {\n try {\n const moduleUrl = new URL(import.meta.url);\n binaryDir = path.join(path.dirname(moduleUrl.pathname), \"..\", \"bin\");\n } catch {\n binaryDir = path.join(__dirname, \"..\", \"bin\");\n }\n }\n\n if (!fs.existsSync(binaryDir)) {\n return;\n }\n\n const configPath = path.join(binaryDir, \"yt-dlp.conf\");\n const configLines: string[] = [];\n const nodePath = getNodeBinaryPath();\n\n if (nodePath) {\n configLines.push(`--js-runtimes node:${nodePath}`);\n }\n\n configLines.push(\"--remote-components ejs:npm\");\n\n if (cookiesPath && fs.existsSync(cookiesPath)) {\n configLines.push(`--cookies ${cookiesPath}`);\n } else if (cookiesFromBrowser) {\n configLines.push(`--cookies-from-browser ${cookiesFromBrowser}`);\n }\n\n fs.writeFileSync(configPath, configLines.join(\"\\n\") + \"\\n\", \"utf-8\");\n } catch (error) {}\n}\n\nexport class PlayEngine {\n private readonly opts: Required<\n Pick<\n PlayEngineOptions,\n | \"ttlMs\"\n | \"maxPreloadDurationSeconds\"\n | \"preferredAudioKbps\"\n | \"preferredVideoP\"\n | \"preloadBuffer\"\n | \"cleanupIntervalMs\"\n | \"concurrentFragments\"\n >\n > &\n Pick<PlayEngineOptions, \"useAria2c\" | \"logger\">;\n\n private readonly paths: { baseDir: string; cacheDir: string };\n readonly cache: CacheStore;\n private readonly ytdlp: YtDlpClient;\n\n private static lastUpdateCheck: number = 0;\n private static updatePromise: Promise<void> | null = null;\n\n constructor(options: PlayEngineOptions = {}) {\n this.opts = {\n ttlMs: options.ttlMs ?? 3 * 60_000,\n maxPreloadDurationSeconds: options.maxPreloadDurationSeconds ?? 20 * 60,\n preferredAudioKbps: options.preferredAudioKbps ?? 128,\n preferredVideoP: options.preferredVideoP ?? 720,\n preloadBuffer: options.preloadBuffer ?? true,\n cleanupIntervalMs: options.cleanupIntervalMs ?? 30_000,\n concurrentFragments: options.concurrentFragments ?? 5,\n useAria2c: options.useAria2c,\n logger: options.logger,\n };\n\n this.paths = resolvePaths(options.cacheDir);\n this.cache = new CacheStore({\n cleanupIntervalMs: this.opts.cleanupIntervalMs,\n });\n this.cache.start();\n\n setupYtDlpConfig(\n options.ytdlpBinaryPath,\n options.cookiesPath,\n options.cookiesFromBrowser,\n );\n\n this.ytdlp = new YtDlpClient({\n binaryPath: options.ytdlpBinaryPath,\n ffmpegPath: options.ffmpegPath,\n aria2cPath: options.aria2cPath,\n useAria2c: this.opts.useAria2c,\n concurrentFragments: this.opts.concurrentFragments,\n timeoutMs: options.ytdlpTimeoutMs ?? 300_000,\n cookiesPath: options.cookiesPath,\n cookiesFromBrowser: options.cookiesFromBrowser,\n });\n\n this.stream = new StreamEngine(this.ytdlp);\n\n this.backgroundUpdateCheck();\n }\n\n public readonly stream: StreamEngine;\n\n private backgroundUpdateCheck(): void {\n const now = Date.now();\n\n if (now - PlayEngine.lastUpdateCheck < UPDATE_CHECK_INTERVAL) {\n return;\n }\n\n if (PlayEngine.updatePromise) {\n return;\n }\n\n PlayEngine.updatePromise = (async () => {\n try {\n PlayEngine.lastUpdateCheck = now;\n const scriptPath = new URL(\n \"../scripts/check-ytdlp-update.mjs\",\n import.meta.url,\n );\n const { checkAndUpdate } = await import(scriptPath.href);\n const updated = await checkAndUpdate();\n\n if (updated) {\n this.opts.logger?.info?.(\"✓ yt-dlp updated to latest version\");\n }\n } catch (error) {\n this.opts.logger?.debug?.(\"Update check failed (will retry later)\");\n } finally {\n PlayEngine.updatePromise = null;\n }\n })();\n }\n\n private async forceUpdateCheck(): Promise<void> {\n if (PlayEngine.updatePromise) {\n this.opts.logger?.info?.(\"Update already in progress, waiting...\");\n return PlayEngine.updatePromise;\n }\n\n PlayEngine.updatePromise = (async () => {\n try {\n this.opts.logger?.warn?.(\n \"<!> Download failed. Forcing yt-dlp update check...\",\n );\n\n const scriptPath = new URL(\n \"../scripts/check-ytdlp-update.mjs\",\n import.meta.url,\n );\n const { checkAndUpdate } = await import(scriptPath.href);\n const updated = await checkAndUpdate();\n\n if (updated) {\n this.opts.logger?.info?.(\"✓ yt-dlp updated successfully\");\n PlayEngine.lastUpdateCheck = Date.now();\n }\n } catch (error) {\n this.opts.logger?.error?.(\"Failed to update yt-dlp:\", error);\n } finally {\n PlayEngine.updatePromise = null;\n }\n })();\n\n return PlayEngine.updatePromise;\n }\n\n generateRequestId(prefix = \"play\"): string {\n return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;\n }\n\n async search(query: string): Promise<PlayMetadata | null> {\n return searchBest(query);\n }\n\n getFromCache(requestId: string): CacheEntry | undefined {\n return this.cache.get(requestId);\n }\n\n async preload(metadata: PlayMetadata, requestId: string): Promise<void> {\n const normalized = normalizeYoutubeUrl(metadata.url);\n if (!normalized) throw new Error(\"Invalid YouTube URL.\");\n\n const isLongVideo = metadata.durationSeconds > 3600;\n\n if (metadata.durationSeconds > this.opts.maxPreloadDurationSeconds) {\n this.opts.logger?.warn?.(\n `Video too long for preload (${Math.floor(metadata.durationSeconds / 60)}min). Will use direct download with reduced quality.`,\n );\n }\n\n const normalizedMeta: PlayMetadata = { ...metadata, url: normalized };\n\n this.cache.set(requestId, {\n metadata: normalizedMeta,\n audio: null,\n video: null,\n expiresAt: Date.now() + this.opts.ttlMs,\n loading: true,\n });\n\n const audioKbps = isLongVideo\n ? 96\n : pickQuality(this.opts.preferredAudioKbps, AUDIO_QUALITIES);\n\n const audioTask = this.preloadOne(\n requestId,\n \"audio\",\n normalized,\n audioKbps,\n );\n\n const tasks = isLongVideo\n ? [audioTask]\n : [\n audioTask,\n this.preloadOne(\n requestId,\n \"video\",\n normalized,\n pickQuality(this.opts.preferredVideoP, VIDEO_QUALITIES),\n ),\n ];\n\n if (isLongVideo) {\n this.opts.logger?.info?.(\n `Long video detected (${Math.floor(metadata.durationSeconds / 60)}min). Audio only mode (96kbps).`,\n );\n }\n\n await Promise.allSettled(tasks);\n this.cache.markLoading(requestId, false);\n }\n\n async getOrDownload(\n requestId: string,\n type: MediaType,\n ): Promise<{ metadata: PlayMetadata; file: CachedFile; direct: boolean }> {\n const entry = this.cache.get(requestId);\n if (!entry) throw new Error(\"Request not found (cache miss).\");\n\n const cached = entry[type];\n if (cached?.path && fs.existsSync(cached.path) && cached.size > 0) {\n return { metadata: entry.metadata, file: cached, direct: false };\n }\n\n const normalized = normalizeYoutubeUrl(entry.metadata.url);\n if (!normalized) throw new Error(\"Invalid YouTube URL.\");\n\n const directFile = await this.downloadDirect(type, normalized);\n return { metadata: entry.metadata, file: directFile, direct: true };\n }\n\n async waitCache(\n requestId: string,\n type: MediaType,\n timeoutMs = 8_000,\n intervalMs = 500,\n ): Promise<CachedFile | null> {\n const started = Date.now();\n while (Date.now() - started < timeoutMs) {\n const entry = this.cache.get(requestId);\n const f = entry?.[type];\n if (f?.path && fs.existsSync(f.path) && f.size > 0) return f;\n await new Promise((r) => setTimeout(r, intervalMs));\n }\n return null;\n }\n\n cleanup(requestId: string): void {\n this.cache.delete(requestId);\n }\n\n private async preloadOne(\n requestId: string,\n type: MediaType,\n youtubeUrl: string,\n quality: number,\n ): Promise<void> {\n try {\n await this._executePreload(requestId, type, youtubeUrl, quality);\n } catch (err) {\n this.opts.logger?.error?.(\n `preload ${type} failed, forcing update...`,\n err,\n );\n await this.forceUpdateCheck();\n\n this.opts.logger?.info?.(\">> Retrying preload after update...\");\n await this._executePreload(requestId, type, youtubeUrl, quality);\n }\n }\n\n private async _executePreload(\n requestId: string,\n type: MediaType,\n youtubeUrl: string,\n quality: number,\n ): Promise<void> {\n const uniqueSuffix = Math.random().toString(36).slice(2, 8);\n const safeTitle = sanitizeFilename(`temp_${Date.now()}_${uniqueSuffix}`);\n const ext = type === \"audio\" ? \"m4a\" : \"mp4\";\n const filename = `${type}_${requestId}_${safeTitle}.${ext}`;\n const filePath = path.join(this.paths.cacheDir, filename);\n\n const info: DownloadInfo =\n type === \"audio\"\n ? await this.ytdlp.getAudio(youtubeUrl, quality, filePath)\n : await this.ytdlp.getVideo(youtubeUrl, quality, filePath);\n\n const stats = fs.statSync(filePath);\n const size = stats.size;\n\n let buffer: Buffer | undefined;\n if (this.opts.preloadBuffer) {\n buffer = await fs.promises.readFile(filePath);\n }\n\n const cached: CachedFile = {\n path: filePath,\n size,\n info: { quality: info.quality },\n buffer,\n };\n\n this.cache.setFile(requestId, type, cached);\n this.opts.logger?.debug?.(`preloaded ${type} ${size} bytes: ${filename}`);\n }\n\n private async downloadDirect(\n type: MediaType,\n youtubeUrl: string,\n ): Promise<CachedFile> {\n try {\n return await this._executeDownloadDirect(type, youtubeUrl, false);\n } catch (err) {\n this.opts.logger?.error?.(\"Direct download failed:\", err);\n await this.forceUpdateCheck();\n\n this.opts.logger?.info?.(\">> Retrying download after update...\");\n return await this._executeDownloadDirect(type, youtubeUrl, true);\n }\n }\n\n private async _executeDownloadDirect(\n type: MediaType,\n youtubeUrl: string,\n isRetry: boolean,\n ): Promise<CachedFile> {\n const audioKbps = pickQuality(\n this.opts.preferredAudioKbps,\n AUDIO_QUALITIES,\n );\n const videoP = pickQuality(this.opts.preferredVideoP, VIDEO_QUALITIES);\n const ext = type === \"audio\" ? \"m4a\" : \"mp4\";\n const prefix = isRetry ? \"direct_retry\" : \"direct\";\n const uniqueSuffix = Math.random().toString(36).slice(2, 8);\n const safeTitle = sanitizeFilename(\n `${prefix}_${Date.now()}_${uniqueSuffix}`,\n );\n const filePath = path.join(\n this.paths.cacheDir,\n `${type}_${safeTitle}.${ext}`,\n );\n\n const info =\n type === \"audio\"\n ? await this.ytdlp.getAudio(youtubeUrl, audioKbps, filePath)\n : await this.ytdlp.getVideo(youtubeUrl, videoP, filePath);\n\n const stats = fs.statSync(filePath);\n return {\n path: filePath,\n size: stats.size,\n info: { quality: info.quality },\n };\n }\n}\n","import fs from \"node:fs\";\n\nimport type { CacheEntry, MediaType } from \"./types.js\";\n\nexport class CacheStore {\n private readonly store = new Map<string, CacheEntry>();\n private cleanupTimer?: NodeJS.Timeout;\n\n constructor(\n private readonly opts: {\n cleanupIntervalMs: number;\n }\n ) {}\n\n get(requestId: string): CacheEntry | undefined {\n return this.store.get(requestId);\n }\n\n set(requestId: string, entry: CacheEntry): void {\n this.store.set(requestId, entry);\n }\n\n has(requestId: string): boolean {\n return this.store.has(requestId);\n }\n\n delete(requestId: string): void {\n this.cleanupEntry(requestId);\n this.store.delete(requestId);\n }\n\n markLoading(requestId: string, loading: boolean): void {\n const e = this.store.get(requestId);\n if (e) e.loading = loading;\n }\n\n setFile(\n requestId: string,\n type: MediaType,\n file: CacheEntry[MediaType]\n ): void {\n const e = this.store.get(requestId);\n if (!e) return;\n e[type] = file as any;\n }\n\n cleanupExpired(now = Date.now()): number {\n let removed = 0;\n for (const [requestId, entry] of this.store.entries()) {\n if (now > entry.expiresAt) {\n this.delete(requestId);\n removed++;\n }\n }\n return removed;\n }\n\n start(): void {\n if (this.cleanupTimer) return;\n\n this.cleanupTimer = setInterval(() => {\n this.cleanupExpired(Date.now());\n }, this.opts.cleanupIntervalMs);\n\n this.cleanupTimer.unref();\n }\n\n stop(): void {\n if (!this.cleanupTimer) return;\n clearInterval(this.cleanupTimer);\n this.cleanupTimer = undefined;\n }\n\n private cleanupEntry(requestId: string) {\n const entry = this.store.get(requestId);\n if (!entry) return;\n\n ([\"audio\", \"video\"] as const).forEach((type) => {\n const f = entry[type];\n if (f?.path && fs.existsSync(f.path)) {\n try {\n fs.unlinkSync(f.path);\n } catch {\n // ignore\n }\n }\n });\n }\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport os from \"node:os\";\n\nexport interface ResolvedPaths {\n baseDir: string;\n cacheDir: string;\n}\n\nexport function ensureDirSync(dirPath: string) {\n fs.mkdirSync(dirPath, { recursive: true, mode: 0o777 });\n\n try {\n fs.chmodSync(dirPath, 0o777);\n } catch {\n // ignore\n }\n\n fs.accessSync(dirPath, fs.constants.R_OK | fs.constants.W_OK);\n}\n\nexport function resolvePaths(cacheDir?: string): ResolvedPaths {\n const baseDir = cacheDir?.trim()\n ? cacheDir\n : path.join(os.tmpdir(), \"yt-play\");\n const resolvedBase = path.resolve(baseDir);\n\n const resolvedCache = path.join(resolvedBase);\n\n ensureDirSync(resolvedBase);\n ensureDirSync(resolvedCache);\n\n return {\n baseDir: resolvedBase,\n cacheDir: resolvedCache,\n };\n}\n","import { spawn } from \"node:child_process\";\nimport path from \"node:path\";\nimport fs from \"node:fs\";\nimport type { DownloadInfo } from \"./types.js\";\n\nlet __dirname: string;\ntry {\n // @ts-ignore\n __dirname = path.dirname(new URL(import.meta.url).pathname);\n} catch {\n // @ts-ignore\n __dirname = typeof __dirname !== \"undefined\" ? __dirname : process.cwd();\n}\n\nexport interface YtDlpClientOptions {\n binaryPath?: string;\n ffmpegPath?: string;\n aria2cPath?: string;\n timeoutMs?: number;\n useAria2c?: boolean;\n concurrentFragments?: number;\n cookiesPath?: string;\n cookiesFromBrowser?: string;\n}\n\nexport interface YtDlpVideoInfo {\n id: string;\n title: string;\n uploader?: string;\n duration: number;\n thumbnail?: string;\n}\n\nexport interface YtDlpPlaylistItem {\n id: string;\n title: string;\n url: string;\n duration?: number;\n uploader?: string;\n directUrl?: string;\n}\n\nexport interface YtDlpResolvedItem extends YtDlpPlaylistItem {\n directUrl: string;\n}\n\nexport interface YtDlpPlaylistInfo {\n id: string;\n title: string;\n uploader?: string;\n entries: YtDlpPlaylistItem[] | YtDlpResolvedItem[];\n}\n\nexport interface YtDlpPlaylistOptions {\n limit?: number;\n resolveLinks?: boolean;\n type?: \"audio\" | \"video\" | \"both\";\n batchSize?: number;\n}\n\nexport class YtDlpClient {\n private readonly binaryPath: string;\n private readonly ffmpegPath?: string;\n private readonly aria2cPath?: string;\n private readonly timeoutMs: number;\n private readonly useAria2c: boolean;\n private readonly concurrentFragments: number;\n private readonly cookiesPath?: string;\n private readonly cookiesFromBrowser?: string;\n\n constructor(opts: YtDlpClientOptions = {}) {\n this.binaryPath = opts.binaryPath || this.detectYtDlp();\n this.ffmpegPath = opts.ffmpegPath;\n this.timeoutMs = opts.timeoutMs ?? 300_000;\n this.concurrentFragments = opts.concurrentFragments ?? 5;\n this.cookiesPath = opts.cookiesPath;\n this.cookiesFromBrowser = opts.cookiesFromBrowser;\n\n this.aria2cPath = opts.aria2cPath || this.detectAria2c();\n this.useAria2c = opts.useAria2c ?? !!this.aria2cPath;\n }\n\n private detectYtDlp(): string {\n const packageRoot = path.resolve(__dirname, \"../..\");\n const bundledPaths = [\n path.join(packageRoot, \"bin\", \"yt-dlp\"),\n path.join(packageRoot, \"bin\", \"yt-dlp.exe\"),\n ];\n\n for (const p of bundledPaths) {\n if (fs.existsSync(p)) {\n return p;\n }\n }\n\n try {\n const { execSync } = require(\"node:child_process\");\n const cmd =\n process.platform === \"win32\" ? \"where yt-dlp\" : \"which yt-dlp\";\n const result = execSync(cmd, { encoding: \"utf-8\" }).trim();\n if (result) return result.split(\"\\n\")[0];\n } catch {}\n\n return \"yt-dlp\";\n }\n\n private detectAria2c(): string | undefined {\n const packageRoot = path.resolve(__dirname, \"../..\");\n const bundledPaths = [\n path.join(packageRoot, \"bin\", \"aria2c\"),\n path.join(packageRoot, \"bin\", \"aria2c.exe\"),\n ];\n\n for (const p of bundledPaths) {\n if (fs.existsSync(p)) {\n return p;\n }\n }\n\n try {\n const { execSync } = require(\"node:child_process\");\n const cmd =\n process.platform === \"win32\" ? \"where aria2c\" : \"which aria2c\";\n const result = execSync(cmd, { encoding: \"utf-8\" }).trim();\n if (result) return result.split(\"\\n\")[0];\n } catch {}\n\n return undefined;\n }\n\n public async exec(args: string[]): Promise<string> {\n return new Promise((resolve, reject) => {\n let allArgs = [...args];\n\n if (this.ffmpegPath) {\n allArgs = [\"--ffmpeg-location\", this.ffmpegPath, ...allArgs];\n }\n\n if (this.cookiesPath && fs.existsSync(this.cookiesPath)) {\n allArgs = [\"--cookies\", this.cookiesPath, ...allArgs];\n }\n\n if (this.cookiesFromBrowser) {\n allArgs = [\n \"--cookies-from-browser\",\n this.cookiesFromBrowser,\n ...allArgs,\n ];\n }\n\n const proc = spawn(this.binaryPath, allArgs, {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n let stdout = \"\";\n let stderr = \"\";\n\n proc.stdout.on(\"data\", (chunk) => {\n stdout += chunk.toString();\n });\n\n proc.stderr.on(\"data\", (chunk) => {\n stderr += chunk.toString();\n });\n\n const timer = setTimeout(() => {\n proc.kill(\"SIGKILL\");\n reject(new Error(`yt-dlp timeout after ${this.timeoutMs}ms`));\n }, this.timeoutMs);\n\n proc.on(\"close\", (code) => {\n clearTimeout(timer);\n if (code === 0) {\n resolve(stdout);\n } else {\n reject(\n new Error(\n `yt-dlp exited with code ${code}. stderr: ${stderr.slice(0, 500)}`,\n ),\n );\n }\n });\n\n proc.on(\"error\", (err) => {\n clearTimeout(timer);\n reject(err);\n });\n });\n }\n\n async getInfo(youtubeUrl: string): Promise<YtDlpVideoInfo> {\n const stdout = await this.exec([\n \"-J\",\n \"--no-warnings\",\n \"--no-playlist\",\n youtubeUrl,\n ]);\n const info = JSON.parse(stdout) as YtDlpVideoInfo;\n return info;\n }\n\n async getPlaylistInfo(\n youtubeUrl: string,\n opts: YtDlpPlaylistOptions = {},\n ): Promise<YtDlpPlaylistInfo> {\n const stdout = await this.exec([\n \"-J\",\n \"--flat-playlist\",\n \"--no-warnings\",\n youtubeUrl,\n ]);\n\n const info = JSON.parse(stdout);\n\n let entries: YtDlpPlaylistItem[] = (info.entries || [])\n .filter((entry: any) => {\n const t = entry.title || \"\";\n return (\n entry.id &&\n t &&\n !t.includes(\"[Deleted video]\") &&\n !t.includes(\"[Private video]\")\n );\n })\n .map((entry: any) => ({\n id: entry.id,\n title: entry.title,\n url: `https://www.youtube.com/watch?v=${entry.id}`,\n duration: entry.duration,\n uploader: entry.uploader || info.uploader,\n }));\n\n if (opts.resolveLinks) {\n entries = await this.resolvePlaylistItems(\n entries,\n opts.limit && opts.limit > 0 ? opts.limit : entries.length,\n opts.type || \"audio\",\n opts.batchSize || 5,\n );\n } else if (opts.limit && opts.limit > 0) {\n entries = entries.slice(0, opts.limit);\n }\n\n return {\n id: info.id,\n title: info.title,\n uploader: info.uploader,\n entries,\n };\n }\n\n async getDirectUrl(\n youtubeUrl: string,\n type: \"audio\" | \"video\" | \"both\" = \"audio\",\n ): Promise<string> {\n const format =\n type === \"audio\"\n ? \"bestaudio[ext=m4a]/bestaudio/best\"\n : \"best[ext=mp4]/best\";\n\n const stdout = await this.exec([\n \"-f\",\n format,\n \"-g\",\n \"--no-warnings\",\n youtubeUrl,\n ]);\n\n return stdout.trim().split(\"\\n\")[0];\n }\n\n async resolvePlaylistItems(\n entries: YtDlpPlaylistItem[],\n limit: number,\n type: \"audio\" | \"video\" | \"both\" = \"audio\",\n batchSize: number = 5,\n ): Promise<YtDlpResolvedItem[]> {\n const resolved: YtDlpResolvedItem[] = [];\n let currentIndex = 0;\n\n while (resolved.length < limit && currentIndex < entries.length) {\n const needed = limit - resolved.length;\n const takeCount = Math.min(\n batchSize,\n needed,\n entries.length - currentIndex,\n );\n\n const batch = entries.slice(currentIndex, currentIndex + takeCount);\n currentIndex += takeCount;\n\n const promises = batch.map(async (item) => {\n try {\n const directUrl = await this.getDirectUrl(item.url, type);\n return { ...item, directUrl } as YtDlpResolvedItem;\n } catch (err) {\n return null;\n }\n });\n\n const results = await Promise.all(promises);\n\n for (const res of results) {\n if (res && resolved.length < limit) {\n resolved.push(res);\n }\n }\n }\n\n return resolved;\n }\n\n private buildOptimizationArgs(): string[] {\n const args: string[] = [\n \"--no-warnings\",\n \"--no-playlist\",\n \"--no-check-certificates\",\n \"--concurrent-fragments\",\n String(this.concurrentFragments),\n ];\n\n if (this.useAria2c && this.aria2cPath) {\n args.push(\"--downloader\", this.aria2cPath);\n args.push(\"--downloader-args\", \"aria2c:-x 16 -s 16 -k 1M\");\n }\n\n return args;\n }\n\n async getAudio(\n youtubeUrl: string,\n qualityKbps: number,\n outputPath: string,\n ): Promise<DownloadInfo> {\n const info = await this.getInfo(youtubeUrl);\n const format = \"bestaudio[ext=m4a]/bestaudio/best\";\n\n const args = [\n \"-f\",\n format,\n \"-o\",\n outputPath,\n ...this.buildOptimizationArgs(),\n youtubeUrl,\n ];\n\n try {\n await this.exec(args);\n } catch (err) {\n [outputPath, `${outputPath}.part`, `${outputPath}.ytdl`].forEach((f) => {\n if (fs.existsSync(f)) {\n try {\n fs.unlinkSync(f);\n } catch {}\n }\n });\n throw err;\n }\n\n if (!fs.existsSync(outputPath)) {\n throw new Error(`yt-dlp failed to create audio file: ${outputPath}`);\n }\n\n const duration = this.formatDuration(info.duration);\n\n return {\n title: info.title,\n author: info.uploader,\n duration,\n quality: `${qualityKbps}kbps m4a`,\n filename: path.basename(outputPath),\n downloadUrl: outputPath,\n };\n }\n\n async getVideo(\n youtubeUrl: string,\n qualityP: number,\n outputPath: string,\n ): Promise<DownloadInfo> {\n const info = await this.getInfo(youtubeUrl);\n\n const format = `bestvideo[height<=${qualityP}][ext=mp4][vcodec^=avc]+bestaudio[ext=m4a]/bestvideo[height<=${qualityP}][vcodec!=av1][vcodec!=vp9]+bestaudio/best[height<=${qualityP}]`;\n\n const args = [\n \"-f\",\n format,\n \"--merge-output-format\",\n \"mp4\",\n \"-o\",\n outputPath,\n ...this.buildOptimizationArgs(),\n youtubeUrl,\n ];\n\n try {\n await this.exec(args);\n } catch (err) {\n [outputPath, `${outputPath}.part`, `${outputPath}.ytdl`].forEach((f) => {\n if (fs.existsSync(f)) {\n try {\n fs.unlinkSync(f);\n } catch {}\n }\n });\n throw err;\n }\n\n if (!fs.existsSync(outputPath)) {\n throw new Error(`yt-dlp failed to create video file: ${outputPath}`);\n }\n\n const duration = this.formatDuration(info.duration);\n\n return {\n title: info.title,\n author: info.uploader,\n duration,\n quality: `${qualityP}p H.264`,\n filename: path.basename(outputPath),\n downloadUrl: outputPath,\n };\n }\n\n private formatDuration(seconds: number): string {\n if (!seconds) return \"0:00\";\n const h = Math.floor(seconds / 3600);\n const m = Math.floor((seconds % 3600) / 60);\n const s = Math.floor(seconds % 60);\n if (h > 0) {\n return `${h}:${m.toString().padStart(2, \"0\")}:${s.toString().padStart(2, \"0\")}`;\n }\n return `${m}:${s.toString().padStart(2, \"0\")}`;\n }\n}\n","import yts from \"yt-search\";\n\nimport type { PlayMetadata } from \"./types.js\";\n\nexport function stripWeirdUrlWrappers(input: string): string {\n let s = (input || \"\").trim();\n const mdAll = [...s.matchAll(/\\[[^\\]]*\\]\\((https?:\\/\\/[^)\\s]+)\\)/gi)];\n if (mdAll.length > 0) return mdAll[0][1].trim();\n s = s.replace(/^<([^>]+)>$/, \"$1\").trim();\n s = s.replace(/^[\"'`](.*)[\"'`]$/, \"$1\").trim();\n\n return s;\n}\n\nexport function getYouTubeVideoId(input: string): string | null {\n const regex =\n /(?:https?:\\/\\/)?(?:www\\.)?(?:youtube\\.com\\/(?:[^\\/]+\\/.+\\/|(?:v|e(?:mbed)?)\\/|.*[?&]v=|shorts\\/)|youtu\\.be\\/)([a-zA-Z0-9_-]{11})(?:[?&]|$)/i;\n\n const match = (input || \"\").match(regex);\n return match ? match[1] : null;\n}\n\nexport function getYouTubePlaylistId(input: string): string | null {\n const regex = /[?&]list=([a-zA-Z0-9_-]+)/i;\n const match = (input || \"\").match(regex);\n return match ? match[1] : null;\n}\n\nexport function normalizeYoutubeUrl(input: string): string | null {\n const cleaned0 = stripWeirdUrlWrappers(input);\n\n const firstUrl = cleaned0.match(/https?:\\/\\/[^\\s)]+/i)?.[0] ?? cleaned0;\n\n const id = getYouTubeVideoId(firstUrl);\n if (!id) return null;\n\n return `https://www.youtube.com/watch?v=${id}`;\n}\n\nexport async function searchBest(query: string): Promise<PlayMetadata | null> {\n const videoId = getYouTubeVideoId(query);\n\n if (videoId) {\n const video = await yts({ videoId });\n\n if (!video) return null;\n\n const durationSeconds = video.duration?.seconds ?? 0;\n const normalizedUrl = normalizeYoutubeUrl(video.url) ?? video.url;\n\n return {\n title: video.title || \"Untitled\",\n author: video.author?.name || undefined,\n duration: video.duration?.timestamp || undefined,\n thumb: video.image || video.thumbnail || undefined,\n videoId: video.videoId,\n url: normalizedUrl,\n durationSeconds,\n };\n }\n\n const result = await yts(query);\n const v = result?.videos?.[0];\n if (!v) return null;\n\n const durationSeconds = v.duration?.seconds ?? 0;\n const normalizedUrl = normalizeYoutubeUrl(v.url) ?? v.url;\n\n return {\n title: v.title || \"Untitled\",\n author: v.author?.name || undefined,\n duration: v.duration?.timestamp || undefined,\n thumb: v.image || v.thumbnail || undefined,\n videoId: v.videoId,\n url: normalizedUrl,\n durationSeconds,\n };\n}\n\nexport async function searchPlaylistBest(\n query: string,\n): Promise<PlayMetadata | null> {\n const listId = getYouTubePlaylistId(query);\n\n if (listId) {\n try {\n const list = await yts({ listId });\n\n if (!list) return null;\n\n return {\n title: list.title || \"Untitled\",\n author: list.author?.name || undefined,\n duration: undefined,\n durationSeconds: 0,\n thumb: list.image || list.thumbnail || undefined,\n videoId: listId,\n url: list.url || `https://www.youtube.com/playlist?list=${listId}`,\n };\n } catch (err) {}\n }\n\n const result = await yts(query);\n const p = result?.playlists?.[0];\n if (!p) return null;\n\n return {\n title: p.title || \"Untitled\",\n author: p.author?.name || undefined,\n duration: undefined,\n durationSeconds: 0,\n thumb: p.image || p.thumbnail || undefined,\n videoId: p.listId,\n url: p.url || `https://www.youtube.com/playlist?list=${p.listId}`,\n };\n}\n","import { spawn } from \"node:child_process\";\nimport type { StreamInfo } from \"./types.js\";\nimport type { YtDlpClient } from \"./ytdlp-client.js\";\n\nexport class StreamEngine {\n constructor(private readonly ytdlpClient: YtDlpClient) {}\n\n private getBinaryPath(): string {\n return (this.ytdlpClient as any).binaryPath;\n }\n\n async getAudioStream(\n youtubeUrl: string,\n qualityKbps: number = 128,\n ): Promise<StreamInfo> {\n const format = \"bestaudio[ext=m4a]/bestaudio/best\";\n\n const args = [\n \"-f\",\n format,\n \"-o\",\n \"-\",\n \"--no-warnings\",\n \"--no-playlist\",\n youtubeUrl,\n ];\n\n const proc = spawn(this.getBinaryPath(), args, {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n proc.stderr.on(\"data\", () => {});\n\n return {\n stream: proc.stdout,\n quality: `${qualityKbps}kbps`,\n format: \"m4a\",\n };\n }\n\n async getVideoStream(\n youtubeUrl: string,\n qualityP: number = 720,\n ): Promise<StreamInfo> {\n const format = `best[height<=${qualityP}][ext=mp4]/best`;\n\n const args = [\n \"-f\",\n format,\n \"-o\",\n \"-\",\n \"--no-warnings\",\n \"--no-playlist\",\n youtubeUrl,\n ];\n\n const proc = spawn(this.getBinaryPath(), args, {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n proc.stderr.on(\"data\", () => {});\n\n return {\n stream: proc.stdout,\n quality: `${qualityP}p`,\n format: \"mp4\",\n };\n }\n}\n","import type { YtDlpClient } from \"./ytdlp-client.js\";\nimport type {\n YtDlpVideoMetadata,\n YtDlpChannelMetadata,\n StalkChannelOptions,\n} from \"./types.js\";\n\nexport class StalkerEngine {\n constructor(private readonly ytdlpClient: YtDlpClient) {}\n\n async stalkVideoOrLive(youtubeUrl: string): Promise<YtDlpVideoMetadata> {\n const args = [\"-J\", \"--no-warnings\", \"--no-playlist\", youtubeUrl];\n\n const stdout = await this.ytdlpClient.exec(args);\n return JSON.parse(stdout) as YtDlpVideoMetadata;\n }\n\n async stalkChannel(\n youtubeUrl: string,\n opts: StalkChannelOptions = {},\n ): Promise<YtDlpChannelMetadata> {\n const args = [\"-J\", \"--no-warnings\"];\n\n if (opts.flat) {\n args.push(\"--flat-playlist\");\n }\n\n if (opts.playlistItems) {\n args.push(\"--playlist-items\", opts.playlistItems);\n } else {\n if (opts.startItem && opts.startItem > 0) {\n args.push(\"--playlist-start\", opts.startItem.toString());\n }\n if (opts.endItem && opts.endItem > 0) {\n args.push(\"--playlist-end\", opts.endItem.toString());\n }\n }\n\n let targetUrl = youtubeUrl.trim().replace(/\\/$/, \"\");\n if (opts.tab) {\n targetUrl = targetUrl.replace(\n /\\/(videos|shorts|streams|popular|featured)$/i,\n \"\",\n );\n if (opts.tab !== \"featured\") {\n targetUrl = `${targetUrl}/${opts.tab}`;\n }\n }\n\n args.push(targetUrl);\n\n try {\n const stdout = await this.ytdlpClient.exec(args);\n return JSON.parse(stdout) as YtDlpChannelMetadata;\n } catch (error: any) {\n const msg = error.message || \"\";\n\n if (\n msg.includes(\"This channel does not have a\") ||\n msg.includes(\"404\") ||\n msg.includes(\"does not exist\")\n ) {\n return {\n _type: \"playlist\",\n id: targetUrl,\n title: \"Tab Not Found\",\n channel: targetUrl.split(\"/\").pop() || \"Unknown\",\n channel_id: \"Unknown\",\n entries: [],\n } as YtDlpChannelMetadata;\n }\n\n throw error;\n }\n }\n}\n"],"mappings":"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","names":["index_exports","__export","PlayEngine","StalkerEngine","StreamEngine","YtDlpClient","getYouTubeVideoId","normalizeYoutubeUrl","searchBest","searchPlaylistBest","__toCommonJS","import_node_fs","import_node_path","import_node_child_process","import_node_fs","CacheStore","opts","requestId","entry","loading","e","type","file","now","removed","f","fs","import_node_fs","import_node_path","import_node_os","ensureDirSync","dirPath","fs","resolvePaths","cacheDir","baseDir","path","os","resolvedBase","resolvedCache","import_node_child_process","import_node_path","import_node_fs","import_meta","__dirname","path","YtDlpClient","opts","packageRoot","bundledPaths","p","fs","execSync","cmd","result","args","resolve","reject","allArgs","proc","stdout","stderr","chunk","timer","code","err","youtubeUrl","info","entries","entry","t","type","format","limit","batchSize","resolved","currentIndex","needed","takeCount","batch","promises","item","directUrl","results","res","qualityKbps","outputPath","f","duration","qualityP","seconds","h","m","s","import_yt_search","stripWeirdUrlWrappers","input","s","mdAll","getYouTubeVideoId","regex","match","getYouTubePlaylistId","normalizeYoutubeUrl","cleaned0","firstUrl","id","searchBest","query","videoId","video","yts","durationSeconds","normalizedUrl","v","searchPlaylistBest","listId","list","p","import_node_child_process","StreamEngine","ytdlpClient","youtubeUrl","qualityKbps","args","proc","qualityP","import_meta","AUDIO_QUALITIES","VIDEO_QUALITIES","UPDATE_CHECK_INTERVAL","pickQuality","requested","available","sanitizeFilename","filename","getNodeBinaryPath","setupYtDlpConfig","ytdlpBinaryPath","cookiesPath","cookiesFromBrowser","binaryDir","path","moduleUrl","fs","configPath","configLines","nodePath","PlayEngine","_PlayEngine","options","resolvePaths","CacheStore","YtDlpClient","StreamEngine","now","scriptPath","checkAndUpdate","error","prefix","query","searchBest","requestId","metadata","normalized","normalizeYoutubeUrl","isLongVideo","normalizedMeta","audioKbps","audioTask","tasks","type","entry","cached","directFile","timeoutMs","intervalMs","started","f","r","youtubeUrl","quality","err","uniqueSuffix","safeTitle","filePath","info","size","buffer","isRetry","videoP","ext","stats","StalkerEngine","ytdlpClient","youtubeUrl","args","stdout","opts","targetUrl","error","msg"]}