@irithell-js/yt-play 0.2.3 → 0.2.6

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
@@ -4,11 +4,13 @@ High-performance YouTube audio/video download engine with intelligent caching, b
4
4
 
5
5
  ## Features
6
6
 
7
- - **Bundled Binaries** - yt-dlp and aria2c included (no system dependencies)
7
+ - **Bundled Binaries** - yt-dlp and aria2c included (no system dependencies)
8
+ - **Auto-Update System** - yt-dlp updates automatically when needed
8
9
  - **Ultra Fast Downloads** - aria2c acceleration (up to 5x faster)
9
10
  - **Intelligent Caching** - TTL-based cache with automatic cleanup
10
11
  - **Smart Quality** - Auto-reduces quality for long videos (>1h)
11
- - **Container Ready** - Works in Docker/isolated environments
12
+ - **Container Ready** - Works in Docker/isolated environments with cookie support
13
+ - **Auto-Configuration** - Creates yt-dlp.conf with optimal settings
12
14
  - **Cross-Platform** - Linux (x64/arm64), macOS, Windows
13
15
  - **Zero Config** - Auto-detects binaries and optimizes settings
14
16
  - **TypeScript** - Full type definitions included
@@ -71,6 +73,21 @@ async function download() {
71
73
  download();
72
74
  ```
73
75
 
76
+ ### Direct URL Support
77
+
78
+ ```typescript
79
+ // Search by name
80
+ const metadata1 = await engine.search("never gonna give you up");
81
+
82
+ // Or use direct YouTube URL
83
+ const metadata2 = await engine.search(
84
+ "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
85
+ );
86
+
87
+ // Both return the same metadata format
88
+ console.log(metadata2.title); // "Rick Astley - Never Gonna Give You Up"
89
+ ```
90
+
74
91
  ## Configuration
75
92
 
76
93
  ### Constructor Options
@@ -98,11 +115,48 @@ const engine = new PlayEngine({
98
115
  aria2cPath: "./bin/aria2c",
99
116
  ffmpegPath: "/usr/bin/ffmpeg", // Optional
100
117
 
118
+ // Cookies (required for VPS/Docker environments)
119
+ cookiesPath: "./cookies.txt", // Path to Netscape cookies file
120
+ // OR
121
+ cookiesFromBrowser: "firefox", // Extract from browser: chrome, firefox, edge, safari
122
+
101
123
  // Logging
102
124
  logger: console, // Logger instance (optional)
103
125
  });
104
126
  ```
105
127
 
128
+ ### Cookie Configuration (VPS/Docker)
129
+
130
+ For headless environments (VPS, Docker), YouTube authentication is required:
131
+
132
+ **Option 1: Export cookies from browser**
133
+
134
+ 1. Install browser extension: "Get cookies.txt LOCALLY"
135
+ 2. Visit youtube.com and login
136
+ 3. Export cookies to `cookies.txt`
137
+ 4. Use in PlayEngine:
138
+
139
+ ```typescript
140
+ const engine = new PlayEngine({
141
+ cookiesPath: "/path/to/cookies.txt",
142
+ });
143
+ ```
144
+
145
+ **Option 2: Extract directly from browser** (requires browser installed)
146
+
147
+ ```typescript
148
+ const engine = new PlayEngine({
149
+ cookiesFromBrowser: "firefox", // chrome, firefox, edge, safari
150
+ });
151
+ ```
152
+
153
+ The engine automatically creates `yt-dlp.conf` with:
154
+
155
+ - Node.js runtime detection
156
+ - EJS remote components
157
+ - Cookie authentication
158
+ - Optimal download settings
159
+
106
160
  ### Quality Presets
107
161
 
108
162
  ```typescript
@@ -131,10 +185,17 @@ const lq = new PlayEngine({
131
185
 
132
186
  #### `search(query: string): Promise<PlayMetadata | null>`
133
187
 
134
- Search for a video on YouTube.
188
+ Search for a video on YouTube or get metadata from URL.
135
189
 
136
190
  ```typescript
191
+ // Search by name
137
192
  const metadata = await engine.search("artist - song name");
193
+
194
+ // Or use direct URL
195
+ const metadata = await engine.search(
196
+ "https://www.youtube.com/watch?v=VIDEO_ID",
197
+ );
198
+
138
199
  // Returns: { title, author, duration, durationSeconds, thumb, videoId, url }
139
200
  ```
140
201
 
@@ -203,6 +264,37 @@ if (entry) {
203
264
  }
204
265
  ```
205
266
 
267
+ ## Auto-Update System
268
+
269
+ The engine includes an intelligent auto-update system for yt-dlp:
270
+
271
+ ### How it works
272
+
273
+ 1. **Constant Background Checks** - Checks GitHub for new yt-dlp releases
274
+ 2. **Instant Updates on Failure** - If a download fails, immediately checks and updates yt-dlp
275
+ 3. **Automatic Retry** - After updating, automatically retries the failed download
276
+ 4. **Version Detection** - Compares installed binary version with latest GitHub release
277
+ 5. **Zero Downtime** - Updates happen in background without blocking your application
278
+
279
+ ### Update Behavior
280
+
281
+ ```typescript
282
+ const engine = new PlayEngine();
283
+ // ✓ Checks for updates in background
284
+
285
+ // If download fails
286
+ await engine.getOrDownload(requestId, "audio");
287
+ // <!> Download failed. Forcing yt-dlp update check...
288
+ // >> Updating yt-dlp to 2026.02.04...
289
+ // ✓ Updated to 2026.02.04
290
+ // >> Retrying download after update...
291
+ // ✓ Success
292
+ ```
293
+
294
+ ### Manual Update Check
295
+
296
+ To force an update check, simply trigger a download. The system will automatically update if needed.
297
+
206
298
  ## Advanced Usage
207
299
 
208
300
  ### Handle Long Videos (>1h)
@@ -255,6 +347,7 @@ try {
255
347
  console.log("Success:", file.path);
256
348
  } catch (error) {
257
349
  console.error("Download failed:", error.message);
350
+ // Engine automatically tries to update yt-dlp and retry
258
351
  }
259
352
  ```
260
353
 
@@ -270,14 +363,14 @@ With aria2c enabled (default):
270
363
 
271
364
  _Times may vary based on network speed and YouTube throttling_
272
365
 
273
- _The values ​​are based on local tests with optimized caching, for downloading long videos use direct download_
366
+ _The values are based on local tests with optimized caching, for downloading long videos use direct download_
274
367
 
275
368
  ## File Formats
276
369
 
277
370
  - **Audio**: M4A (native format, no conversion needed)
278
371
  - **Video**: MP4 (with audio merged)
279
372
 
280
- M4A provides better quality-to-size ratio and downloads 10-20x faster (no re-encoding).
373
+ M4A provides better quality-to-size ratio and downloads 2-5x faster (no re-encoding).
281
374
 
282
375
  ## Requirements
283
376
 
@@ -287,13 +380,19 @@ M4A provides better quality-to-size ratio and downloads 10-20x faster (no re-enc
287
380
 
288
381
  ## Binaries
289
382
 
290
- The package automatically downloads:
383
+ The package automatically downloads and manages:
291
384
 
292
- - **yt-dlp** v2025.12.08 (35 MB)
385
+ - **yt-dlp** (auto-updates to latest version) (~35 MB)
293
386
  - **aria2c** v1.37.0 (12 MB)
294
387
 
295
388
  Binaries are platform-specific and downloaded on first `npm install`.
296
389
 
390
+ ### Auto-Update Schedule
391
+
392
+ - **Background Check**: Constant version check
393
+ - **On-Demand**: Immediately when download fails
394
+ - **Version Source**: GitHub Releases API
395
+
297
396
  ### Supported Platforms
298
397
 
299
398
  - Linux x64 / arm64
@@ -337,6 +436,25 @@ Binaries are auto-downloaded to `node_modules/@irithell-js/yt-play/bin/`. If mis
337
436
  npm rebuild @irithell-js/yt-play
338
437
  ```
339
438
 
439
+ ### Cookie Authentication Issues
440
+
441
+ If downloads fail with "Sign in to confirm you're not a bot":
442
+
443
+ 1. Export fresh cookies from your browser
444
+ 2. Ensure cookies.txt is in Netscape format
445
+ 3. Verify cookie file path is correct
446
+ 4. Try `cookiesFromBrowser` option if browser is installed
447
+
448
+ ### Update Check Failures
449
+
450
+ If auto-update fails:
451
+
452
+ ```bash
453
+ # Manually update binaries
454
+ cd node_modules/@irithell-js/yt-play
455
+ node scripts/setup-binaries.mjs
456
+ ```
457
+
340
458
  ## License
341
459
 
342
460
  MIT
@@ -349,6 +467,23 @@ Issues and PRs welcome!
349
467
 
350
468
  Deprecated versions have been removed to prevent errors during use.
351
469
 
470
+ ### 0.2.6 (Latest)
471
+
472
+ - Added auto-update system for yt-dlp
473
+ - Direct YouTube URL support in search()
474
+ - Auto-configuration system (creates yt-dlp.conf)
475
+ - Cookie authentication support (cookiesPath/cookiesFromBrowser)
476
+ - Fixed URL search returning wrong video
477
+ - Improved error handling with automatic retry after update
478
+
479
+ ### 0.2.5
480
+
481
+ - Added support to direct cookies extraction in pre-built browsers
482
+
483
+ ### 0.2.4
484
+
485
+ - Added support to cookies.txt
486
+
352
487
  ### 0.2.3
353
488
 
354
489
  - Updated documentation
package/dist/index.cjs CHANGED
@@ -1,4 +1,6 @@
1
- "use strict";var L=Object.create;var w=Object.defineProperty;var R=Object.getOwnPropertyDescriptor;var U=Object.getOwnPropertyNames;var B=Object.getPrototypeOf,K=Object.prototype.hasOwnProperty;var q=(n,t)=>{for(var e in t)w(n,e,{get:t[e],enumerable:!0})},I=(n,t,e,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of U(t))!K.call(n,i)&&i!==e&&w(n,i,{get:()=>t[i],enumerable:!(r=R(t,i))||r.enumerable});return n};var u=(n,t,e)=>(e=n!=null?L(B(n)):{},I(t||!n||!n.__esModule?w(e,"default",{value:n,enumerable:!0}):e,n)),Q=n=>I(w({},"__esModule",{value:!0}),n);var N={};q(N,{PlayEngine:()=>D,YtDlpClient:()=>m,getYouTubeVideoId:()=>S,normalizeYoutubeUrl:()=>f,searchBest:()=>P});module.exports=Q(N);var g=u(require("fs"),1),$=u(require("path"),1);var M=u(require("fs"),1),v=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&&M.default.existsSync(i.path))try{M.default.unlinkSync(i.path)}catch{}})}};var p=u(require("fs"),1),b=u(require("path"),1),C=u(require("os"),1);function E(n){p.default.mkdirSync(n,{recursive:!0,mode:511});try{p.default.chmodSync(n,511)}catch{}p.default.accessSync(n,p.default.constants.R_OK|p.default.constants.W_OK)}function F(n){let t=n?.trim()?n:b.default.join(C.default.tmpdir(),"yt-play"),e=b.default.resolve(t),r=b.default.join(e);return E(e),E(r),{baseDir:e,cacheDir:r}}var O=require("child_process"),d=u(require("path"),1),y=u(require("fs"),1),_=require("url"),W={},h;try{h=d.default.dirname((0,_.fileURLToPath)(W.url))}catch{h=typeof h<"u"?h:process.cwd()}var m=class{binaryPath;ffmpegPath;aria2cPath;timeoutMs;useAria2c;concurrentFragments;constructor(t={}){this.binaryPath=t.binaryPath||this.detectYtDlp(),this.ffmpegPath=t.ffmpegPath,this.timeoutMs=t.timeoutMs??3e5,this.concurrentFragments=t.concurrentFragments??5,this.aria2cPath=t.aria2cPath||this.detectAria2c(),this.useAria2c=t.useAria2c??!!this.aria2cPath}detectYtDlp(){let t=d.default.resolve(h,"../.."),e=[d.default.join(t,"bin","yt-dlp"),d.default.join(t,"bin","yt-dlp.exe")];for(let r of e)if(y.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=d.default.resolve(h,"../.."),e=[d.default.join(t,"bin","aria2c"),d.default.join(t,"bin","aria2c.exe")];for(let r of e)if(y.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=this.ffmpegPath?["--ffmpeg-location",this.ffmpegPath,...t]:t,a=(0,O.spawn)(this.binaryPath,i,{stdio:["ignore","pipe","pipe"]}),s="",o="";a.stdout.on("data",l=>{s+=l.toString()}),a.stderr.on("data",l=>{o+=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(s):r(new Error(`yt-dlp exited with code ${l}. stderr: ${o.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)}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),s=["-f","bestaudio[ext=m4a]/bestaudio/best","-o",r,...this.buildOptimizationArgs(),t];if(await this.exec(s),!y.default.existsSync(r))throw new Error(`yt-dlp failed to create audio file: ${r}`);let o=this.formatDuration(i.duration);return{title:i.title,author:i.uploader,duration:o,quality:`${e}kbps m4a`,filename:d.default.basename(r),downloadUrl:r}}async getVideo(t,e,r){let i=await this.getInfo(t),s=["-f",`bestvideo[height<=${e}][ext=mp4]+bestaudio[ext=m4a]/best[height<=${e}]`,"--merge-output-format","mp4","-o",r,...this.buildOptimizationArgs(),t];if(await this.exec(s),!y.default.existsSync(r))throw new Error(`yt-dlp failed to create video file: ${r}`);let o=this.formatDuration(i.duration);return{title:i.title,author:i.uploader,duration:o,quality:`${e}p`,filename:d.default.basename(r),downloadUrl:r}}mapAudioQuality(t){return t>=320?"0":t>=256?"2":t>=192?"3":t>=128?"5":t>=96?"7":"9"}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 z=u(require("yt-search"),1);function J(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 S(n){let t=/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=|shorts\/)|youtu\.be\/)([^"&?\/\s]{11})/i,e=(n||"").match(t);return e?e[1]:null}function f(n){let t=J(n),e=t.match(/https?:\/\/[^\s)]+/i)?.[0]??t,r=S(e);return r?`https://www.youtube.com/watch?v=${r}`:null}async function P(n){let e=(await(0,z.default)(n))?.videos?.[0];if(!e)return null;let r=e.duration?.seconds??0,i=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:i,durationSeconds:r}}var V=[320,256,192,128,96,64],Y=[1080,720,480,360];function x(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)}var D=class{opts;paths;cache;ytdlp;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=F(t.cacheDir),this.cache=new v({cleanupIntervalMs:this.opts.cleanupIntervalMs}),this.cache.start(),this.ytdlp=new m({binaryPath:t.ytdlpBinaryPath,ffmpegPath:t.ffmpegPath,useAria2c:this.opts.useAria2c,concurrentFragments:this.opts.concurrentFragments,timeoutMs:t.ytdlpTimeoutMs??3e5})}generateRequestId(t="play"){return`${t}_${Date.now()}_${Math.random().toString(36).slice(2,8)}`}async search(t){return P(t)}getFromCache(t){return this.cache.get(t)}async preload(t,e){let r=f(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 s=i?96:x(this.opts.preferredAudioKbps,V),o=this.preloadOne(e,"audio",r,s),c=i?[o]:[o,this.preloadOne(e,"video",r,x(this.opts.preferredVideoP,Y))];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&&g.default.existsSync(i.path)&&i.size>0)return{metadata:r.metadata,file:i,direct:!1};let a=f(r.metadata.url);if(!a)throw new Error("Invalid YouTube URL.");let s=await this.downloadDirect(e,a);return{metadata:r.metadata,file:s,direct:!0}}async waitCache(t,e,r=8e3,i=500){let a=Date.now();for(;Date.now()-a<r;){let o=this.cache.get(t)?.[e];if(o?.path&&g.default.existsSync(o.path)&&o.size>0)return o;await new Promise(c=>setTimeout(c,i))}return null}cleanup(t){this.cache.delete(t)}async preloadOne(t,e,r,i){try{let a=k(`temp_${Date.now()}`),o=`${e}_${t}_${a}.${e==="audio"?"m4a":"mp4"}`,c=$.default.join(this.paths.cacheDir,o),l=e==="audio"?await this.ytdlp.getAudio(r,i,c):await this.ytdlp.getVideo(r,i,c),T=g.default.statSync(c).size,A;this.opts.preloadBuffer&&(A=await g.default.promises.readFile(c));let j={path:c,size:T,info:{quality:l.quality},buffer:A};this.cache.setFile(t,e,j),this.opts.logger?.debug?.(`preloaded ${e} ${T} bytes: ${o}`)}catch(a){this.opts.logger?.error?.(`preload ${e} failed`,a)}}async downloadDirect(t,e){let r=x(this.opts.preferredAudioKbps,V),i=x(this.opts.preferredVideoP,Y),a=t==="audio"?"m4a":"mp4",s=k(`direct_${Date.now()}`),o=$.default.join(this.paths.cacheDir,`${t}_${s}.${a}`),c=t==="audio"?await this.ytdlp.getAudio(e,r,o):await this.ytdlp.getVideo(e,i,o),l=g.default.statSync(o);return{path:o,size:l.size,info:{quality:c.quality}}}};0&&(module.exports={PlayEngine,YtDlpClient,getYouTubeVideoId,normalizeYoutubeUrl,searchBest});
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=(n,t)=>{for(var r in t)b(n,r,{get:t[r],enumerable:!0})},j=(n,t,r,e)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of K(t))!N.call(n,i)&&i!==r&&b(n,i,{get:()=>t[i],enumerable:!(e=R(t,i))||e.enumerable});return n};var u=(n,t,r)=>(r=n!=null?L(q(n)):{},j(t||!n||!n.__esModule?b(r,"default",{value:n,enumerable:!0}):r,n)),J=n=>j(b({},"__esModule",{value:!0}),n);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),_=require("child_process");var M=u(require("fs"),1),v=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&&M.default.existsSync(i.path))try{M.default.unlinkSync(i.path)}catch{}})}};var m=u(require("fs"),1),k=u(require("path"),1),B=u(require("os"),1);function z(n){m.default.mkdirSync(n,{recursive:!0,mode:511});try{m.default.chmodSync(n,511)}catch{}m.default.accessSync(n,m.default.constants.R_OK|m.default.constants.W_OK)}function O(n){let t=n?.trim()?n:k.default.join(B.default.tmpdir(),"yt-play"),r=k.default.resolve(t),e=k.default.join(r);return z(r),z(e),{baseDir:r,cacheDir:e}}var V=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",o=e(i,{encoding:"utf-8"}).trim();if(o)return o.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",o=e(i,{encoding:"utf-8"}).trim();if(o)return o.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 o=(0,V.spawn)(this.binaryPath,i,{stdio:["ignore","pipe","pipe"]}),a="",s="";o.stdout.on("data",d=>{a+=d.toString()}),o.stderr.on("data",d=>{s+=d.toString()});let c=setTimeout(()=>{o.kill("SIGKILL"),e(new Error(`yt-dlp timeout after ${this.timeoutMs}ms`))},this.timeoutMs);o.on("close",d=>{clearTimeout(c),d===0?r(a):e(new Error(`yt-dlp exited with code ${d}. stderr: ${s.slice(0,500)}`))}),o.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]+bestaudio[ext=m4a]/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`,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 $=u(require("yt-search"),1);function G(n){let t=(n||"").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(n){let t=/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=|shorts\/)|youtu\.be\/)([^"&?\/\s]{11})/i,r=(n||"").match(t);return r?r[1]:null}function f(n){let t=G(n),r=t.match(/https?:\/\/[^\s)]+/i)?.[0]??t,e=x(r);return e?`https://www.youtube.com/watch?v=${e}`:null}async function D(n){let t=x(n);if(t){let a=await(0,$.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,$.default)(n))?.videos?.[0];if(!e)return null;let i=e.duration?.seconds??0,o=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:o,durationSeconds:i}}var C={},A=[320,256,192,128,96,64],T=[1080,720,480,360],H=36e5;function P(n,t){return t.includes(n)?n:t[0]}function I(n){return(n||"").replace(/[\\/:*?"<>|]/g,"").replace(/[^\w\s-]/gi,"").trim().replace(/\s+/g," ").substring(0,100)}function X(){try{return(0,_.execSync)("which node",{encoding:"utf-8"}).trim()||null}catch{return process.execPath||null}}function Z(n,t,r){try{let e;if(n)e=h.default.dirname(n);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"),o=[],a=X();a&&o.push(`--js-runtimes node:${a}`),o.push("--remote-components ejs:npm"),t&&p.default.existsSync(t)?o.push(`--cookies ${t}`):r&&o.push(`--cookies-from-browser ${r}`),p.default.writeFileSync(i,o.join(`
4
+ `)+`
5
+ `,"utf-8")}catch(e){console.warn("Failed to create yt-dlp.conf:",e)}}var S=class n{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=O(t.cacheDir),this.cache=new v({cleanupIntervalMs:this.opts.cleanupIntervalMs}),this.cache.start(),Z(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-n.lastUpdateCheck<H||(async()=>{try{n.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(n.isUpdating){this.opts.logger?.info?.("Update already in progress, skipping...");return}try{n.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"),n.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{n.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 o={...t,url:e};this.cache.set(r,{metadata:o,audio:null,video:null,expiresAt:Date.now()+this.opts.ttlMs,loading:!0});let a=i?96:P(this.opts.preferredAudioKbps,A),s=this.preloadOne(r,"audio",e,a),c=i?[s]:[s,this.preloadOne(r,"video",e,P(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 o=f(e.metadata.url);if(!o)throw new Error("Invalid YouTube URL.");let a=await this.downloadDirect(r,o);return{metadata:e.metadata,file:a,direct:!0}}async waitCache(t,r,e=8e3,i=500){let o=Date.now();for(;Date.now()-o<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 o=I(`temp_${Date.now()}`),s=`${r}_${t}_${o}.${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(o){throw this.opts.logger?.error?.(`preload ${r} failed`,o),await this.forceUpdateCheck(),o}}async downloadDirect(t,r){try{let e=P(this.opts.preferredAudioKbps,A),i=P(this.opts.preferredVideoP,T),o=t==="audio"?"m4a":"mp4",a=I(`direct_${Date.now()}`),s=h.default.join(this.paths.cacheDir,`${t}_${a}.${o}`),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=P(this.opts.preferredAudioKbps,A),o=P(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,o,c),F=p.default.statSync(c);return{path:c,size:F.size,info:{quality:d.quality}}}}};0&&(module.exports={PlayEngine,YtDlpClient,getYouTubeVideoId,normalizeYoutubeUrl,searchBest});
4
6
  //# 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 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;\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\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, \"logger\" | \"useAria2c\">;\n\n private readonly paths: { baseDir: string; cacheDir: string };\n private readonly cache: CacheStore;\n private readonly ytdlp: YtDlpClient;\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 this.ytdlp = new YtDlpClient({\n binaryPath: options.ytdlpBinaryPath,\n ffmpegPath: options.ffmpegPath,\n useAria2c: this.opts.useAria2c,\n concurrentFragments: this.opts.concurrentFragments,\n timeoutMs: options.ytdlpTimeoutMs ?? 300_000, // 5min default\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; // >1h\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 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 // Vídeos longos (>1h): áudio 96kbps, sem vídeo\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 // Só baixa vídeo se for menor que 1h\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\"; // Mudou de mp3 para m4a\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 }\n }\n\n private async downloadDirect(\n type: MediaType,\n youtubeUrl: string,\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\"; // Mudou de mp3 para m4a\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 }\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 { fileURLToPath } from \"node:url\";\nimport type { DownloadInfo } from \"./types.js\";\n\nlet __dirname: string;\ntry {\n // @ts-ignore\n __dirname = path.dirname(fileURLToPath(import.meta.url));\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}\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\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\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 // yt-dlp not found\n }\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 // aria2c not found\n }\n\n return undefined;\n }\n\n private async exec(args: string[]): Promise<string> {\n return new Promise((resolve, reject) => {\n const allArgs = this.ffmpegPath\n ? [\"--ffmpeg-location\", this.ffmpegPath, ...args]\n : args;\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 const format = `bestvideo[height<=${qualityP}][ext=mp4]+bestaudio[ext=m4a]/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`,\n filename: path.basename(outputPath),\n downloadUrl: outputPath,\n };\n }\n\n private mapAudioQuality(kbps: number): string {\n if (kbps >= 320) return \"0\";\n if (kbps >= 256) return \"2\";\n if (kbps >= 192) return \"3\";\n if (kbps >= 128) return \"5\";\n if (kbps >= 96) return \"7\";\n return \"9\";\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\\/)([^\"&?\\/\\s]{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 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\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":"0jBAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,gBAAAE,EAAA,gBAAAC,EAAA,sBAAAC,EAAA,wBAAAC,EAAA,eAAAC,IAAA,eAAAC,EAAAP,GCAA,IAAAQ,EAAe,mBACfC,EAAiB,qBCDjB,IAAAC,EAAe,mBAIFC,EAAN,KAAiB,CAItB,YACmBC,EAGjB,CAHiB,UAAAA,CAGhB,CAPc,MAAQ,IAAI,IACrB,aAQR,IAAIC,EAA2C,CAC7C,OAAO,KAAK,MAAM,IAAIA,CAAS,CACjC,CAEA,IAAIA,EAAmBC,EAAyB,CAC9C,KAAK,MAAM,IAAID,EAAWC,CAAK,CACjC,CAEA,IAAID,EAA4B,CAC9B,OAAO,KAAK,MAAM,IAAIA,CAAS,CACjC,CAEA,OAAOA,EAAyB,CAC9B,KAAK,aAAaA,CAAS,EAC3B,KAAK,MAAM,OAAOA,CAAS,CAC7B,CAEA,YAAYA,EAAmBE,EAAwB,CACrD,IAAMC,EAAI,KAAK,MAAM,IAAIH,CAAS,EAC9BG,IAAGA,EAAE,QAAUD,EACrB,CAEA,QACEF,EACAI,EACAC,EACM,CACN,IAAMF,EAAI,KAAK,MAAM,IAAIH,CAAS,EAC7BG,IACLA,EAAEC,CAAI,EAAIC,EACZ,CAEA,eAAeC,EAAM,KAAK,IAAI,EAAW,CACvC,IAAIC,EAAU,EACd,OAAW,CAACP,EAAWC,CAAK,IAAK,KAAK,MAAM,QAAQ,EAC9CK,EAAML,EAAM,YACd,KAAK,OAAOD,CAAS,EACrBO,KAGJ,OAAOA,CACT,CAEA,OAAc,CACR,KAAK,eAET,KAAK,aAAe,YAAY,IAAM,CACpC,KAAK,eAAe,KAAK,IAAI,CAAC,CAChC,EAAG,KAAK,KAAK,iBAAiB,EAE9B,KAAK,aAAa,MAAM,EAC1B,CAEA,MAAa,CACN,KAAK,eACV,cAAc,KAAK,YAAY,EAC/B,KAAK,aAAe,OACtB,CAEQ,aAAaP,EAAmB,CACtC,IAAMC,EAAQ,KAAK,MAAM,IAAID,CAAS,EACjCC,GAEJ,CAAC,QAAS,OAAO,EAAY,QAASG,GAAS,CAC9C,IAAMI,EAAIP,EAAMG,CAAI,EACpB,GAAII,GAAG,MAAQ,EAAAC,QAAG,WAAWD,EAAE,IAAI,EACjC,GAAI,CACF,EAAAC,QAAG,WAAWD,EAAE,IAAI,CACtB,MAAQ,CAER,CAEJ,CAAC,CACH,CACF,ECxFA,IAAAE,EAAe,mBACfC,EAAiB,qBACjBC,EAAe,mBAOR,SAASC,EAAcC,EAAiB,CAC7C,EAAAC,QAAG,UAAUD,EAAS,CAAE,UAAW,GAAM,KAAM,GAAM,CAAC,EAEtD,GAAI,CACF,EAAAC,QAAG,UAAUD,EAAS,GAAK,CAC7B,MAAQ,CAER,CAEA,EAAAC,QAAG,WAAWD,EAAS,EAAAC,QAAG,UAAU,KAAO,EAAAA,QAAG,UAAU,IAAI,CAC9D,CAEO,SAASC,EAAaC,EAAkC,CAC7D,IAAMC,EAAUD,GAAU,KAAK,EAC3BA,EACA,EAAAE,QAAK,KAAK,EAAAC,QAAG,OAAO,EAAG,SAAS,EAC9BC,EAAe,EAAAF,QAAK,QAAQD,CAAO,EAEnCI,EAAgB,EAAAH,QAAK,KAAKE,CAAY,EAE5C,OAAAR,EAAcQ,CAAY,EAC1BR,EAAcS,CAAa,EAEpB,CACL,QAASD,EACT,SAAUC,CACZ,CACF,CCpCA,IAAAC,EAAsB,yBACtBC,EAAiB,qBACjBC,EAAe,mBACfC,EAA8B,eAH9BC,EAAA,GAMIC,EACJ,GAAI,CAEFA,EAAY,EAAAC,QAAK,WAAQ,iBAAcF,EAAY,GAAG,CAAC,CACzD,MAAQ,CAENC,EAAY,OAAOA,EAAc,IAAcA,EAAY,QAAQ,IAAI,CACzE,CAmBO,IAAME,EAAN,KAAkB,CACN,WACA,WACA,WACA,UACA,UACA,oBAEjB,YAAYC,EAA2B,CAAC,EAAG,CACzC,KAAK,WAAaA,EAAK,YAAc,KAAK,YAAY,EACtD,KAAK,WAAaA,EAAK,WACvB,KAAK,UAAYA,EAAK,WAAa,IACnC,KAAK,oBAAsBA,EAAK,qBAAuB,EAEvD,KAAK,WAAaA,EAAK,YAAc,KAAK,aAAa,EACvD,KAAK,UAAYA,EAAK,WAAa,CAAC,CAAC,KAAK,UAC5C,CAEQ,aAAsB,CAC5B,IAAMC,EAAc,EAAAH,QAAK,QAAQD,EAAW,OAAO,EAC7CK,EAAe,CACnB,EAAAJ,QAAK,KAAKG,EAAa,MAAO,QAAQ,EACtC,EAAAH,QAAK,KAAKG,EAAa,MAAO,YAAY,CAC5C,EAEA,QAAWE,KAAKD,EACd,GAAI,EAAAE,QAAG,WAAWD,CAAC,EACjB,OAAOA,EAIX,GAAI,CACF,GAAM,CAAE,SAAAE,CAAS,EAAI,QAAQ,eAAoB,EAC3CC,EACJ,QAAQ,WAAa,QAAU,eAAiB,eAC5CC,EAASF,EAASC,EAAK,CAAE,SAAU,OAAQ,CAAC,EAAE,KAAK,EACzD,GAAIC,EAAQ,OAAOA,EAAO,MAAM;AAAA,CAAI,EAAE,CAAC,CACzC,MAAQ,CAER,CAEA,MAAO,QACT,CAEQ,cAAmC,CACzC,IAAMN,EAAc,EAAAH,QAAK,QAAQD,EAAW,OAAO,EAC7CK,EAAe,CACnB,EAAAJ,QAAK,KAAKG,EAAa,MAAO,QAAQ,EACtC,EAAAH,QAAK,KAAKG,EAAa,MAAO,YAAY,CAC5C,EAEA,QAAWE,KAAKD,EACd,GAAI,EAAAE,QAAG,WAAWD,CAAC,EACjB,OAAOA,EAIX,GAAI,CACF,GAAM,CAAE,SAAAE,CAAS,EAAI,QAAQ,eAAoB,EAC3CC,EACJ,QAAQ,WAAa,QAAU,eAAiB,eAC5CC,EAASF,EAASC,EAAK,CAAE,SAAU,OAAQ,CAAC,EAAE,KAAK,EACzD,GAAIC,EAAQ,OAAOA,EAAO,MAAM;AAAA,CAAI,EAAE,CAAC,CACzC,MAAQ,CAER,CAGF,CAEA,MAAc,KAAKC,EAAiC,CAClD,OAAO,IAAI,QAAQ,CAACC,EAASC,IAAW,CACtC,IAAMC,EAAU,KAAK,WACjB,CAAC,oBAAqB,KAAK,WAAY,GAAGH,CAAI,EAC9CA,EAEEI,KAAO,SAAM,KAAK,WAAYD,EAAS,CAC3C,MAAO,CAAC,SAAU,OAAQ,MAAM,CAClC,CAAC,EAEGE,EAAS,GACTC,EAAS,GAEbF,EAAK,OAAO,GAAG,OAASG,GAAU,CAChCF,GAAUE,EAAM,SAAS,CAC3B,CAAC,EAEDH,EAAK,OAAO,GAAG,OAASG,GAAU,CAChCD,GAAUC,EAAM,SAAS,CAC3B,CAAC,EAED,IAAMC,EAAQ,WAAW,IAAM,CAC7BJ,EAAK,KAAK,SAAS,EACnBF,EAAO,IAAI,MAAM,wBAAwB,KAAK,SAAS,IAAI,CAAC,CAC9D,EAAG,KAAK,SAAS,EAEjBE,EAAK,GAAG,QAAUK,GAAS,CACzB,aAAaD,CAAK,EACdC,IAAS,EACXR,EAAQI,CAAM,EAEdH,EACE,IAAI,MACF,2BAA2BO,CAAI,aAAaH,EAAO,MAAM,EAAG,GAAG,CAAC,EAClE,CACF,CAEJ,CAAC,EAEDF,EAAK,GAAG,QAAUM,GAAQ,CACxB,aAAaF,CAAK,EAClBN,EAAOQ,CAAG,CACZ,CAAC,CACH,CAAC,CACH,CAEA,MAAM,QAAQC,EAA6C,CACzD,IAAMN,EAAS,MAAM,KAAK,KAAK,CAC7B,KACA,gBACA,gBACAM,CACF,CAAC,EAED,OADa,KAAK,MAAMN,CAAM,CAEhC,CAEQ,uBAAkC,CACxC,IAAML,EAAiB,CACrB,gBACA,gBACA,0BACA,yBACA,OAAO,KAAK,mBAAmB,CACjC,EAEA,OAAI,KAAK,WAAa,KAAK,aACzBA,EAAK,KAAK,eAAgB,KAAK,UAAU,EACzCA,EAAK,KAAK,oBAAqB,0BAA0B,GAGpDA,CACT,CAEA,MAAM,SACJW,EACAC,EACAC,EACuB,CACvB,IAAMC,EAAO,MAAM,KAAK,QAAQH,CAAU,EAGpCX,EAAO,CACX,KAHa,oCAKb,KACAa,EACA,GAAG,KAAK,sBAAsB,EAC9BF,CACF,EAIA,GAFA,MAAM,KAAK,KAAKX,CAAI,EAEhB,CAAC,EAAAJ,QAAG,WAAWiB,CAAU,EAC3B,MAAM,IAAI,MAAM,uCAAuCA,CAAU,EAAE,EAGrE,IAAME,EAAW,KAAK,eAAeD,EAAK,QAAQ,EAElD,MAAO,CACL,MAAOA,EAAK,MACZ,OAAQA,EAAK,SACb,SAAAC,EACA,QAAS,GAAGH,CAAW,WACvB,SAAU,EAAAtB,QAAK,SAASuB,CAAU,EAClC,YAAaA,CACf,CACF,CAEA,MAAM,SACJF,EACAK,EACAH,EACuB,CACvB,IAAMC,EAAO,MAAM,KAAK,QAAQH,CAAU,EAGpCX,EAAO,CACX,KAHa,qBAAqBgB,CAAQ,8CAA8CA,CAAQ,IAKhG,wBACA,MACA,KACAH,EACA,GAAG,KAAK,sBAAsB,EAC9BF,CACF,EAIA,GAFA,MAAM,KAAK,KAAKX,CAAI,EAEhB,CAAC,EAAAJ,QAAG,WAAWiB,CAAU,EAC3B,MAAM,IAAI,MAAM,uCAAuCA,CAAU,EAAE,EAGrE,IAAME,EAAW,KAAK,eAAeD,EAAK,QAAQ,EAElD,MAAO,CACL,MAAOA,EAAK,MACZ,OAAQA,EAAK,SACb,SAAAC,EACA,QAAS,GAAGC,CAAQ,IACpB,SAAU,EAAA1B,QAAK,SAASuB,CAAU,EAClC,YAAaA,CACf,CACF,CAEQ,gBAAgBI,EAAsB,CAC5C,OAAIA,GAAQ,IAAY,IACpBA,GAAQ,IAAY,IACpBA,GAAQ,IAAY,IACpBA,GAAQ,IAAY,IACpBA,GAAQ,GAAW,IAChB,GACT,CAEQ,eAAeC,EAAyB,CAC9C,GAAI,CAACA,EAAS,MAAO,OACrB,IAAMC,EAAI,KAAK,MAAMD,EAAU,IAAI,EAC7BE,EAAI,KAAK,MAAOF,EAAU,KAAQ,EAAE,EACpCG,EAAI,KAAK,MAAMH,EAAU,EAAE,EACjC,OAAIC,EAAI,EACC,GAAGA,CAAC,IAAIC,EAAE,SAAS,EAAE,SAAS,EAAG,GAAG,CAAC,IAAIC,EAAE,SAAS,EAAE,SAAS,EAAG,GAAG,CAAC,GAExE,GAAGD,CAAC,IAAIC,EAAE,SAAS,EAAE,SAAS,EAAG,GAAG,CAAC,EAC9C,CACF,EC3QA,IAAAC,EAAgB,0BAIT,SAASC,EAAsBC,EAAuB,CAC3D,IAAIC,GAAKD,GAAS,IAAI,KAAK,EACrBE,EAAQ,CAAC,GAAGD,EAAE,SAAS,sCAAsC,CAAC,EACpE,OAAIC,EAAM,OAAS,EAAUA,EAAM,CAAC,EAAE,CAAC,EAAE,KAAK,GAC9CD,EAAIA,EAAE,QAAQ,cAAe,IAAI,EAAE,KAAK,EACxCA,EAAIA,EAAE,QAAQ,mBAAoB,IAAI,EAAE,KAAK,EAEtCA,EACT,CAEO,SAASE,EAAkBH,EAA8B,CAC9D,IAAMI,EACJ,iIAEIC,GAASL,GAAS,IAAI,MAAMI,CAAK,EACvC,OAAOC,EAAQA,EAAM,CAAC,EAAI,IAC5B,CAEO,SAASC,EAAoBN,EAA8B,CAChE,IAAMO,EAAWR,EAAsBC,CAAK,EAEtCQ,EAAWD,EAAS,MAAM,qBAAqB,IAAI,CAAC,GAAKA,EAEzDE,EAAKN,EAAkBK,CAAQ,EACrC,OAAKC,EAEE,mCAAmCA,CAAE,GAF5B,IAGlB,CAEA,eAAsBC,EAAWC,EAA6C,CAE5E,IAAMC,GADS,QAAM,EAAAC,SAAIF,CAAK,IACZ,SAAS,CAAC,EAC5B,GAAI,CAACC,EAAG,OAAO,KAEf,IAAME,EAAkBF,EAAE,UAAU,SAAW,EAEzCG,EAAgBT,EAAoBM,EAAE,GAAG,GAAKA,EAAE,IAEtD,MAAO,CACL,MAAOA,EAAE,OAAS,WAClB,OAAQA,EAAE,QAAQ,MAAQ,OAC1B,SAAUA,EAAE,UAAU,WAAa,OACnC,MAAOA,EAAE,OAASA,EAAE,WAAa,OACjC,QAASA,EAAE,QACX,IAAKG,EACL,gBAAAD,CACF,CACF,CJpCA,IAAME,EAAkB,CAAC,IAAK,IAAK,IAAK,IAAK,GAAI,EAAE,EAC7CC,EAAkB,CAAC,KAAM,IAAK,IAAK,GAAG,EAE5C,SAASC,EACPC,EACAC,EACG,CACH,OAAQA,EAAgC,SAASD,CAAS,EACtDA,EACAC,EAAU,CAAC,CACjB,CAEA,SAASC,EAAiBC,EAA0B,CAClD,OAAQA,GAAY,IACjB,QAAQ,gBAAiB,EAAE,EAC3B,QAAQ,aAAc,EAAE,EACxB,KAAK,EACL,QAAQ,OAAQ,GAAG,EACnB,UAAU,EAAG,GAAG,CACrB,CAEO,IAAMC,EAAN,KAAiB,CACL,KAcA,MACA,MACA,MAEjB,YAAYC,EAA6B,CAAC,EAAG,CAC3C,KAAK,KAAO,CACV,MAAOA,EAAQ,OAAS,EAAI,IAC5B,0BAA2BA,EAAQ,2BAA6B,KAChE,mBAAoBA,EAAQ,oBAAsB,IAClD,gBAAiBA,EAAQ,iBAAmB,IAC5C,cAAeA,EAAQ,eAAiB,GACxC,kBAAmBA,EAAQ,mBAAqB,IAChD,oBAAqBA,EAAQ,qBAAuB,EACpD,UAAWA,EAAQ,UACnB,OAAQA,EAAQ,MAClB,EAEA,KAAK,MAAQC,EAAaD,EAAQ,QAAQ,EAC1C,KAAK,MAAQ,IAAIE,EAAW,CAC1B,kBAAmB,KAAK,KAAK,iBAC/B,CAAC,EACD,KAAK,MAAM,MAAM,EAEjB,KAAK,MAAQ,IAAIC,EAAY,CAC3B,WAAYH,EAAQ,gBACpB,WAAYA,EAAQ,WACpB,UAAW,KAAK,KAAK,UACrB,oBAAqB,KAAK,KAAK,oBAC/B,UAAWA,EAAQ,gBAAkB,GACvC,CAAC,CACH,CAEA,kBAAkBI,EAAS,OAAgB,CACzC,MAAO,GAAGA,CAAM,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,EAAG,CAAC,CAAC,EAC1E,CAEA,MAAM,OAAOC,EAA6C,CACxD,OAAOC,EAAWD,CAAK,CACzB,CAEA,aAAaE,EAA2C,CACtD,OAAO,KAAK,MAAM,IAAIA,CAAS,CACjC,CAEA,MAAM,QAAQC,EAAwBD,EAAkC,CACtE,IAAME,EAAaC,EAAoBF,EAAS,GAAG,EACnD,GAAI,CAACC,EAAY,MAAM,IAAI,MAAM,sBAAsB,EAEvD,IAAME,EAAcH,EAAS,gBAAkB,KAE3CA,EAAS,gBAAkB,KAAK,KAAK,2BACvC,KAAK,KAAK,QAAQ,OAChB,+BAA+B,KAAK,MAAMA,EAAS,gBAAkB,EAAE,CAAC,sDAC1E,EAGF,IAAMI,EAA+B,CAAE,GAAGJ,EAAU,IAAKC,CAAW,EACpE,KAAK,MAAM,IAAIF,EAAW,CACxB,SAAUK,EACV,MAAO,KACP,MAAO,KACP,UAAW,KAAK,IAAI,EAAI,KAAK,KAAK,MAClC,QAAS,EACX,CAAC,EAGD,IAAMC,EAAYF,EACd,GACAjB,EAAY,KAAK,KAAK,mBAAoBF,CAAe,EAEvDsB,EAAY,KAAK,WACrBP,EACA,QACAE,EACAI,CACF,EAGME,EAAQJ,EACV,CAACG,CAAS,EACV,CACEA,EACA,KAAK,WACHP,EACA,QACAE,EACAf,EAAY,KAAK,KAAK,gBAAiBD,CAAe,CACxD,CACF,EAEAkB,GACF,KAAK,KAAK,QAAQ,OAChB,wBAAwB,KAAK,MAAMH,EAAS,gBAAkB,EAAE,CAAC,iCACnE,EAGF,MAAM,QAAQ,WAAWO,CAAK,EAC9B,KAAK,MAAM,YAAYR,EAAW,EAAK,CACzC,CAEA,MAAM,cACJA,EACAS,EACwE,CACxE,IAAMC,EAAQ,KAAK,MAAM,IAAIV,CAAS,EACtC,GAAI,CAACU,EAAO,MAAM,IAAI,MAAM,iCAAiC,EAE7D,IAAMC,EAASD,EAAMD,CAAI,EACzB,GAAIE,GAAQ,MAAQ,EAAAC,QAAG,WAAWD,EAAO,IAAI,GAAKA,EAAO,KAAO,EAC9D,MAAO,CAAE,SAAUD,EAAM,SAAU,KAAMC,EAAQ,OAAQ,EAAM,EAGjE,IAAMT,EAAaC,EAAoBO,EAAM,SAAS,GAAG,EACzD,GAAI,CAACR,EAAY,MAAM,IAAI,MAAM,sBAAsB,EAEvD,IAAMW,EAAa,MAAM,KAAK,eAAeJ,EAAMP,CAAU,EAC7D,MAAO,CAAE,SAAUQ,EAAM,SAAU,KAAMG,EAAY,OAAQ,EAAK,CACpE,CAEA,MAAM,UACJb,EACAS,EACAK,EAAY,IACZC,EAAa,IACe,CAC5B,IAAMC,EAAU,KAAK,IAAI,EACzB,KAAO,KAAK,IAAI,EAAIA,EAAUF,GAAW,CAEvC,IAAMG,EADQ,KAAK,MAAM,IAAIjB,CAAS,IACpBS,CAAI,EACtB,GAAIQ,GAAG,MAAQ,EAAAL,QAAG,WAAWK,EAAE,IAAI,GAAKA,EAAE,KAAO,EAAG,OAAOA,EAC3D,MAAM,IAAI,QAASC,GAAM,WAAWA,EAAGH,CAAU,CAAC,CACpD,CACA,OAAO,IACT,CAEA,QAAQf,EAAyB,CAC/B,KAAK,MAAM,OAAOA,CAAS,CAC7B,CAEA,MAAc,WACZA,EACAS,EACAU,EACAC,EACe,CACf,GAAI,CACF,IAAMC,EAAY/B,EAAiB,QAAQ,KAAK,IAAI,CAAC,EAAE,EAEjDC,EAAW,GAAGkB,CAAI,IAAIT,CAAS,IAAIqB,CAAS,IADtCZ,IAAS,QAAU,MAAQ,KACkB,GACnDa,EAAW,EAAAC,QAAK,KAAK,KAAK,MAAM,SAAUhC,CAAQ,EAElDiC,EACJf,IAAS,QACL,MAAM,KAAK,MAAM,SAASU,EAAYC,EAASE,CAAQ,EACvD,MAAM,KAAK,MAAM,SAASH,EAAYC,EAASE,CAAQ,EAGvDG,EADQ,EAAAb,QAAG,SAASU,CAAQ,EACf,KAEfI,EACA,KAAK,KAAK,gBACZA,EAAS,MAAM,EAAAd,QAAG,SAAS,SAASU,CAAQ,GAG9C,IAAMX,EAAqB,CACzB,KAAMW,EACN,KAAAG,EACA,KAAM,CAAE,QAASD,EAAK,OAAQ,EAC9B,OAAAE,CACF,EAEA,KAAK,MAAM,QAAQ1B,EAAWS,EAAME,CAAM,EAC1C,KAAK,KAAK,QAAQ,QAAQ,aAAaF,CAAI,IAAIgB,CAAI,WAAWlC,CAAQ,EAAE,CAC1E,OAASoC,EAAK,CACZ,KAAK,KAAK,QAAQ,QAAQ,WAAWlB,CAAI,UAAWkB,CAAG,CACzD,CACF,CAEA,MAAc,eACZlB,EACAU,EACqB,CACrB,IAAMb,EAAYnB,EAChB,KAAK,KAAK,mBACVF,CACF,EACM2C,EAASzC,EAAY,KAAK,KAAK,gBAAiBD,CAAe,EAC/D2C,EAAMpB,IAAS,QAAU,MAAQ,MACjCY,EAAY/B,EAAiB,UAAU,KAAK,IAAI,CAAC,EAAE,EACnDgC,EAAW,EAAAC,QAAK,KACpB,KAAK,MAAM,SACX,GAAGd,CAAI,IAAIY,CAAS,IAAIQ,CAAG,EAC7B,EAEML,EACJf,IAAS,QACL,MAAM,KAAK,MAAM,SAASU,EAAYb,EAAWgB,CAAQ,EACzD,MAAM,KAAK,MAAM,SAASH,EAAYS,EAAQN,CAAQ,EAEtDQ,EAAQ,EAAAlB,QAAG,SAASU,CAAQ,EAClC,MAAO,CACL,KAAMA,EACN,KAAMQ,EAAM,KACZ,KAAM,CAAE,QAASN,EAAK,OAAQ,CAChC,CACF,CACF","names":["index_exports","__export","PlayEngine","YtDlpClient","getYouTubeVideoId","normalizeYoutubeUrl","searchBest","__toCommonJS","import_node_fs","import_node_path","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_node_url","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","kbps","seconds","h","m","s","import_yt_search","stripWeirdUrlWrappers","input","s","mdAll","getYouTubeVideoId","regex","match","normalizeYoutubeUrl","cleaned0","firstUrl","id","searchBest","query","v","yts","durationSeconds","normalizedUrl","AUDIO_QUALITIES","VIDEO_QUALITIES","pickQuality","requested","available","sanitizeFilename","filename","PlayEngine","options","resolvePaths","CacheStore","YtDlpClient","prefix","query","searchBest","requestId","metadata","normalized","normalizeYoutubeUrl","isLongVideo","normalizedMeta","audioKbps","audioTask","tasks","type","entry","cached","fs","directFile","timeoutMs","intervalMs","started","f","r","youtubeUrl","quality","safeTitle","filePath","path","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"],"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 const format = `bestvideo[height<=${qualityP}][ext=mp4]+bestaudio[ext=m4a]/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`,\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\\/)([^\"&?\\/\\s]{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"]}
