@irithell-js/yt-play 0.1.3 → 0.2.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
@@ -1,90 +1,371 @@
1
- # @irithell/yt-play
1
+ # @irithell-js/yt-play
2
2
 
3
- YouTube search + áudio/vídeo download engine (via SaveTube) com cache TTL opcional.
3
+ High-performance YouTube audio/video download engine with intelligent caching, built-in yt-dlp and aria2c binaries for blazing fast downloads.
4
4
 
5
- ## Instalação
5
+ ## Features
6
+
7
+ - ✅ **Bundled Binaries** - yt-dlp and aria2c included (no system dependencies)
8
+ - **Ultra Fast Downloads** - aria2c acceleration (up to 5x faster)
9
+ - **Intelligent Caching** - TTL-based cache with automatic cleanup
10
+ - **Smart Quality** - Auto-reduces quality for long videos (>1h)
11
+ - **Container Ready** - Works in Docker/isolated environments
12
+ - **Cross-Platform** - Linux (x64/arm64), macOS, Windows
13
+ - **Zero Config** - Auto-detects binaries and optimizes settings
14
+ - **TypeScript** - Full type definitions included
15
+ - **Dual Format** - ESM and CommonJS support
16
+
17
+ ## Installation
6
18
 
7
19
  ```bash
8
- npm i @irithell-js/yt-play
20
+ npm install @irithell-js/yt-play
21
+ ```
22
+
23
+ Binaries (yt-dlp + aria2c) are automatically downloaded during installation (this may take a few seconds during the first use).
24
+
25
+ ## Quick Start
26
+
27
+ ### Basic Usage (ESM)
28
+
29
+ ```typescript
30
+ import { PlayEngine } from "@irithell-js/yt-play";
31
+
32
+ const engine = new PlayEngine();
33
+
34
+ // Search and download
35
+ const metadata = await engine.search("linkin park numb");
36
+ if (!metadata) throw new Error("Not found");
37
+
38
+ const requestId = engine.generateRequestId();
39
+ await engine.preload(metadata, requestId);
40
+
41
+ // Get audio file
42
+ const { file: audioFile } = await engine.getOrDownload(requestId, "audio");
43
+ console.log("Audio:", audioFile.path);
44
+
45
+ // Get video file
46
+ const { file: videoFile } = await engine.getOrDownload(requestId, "video");
47
+ console.log("Video:", videoFile.path);
48
+
49
+ // Cleanup cache
50
+ engine.cleanup(requestId);
9
51
  ```
10
52
 
11
- ## Uso básico
53
+ ### Basic Usage (CommonJS)
54
+
55
+ ```javascript
56
+ const { PlayEngine } = require("@irithell-js/yt-play");
57
+
58
+ const engine = new PlayEngine();
59
+
60
+ async function download() {
61
+ const metadata = await engine.search("song name");
62
+ const requestId = engine.generateRequestId();
63
+ await engine.preload(metadata, requestId);
64
+
65
+ const { file } = await engine.getOrDownload(requestId, "audio");
66
+ console.log("Downloaded:", file.path);
67
+
68
+ engine.cleanup(requestId);
69
+ }
70
+
71
+ download();
72
+ ```
12
73
 
13
- ### 1) Buscar e iniciar preload
74
+ ## Configuration
14
75
 
15
- ```ts
16
- import { PlayEngine } from "@irithell/yt-play";
76
+ ### Constructor Options
17
77
 
78
+ ```typescript
18
79
  const engine = new PlayEngine({
19
- // opcional
20
- ttlMs: 3 * 60_000,
80
+ // Cache settings
81
+ cacheDir: "./cache", // Cache directory (default: OS temp)
82
+ ttlMs: 5 * 60_000, // Cache TTL in ms (default: 3min)
83
+ cleanupIntervalMs: 30_000, // Cleanup interval (default: 30s)
84
+ preloadBuffer: true, // Load files into RAM (default: true)
85
+
86
+ // Quality settings
87
+ preferredAudioKbps: 128, // Audio quality: 320|256|192|128|96|64
88
+ preferredVideoP: 720, // Video quality: 1080|720|480|360
89
+ maxPreloadDurationSeconds: 1200, // Max duration for preload (default: 20min)
90
+
91
+ // Performance settings (auto-optimized)
92
+ useAria2c: true, // Use aria2c for downloads (default: auto)
93
+ concurrentFragments: 8, // Parallel fragments (default: 5)
94
+ ytdlpTimeoutMs: 300_000, // yt-dlp timeout (default: 5min)
95
+
96
+ // Binary paths (optional - auto-detected)
97
+ ytdlpBinaryPath: "./bin/yt-dlp",
98
+ aria2cPath: "./bin/aria2c",
99
+ ffmpegPath: "/usr/bin/ffmpeg", // Optional
100
+
101
+ // Logging
102
+ logger: console, // Logger instance (optional)
103
+ });
104
+ ```
105
+
106
+ ### Quality Presets
107
+
108
+ ```typescript
109
+ // High quality (larger files)
110
+ const hq = new PlayEngine({
111
+ preferredAudioKbps: 320,
112
+ preferredVideoP: 1080,
113
+ });
114
+
115
+ // Balanced (recommended)
116
+ const balanced = new PlayEngine({
21
117
  preferredAudioKbps: 128,
22
118
  preferredVideoP: 720,
23
- preloadBuffer: true,
24
119
  });
25
120
 
26
- const metadata = await engine.search("linkin park numb");
27
- if (!metadata) throw new Error("Nada encontrado");
121
+ // Low quality (faster, smaller)
122
+ const lq = new PlayEngine({
123
+ preferredAudioKbps: 96,
124
+ preferredVideoP: 480,
125
+ });
126
+ ```
28
127
 
29
- const requestId = engine.generateRequestId();
30
- await engine.preload(metadata, requestId);
128
+ ## API Reference
31
129
 
32
- // depois você usa requestId pra pedir audio/video
130
+ ### PlayEngine Methods
131
+
132
+ #### `search(query: string): Promise<PlayMetadata | null>`
133
+
134
+ Search for a video on YouTube.
135
+
136
+ ```typescript
137
+ const metadata = await engine.search("artist - song name");
138
+ // Returns: { title, author, duration, durationSeconds, thumb, videoId, url }
33
139
  ```
34
140
 
35
- ### 2) Obter do cache (ou baixar direto)
141
+ #### `generateRequestId(prefix?: string): string`
142
+
143
+ Generate unique request ID for caching.
36
144
 
37
- ```ts
38
- // audio
39
- const audio = await engine.getOrDownload(requestId, "audio");
40
- console.log(audio.metadata.title, audio.file.info.quality, audio.file.path);
145
+ ```typescript
146
+ const requestId = engine.generateRequestId("audio"); // "audio_1234567890_abc123"
147
+ ```
41
148
 
42
- // video
43
- const video = await engine.getOrDownload(requestId, "video");
44
- console.log(video.file.info.quality, video.file.path);
149
+ #### `preload(metadata: PlayMetadata, requestId: string): Promise<void>`
45
150
 
46
- // se você não quiser manter o arquivo, apague depois de enviar
47
- engine.cleanup(requestId);
151
+ Pre-download audio and video in parallel (cached for TTL duration).
152
+
153
+ ```typescript
154
+ await engine.preload(metadata, requestId);
155
+ // Downloads audio + video if <1h, only audio if >1h (96kbps)
48
156
  ```
49
157
 
50
- ### 3) Esperar o cache “aquecer” (opcional)
158
+ #### `getOrDownload(requestId: string, type: 'audio' | 'video'): Promise<Result>`
51
159
 
52
- ```ts
53
- const cached = await engine.waitCache(requestId, "audio", 8000, 500);
160
+ Get file from cache or download directly.
161
+
162
+ ```typescript
163
+ const result = await engine.getOrDownload(requestId, "audio");
164
+ // Returns: { metadata, file: { path, size, info, buffer? }, direct: boolean }
165
+
166
+ console.log(result.file.path); // "/tmp/cache/audio_xxx.m4a"
167
+ console.log(result.file.size); // 8457234 (bytes)
168
+ console.log(result.file.info.quality); // "128kbps m4a"
169
+ console.log(result.direct); // false if from cache, true if direct download
170
+ ```
54
171
 
172
+ #### `waitCache(requestId: string, type: 'audio' | 'video', timeoutMs?: number, intervalMs?: number): Promise<CachedFile | null>`
173
+
174
+ Wait for cache to be ready (useful for checking preload status).
175
+
176
+ ```typescript
177
+ const cached = await engine.waitCache(requestId, "audio", 8000, 500);
55
178
  if (cached) {
56
- // já está no disco (e possivelmente com buffer em RAM)
57
- console.log("cache pronto", cached.path);
179
+ console.log("Cache ready:", cached.path);
58
180
  } else {
59
- // não ficou pronto a tempo — você pode cair pro getOrDownload
60
- const direct = await engine.getOrDownload(requestId, "audio");
61
- console.log("fallback", direct.file.path);
181
+ console.log("Timeout - falling back to direct download");
182
+ }
183
+ ```
184
+
185
+ #### `cleanup(requestId: string): void`
186
+
187
+ Remove cached files for a request.
188
+
189
+ ```typescript
190
+ engine.cleanup(requestId); // Deletes audio + video from cache
191
+ ```
192
+
193
+ #### `getFromCache(requestId: string): CacheEntry | undefined`
194
+
195
+ Get cache entry metadata (without downloading).
196
+
197
+ ```typescript
198
+ const entry = engine.getFromCache(requestId);
199
+ if (entry) {
200
+ console.log(entry.metadata.title);
201
+ console.log(entry.audio?.path);
202
+ console.log(entry.loading); // true if preload in progress
62
203
  }
63
204
  ```
64
205
 
65
- ## API
206
+ ## Advanced Usage
66
207
 
67
- ### `new PlayEngine(options?)`
208
+ ### Handle Long Videos (>1h)
68
209
 
69
- Opções principais:
210
+ ```typescript
211
+ const metadata = await engine.search("2h music mix");
70
212
 
71
- - `cacheDir?: string` Diretório base do cache (default: pasta temporária do sistema).
72
- - `ttlMs?: number` TTL dos arquivos em cache.
73
- - `preferredAudioKbps?: 320 | 256 | 192 | 128 | 96 | 64`
74
- - `preferredVideoP?: 1080 | 720 | 480 | 360`
75
- - `preloadBuffer?: boolean` Se true, lê o arquivo e deixa `buffer` pronto (mais RAM, envio mais rápido).
76
- - `cleanupIntervalMs?: number` Intervalo do GC do cache.
77
- - `logger?: { info|warn|error|debug }` Logger opcional.
213
+ // Automatically uses:
214
+ // - Audio: 96kbps (reduced quality)
215
+ // - Video: skipped (audio only)
216
+ await engine.preload(metadata, requestId);
217
+
218
+ const { file } = await engine.getOrDownload(requestId, "audio");
219
+ // Fast download with reduced quality
220
+ ```
221
+
222
+ ### Custom Cache Directory
223
+
224
+ ```typescript
225
+ import path from "path";
226
+
227
+ const engine = new PlayEngine({
228
+ cacheDir: path.join(process.cwd(), "downloads"),
229
+ ttlMs: 10 * 60_000, // 10 minutes
230
+ });
231
+ ```
232
+
233
+ ### Performance Monitoring
234
+
235
+ ```typescript
236
+ const startTime = Date.now();
237
+ await engine.preload(metadata, requestId);
238
+ const preloadTime = Date.now() - startTime;
239
+
240
+ console.log(`Preload took ${(preloadTime / 1000).toFixed(2)}s`);
241
+ ```
242
+
243
+ ### Error Handling
244
+
245
+ ```typescript
246
+ try {
247
+ const metadata = await engine.search("non-existent-video");
248
+ if (!metadata) {
249
+ console.error("Video not found");
250
+ return;
251
+ }
252
+
253
+ await engine.preload(metadata, requestId);
254
+ const { file } = await engine.getOrDownload(requestId, "audio");
255
+ console.log("Success:", file.path);
256
+ } catch (error) {
257
+ console.error("Download failed:", error.message);
258
+ }
259
+ ```
260
+
261
+ ## Performance
262
+
263
+ With aria2c enabled (default):
264
+
265
+ | Video Length | Audio Download | Video Download | Total Time |
266
+ | ------------ | -------------- | -------------- | ---------- |
267
+ | 5 min | ~3-5s | ~6-8s | ~8s |
268
+ | 1 hour | ~15-20s | Audio only | ~20s |
269
+ | 2 hours | ~25-30s | Audio only | ~30s |
270
+
271
+ _Times may vary based on network speed and YouTube throttling_
272
+
273
+ _The values ​​are based on local tests with optimized caching, for downloading long videos use direct download_
274
+
275
+ ## File Formats
276
+
277
+ - **Audio**: M4A (native format, no conversion needed)
278
+ - **Video**: MP4 (with audio merged)
279
+
280
+ M4A provides better quality-to-size ratio and downloads 10-20x faster (no re-encoding).
281
+
282
+ ## Requirements
283
+
284
+ - Node.js >= 18.0.0
285
+ - ~50MB disk space for binaries (auto-downloaded)
286
+ - Optional: ffmpeg for advanced features
287
+
288
+ ## Binaries
289
+
290
+ The package automatically downloads:
291
+
292
+ - **yt-dlp** v2025.12.08 (35 MB)
293
+ - **aria2c** v1.37.0 (12 MB)
78
294
 
79
- ### Métodos
295
+ Binaries are platform-specific and downloaded on first `npm install`.
80
296
 
81
- - `search(query): Promise<PlayMetadata | null>`
82
- - `generateRequestId(prefix?): string`
83
- - `preload(metadata, requestId): Promise<void>`
84
- - `getOrDownload(requestId, 'audio'|'video'): Promise<{ metadata; file; direct }>`
85
- - `waitCache(requestId, 'audio'|'video', timeoutMs?, intervalMs?): Promise<CachedFile | null>`
86
- - `cleanup(requestId): void`
297
+ ### Supported Platforms
87
298
 
88
- ## Licença
299
+ - Linux x64 / arm64
300
+ - macOS x64 / arm64 (Apple Silicon)
301
+ - Windows x64
302
+
303
+ ### Manual Binary Paths
304
+
305
+ ```typescript
306
+ const engine = new PlayEngine({
307
+ ytdlpBinaryPath: "/custom/path/yt-dlp",
308
+ aria2cPath: "/custom/path/aria2c",
309
+ });
310
+ ```
311
+
312
+ ## Troubleshooting
313
+
314
+ ### Slow Downloads
315
+
316
+ ```typescript
317
+ // Enable aria2c explicitly
318
+ const engine = new PlayEngine({
319
+ useAria2c: true,
320
+ concurrentFragments: 10, // Increase parallelism
321
+ });
322
+ ```
323
+
324
+ ### Cache Issues
325
+
326
+ ```typescript
327
+ // Clear cache directory manually
328
+ import fs from "fs";
329
+ fs.rmSync("./cache", { recursive: true, force: true });
330
+ ```
331
+
332
+ ### Binary Not Found
333
+
334
+ Binaries are auto-downloaded to `node_modules/@irithell-js/yt-play/bin/`. If missing:
335
+
336
+ ```bash
337
+ npm rebuild @irithell-js/yt-play
338
+ ```
339
+
340
+ ## License
89
341
 
90
342
  MIT
343
+
344
+ ## Contributing
345
+
346
+ Issues and PRs welcome!
347
+
348
+ ## Changelog
349
+
350
+ Deprecated versions have been removed to prevent errors during use.
351
+
352
+ ### 0.2.3
353
+
354
+ - Updated documentation
355
+ - Improved error messages
356
+
357
+ ### 0.2.2
358
+
359
+ - Many syntax errors fixed
360
+
361
+ ### 0.2.1
362
+
363
+ - Added auto-detection for yt-dlp and aria2c binaries
364
+ - Fixed CommonJS compatibility
365
+ - Improved error handling for long videos
366
+
367
+ ### 0.2.0
368
+
369
+ - Initial release with bundled binaries
370
+ - aria2c acceleration support
371
+ - Intelligent caching system
package/bin/.platform ADDED
@@ -0,0 +1 @@
1
+ linux-x64
package/bin/aria2c ADDED
Binary file
package/bin/yt-dlp ADDED
Binary file
package/dist/index.cjs CHANGED
@@ -1,2 +1,4 @@
1
- "use strict";var W=Object.create;var h=Object.defineProperty;var Y=Object.getOwnPropertyDescriptor;var q=Object.getOwnPropertyNames;var G=Object.getPrototypeOf,H=Object.prototype.hasOwnProperty;var N=(o,e)=>{for(var t in e)h(o,t,{get:e[t],enumerable:!0})},I=(o,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of q(e))!H.call(o,r)&&r!==t&&h(o,r,{get:()=>e[r],enumerable:!(n=Y(e,r))||n.enumerable});return o};var c=(o,e,t)=>(t=o!=null?W(G(o)):{},I(e||!o||!o.__esModule?h(t,"default",{value:o,enumerable:!0}):t,o)),Q=o=>I(h({},"__esModule",{value:!0}),o);var re={};N(re,{PlayEngine:()=>M});module.exports=Q(re);var x=c(require("fs"),1),P=c(require("path"),1);var S=c(require("fs"),1),m=class{constructor(e){this.opts=e}store=new Map;cleanupTimer;get(e){return this.store.get(e)}set(e,t){this.store.set(e,t)}has(e){return this.store.has(e)}delete(e){this.cleanupEntry(e),this.store.delete(e)}markLoading(e,t){let n=this.store.get(e);n&&(n.loading=t)}setFile(e,t,n){let r=this.store.get(e);r&&(r[t]=n)}cleanupExpired(e=Date.now()){let t=0;for(let[n,r]of this.store.entries())e>r.expiresAt&&(this.delete(n),t++);return t}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(e){let t=this.store.get(e);t&&["audio","video"].forEach(n=>{let r=t[n];if(r?.path&&S.default.existsSync(r.path))try{S.default.unlinkSync(r.path)}catch{}})}};var p=c(require("fs"),1),g=c(require("path"),1),B=c(require("os"),1);function y(o){p.default.mkdirSync(o,{recursive:!0,mode:511});try{p.default.chmodSync(o,511)}catch{}p.default.accessSync(o,p.default.constants.R_OK|p.default.constants.W_OK)}function $(o){let e=o?.trim()?o:g.default.join(B.default.tmpdir(),"yt-play"),t=g.default.resolve(e),n=g.default.join(t,"play-cache");return y(t),y(n),{baseDir:t,cacheDir:n}}var C=c(require("axios"),1),k=require("crypto"),O=c(require("http"),1),U=c(require("https"),1),J="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36";function X(o){let e="C5D58EF67A7584E4A29F6C35BBC4EB12",t=Buffer.from(o,"base64"),n=t.subarray(0,16),r=t.subarray(16),i=Buffer.from(e,"hex"),a=(0,k.createDecipheriv)("aes-128-cbc",i,n),s=Buffer.concat([a.update(r),a.final()]);return JSON.parse(s.toString("utf8"))}function Z(o){if(o.axios)return o.axios;let e=o.timeoutMs??6e4;return C.default.create({timeout:e,maxRedirects:0,validateStatus:t=>t>=200&&t<400,httpAgent:new O.Agent({keepAlive:!0}),httpsAgent:new U.Agent({keepAlive:!0}),headers:{"User-Agent":o.userAgent??J,"Accept-Language":"en-US,en;q=0.9",Referer:"https://www.youtube.com",Origin:"https://www.youtube.com"}})}var w=class{constructor(e={}){this.opts=e;this.axiosBase=Z(e)}axiosBase;async getCdnBase(){let t=(await this.axiosBase.get("https://media.savetube.me/api/random-cdn",{timeout:5e3})).data?.cdn;if(!t)throw new Error("SaveTube random-cdn returned no cdn host.");return/^https?:\/\//i.test(t)?t:`https://${t}`}async getInfo(e){let t=await this.getCdnBase(),r=(await this.axiosBase.post(`${t}/v2/info`,{url:e},{headers:{"Content-Type":"application/json",Accept:"application/json"}})).data;if(!r?.data)throw new Error("Invalid SaveTube /v2/info response (missing data).");let i=X(r.data);if(!i?.key)throw new Error("Invalid SaveTube decoded info (missing key).");return{cdnBase:t,info:i}}async getAudio(e,t){let{cdnBase:n,info:r}=await this.getInfo(e),a=(await this.axiosBase.post(`${n}/download`,{downloadType:"audio",quality:String(t),key:r.key},{headers:{"Content-Type":"application/json",Accept:"application/json"}})).data?.data?.downloadUrl;if(!a)throw new Error("Invalid SaveTube audio downloadUrl.");return{title:r.title,author:r.author,duration:r.duration,quality:`${t}kbps`,filename:`${r.title??"audio"} ${t}kbps.mp3`,downloadUrl:a}}async getVideo(e,t){let{cdnBase:n,info:r}=await this.getInfo(e),a=(await this.axiosBase.post(`${n}/download`,{downloadType:"video",quality:t,key:r.key},{headers:{"Content-Type":"application/json",Accept:"application/json"}})).data?.data?.downloadUrl;if(!a)throw new Error("Invalid SaveTube video downloadUrl.");return{title:r.title,author:r.author,duration:r.duration,quality:`${t}p`,filename:`${r.title??"video"} ${t}p.mp4`,downloadUrl:a}}};var v=c(require("fs"),1),F=c(require("path"),1),_=c(require("axios"),1);async function A(o,e,t={}){let n=t.timeoutMs??12e4,r=t.fileMode??438;y(F.default.dirname(e));let a=await(t.axios??_.default).request({url:o,method:"GET",responseType:"stream",timeout:n}),s=v.default.createWriteStream(e,{mode:r});await new Promise((l,f)=>{let u=!1,D=()=>{u||(u=!0,l())},E=L=>{u||(u=!0,f(L))};s.on("finish",D),s.on("error",E),a.data.on("error",E),a.data.pipe(s)});let d=v.default.statSync(e);if(d.size===0){try{v.default.unlinkSync(e)}catch{}throw new Error("Downloaded file is empty.")}return{size:d.size}}var z=c(require("yt-search"),1);function ee(o){let e=(o||"").trim(),t=[...e.matchAll(/\[[^\]]*\]\((https?:\/\/[^)\s]+)\)/gi)];return t.length>0?t[0][1].trim():(e=e.replace(/^<([^>]+)>$/,"$1").trim(),e=e.replace(/^["'`](.*)["'`]$/,"$1").trim(),e)}function te(o){let e=/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=|shorts\/)|youtu\.be\/)([^"&?\/\s]{11})/i,t=(o||"").match(e);return t?t[1]:null}function b(o){let e=ee(o),t=e.match(/https?:\/\/[^\s)]+/i)?.[0]??e,n=te(t);return n?`https://www.youtube.com/watch?v=${n}`:null}async function j(o){let t=(await(0,z.default)(o))?.videos?.[0];if(!t)return null;let n=t.duration?.seconds??0,r=b(t.url)??t.url;return{title:t.title||"Untitled",author:t.author?.name||void 0,duration:t.duration?.timestamp||void 0,thumb:t.image||t.thumbnail||void 0,videoId:t.videoId,url:r,durationSeconds:n}}var R=[320,256,192,128,96,64],V=[1080,720,480,360];function T(o,e){return e.includes(o)?o:e[0]}function K(o){return(o||"").replace(/[\\\/:*?"<>|]/g,"").replace(/[^\w\s-]/gi,"").trim().replace(/\s+/g," ").substring(0,100)}var M=class{opts;paths;cache;saveTube;constructor(e={}){this.opts={ttlMs:e.ttlMs??3*6e4,maxPreloadDurationSeconds:e.maxPreloadDurationSeconds??1200,preferredAudioKbps:e.preferredAudioKbps??128,preferredVideoP:e.preferredVideoP??720,preloadBuffer:e.preloadBuffer??!0,cleanupIntervalMs:e.cleanupIntervalMs??3e4,logger:e.logger},this.paths=$(e.cacheDir),this.cache=new m({cleanupIntervalMs:this.opts.cleanupIntervalMs}),this.cache.start(),this.saveTube=new w({})}generateRequestId(e="play"){return`${e}_${Date.now()}_${Math.random().toString(36).slice(2,8)}`}async search(e){return j(e)}getFromCache(e){return this.cache.get(e)}async preload(e,t){if(e.durationSeconds>this.opts.maxPreloadDurationSeconds)throw new Error("Video too long for preload.");let n=b(e.url);if(!n)throw new Error("Invalid YouTube URL.");let r={...e,url:n};this.cache.set(t,{metadata:r,audio:null,video:null,expiresAt:Date.now()+this.opts.ttlMs,loading:!0});let i=T(this.opts.preferredAudioKbps,R),a=T(this.opts.preferredVideoP,V),s=this.preloadOne(t,"audio",n,i),d=this.preloadOne(t,"video",n,a);Promise.allSettled([s,d]).finally(()=>this.cache.markLoading(t,!1)).catch(()=>this.cache.markLoading(t,!1))}async getOrDownload(e,t){let n=this.cache.get(e);if(!n)throw new Error("Request not found (cache miss).");let r=n[t];if(r?.path&&x.default.existsSync(r.path)&&r.size>0)return{metadata:n.metadata,file:r,direct:!1};let i=b(n.metadata.url);if(!i)throw new Error("Invalid YouTube URL.");let a=await this.downloadDirect(t,i);return{metadata:n.metadata,file:a,direct:!0}}async waitCache(e,t,n=8e3,r=500){let i=Date.now();for(;Date.now()-i<n;){let s=this.cache.get(e)?.[t];if(s?.path&&x.default.existsSync(s.path)&&s.size>0)return s;await new Promise(d=>setTimeout(d,r))}return null}cleanup(e){this.cache.delete(e)}async preloadOne(e,t,n,r){try{let i=t==="audio"?await this.saveTube.getAudio(n,r):await this.saveTube.getVideo(n,r),a=K(i.title??t),d=`${t}_${e}_${a}.${t==="audio"?"mp3":"mp4"}`,l=P.default.join(this.paths.cacheDir,d),{size:f}=await A(i.downloadUrl,l),u;this.opts.preloadBuffer&&(u=await x.default.promises.readFile(l));let D={path:l,size:f,info:{quality:i.quality},buffer:u};this.cache.setFile(e,t,D),this.opts.logger?.debug?.(`preloaded ${t} ${f} bytes: ${d}`)}catch(i){this.opts.logger?.error?.(`preload ${t} failed`,i)}}async downloadDirect(e,t){let n=T(this.opts.preferredAudioKbps,R),r=T(this.opts.preferredVideoP,V),i=e==="audio"?await this.saveTube.getAudio(t,n):await this.saveTube.getVideo(t,r),a=e==="audio"?"mp3":"mp4",s=K(i.title??e),d=P.default.join(this.paths.cacheDir,`${e}_direct_${Date.now()}_${s}.${a}`),{size:l}=await A(i.downloadUrl,d);return{path:d,size:l,info:{quality:i.quality}}}};0&&(module.exports={PlayEngine});
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});
2
4
  //# 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/savetube.ts","../src/core/download.ts","../src/core/youtube.ts"],"sourcesContent":["export type {\n PlayMetadata,\n CachedFile,\n PlayEngineOptions,\n} from \"./core/types.js\";\n\nexport { PlayEngine } from \"./core/play-engine.js\";\n","import fs from \"node:fs\";\nimport path from \"node:path\";\n\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 { SaveTubeClient } from \"./savetube.js\";\nimport { downloadToBuffer, downloadToFile } from \"./download.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 >\n > &\n Pick<PlayEngineOptions, \"logger\">;\n\n private readonly paths: { baseDir: string; cacheDir: string };\n private readonly cache: CacheStore;\n private readonly saveTube: SaveTubeClient;\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 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.saveTube = new SaveTubeClient({\n // mantém axios interno\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 if (metadata.durationSeconds > this.opts.maxPreloadDurationSeconds) {\n throw new Error(\"Video too long for preload.\");\n }\n\n const normalized = normalizeYoutubeUrl(metadata.url);\n if (!normalized) throw new Error(\"Invalid YouTube URL.\");\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 = pickQuality(\n this.opts.preferredAudioKbps,\n AUDIO_QUALITIES\n );\n const videoP = pickQuality(this.opts.preferredVideoP, VIDEO_QUALITIES);\n\n const audioTask = this.preloadOne(\n requestId,\n \"audio\",\n normalized,\n audioKbps\n );\n const videoTask = this.preloadOne(requestId, \"video\", normalized, videoP);\n\n Promise.allSettled([audioTask, videoTask])\n .finally(() => this.cache.markLoading(requestId, false))\n .catch(() => 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 info: DownloadInfo =\n type === \"audio\"\n ? await this.saveTube.getAudio(youtubeUrl, quality)\n : await this.saveTube.getVideo(youtubeUrl, quality);\n\n const safeTitle = sanitizeFilename(info.title ?? type);\n const ext = type === \"audio\" ? \"mp3\" : \"mp4\";\n const filename = `${type}_${requestId}_${safeTitle}.${ext}`;\n const filePath = path.join(this.paths.cacheDir, filename);\n\n const { size } = await downloadToFile(info.downloadUrl, filePath);\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\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\n const info =\n type === \"audio\"\n ? await this.saveTube.getAudio(youtubeUrl, audioKbps)\n : await this.saveTube.getVideo(youtubeUrl, videoP);\n\n const ext = type === \"audio\" ? \"mp3\" : \"mp4\";\n const safeTitle = sanitizeFilename(info.title ?? type);\n const filePath = path.join(\n this.paths.cacheDir,\n `${type}_direct_${Date.now()}_${safeTitle}.${ext}`\n );\n\n const { size } = await downloadToFile(info.downloadUrl, filePath);\n\n return {\n path: filePath,\n 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, \"play-cache\");\n\n ensureDirSync(resolvedBase);\n ensureDirSync(resolvedCache);\n\n return {\n baseDir: resolvedBase,\n cacheDir: resolvedCache,\n };\n}\n","import axios, { type AxiosInstance } from \"axios\";\nimport { createDecipheriv } from \"node:crypto\";\nimport * as http from \"node:http\";\nimport * as https from \"node:https\";\n\nimport type { DownloadInfo } from \"./types.js\";\n\nexport interface SaveTubeClientOptions {\n axios?: AxiosInstance;\n timeoutMs?: number;\n userAgent?: string;\n}\n\ntype SaveTubeInfo = {\n key: string;\n title?: string;\n author?: string;\n duration?: string;\n};\n\nconst DEFAULT_UA =\n \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36\";\n\nfunction decode(enc: string): any {\n const secretKeyHex = \"C5D58EF67A7584E4A29F6C35BBC4EB12\";\n const data = Buffer.from(enc, \"base64\");\n const iv = data.subarray(0, 16);\n const content = data.subarray(16);\n\n const key = Buffer.from(secretKeyHex, \"hex\");\n const decipher = createDecipheriv(\"aes-128-cbc\", key, iv);\n const decrypted = Buffer.concat([decipher.update(content), decipher.final()]);\n return JSON.parse(decrypted.toString(\"utf8\"));\n}\n\nfunction createAxiosBase(opts: SaveTubeClientOptions): AxiosInstance {\n if (opts.axios) return opts.axios;\n\n const timeout = opts.timeoutMs ?? 60_000;\n return axios.create({\n timeout,\n maxRedirects: 0,\n validateStatus: (s) => s >= 200 && s < 400,\n httpAgent: new http.Agent({ keepAlive: true }),\n httpsAgent: new https.Agent({ keepAlive: true }),\n headers: {\n \"User-Agent\": opts.userAgent ?? DEFAULT_UA,\n \"Accept-Language\": \"en-US,en;q=0.9\",\n Referer: \"https://www.youtube.com\",\n Origin: \"https://www.youtube.com\",\n },\n });\n}\n\nexport class SaveTubeClient {\n private readonly axiosBase: AxiosInstance;\n\n constructor(private readonly opts: SaveTubeClientOptions = {}) {\n this.axiosBase = createAxiosBase(opts);\n }\n\n private async getCdnBase(): Promise<string> {\n const cdnResponse = await this.axiosBase.get(\n \"https://media.savetube.me/api/random-cdn\",\n {\n timeout: 5_000,\n }\n );\n\n const cdnHost = cdnResponse.data?.cdn as string | undefined;\n if (!cdnHost) throw new Error(\"SaveTube random-cdn returned no cdn host.\");\n\n return /^https?:\\/\\//i.test(cdnHost) ? cdnHost : `https://${cdnHost}`;\n }\n\n async getInfo(\n youtubeUrl: string\n ): Promise<{ cdnBase: string; info: SaveTubeInfo }> {\n const cdnBase = await this.getCdnBase();\n\n const infoResponse = await this.axiosBase.post(\n `${cdnBase}/v2/info`,\n { url: youtubeUrl },\n {\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n }\n );\n\n const raw = infoResponse.data;\n if (!raw?.data)\n throw new Error(\"Invalid SaveTube /v2/info response (missing data).\");\n\n const decoded = decode(raw.data);\n\n if (!decoded?.key)\n throw new Error(\"Invalid SaveTube decoded info (missing key).\");\n\n return { cdnBase, info: decoded as SaveTubeInfo };\n }\n\n async getAudio(\n youtubeUrl: string,\n qualityKbps: number\n ): Promise<DownloadInfo> {\n const { cdnBase, info } = await this.getInfo(youtubeUrl);\n\n const resp = await this.axiosBase.post(\n `${cdnBase}/download`,\n { downloadType: \"audio\", quality: String(qualityKbps), key: info.key },\n {\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n }\n );\n\n const downloadUrl = resp.data?.data?.downloadUrl as string | undefined;\n if (!downloadUrl) throw new Error(\"Invalid SaveTube audio downloadUrl.\");\n\n return {\n title: info.title,\n author: info.author,\n duration: info.duration,\n quality: `${qualityKbps}kbps`,\n filename: `${info.title ?? \"audio\"} ${qualityKbps}kbps.mp3`,\n downloadUrl,\n };\n }\n\n async getVideo(youtubeUrl: string, qualityP: number): Promise<DownloadInfo> {\n const { cdnBase, info } = await this.getInfo(youtubeUrl);\n\n const resp = await this.axiosBase.post(\n `${cdnBase}/download`,\n { downloadType: \"video\", quality: qualityP, key: info.key },\n {\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n }\n );\n\n const downloadUrl = resp.data?.data?.downloadUrl as string | undefined;\n if (!downloadUrl) throw new Error(\"Invalid SaveTube video downloadUrl.\");\n\n return {\n title: info.title,\n author: info.author,\n duration: info.duration,\n quality: `${qualityP}p`,\n filename: `${info.title ?? \"video\"} ${qualityP}p.mp4`,\n downloadUrl,\n };\n }\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport axios, { type AxiosInstance } from \"axios\";\n\nimport { ensureDirSync } from \"./paths.js\";\n\nexport interface DownloadToFileOptions {\n axios?: AxiosInstance;\n timeoutMs?: number;\n fileMode?: number;\n}\n\nexport async function downloadToFile(\n url: string,\n filePath: string,\n opts: DownloadToFileOptions = {}\n): Promise<{ size: number }> {\n const timeoutMs = opts.timeoutMs ?? 120_000;\n const fileMode = opts.fileMode ?? 0o666;\n\n ensureDirSync(path.dirname(filePath));\n\n const client = opts.axios ?? axios;\n\n const response = await client.request({\n url,\n method: \"GET\",\n responseType: \"stream\",\n timeout: timeoutMs,\n });\n\n const writer = fs.createWriteStream(filePath, { mode: fileMode });\n\n await new Promise<void>((resolve, reject) => {\n let done = false;\n\n const finish = () => {\n if (done) return;\n done = true;\n resolve();\n };\n\n const fail = (err: any) => {\n if (done) return;\n done = true;\n reject(err);\n };\n\n writer.on(\"finish\", finish);\n writer.on(\"error\", fail);\n response.data.on(\"error\", fail);\n\n response.data.pipe(writer);\n });\n\n const stats = fs.statSync(filePath);\n if (stats.size === 0) {\n try {\n fs.unlinkSync(filePath);\n } catch {\n // ignore\n }\n throw new Error(\"Downloaded file is empty.\");\n }\n\n return { size: stats.size };\n}\n\nexport interface DownloadToBufferOptions {\n axios?: AxiosInstance;\n timeoutMs?: number;\n maxBytes?: number;\n}\n\nexport async function downloadToBuffer(\n url: string,\n opts: DownloadToBufferOptions = {}\n): Promise<Buffer> {\n const timeoutMs = opts.timeoutMs ?? 120_000;\n const maxBytes = opts.maxBytes ?? 200 * 1024 * 1024;\n\n const client = opts.axios ?? axios;\n\n const response = await client.request({\n url,\n method: \"GET\",\n responseType: \"arraybuffer\",\n timeout: timeoutMs,\n });\n\n const buf = Buffer.from(response.data);\n if (buf.length === 0) throw new Error(\"Downloaded buffer is empty.\");\n if (buf.length > maxBytes)\n throw new Error(`Downloaded buffer exceeds maxBytes (${maxBytes}).`);\n\n return buf;\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,GAAA,GAAAC,EAAAD,GAAA,gBAAAE,IAAA,eAAAC,EAAAH,ICAA,IAAAI,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,EAAc,YAAY,EAE1D,OAAAR,EAAcQ,CAAY,EAC1BR,EAAcS,CAAa,EAEpB,CACL,QAASD,EACT,SAAUC,CACZ,CACF,CCpCA,IAAAC,EAA0C,sBAC1CC,EAAiC,kBACjCC,EAAsB,qBACtBC,EAAuB,sBAiBjBC,EACJ,kHAEF,SAASC,EAAOC,EAAkB,CAChC,IAAMC,EAAe,mCACfC,EAAO,OAAO,KAAKF,EAAK,QAAQ,EAChCG,EAAKD,EAAK,SAAS,EAAG,EAAE,EACxBE,EAAUF,EAAK,SAAS,EAAE,EAE1BG,EAAM,OAAO,KAAKJ,EAAc,KAAK,EACrCK,KAAW,oBAAiB,cAAeD,EAAKF,CAAE,EAClDI,EAAY,OAAO,OAAO,CAACD,EAAS,OAAOF,CAAO,EAAGE,EAAS,MAAM,CAAC,CAAC,EAC5E,OAAO,KAAK,MAAMC,EAAU,SAAS,MAAM,CAAC,CAC9C,CAEA,SAASC,EAAgBC,EAA4C,CACnE,GAAIA,EAAK,MAAO,OAAOA,EAAK,MAE5B,IAAMC,EAAUD,EAAK,WAAa,IAClC,OAAO,EAAAE,QAAM,OAAO,CAClB,QAAAD,EACA,aAAc,EACd,eAAiBE,GAAMA,GAAK,KAAOA,EAAI,IACvC,UAAW,IAAS,QAAM,CAAE,UAAW,EAAK,CAAC,EAC7C,WAAY,IAAU,QAAM,CAAE,UAAW,EAAK,CAAC,EAC/C,QAAS,CACP,aAAcH,EAAK,WAAaX,EAChC,kBAAmB,iBACnB,QAAS,0BACT,OAAQ,yBACV,CACF,CAAC,CACH,CAEO,IAAMe,EAAN,KAAqB,CAG1B,YAA6BJ,EAA8B,CAAC,EAAG,CAAlC,UAAAA,EAC3B,KAAK,UAAYD,EAAgBC,CAAI,CACvC,CAJiB,UAMjB,MAAc,YAA8B,CAQ1C,IAAMK,GAPc,MAAM,KAAK,UAAU,IACvC,2CACA,CACE,QAAS,GACX,CACF,GAE4B,MAAM,IAClC,GAAI,CAACA,EAAS,MAAM,IAAI,MAAM,2CAA2C,EAEzE,MAAO,gBAAgB,KAAKA,CAAO,EAAIA,EAAU,WAAWA,CAAO,EACrE,CAEA,MAAM,QACJC,EACkD,CAClD,IAAMC,EAAU,MAAM,KAAK,WAAW,EAahCC,GAXe,MAAM,KAAK,UAAU,KACxC,GAAGD,CAAO,WACV,CAAE,IAAKD,CAAW,EAClB,CACE,QAAS,CACP,eAAgB,mBAChB,OAAQ,kBACV,CACF,CACF,GAEyB,KACzB,GAAI,CAACE,GAAK,KACR,MAAM,IAAI,MAAM,oDAAoD,EAEtE,IAAMC,EAAUnB,EAAOkB,EAAI,IAAI,EAE/B,GAAI,CAACC,GAAS,IACZ,MAAM,IAAI,MAAM,8CAA8C,EAEhE,MAAO,CAAE,QAAAF,EAAS,KAAME,CAAwB,CAClD,CAEA,MAAM,SACJH,EACAI,EACuB,CACvB,GAAM,CAAE,QAAAH,EAAS,KAAAI,CAAK,EAAI,MAAM,KAAK,QAAQL,CAAU,EAajDM,GAXO,MAAM,KAAK,UAAU,KAChC,GAAGL,CAAO,YACV,CAAE,aAAc,QAAS,QAAS,OAAOG,CAAW,EAAG,IAAKC,EAAK,GAAI,EACrE,CACE,QAAS,CACP,eAAgB,mBAChB,OAAQ,kBACV,CACF,CACF,GAEyB,MAAM,MAAM,YACrC,GAAI,CAACC,EAAa,MAAM,IAAI,MAAM,qCAAqC,EAEvE,MAAO,CACL,MAAOD,EAAK,MACZ,OAAQA,EAAK,OACb,SAAUA,EAAK,SACf,QAAS,GAAGD,CAAW,OACvB,SAAU,GAAGC,EAAK,OAAS,OAAO,IAAID,CAAW,WACjD,YAAAE,CACF,CACF,CAEA,MAAM,SAASN,EAAoBO,EAAyC,CAC1E,GAAM,CAAE,QAAAN,EAAS,KAAAI,CAAK,EAAI,MAAM,KAAK,QAAQL,CAAU,EAajDM,GAXO,MAAM,KAAK,UAAU,KAChC,GAAGL,CAAO,YACV,CAAE,aAAc,QAAS,QAASM,EAAU,IAAKF,EAAK,GAAI,EAC1D,CACE,QAAS,CACP,eAAgB,mBAChB,OAAQ,kBACV,CACF,CACF,GAEyB,MAAM,MAAM,YACrC,GAAI,CAACC,EAAa,MAAM,IAAI,MAAM,qCAAqC,EAEvE,MAAO,CACL,MAAOD,EAAK,MACZ,OAAQA,EAAK,OACb,SAAUA,EAAK,SACf,QAAS,GAAGE,CAAQ,IACpB,SAAU,GAAGF,EAAK,OAAS,OAAO,IAAIE,CAAQ,QAC9C,YAAAD,CACF,CACF,CACF,EC/JA,IAAAE,EAAe,mBACfC,EAAiB,qBACjBC,EAA0C,sBAU1C,eAAsBC,EACpBC,EACAC,EACAC,EAA8B,CAAC,EACJ,CAC3B,IAAMC,EAAYD,EAAK,WAAa,KAC9BE,EAAWF,EAAK,UAAY,IAElCG,EAAc,EAAAC,QAAK,QAAQL,CAAQ,CAAC,EAIpC,IAAMM,EAAW,MAFFL,EAAK,OAAS,EAAAM,SAEC,QAAQ,CACpC,IAAAR,EACA,OAAQ,MACR,aAAc,SACd,QAASG,CACX,CAAC,EAEKM,EAAS,EAAAC,QAAG,kBAAkBT,EAAU,CAAE,KAAMG,CAAS,CAAC,EAEhE,MAAM,IAAI,QAAc,CAACO,EAASC,IAAW,CAC3C,IAAIC,EAAO,GAELC,EAAS,IAAM,CACfD,IACJA,EAAO,GACPF,EAAQ,EACV,EAEMI,EAAQC,GAAa,CACrBH,IACJA,EAAO,GACPD,EAAOI,CAAG,EACZ,EAEAP,EAAO,GAAG,SAAUK,CAAM,EAC1BL,EAAO,GAAG,QAASM,CAAI,EACvBR,EAAS,KAAK,GAAG,QAASQ,CAAI,EAE9BR,EAAS,KAAK,KAAKE,CAAM,CAC3B,CAAC,EAED,IAAMQ,EAAQ,EAAAP,QAAG,SAAST,CAAQ,EAClC,GAAIgB,EAAM,OAAS,EAAG,CACpB,GAAI,CACF,EAAAP,QAAG,WAAWT,CAAQ,CACxB,MAAQ,CAER,CACA,MAAM,IAAI,MAAM,2BAA2B,CAC7C,CAEA,MAAO,CAAE,KAAMgB,EAAM,IAAK,CAC5B,CClEA,IAAAC,EAAgB,0BAIT,SAASC,GAAsBC,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,GAAkBH,EAA8B,CAC9D,IAAMI,EACJ,iIAEIC,GAASL,GAAS,IAAI,MAAMI,CAAK,EACvC,OAAOC,EAAQA,EAAM,CAAC,EAAI,IAC5B,CAEO,SAASC,EAAoBN,EAA8B,CAChE,IAAMO,EAAWR,GAAsBC,CAAK,EAEtCQ,EAAWD,EAAS,MAAM,qBAAqB,IAAI,CAAC,GAAKA,EAEzDE,EAAKN,GAAkBK,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,CLlCA,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,iBAAkB,EAAE,EAC5B,QAAQ,aAAc,EAAE,EACxB,KAAK,EACL,QAAQ,OAAQ,GAAG,EACnB,UAAU,EAAG,GAAG,CACrB,CAEO,IAAMC,EAAN,KAAiB,CACL,KAaA,MACA,MACA,SAEjB,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,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,SAAW,IAAIC,EAAe,CAEnC,CAAC,CACH,CAEA,kBAAkBC,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,GAAIC,EAAS,gBAAkB,KAAK,KAAK,0BACvC,MAAM,IAAI,MAAM,6BAA6B,EAG/C,IAAMC,EAAaC,EAAoBF,EAAS,GAAG,EACnD,GAAI,CAACC,EAAY,MAAM,IAAI,MAAM,sBAAsB,EAEvD,IAAME,EAA+B,CAAE,GAAGH,EAAU,IAAKC,CAAW,EAEpE,KAAK,MAAM,IAAIF,EAAW,CACxB,SAAUI,EACV,MAAO,KACP,MAAO,KACP,UAAW,KAAK,IAAI,EAAI,KAAK,KAAK,MAClC,QAAS,EACX,CAAC,EAED,IAAMC,EAAYlB,EAChB,KAAK,KAAK,mBACVF,CACF,EACMqB,EAASnB,EAAY,KAAK,KAAK,gBAAiBD,CAAe,EAE/DqB,EAAY,KAAK,WACrBP,EACA,QACAE,EACAG,CACF,EACMG,EAAY,KAAK,WAAWR,EAAW,QAASE,EAAYI,CAAM,EAExE,QAAQ,WAAW,CAACC,EAAWC,CAAS,CAAC,EACtC,QAAQ,IAAM,KAAK,MAAM,YAAYR,EAAW,EAAK,CAAC,EACtD,MAAM,IAAM,KAAK,MAAM,YAAYA,EAAW,EAAK,CAAC,CACzD,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,EACJZ,IAAS,QACL,MAAM,KAAK,SAAS,SAASU,EAAYC,CAAO,EAChD,MAAM,KAAK,SAAS,SAASD,EAAYC,CAAO,EAEhDE,EAAYhC,EAAiB+B,EAAK,OAASZ,CAAI,EAE/ClB,EAAW,GAAGkB,CAAI,IAAIT,CAAS,IAAIsB,CAAS,IADtCb,IAAS,QAAU,MAAQ,KACkB,GACnDc,EAAW,EAAAC,QAAK,KAAK,KAAK,MAAM,SAAUjC,CAAQ,EAElD,CAAE,KAAAkC,CAAK,EAAI,MAAMC,EAAeL,EAAK,YAAaE,CAAQ,EAE5DI,EACA,KAAK,KAAK,gBACZA,EAAS,MAAM,EAAAf,QAAG,SAAS,SAASW,CAAQ,GAG9C,IAAMZ,EAAqB,CACzB,KAAMY,EACN,KAAAE,EACA,KAAM,CAAE,QAASJ,EAAK,OAAQ,EAC9B,OAAAM,CACF,EAEA,KAAK,MAAM,QAAQ3B,EAAWS,EAAME,CAAM,EAE1C,KAAK,KAAK,QAAQ,QAAQ,aAAaF,CAAI,IAAIgB,CAAI,WAAWlC,CAAQ,EAAE,CAC1E,OAASqC,EAAK,CACZ,KAAK,KAAK,QAAQ,QAAQ,WAAWnB,CAAI,UAAWmB,CAAG,CACzD,CACF,CAEA,MAAc,eACZnB,EACAU,EACqB,CACrB,IAAMd,EAAYlB,EAChB,KAAK,KAAK,mBACVF,CACF,EACMqB,EAASnB,EAAY,KAAK,KAAK,gBAAiBD,CAAe,EAE/DmC,EACJZ,IAAS,QACL,MAAM,KAAK,SAAS,SAASU,EAAYd,CAAS,EAClD,MAAM,KAAK,SAAS,SAASc,EAAYb,CAAM,EAE/CuB,EAAMpB,IAAS,QAAU,MAAQ,MACjCa,EAAYhC,EAAiB+B,EAAK,OAASZ,CAAI,EAC/Cc,EAAW,EAAAC,QAAK,KACpB,KAAK,MAAM,SACX,GAAGf,CAAI,WAAW,KAAK,IAAI,CAAC,IAAIa,CAAS,IAAIO,CAAG,EAClD,EAEM,CAAE,KAAAJ,CAAK,EAAI,MAAMC,EAAeL,EAAK,YAAaE,CAAQ,EAEhE,MAAO,CACL,KAAMA,EACN,KAAAE,EACA,KAAM,CAAE,QAASJ,EAAK,OAAQ,CAChC,CACF,CACF","names":["index_exports","__export","PlayEngine","__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_axios","import_node_crypto","http","https","DEFAULT_UA","decode","enc","secretKeyHex","data","iv","content","key","decipher","decrypted","createAxiosBase","opts","timeout","axios","s","SaveTubeClient","cdnHost","youtubeUrl","cdnBase","raw","decoded","qualityKbps","info","downloadUrl","qualityP","import_node_fs","import_node_path","import_axios","downloadToFile","url","filePath","opts","timeoutMs","fileMode","ensureDirSync","path","response","axios","writer","fs","resolve","reject","done","finish","fail","err","stats","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","SaveTubeClient","prefix","query","searchBest","requestId","metadata","normalized","normalizeYoutubeUrl","normalizedMeta","audioKbps","videoP","audioTask","videoTask","type","entry","cached","fs","directFile","timeoutMs","intervalMs","started","f","r","youtubeUrl","quality","info","safeTitle","filePath","path","size","downloadToFile","buffer","err","ext"]}
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"]}
package/dist/index.d.cts CHANGED
@@ -8,6 +8,14 @@ interface PlayMetadata {
8
8
  videoId: string;
9
9
  url: string;
10
10
  }