package/dist/index.d.cts CHANGED
@@ -33,32 +33,57 @@ interface CacheEntry {
33
33
  }
34
34
  interface PlayEngineOptions {
35
35
  cacheDir?: string;
36
+ ytdlpBinaryPath?: string;
37
+ ffmpegPath?: string;
38
+ aria2cPath?: string;
39
+ ytdlpTimeoutMs?: number;
36
40
  ttlMs?: number;
37
41
  maxPreloadDurationSeconds?: number;
38
- preferredAudioKbps?: 320 | 256 | 192 | 128 | 96 | 64;
39
- preferredVideoP?: 1080 | 720 | 480 | 360;
42
+ preferredAudioKbps?: number;
43
+ preferredVideoP?: number;
40
44
  preloadBuffer?: boolean;
41
45
  cleanupIntervalMs?: number;
42
- ytdlpBinaryPath?: string;
43
- ffmpegPath?: string;
44
- aria2cPath?: string;
45
46
  useAria2c?: boolean;
46
47
  concurrentFragments?: number;
47
- ytdlpTimeoutMs?: number;
48
+ cookiesPath?: string;
49
+ cookiesFromBrowser?: string;
48
50
  logger?: {
51
+ debug?: (...args: any[]) => void;
49
52
  info?: (...args: any[]) => void;
50
53
  warn?: (...args: any[]) => void;
51
54
  error?: (...args: any[]) => void;
52
- debug?: (...args: any[]) => void;
53
55
  };
54
56
  }