11
+ interface DownloadInfo {
12
+ title?: string;
13
+ author?: string;
14
+ duration?: string;
15
+ filename: string;
16
+ quality: string;
17
+ downloadUrl: string;
18
+ }
11
19
  interface CachedFile {
12
20
  path: string;
13
21
  size: number;
@@ -31,6 +39,12 @@ interface PlayEngineOptions {
31
39
  preferredVideoP?: 1080 | 720 | 480 | 360;
32
40
  preloadBuffer?: boolean;
33
41
  cleanupIntervalMs?: number;
42
+ ytdlpBinaryPath?: string;
43
+ ffmpegPath?: string;
44
+ aria2cPath?: string;
45
+ useAria2c?: boolean;
46
+ concurrentFragments?: number;
47
+ ytdlpTimeoutMs?: number;
34
48
  logger?: {
35
49
  info?: (...args: any[]) => void;
36
50
  warn?: (...args: any[]) => void;
@@ -43,7 +57,7 @@ declare class PlayEngine {
43
57
  private readonly opts;
44
58
  private readonly paths;
45
59
  private readonly cache;
46
- private readonly saveTube;
60
+ private readonly ytdlp;
47
61
  constructor(options?: PlayEngineOptions);
48
62
  generateRequestId(prefix?: string): string;
49
63
  search(query: string): Promise<PlayMetadata | null>;
@@ -60,4 +74,42 @@ declare class PlayEngine {
60
74
  private downloadDirect;
61
75
  }
62
76
 
63
- export { type CachedFile, PlayEngine, type PlayEngineOptions, type PlayMetadata };
77
+ declare function getYouTubeVideoId(input: string): string | null;
78
+ declare function normalizeYoutubeUrl(input: string): string | null;
79
+ declare function searchBest(query: string): Promise<PlayMetadata | null>;
80
+
81
+ interface YtDlpClientOptions {
82
+ binaryPath?: string;
83
+ ffmpegPath?: string;
84
+ aria2cPath?: string;
85
+ timeoutMs?: number;
86
+ useAria2c?: boolean;
87
+ concurrentFragments?: number;
88
+ }
89
+ interface YtDlpVideoInfo {
90
+ id: string;
91
+ title: string;
92
+ uploader?: string;
93
+ duration: number;
94
+ thumbnail?: string;
95
+ }
96
+ declare class YtDlpClient {
97
+ private readonly binaryPath;
98
+ private readonly ffmpegPath?;
99
+ private readonly aria2cPath?;
100
+ private readonly timeoutMs;
101
+ private readonly useAria2c;
102
+ private readonly concurrentFragments;
103
+ constructor(opts?: YtDlpClientOptions);
104
+ private detectYtDlp;
105
+ private detectAria2c;
106
+ private exec;
107
+ getInfo(youtubeUrl: string): Promise<YtDlpVideoInfo>;
108
+ private buildOptimizationArgs;
109
+ getAudio(youtubeUrl: string, qualityKbps: number, outputPath: string): Promise<DownloadInfo>;
110
+ getVideo(youtubeUrl: string, qualityP: number, outputPath: string): Promise<DownloadInfo>;
111
+ private mapAudioQuality;
112
+ private formatDuration;
113
+ }
114
+
115
+ export { type CacheEntry, type CachedFile, type DownloadInfo, type MediaType, PlayEngine, type PlayEngineOptions, type PlayMetadata, YtDlpClient, getYouTubeVideoId, normalizeYoutubeUrl, searchBest };
package/dist/index.d.ts CHANGED
@@ -8,6 +8,14 @@ interface PlayMetadata {
8
8
  videoId: string;
9
9
  url: string;
10
10
  }
11
+ interface DownloadInfo {
12
+ title?: string;
13
+ author?: string;
14
+ duration?: string;
15
+ filename: string;
16
+ quality: string;
17
+ downloadUrl: string;
18
+ }
11
19
  interface CachedFile {
12
20
  path: string;
13
21
  size: number;
@@ -31,6 +39,12 @@ interface PlayEngineOptions {
31
39
  preferredVideoP?: 1080 | 720 | 480 | 360;
32
40
  preloadBuffer?: boolean;
33
41
  cleanupIntervalMs?: number;
42
+ ytdlpBinaryPath?: string;
43
+ ffmpegPath?: string;
44
+ aria2cPath?: string;
45
+ useAria2c?: boolean;
46
+ concurrentFragments?: number;
47
+ ytdlpTimeoutMs?: number;
34
48
  logger?: {
35
49
  info?: (...args: any[]) => void;
36
50
  warn?: (...args: any[]) => void;
@@ -43,7 +57,7 @@ declare class PlayEngine {
43
57
  private readonly opts;
44
58
  private readonly paths;
45
59
  private readonly cache;
46
- private readonly saveTube;
60
+ private readonly ytdlp;
47
61
  constructor(options?: PlayEngineOptions);
48
62
  generateRequestId(prefix?: string): string;
49
63
  search(query: string): Promise<PlayMetadata | null>;
@@ -60,4 +74,42 @@ declare class PlayEngine {
60
74
  private downloadDirect;
61
75
  }
62
76
 
63
- export { type CachedFile, PlayEngine, type PlayEngineOptions, type PlayMetadata };
77
+ declare function getYouTubeVideoId(input: string): string | null;
78
+ declare function normalizeYoutubeUrl(input: string): string | null;
79
+ declare function searchBest(query: string): Promise<PlayMetadata | null>;
80
+
81
+ interface YtDlpClientOptions {
82
+ binaryPath?: string;
83
+ ffmpegPath?: string;
84
+ aria2cPath?: string;
85
+ timeoutMs?: number;
86
+ useAria2c?: boolean;
87
+ concurrentFragments?: number;
88
+ }
89
+ interface YtDlpVideoInfo {
90
+ id: string;
91
+ title: string;
92
+ uploader?: string;
93
+ duration: number;
94
+ thumbnail?: string;
95
+ }
96
+ declare class YtDlpClient {
97
+ private readonly binaryPath;
98
+ private readonly ffmpegPath?;
99
+ private readonly aria2cPath?;
100
+ private readonly timeoutMs;
101
+ private readonly useAria2c;
102
+ private readonly concurrentFragments;
103
+ constructor(opts?: YtDlpClientOptions);
104
+ private detectYtDlp;
105
+ private detectAria2c;
106
+ private exec;
107
+ getInfo(youtubeUrl: string): Promise<YtDlpVideoInfo>;
108
+ private buildOptimizationArgs;
109
+ getAudio(youtubeUrl: string, qualityKbps: number, outputPath: string): Promise<DownloadInfo>;
110
+ getVideo(youtubeUrl: string, qualityP: number, outputPath: string): Promise<DownloadInfo>;
111
+ private mapAudioQuality;
112
+ private formatDuration;
113
+ }
114
+
115
+ export { type CacheEntry, type CachedFile, type DownloadInfo, type MediaType, PlayEngine, type PlayEngineOptions, type PlayMetadata, YtDlpClient, getYouTubeVideoId, normalizeYoutubeUrl, searchBest };
package/dist/index.mjs CHANGED
@@ -1,2 +1,4 @@
1
- import x from"fs";import B from"path";import S from"fs";var f=class{constructor(e){this.opts=e}store=new Map;cleanupTimer;get(e){return this.store.get(e)}set(e,t){this.store.set(e,t)}has(e){return this.store.has(e)}delete(e){this.cleanupEntry(e),this.store.delete(e)}markLoading(e,t){let r=this.store.get(e);r&&(r.loading=t)}setFile(e,t,r){let o=this.store.get(e);o&&(o[t]=r)}cleanupExpired(e=Date.now()){let t=0;for(let[r,o]of this.store.entries())e>o.expiresAt&&(this.delete(r),t++);return t}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(e){let t=this.store.get(e);t&&["audio","video"].forEach(r=>{let o=t[r];if(o?.path&&S.existsSync(o.path))try{S.unlinkSync(o.path)}catch{}})}};import u from"fs";import v from"path";import U from"os";function h(n){u.mkdirSync(n,{recursive:!0,mode:511});try{u.chmodSync(n,511)}catch{}u.accessSync(n,u.constants.R_OK|u.constants.W_OK)}function A(n){let e=n?.trim()?n:v.join(U.tmpdir(),"yt-play"),t=v.resolve(e),r=v.join(t,"play-cache");return h(t),h(r),{baseDir:t,cacheDir:r}}import F from"axios";import{createDecipheriv as _}from"crypto";import*as P from"http";import*as E from"https";var z="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36";function j(n){let e="C5D58EF67A7584E4A29F6C35BBC4EB12",t=Buffer.from(n,"base64"),r=t.subarray(0,16),o=t.subarray(16),i=Buffer.from(e,"hex"),a=_("aes-128-cbc",i,r),s=Buffer.concat([a.update(o),a.final()]);return JSON.parse(s.toString("utf8"))}function R(n){if(n.axios)return n.axios;let e=n.timeoutMs??6e4;return F.create({timeout:e,maxRedirects:0,validateStatus:t=>t>=200&&t<400,httpAgent:new P.Agent({keepAlive:!0}),httpsAgent:new E.Agent({keepAlive:!0}),headers:{"User-Agent":n.userAgent??z,"Accept-Language":"en-US,en;q=0.9",Referer:"https://www.youtube.com",Origin:"https://www.youtube.com"}})}var m=class{constructor(e={}){this.opts=e;this.axiosBase=R(e)}axiosBase;async getCdnBase(){let t=(await this.axiosBase.get("https://media.savetube.me/api/random-cdn",{timeout:5e3})).data?.cdn;if(!t)throw new Error("SaveTube random-cdn returned no cdn host.");return/^https?:\/\//i.test(t)?t:`https://${t}`}async getInfo(e){let t=await this.getCdnBase(),o=(await this.axiosBase.post(`${t}/v2/info`,{url:e},{headers:{"Content-Type":"application/json",Accept:"application/json"}})).data;if(!o?.data)throw new Error("Invalid SaveTube /v2/info response (missing data).");let i=j(o.data);if(!i?.key)throw new Error("Invalid SaveTube decoded info (missing key).");return{cdnBase:t,info:i}}async getAudio(e,t){let{cdnBase:r,info:o}=await this.getInfo(e),a=(await this.axiosBase.post(`${r}/download`,{downloadType:"audio",quality:String(t),key:o.key},{headers:{"Content-Type":"application/json",Accept:"application/json"}})).data?.data?.downloadUrl;if(!a)throw new Error("Invalid SaveTube audio downloadUrl.");return{title:o.title,author:o.author,duration:o.duration,quality:`${t}kbps`,filename:`${o.title??"audio"} ${t}kbps.mp3`,downloadUrl:a}}async getVideo(e,t){let{cdnBase:r,info:o}=await this.getInfo(e),a=(await this.axiosBase.post(`${r}/download`,{downloadType:"video",quality:t,key:o.key},{headers:{"Content-Type":"application/json",Accept:"application/json"}})).data?.data?.downloadUrl;if(!a)throw new Error("Invalid SaveTube video downloadUrl.");return{title:o.title,author:o.author,duration:o.duration,quality:`${t}p`,filename:`${o.title??"video"} ${t}p.mp4`,downloadUrl:a}}};import b from"fs";import V from"path";import K from"axios";async function T(n,e,t={}){let r=t.timeoutMs??12e4,o=t.fileMode??438;h(V.dirname(e));let a=await(t.axios??K).request({url:n,method:"GET",responseType:"stream",timeout:r}),s=b.createWriteStream(e,{mode:o});await new Promise((d,p)=>{let l=!1,w=()=>{l||(l=!0,d())},D=O=>{l||(l=!0,p(O))};s.on("finish",w),s.on("error",D),a.data.on("error",D),a.data.pipe(s)});let c=b.statSync(e);if(c.size===0){try{b.unlinkSync(e)}catch{}throw new Error("Downloaded file is empty.")}return{size:c.size}}import L from"yt-search";function W(n){let e=(n||"").trim(),t=[...e.matchAll(/\[[^\]]*\]\((https?:\/\/[^)\s]+)\)/gi)];return t.length>0?t[0][1].trim():(e=e.replace(/^<([^>]+)>$/,"$1").trim(),e=e.replace(/^["'`](.*)["'`]$/,"$1").trim(),e)}function Y(n){let e=/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=|shorts\/)|youtu\.be\/)([^"&?\/\s]{11})/i,t=(n||"").match(e);return t?t[1]:null}function g(n){let e=W(n),t=e.match(/https?:\/\/[^\s)]+/i)?.[0]??e,r=Y(t);return r?`https://www.youtube.com/watch?v=${r}`:null}async function I(n){let t=(await L(n))?.videos?.[0];if(!t)return null;let r=t.duration?.seconds??0,o=g(t.url)??t.url;return{title:t.title||"Untitled",author:t.author?.name||void 0,duration:t.duration?.timestamp||void 0,thumb:t.image||t.thumbnail||void 0,videoId:t.videoId,url:o,durationSeconds:r}}var $=[320,256,192,128,96,64],C=[1080,720,480,360];function y(n,e){return e.includes(n)?n:e[0]}function k(n){return(n||"").replace(/[\\\/:*?"<>|]/g,"").replace(/[^\w\s-]/gi,"").trim().replace(/\s+/g," ").substring(0,100)}var M=class{opts;paths;cache;saveTube;constructor(e={}){this.opts={ttlMs:e.ttlMs??3*6e4,maxPreloadDurationSeconds:e.maxPreloadDurationSeconds??1200,preferredAudioKbps:e.preferredAudioKbps??128,preferredVideoP:e.preferredVideoP??720,preloadBuffer:e.preloadBuffer??!0,cleanupIntervalMs:e.cleanupIntervalMs??3e4,logger:e.logger},this.paths=A(e.cacheDir),this.cache=new f({cleanupIntervalMs:this.opts.cleanupIntervalMs}),this.cache.start(),this.saveTube=new m({})}generateRequestId(e="play"){return`${e}_${Date.now()}_${Math.random().toString(36).slice(2,8)}`}async search(e){return I(e)}getFromCache(e){return this.cache.get(e)}async preload(e,t){if(e.durationSeconds>this.opts.maxPreloadDurationSeconds)throw new Error("Video too long for preload.");let r=g(e.url);if(!r)throw new Error("Invalid YouTube URL.");let o={...e,url:r};this.cache.set(t,{metadata:o,audio:null,video:null,expiresAt:Date.now()+this.opts.ttlMs,loading:!0});let i=y(this.opts.preferredAudioKbps,$),a=y(this.opts.preferredVideoP,C),s=this.preloadOne(t,"audio",r,i),c=this.preloadOne(t,"video",r,a);Promise.allSettled([s,c]).finally(()=>this.cache.markLoading(t,!1)).catch(()=>this.cache.markLoading(t,!1))}async getOrDownload(e,t){let r=this.cache.get(e);if(!r)throw new Error("Request not found (cache miss).");let o=r[t];if(o?.path&&x.existsSync(o.path)&&o.size>0)return{metadata:r.metadata,file:o,direct:!1};let i=g(r.metadata.url);if(!i)throw new Error("Invalid YouTube URL.");let a=await this.downloadDirect(t,i);return{metadata:r.metadata,file:a,direct:!0}}async waitCache(e,t,r=8e3,o=500){let i=Date.now();for(;Date.now()-i<r;){let s=this.cache.get(e)?.[t];if(s?.path&&x.existsSync(s.path)&&s.size>0)return s;await new Promise(c=>setTimeout(c,o))}return null}cleanup(e){this.cache.delete(e)}async preloadOne(e,t,r,o){try{let i=t==="audio"?await this.saveTube.getAudio(r,o):await this.saveTube.getVideo(r,o),a=k(i.title??t),c=`${t}_${e}_${a}.${t==="audio"?"mp3":"mp4"}`,d=B.join(this.paths.cacheDir,c),{size:p}=await T(i.downloadUrl,d),l;this.opts.preloadBuffer&&(l=await x.promises.readFile(d));let w={path:d,size:p,info:{quality:i.quality},buffer:l};this.cache.setFile(e,t,w),this.opts.logger?.debug?.(`preloaded ${t} ${p} bytes: ${c}`)}catch(i){this.opts.logger?.error?.(`preload ${t} failed`,i)}}async downloadDirect(e,t){let r=y(this.opts.preferredAudioKbps,$),o=y(this.opts.preferredVideoP,C),i=e==="audio"?await this.saveTube.getAudio(t,r):await this.saveTube.getVideo(t,o),a=e==="audio"?"mp3":"mp4",s=k(i.title??e),c=B.join(this.paths.cacheDir,`${e}_direct_${Date.now()}_${s}.${a}`),{size:d}=await T(i.downloadUrl,c);return{path:c,size:d,info:{quality:i.quality}}}};export{M as PlayEngine};
1
+ var M=(n=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(n,{get:(t,e)=>(typeof require<"u"?require:t)[e]}):n)(function(n){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+n+'" is not supported')});import f from"fs";import I from"path";import S from"fs";var g=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&&S.existsSync(i.path))try{S.unlinkSync(i.path)}catch{}})}};import p from"fs";import v from"path";import _ from"os";function $(n){p.mkdirSync(n,{recursive:!0,mode:511});try{p.chmodSync(n,511)}catch{}p.accessSync(n,p.constants.R_OK|p.constants.W_OK)}function T(n){let t=n?.trim()?n:v.join(_.tmpdir(),"yt-play"),e=v.resolve(t),r=v.join(e);return $(e),$(r),{baseDir:e,cacheDir:r}}import{spawn as z}from"child_process";import d from"path";import y from"fs";import{fileURLToPath as V}from"url";var u;try{u=d.dirname(V(import.meta.url))}catch{u=typeof u<"u"?u:process.cwd()}var h=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.resolve(u,"../.."),e=[d.join(t,"bin","yt-dlp"),d.join(t,"bin","yt-dlp.exe")];for(let r of e)if(y.existsSync(r))return r;try{let{execSync:r}=M("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.resolve(u,"../.."),e=[d.join(t,"bin","aria2c"),d.join(t,"bin","aria2c.exe")];for(let r of e)if(y.existsSync(r))return r;try{let{execSync:r}=M("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=z(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.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.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.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.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")}`}};import Y from"yt-search";function k(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 A(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 m(n){let t=k(n),e=t.match(/https?:\/\/[^\s)]+/i)?.[0]??t,r=A(e);return r?`https://www.youtube.com/watch?v=${r}`:null}async function b(n){let e=(await Y(n))?.videos?.[0];if(!e)return null;let r=e.duration?.seconds??0,i=m(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 E=[320,256,192,128,96,64],C=[1080,720,480,360];function w(n,t){return t.includes(n)?n:t[0]}function F(n){return(n||"").replace(/[\\/:*?"<>|]/g,"").replace(/[^\w\s-]/gi,"").trim().replace(/\s+/g," ").substring(0,100)}var P=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=T(t.cacheDir),this.cache=new g({cleanupIntervalMs:this.opts.cleanupIntervalMs}),this.cache.start(),this.ytdlp=new h({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 b(t)}getFromCache(t){return this.cache.get(t)}async preload(t,e){let r=m(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:w(this.opts.preferredAudioKbps,E),o=this.preloadOne(e,"audio",r,s),c=i?[o]:[o,this.preloadOne(e,"video",r,w(this.opts.preferredVideoP,C))];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&&f.existsSync(i.path)&&i.size>0)return{metadata:r.metadata,file:i,direct:!1};let a=m(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&&f.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=F(`temp_${Date.now()}`),o=`${e}_${t}_${a}.${e==="audio"?"m4a":"mp4"}`,c=I.join(this.paths.cacheDir,o),l=e==="audio"?await this.ytdlp.getAudio(r,i,c):await this.ytdlp.getVideo(r,i,c),x=f.statSync(c).size,D;this.opts.preloadBuffer&&(D=await f.promises.readFile(c));let O={path:c,size:x,info:{quality:l.quality},buffer:D};this.cache.setFile(t,e,O),this.opts.logger?.debug?.(`preloaded ${e} ${x} bytes: ${o}`)}catch(a){this.opts.logger?.error?.(`preload ${e} failed`,a)}}async downloadDirect(t,e){let r=w(this.opts.preferredAudioKbps,E),i=w(this.opts.preferredVideoP,C),a=t==="audio"?"m4a":"mp4",s=F(`direct_${Date.now()}`),o=I.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=f.statSync(o);return{path:o,size:l.size,info:{quality:c.quality}}}};export{P as PlayEngine,h as YtDlpClient,A as getYouTubeVideoId,m as normalizeYoutubeUrl,b as searchBest};
2
4
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core/play-engine.ts","../src/core/cache.ts","../src/core/paths.ts","../src/core/savetube.ts","../src/core/download.ts","../src/core/youtube.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\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 { SaveTubeClient } from \"./savetube.js\";\nimport { downloadToBuffer, downloadToFile } from \"./download.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 >\n > &\n Pick<PlayEngineOptions, \"logger\">;\n\n private readonly paths: { baseDir: string; cacheDir: string };\n private readonly cache: CacheStore;\n private readonly saveTube: SaveTubeClient;\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 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.saveTube = new SaveTubeClient({\n // mantém axios interno\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 if (metadata.durationSeconds > this.opts.maxPreloadDurationSeconds) {\n throw new Error(\"Video too long for preload.\");\n }\n\n const normalized = normalizeYoutubeUrl(metadata.url);\n if (!normalized) throw new Error(\"Invalid YouTube URL.\");\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 = pickQuality(\n this.opts.preferredAudioKbps,\n AUDIO_QUALITIES\n );\n const videoP = pickQuality(this.opts.preferredVideoP, VIDEO_QUALITIES);\n\n const audioTask = this.preloadOne(\n requestId,\n \"audio\",\n normalized,\n audioKbps\n );\n const videoTask = this.preloadOne(requestId, \"video\", normalized, videoP);\n\n Promise.allSettled([audioTask, videoTask])\n .finally(() => this.cache.markLoading(requestId, false))\n .catch(() => 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 info: DownloadInfo =\n type === \"audio\"\n ? await this.saveTube.getAudio(youtubeUrl, quality)\n : await this.saveTube.getVideo(youtubeUrl, quality);\n\n const safeTitle = sanitizeFilename(info.title ?? type);\n const ext = type === \"audio\" ? \"mp3\" : \"mp4\";\n const filename = `${type}_${requestId}_${safeTitle}.${ext}`;\n const filePath = path.join(this.paths.cacheDir, filename);\n\n const { size } = await downloadToFile(info.downloadUrl, filePath);\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\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\n const info =\n type === \"audio\"\n ? await this.saveTube.getAudio(youtubeUrl, audioKbps)\n : await this.saveTube.getVideo(youtubeUrl, videoP);\n\n const ext = type === \"audio\" ? \"mp3\" : \"mp4\";\n const safeTitle = sanitizeFilename(info.title ?? type);\n const filePath = path.join(\n this.paths.cacheDir,\n `${type}_direct_${Date.now()}_${safeTitle}.${ext}`\n );\n\n const { size } = await downloadToFile(info.downloadUrl, filePath);\n\n return {\n path: filePath,\n 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, \"play-cache\");\n\n ensureDirSync(resolvedBase);\n ensureDirSync(resolvedCache);\n\n return {\n baseDir: resolvedBase,\n cacheDir: resolvedCache,\n };\n}\n","import axios, { type AxiosInstance } from \"axios\";\nimport { createDecipheriv } from \"node:crypto\";\nimport * as http from \"node:http\";\nimport * as https from \"node:https\";\n\nimport type { DownloadInfo } from \"./types.js\";\n\nexport interface SaveTubeClientOptions {\n axios?: AxiosInstance;\n timeoutMs?: number;\n userAgent?: string;\n}\n\ntype SaveTubeInfo = {\n key: string;\n title?: string;\n author?: string;\n duration?: string;\n};\n\nconst DEFAULT_UA =\n \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36\";\n\nfunction decode(enc: string): any {\n const secretKeyHex = \"C5D58EF67A7584E4A29F6C35BBC4EB12\";\n const data = Buffer.from(enc, \"base64\");\n const iv = data.subarray(0, 16);\n const content = data.subarray(16);\n\n const key = Buffer.from(secretKeyHex, \"hex\");\n const decipher = createDecipheriv(\"aes-128-cbc\", key, iv);\n const decrypted = Buffer.concat([decipher.update(content), decipher.final()]);\n return JSON.parse(decrypted.toString(\"utf8\"));\n}\n\nfunction createAxiosBase(opts: SaveTubeClientOptions): AxiosInstance {\n if (opts.axios) return opts.axios;\n\n const timeout = opts.timeoutMs ?? 60_000;\n return axios.create({\n timeout,\n maxRedirects: 0,\n validateStatus: (s) => s >= 200 && s < 400,\n httpAgent: new http.Agent({ keepAlive: true }),\n httpsAgent: new https.Agent({ keepAlive: true }),\n headers: {\n \"User-Agent\": opts.userAgent ?? DEFAULT_UA,\n \"Accept-Language\": \"en-US,en;q=0.9\",\n Referer: \"https://www.youtube.com\",\n Origin: \"https://www.youtube.com\",\n },\n });\n}\n\nexport class SaveTubeClient {\n private readonly axiosBase: AxiosInstance;\n\n constructor(private readonly opts: SaveTubeClientOptions = {}) {\n this.axiosBase = createAxiosBase(opts);\n }\n\n private async getCdnBase(): Promise<string> {\n const cdnResponse = await this.axiosBase.get(\n \"https://media.savetube.me/api/random-cdn\",\n {\n timeout: 5_000,\n }\n );\n\n const cdnHost = cdnResponse.data?.cdn as string | undefined;\n if (!cdnHost) throw new Error(\"SaveTube random-cdn returned no cdn host.\");\n\n return /^https?:\\/\\//i.test(cdnHost) ? cdnHost : `https://${cdnHost}`;\n }\n\n async getInfo(\n youtubeUrl: string\n ): Promise<{ cdnBase: string; info: SaveTubeInfo }> {\n const cdnBase = await this.getCdnBase();\n\n const infoResponse = await this.axiosBase.post(\n `${cdnBase}/v2/info`,\n { url: youtubeUrl },\n {\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n }\n );\n\n const raw = infoResponse.data;\n if (!raw?.data)\n throw new Error(\"Invalid SaveTube /v2/info response (missing data).\");\n\n const decoded = decode(raw.data);\n\n if (!decoded?.key)\n throw new Error(\"Invalid SaveTube decoded info (missing key).\");\n\n return { cdnBase, info: decoded as SaveTubeInfo };\n }\n\n async getAudio(\n youtubeUrl: string,\n qualityKbps: number\n ): Promise<DownloadInfo> {\n const { cdnBase, info } = await this.getInfo(youtubeUrl);\n\n const resp = await this.axiosBase.post(\n `${cdnBase}/download`,\n { downloadType: \"audio\", quality: String(qualityKbps), key: info.key },\n {\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n }\n );\n\n const downloadUrl = resp.data?.data?.downloadUrl as string | undefined;\n if (!downloadUrl) throw new Error(\"Invalid SaveTube audio downloadUrl.\");\n\n return {\n title: info.title,\n author: info.author,\n duration: info.duration,\n quality: `${qualityKbps}kbps`,\n filename: `${info.title ?? \"audio\"} ${qualityKbps}kbps.mp3`,\n downloadUrl,\n };\n }\n\n async getVideo(youtubeUrl: string, qualityP: number): Promise<DownloadInfo> {\n const { cdnBase, info } = await this.getInfo(youtubeUrl);\n\n const resp = await this.axiosBase.post(\n `${cdnBase}/download`,\n { downloadType: \"video\", quality: qualityP, key: info.key },\n {\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n }\n );\n\n const downloadUrl = resp.data?.data?.downloadUrl as string | undefined;\n if (!downloadUrl) throw new Error(\"Invalid SaveTube video downloadUrl.\");\n\n return {\n title: info.title,\n author: info.author,\n duration: info.duration,\n quality: `${qualityP}p`,\n filename: `${info.title ?? \"video\"} ${qualityP}p.mp4`,\n downloadUrl,\n };\n }\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport axios, { type AxiosInstance } from \"axios\";\n\nimport { ensureDirSync } from \"./paths.js\";\n\nexport interface DownloadToFileOptions {\n axios?: AxiosInstance;\n timeoutMs?: number;\n fileMode?: number;\n}\n\nexport async function downloadToFile(\n url: string,\n filePath: string,\n opts: DownloadToFileOptions = {}\n): Promise<{ size: number }> {\n const timeoutMs = opts.timeoutMs ?? 120_000;\n const fileMode = opts.fileMode ?? 0o666;\n\n ensureDirSync(path.dirname(filePath));\n\n const client = opts.axios ?? axios;\n\n const response = await client.request({\n url,\n method: \"GET\",\n responseType: \"stream\",\n timeout: timeoutMs,\n });\n\n const writer = fs.createWriteStream(filePath, { mode: fileMode });\n\n await new Promise<void>((resolve, reject) => {\n let done = false;\n\n const finish = () => {\n if (done) return;\n done = true;\n resolve();\n };\n\n const fail = (err: any) => {\n if (done) return;\n done = true;\n reject(err);\n };\n\n writer.on(\"finish\", finish);\n writer.on(\"error\", fail);\n response.data.on(\"error\", fail);\n\n response.data.pipe(writer);\n });\n\n const stats = fs.statSync(filePath);\n if (stats.size === 0) {\n try {\n fs.unlinkSync(filePath);\n } catch {\n // ignore\n }\n throw new Error(\"Downloaded file is empty.\");\n }\n\n return { size: stats.size };\n}\n\nexport interface DownloadToBufferOptions {\n axios?: AxiosInstance;\n timeoutMs?: number;\n maxBytes?: number;\n}\n\nexport async function downloadToBuffer(\n url: string,\n opts: DownloadToBufferOptions = {}\n): Promise<Buffer> {\n const timeoutMs = opts.timeoutMs ?? 120_000;\n const maxBytes = opts.maxBytes ?? 200 * 1024 * 1024;\n\n const client = opts.axios ?? axios;\n\n const response = await client.request({\n url,\n method: \"GET\",\n responseType: \"arraybuffer\",\n timeout: timeoutMs,\n });\n\n const buf = Buffer.from(response.data);\n if (buf.length === 0) throw new Error(\"Downloaded buffer is empty.\");\n if (buf.length > maxBytes)\n throw new Error(`Downloaded buffer exceeds maxBytes (${maxBytes}).`);\n\n return buf;\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":"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","names":["fs","path","fs","CacheStore","opts","requestId","entry","loading","e","type","file","now","removed","f","fs","path","os","ensureDirSync","dirPath","resolvePaths","cacheDir","baseDir","resolvedBase","resolvedCache","axios","createDecipheriv","http","https","DEFAULT_UA","decode","enc","secretKeyHex","data","iv","content","key","decipher","decrypted","createAxiosBase","opts","timeout","s","SaveTubeClient","cdnHost","youtubeUrl","cdnBase","raw","decoded","qualityKbps","info","downloadUrl","qualityP","fs","path","axios","downloadToFile","url","filePath","opts","timeoutMs","fileMode","ensureDirSync","path","response","axios","writer","fs","resolve","reject","done","finish","fail","err","stats","yts","stripWeirdUrlWrappers","input","s","mdAll","getYouTubeVideoId","regex","match","normalizeYoutubeUrl","cleaned0","firstUrl","id","searchBest","query","v","durationSeconds","normalizedUrl","AUDIO_QUALITIES","VIDEO_QUALITIES","pickQuality","requested","available","sanitizeFilename","filename","PlayEngine","options","resolvePaths","CacheStore","SaveTubeClient","prefix","query","searchBest","requestId","metadata","normalized","normalizeYoutubeUrl","normalizedMeta","audioKbps","videoP","audioTask","videoTask","type","entry","cached","fs","directFile","timeoutMs","intervalMs","started","f","r","youtubeUrl","quality","info","safeTitle","filePath","path","size","downloadToFile","buffer","err","ext"]}
1
+ {"version":3,"sources":["../src/core/play-engine.ts","../src/core/cache.ts","../src/core/paths.ts","../src/core/ytdlp-client.ts","../src/core/youtube.ts"],"sourcesContent":["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":"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","names":["fs","path","fs","CacheStore","opts","requestId","entry","loading","e","type","file","now","removed","f","fs","path","os","ensureDirSync","dirPath","resolvePaths","cacheDir","baseDir","resolvedBase","resolvedCache","spawn","path","fs","fileURLToPath","__dirname","YtDlpClient","opts","packageRoot","bundledPaths","p","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","yts","stripWeirdUrlWrappers","input","s","mdAll","getYouTubeVideoId","regex","match","normalizeYoutubeUrl","cleaned0","firstUrl","id","searchBest","query","v","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"]}
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "@irithell-js/yt-play",
3
- "version": "0.1.3",
4
- "description": "YouTube search + SaveTube download engine (audio/video) with optional caching.",
3
+ "version": "0.2.3",
4
+ "description": "YouTube search + download engine (audio/video) with optional caching.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "sideEffects": false,
8
8
  "files": [
9
- "dist"
9
+ "dist",
10
+ "bin",
11
+ "scripts"
10
12
  ],
11
13
  "exports": {
12
14
  ".": {
@@ -24,6 +26,7 @@
24
26
  "module": "./dist/index.mjs",
25
27
  "types": "./dist/index.d.ts",
26
28
  "scripts": {
29
+ "postinstall": "node scripts/setup-binaries.mjs",
27
30
  "clean": "rm -rf dist",
28
31
  "build": "tsup",
29
32
  "dev": "tsup --watch",
@@ -31,6 +34,7 @@
31
34
  "pack:check": "npm pack --silent && node -e \"console.log('packed')\""
32
35
  },
33
36
  "dependencies": {
37
+ "adm-zip": "^0.5.16",
34
38
  "axios": "^1.13.2",
35
39
  "yt-search": "^2.13.1"
36
40
  },
@@ -42,5 +46,15 @@
42
46
  },
43
47
  "publishConfig": {
44
48
  "access": "public"
45
- }
49
+ },
50
+ "keywords": [
51
+ "youtube",
52
+ "yt-dlp",
53
+ "download",
54
+ "audio",
55
+ "video",
56
+ "aria2c",
57
+ "search",
58
+ "cache"
59
+ ]
46
60
  }
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import https from "https";
6
+ import { fileURLToPath } from "url";
7
+ import AdmZip from "adm-zip";
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+
12
+ const BINS_DIR = path.join(__dirname, "..", "bin");
13
+ const ARIA2_VERSION = "1.37.0";
14
+ const YTDLP_VERSION = "2025.12.08";
15
+ const PLATFORM_FILE = path.join(BINS_DIR, ".platform");
16
+
17
+ const ARIA2_BINARIES = {
18
+ linux: {
19
+ x64: `https://github.com/abcfy2/aria2-static-build/releases/download/${ARIA2_VERSION}/aria2-x86_64-linux-musl_static.zip`,
20
+ arm64: `https://github.com/abcfy2/aria2-static-build/releases/download/${ARIA2_VERSION}/aria2-aarch64-linux-musl_static.zip`,
21
+ arm: `https://github.com/abcfy2/aria2-static-build/releases/download/${ARIA2_VERSION}/aria2-arm-linux-musleabi_static.zip`,
22
+ },
23
+ darwin: {
24
+ x64: `https://github.com/abcfy2/aria2-static-build/releases/download/${ARIA2_VERSION}/aria2-x86_64-linux-musl_static.zip`,
25
+ arm64: `https://github.com/abcfy2/aria2-static-build/releases/download/${ARIA2_VERSION}/aria2-aarch64-linux-musl_static.zip`,
26
+ },
27
+ win32: {
28
+ x64: `https://github.com/abcfy2/aria2-static-build/releases/download/${ARIA2_VERSION}/aria2-x86_64-w64-mingw32_static.zip`,
29
+ ia32: `https://github.com/abcfy2/aria2-static-build/releases/download/${ARIA2_VERSION}/aria2-i686-w64-mingw32_static.zip`,
30
+ },
31
+ };
32
+
33
+ const YTDLP_BINARIES = {
34
+ linux: {
35
+ x64: `https://github.com/yt-dlp/yt-dlp/releases/download/${YTDLP_VERSION}/yt-dlp_linux`,
36
+ arm64: `https://github.com/yt-dlp/yt-dlp/releases/download/${YTDLP_VERSION}/yt-dlp_linux_aarch64`,
37
+ },
38
+ darwin: {
39
+ x64: `https://github.com/yt-dlp/yt-dlp/releases/download/${YTDLP_VERSION}/yt-dlp_macos`,
40
+ arm64: `https://github.com/yt-dlp/yt-dlp/releases/download/${YTDLP_VERSION}/yt-dlp_macos`,
41
+ },
42
+ win32: {
43
+ x64: `https://github.com/yt-dlp/yt-dlp/releases/download/${YTDLP_VERSION}/yt-dlp.exe`,
44
+ ia32: `https://github.com/yt-dlp/yt-dlp/releases/download/${YTDLP_VERSION}/yt-dlp_x86.exe`,
45
+ arm64: `https://github.com/yt-dlp/yt-dlp/releases/download/${YTDLP_VERSION}/yt-dlp_arm64.exe`,
46
+ },
47
+ };
48
+
49
+ function download(url, dest) {
50
+ return new Promise((resolve, reject) => {
51
+ const file = fs.createWriteStream(dest);
52
+ https
53
+ .get(url, (response) => {
54
+ if (response.statusCode === 302 || response.statusCode === 301) {
55
+ file.close();
56
+ fs.unlinkSync(dest);
57
+ return download(response.headers.location, dest)
58
+ .then(resolve)
59
+ .catch(reject);
60
+ }
61
+ response.pipe(file);
62
+ file.on("finish", () => file.close(resolve));
63
+ })
64
+ .on("error", (err) => {
65
+ fs.unlink(dest, () => {});
66
+ reject(err);
67
+ });
68
+ });
69
+ }
70
+
71
+ function getCurrentPlatform() {
72
+ return `${process.platform}-${process.arch}`;
73
+ }
74
+
75
+ function getPlatformFromFile() {
76
+ try {
77
+ return fs.readFileSync(PLATFORM_FILE, "utf-8").trim();
78
+ } catch {
79
+ return null;
80
+ }
81
+ }
82
+
83
+ function savePlatform() {
84
+ fs.writeFileSync(PLATFORM_FILE, getCurrentPlatform());
85
+ }
86
+
87
+ async function setupAria2c(platform, arch) {
88
+ if (!ARIA2_BINARIES[platform] || !ARIA2_BINARIES[platform][arch]) {
89
+ return;
90
+ }
91
+
92
+ const aria2cPath = path.join(
93
+ BINS_DIR,
94
+ platform === "win32" ? "aria2c.exe" : "aria2c",
95
+ );
96
+
97
+ if (fs.existsSync(aria2cPath)) {
98
+ return;
99
+ }
100
+
101
+ try {
102
+ const url = ARIA2_BINARIES[platform][arch];
103
+ const zipPath = path.join(BINS_DIR, "aria2.zip");
104
+
105
+ await download(url, zipPath);
106
+
107
+ const zip = new AdmZip(zipPath);
108
+ zip.extractAllTo(BINS_DIR, true);
109
+
110
+ const extractedFile = platform === "win32" ? "aria2c.exe" : "aria2c";
111
+ const extractedPath = path.join(BINS_DIR, extractedFile);
112
+
113
+ if (fs.existsSync(extractedPath) && platform !== "win32") {
114
+ fs.chmodSync(extractedPath, 0o755);
115
+ }
116
+
117
+ fs.unlinkSync(zipPath);
118
+ } catch (error) {
119
+ // Silent fail
120
+ }
121
+ }
122
+
123
+ async function setupYtDlp(platform, arch) {
124
+ if (!YTDLP_BINARIES[platform] || !YTDLP_BINARIES[platform][arch]) {
125
+ return;
126
+ }
127
+
128
+ const ytdlpPath = path.join(
129
+ BINS_DIR,
130
+ platform === "win32" ? "yt-dlp.exe" : "yt-dlp",
131
+ );
132
+
133
+ if (fs.existsSync(ytdlpPath)) {
134
+ return;
135
+ }
136
+
137
+ try {
138
+ const url = YTDLP_BINARIES[platform][arch];
139
+ await download(url, ytdlpPath);
140
+
141
+ if (platform !== "win32") {
142
+ fs.chmodSync(ytdlpPath, 0o755);
143
+ }
144
+ } catch (error) {
145
+ // Silent fail
146
+ }
147
+ }
148
+
149
+ async function setup() {
150
+ const platform = process.platform;
151
+ const arch = process.arch;
152
+ const currentPlatform = getCurrentPlatform();
153
+ const savedPlatform = getPlatformFromFile();
154
+
155
+ if (savedPlatform && savedPlatform !== currentPlatform) {
156
+ try {
157
+ const files = fs.readdirSync(BINS_DIR);
158
+ for (const file of files) {
159
+ if (file.startsWith("aria2c") || file.startsWith("yt-dlp")) {
160
+ fs.unlinkSync(path.join(BINS_DIR, file));
161
+ }
162
+ }
163
+ } catch (err) {
164
+ // Ignore cleanup errors
165
+ }
166
+ }
167
+
168
+ fs.mkdirSync(BINS_DIR, { recursive: true });
169
+
170
+ await Promise.all([setupAria2c(platform, arch), setupYtDlp(platform, arch)]);
171
+
172
+ savePlatform();
173
+ }
174
+
175
+ setup().catch(() => {
176
+ process.exit(0);
177
+ });