55
57
 
58
+ declare class CacheStore {
59
+ private readonly opts;
60
+ private readonly store;
61
+ private cleanupTimer?;
62
+ constructor(opts: {
63
+ cleanupIntervalMs: number;
64
+ });
65
+ get(requestId: string): CacheEntry | undefined;
66
+ set(requestId: string, entry: CacheEntry): void;
67
+ has(requestId: string): boolean;
68
+ delete(requestId: string): void;
69
+ markLoading(requestId: string, loading: boolean): void;
70
+ setFile(requestId: string, type: MediaType, file: CacheEntry[MediaType]): void;
71
+ cleanupExpired(now?: number): number;
72
+ start(): void;
73
+ stop(): void;
74
+ private cleanupEntry;
75
+ }
76
+
56
77
  declare class PlayEngine {
57
78
  private readonly opts;
58
79
  private readonly paths;
59
- private readonly cache;
80
+ readonly cache: CacheStore;
60
81
  private readonly ytdlp;
82
+ private static lastUpdateCheck;
83
+ private static isUpdating;
61
84
  constructor(options?: PlayEngineOptions);
85
+ private backgroundUpdateCheck;
86
+ private forceUpdateCheck;
62
87
  generateRequestId(prefix?: string): string;
63
88
  search(query: string): Promise<PlayMetadata | null>;
64
89
  getFromCache(requestId: string): CacheEntry | undefined;
@@ -85,6 +110,8 @@ interface YtDlpClientOptions {
85
110
  timeoutMs?: number;
86
111
  useAria2c?: boolean;
87
112
  concurrentFragments?: number;
113
+ cookiesPath?: string;
114
+ cookiesFromBrowser?: string;
88
115
  }
89
116
  interface YtDlpVideoInfo {
90
117
  id: string;
@@ -100,6 +127,8 @@ declare class YtDlpClient {
100
127
  private readonly timeoutMs;
101
128
  private readonly useAria2c;
102
129
  private readonly concurrentFragments;
130
+ private readonly cookiesPath?;
131
+ private readonly cookiesFromBrowser?;
103
132
  constructor(opts?: YtDlpClientOptions);
104
133
  private detectYtDlp;
105
134
  private detectAria2c;
@@ -108,7 +137,6 @@ declare class YtDlpClient {
108
137
  private buildOptimizationArgs;
109
138
  getAudio(youtubeUrl: string, qualityKbps: number, outputPath: string): Promise<DownloadInfo>;
110
139
  getVideo(youtubeUrl: string, qualityP: number, outputPath: string): Promise<DownloadInfo>;
111
- private mapAudioQuality;
112
140
  private formatDuration;
113
141
  }
114
142