@irithell-js/yt-play 0.1.3 → 0.2.5

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,384 @@
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)
12
54
 
13
- ### 1) Buscar e iniciar preload
55
+ ```javascript
56
+ const { PlayEngine } = require("@irithell-js/yt-play");
14
57
 
15
- ```ts
16
- import { PlayEngine } from "@irithell/yt-play";
58
+ const engine = new PlayEngine();
17
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
+ ```
73
+
74
+ ## Configuration
75
+
76
+ ### Constructor Options
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
+ // cookies (optional for VPS and dockers)
105
+ cookiesPath: "./cookies.txt",
106
+ // or
107
+ cookiesFromBrowser: "firefox", // extract from browser
108
+ });
109
+ ```
110
+
111
+ ### Quality Presets
112
+
113
+ ```typescript
114
+ // High quality (larger files)
115
+ const hq = new PlayEngine({
116
+ preferredAudioKbps: 320,
117
+ preferredVideoP: 1080,
118
+ });
119
+
120
+ // Balanced (recommended)
121
+ const balanced = new PlayEngine({
21
122
  preferredAudioKbps: 128,
22
123
  preferredVideoP: 720,
23
- preloadBuffer: true,
24
124
  });
25
125
 
26
- const metadata = await engine.search("linkin park numb");
27
- if (!metadata) throw new Error("Nada encontrado");
126
+ // Low quality (faster, smaller)
127
+ const lq = new PlayEngine({
128
+ preferredAudioKbps: 96,
129
+ preferredVideoP: 480,
130
+ });
131
+ ```
28
132
 
29
- const requestId = engine.generateRequestId();
30
- await engine.preload(metadata, requestId);
133
+ ## API Reference
134
+
135
+ ### PlayEngine Methods
136
+
137
+ #### `search(query: string): Promise<PlayMetadata | null>`
31
138
 
32
- // depois você usa requestId pra pedir audio/video
139
+ Search for a video on YouTube.
140
+
141
+ ```typescript
142
+ const metadata = await engine.search("artist - song name");
143
+ // Returns: { title, author, duration, durationSeconds, thumb, videoId, url }
33
144
  ```
34
145
 
35
- ### 2) Obter do cache (ou baixar direto)
146
+ #### `generateRequestId(prefix?: string): string`
147
+
148
+ Generate unique request ID for caching.
36
149
 
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);
150
+ ```typescript
151
+ const requestId = engine.generateRequestId("audio"); // "audio_1234567890_abc123"
152
+ ```
41
153
 
42
- // video
43
- const video = await engine.getOrDownload(requestId, "video");
44
- console.log(video.file.info.quality, video.file.path);
154
+ #### `preload(metadata: PlayMetadata, requestId: string): Promise<void>`
45
155
 
46
- // se você não quiser manter o arquivo, apague depois de enviar
47
- engine.cleanup(requestId);
156
+ Pre-download audio and video in parallel (cached for TTL duration).
157
+
158
+ ```typescript
159
+ await engine.preload(metadata, requestId);
160
+ // Downloads audio + video if <1h, only audio if >1h (96kbps)
48
161
  ```
49
162
 
50
- ### 3) Esperar o cache “aquecer” (opcional)
163
+ #### `getOrDownload(requestId: string, type: 'audio' | 'video'): Promise<Result>`
51
164
 
52
- ```ts
53
- const cached = await engine.waitCache(requestId, "audio", 8000, 500);
165
+ Get file from cache or download directly.
166
+
167
+ ```typescript
168
+ const result = await engine.getOrDownload(requestId, "audio");
169
+ // Returns: { metadata, file: { path, size, info, buffer? }, direct: boolean }
54
170
 
171
+ console.log(result.file.path); // "/tmp/cache/audio_xxx.m4a"
172
+ console.log(result.file.size); // 8457234 (bytes)
173
+ console.log(result.file.info.quality); // "128kbps m4a"
174
+ console.log(result.direct); // false if from cache, true if direct download
175
+ ```
176
+
177
+ #### `waitCache(requestId: string, type: 'audio' | 'video', timeoutMs?: number, intervalMs?: number): Promise<CachedFile | null>`
178
+
179
+ Wait for cache to be ready (useful for checking preload status).
180
+
181
+ ```typescript
182
+ const cached = await engine.waitCache(requestId, "audio", 8000, 500);
55
183
  if (cached) {
56
- // já está no disco (e possivelmente com buffer em RAM)
57
- console.log("cache pronto", cached.path);
184
+ console.log("Cache ready:", cached.path);
58
185
  } 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);
186
+ console.log("Timeout - falling back to direct download");
187
+ }
188
+ ```
189
+
190
+ #### `cleanup(requestId: string): void`
191
+
192
+ Remove cached files for a request.
193
+
194
+ ```typescript
195
+ engine.cleanup(requestId); // Deletes audio + video from cache
196
+ ```
197
+
198
+ #### `getFromCache(requestId: string): CacheEntry | undefined`
199
+
200
+ Get cache entry metadata (without downloading).
201
+
202
+ ```typescript
203
+ const entry = engine.getFromCache(requestId);
204
+ if (entry) {
205
+ console.log(entry.metadata.title);
206
+ console.log(entry.audio?.path);
207
+ console.log(entry.loading); // true if preload in progress
62
208
  }
63
209
  ```
64
210
 
65
- ## API
211
+ ## Advanced Usage
66
212
 
67
- ### `new PlayEngine(options?)`
213
+ ### Handle Long Videos (>1h)
68
214
 
69
- Opções principais:
215
+ ```typescript
216
+ const metadata = await engine.search("2h music mix");
70
217
 
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.
218
+ // Automatically uses:
219
+ // - Audio: 96kbps (reduced quality)
220
+ // - Video: skipped (audio only)
221
+ await engine.preload(metadata, requestId);
78
222
 
79
- ### Métodos
223
+ const { file } = await engine.getOrDownload(requestId, "audio");
224
+ // Fast download with reduced quality
225
+ ```
80
226
 
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`
227
+ ### Custom Cache Directory
87
228
 
88
- ## Licença
229
+ ```typescript
230
+ import path from "path";
231
+
232
+ const engine = new PlayEngine({
233
+ cacheDir: path.join(process.cwd(), "downloads"),
234
+ ttlMs: 10 * 60_000, // 10 minutes
235
+ });
236
+ ```
237
+
238
+ ### Performance Monitoring
239
+
240
+ ```typescript
241
+ const startTime = Date.now();
242
+ await engine.preload(metadata, requestId);
243
+ const preloadTime = Date.now() - startTime;
244
+
245
+ console.log(`Preload took ${(preloadTime / 1000).toFixed(2)}s`);
246
+ ```
247
+
248
+ ### Error Handling
249
+
250
+ ```typescript
251
+ try {
252
+ const metadata = await engine.search("non-existent-video");
253
+ if (!metadata) {
254
+ console.error("Video not found");
255
+ return;
256
+ }
257
+
258
+ await engine.preload(metadata, requestId);
259
+ const { file } = await engine.getOrDownload(requestId, "audio");
260
+ console.log("Success:", file.path);
261
+ } catch (error) {
262
+ console.error("Download failed:", error.message);
263
+ }
264
+ ```
265
+
266
+ ## Performance
267
+
268
+ With aria2c enabled (default):
269
+
270
+ | Video Length | Audio Download | Video Download | Total Time |
271
+ | ------------ | -------------- | -------------- | ---------- |
272
+ | 5 min | ~3-5s | ~6-8s | ~8s |
273
+ | 1 hour | ~15-20s | Audio only | ~20s |
274
+ | 2 hours | ~25-30s | Audio only | ~30s |
275
+
276
+ _Times may vary based on network speed and YouTube throttling_
277
+
278
+ _The values ​​are based on local tests with optimized caching, for downloading long videos use direct download_
279
+
280
+ ## File Formats
281
+
282
+ - **Audio**: M4A (native format, no conversion needed)
283
+ - **Video**: MP4 (with audio merged)
284
+
285
+ M4A provides better quality-to-size ratio and downloads 10-20x faster (no re-encoding).
286
+
287
+ ## Requirements
288
+
289
+ - Node.js >= 18.0.0
290
+ - ~50MB disk space for binaries (auto-downloaded)
291
+ - Optional: ffmpeg for advanced features
292
+
293
+ ## Binaries
294
+
295
+ The package automatically downloads:
296
+
297
+ - **yt-dlp** v2025.12.08 (35 MB)
298
+ - **aria2c** v1.37.0 (12 MB)
299
+
300
+ Binaries are platform-specific and downloaded on first `npm install`.
301
+
302
+ ### Supported Platforms
303
+
304
+ - Linux x64 / arm64
305
+ - macOS x64 / arm64 (Apple Silicon)
306
+ - Windows x64
307
+
308
+ ### Manual Binary Paths
309
+
310
+ ```typescript
311
+ const engine = new PlayEngine({
312
+ ytdlpBinaryPath: "/custom/path/yt-dlp",
313
+ aria2cPath: "/custom/path/aria2c",
314
+ });
315
+ ```
316
+
317
+ ## Troubleshooting
318
+
319
+ ### Slow Downloads
320
+
321
+ ```typescript
322
+ // Enable aria2c explicitly
323
+ const engine = new PlayEngine({
324
+ useAria2c: true,
325
+ concurrentFragments: 10, // Increase parallelism
326
+ });
327
+ ```
328
+
329
+ ### Cache Issues
330
+
331
+ ```typescript
332
+ // Clear cache directory manually
333
+ import fs from "fs";
334
+ fs.rmSync("./cache", { recursive: true, force: true });
335
+ ```
336
+
337
+ ### Binary Not Found
338
+
339
+ Binaries are auto-downloaded to `node_modules/@irithell-js/yt-play/bin/`. If missing:
340
+
341
+ ```bash
342
+ npm rebuild @irithell-js/yt-play
343
+ ```
344
+
345
+ ## License
89
346
 
90
347
  MIT
348
+
349
+ ## Contributing
350
+
351
+ Issues and PRs welcome!
352
+
353
+ ## Changelog
354
+
355
+ Deprecated versions have been removed to prevent errors during use.
356
+
357
+ ### 0.2.5
358
+
359
+ - Added support to direct cookies extraction in pre built browsers
360
+
361
+ ### 0.2.4
362
+
363
+ - Added support to cookies.txt
364
+
365
+ ### 0.2.3
366
+
367
+ - Updated documentation
368
+ - Improved error messages
369
+
370
+ ### 0.2.2
371
+
372
+ - Many syntax errors fixed
373
+
374
+ ### 0.2.1
375
+
376
+ - Added auto-detection for yt-dlp and aria2c binaries
377
+ - Fixed CommonJS compatibility
378
+ - Improved error handling for long videos
379
+
380
+ ### 0.2.0
381
+
382
+ - Initial release with bundled binaries
383
+ - aria2c acceleration support
384
+ - 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 Y=Object.create;var w=Object.defineProperty;var j=Object.getOwnPropertyDescriptor;var L=Object.getOwnPropertyNames;var R=Object.getPrototypeOf,U=Object.prototype.hasOwnProperty;var K=(o,t)=>{for(var e in t)w(o,e,{get:t[e],enumerable:!0})},A=(o,t,e,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of L(t))!U.call(o,i)&&i!==e&&w(o,i,{get:()=>t[i],enumerable:!(r=j(t,i))||r.enumerable});return o};var u=(o,t,e)=>(e=o!=null?Y(R(o)):{},A(t||!o||!o.__esModule?w(e,"default",{value:o,enumerable:!0}):e,o)),q=o=>A(w({},"__esModule",{value:!0}),o);var Q={};K(Q,{PlayEngine:()=>D,YtDlpClient:()=>f,getYouTubeVideoId:()=>S,normalizeYoutubeUrl:()=>g,searchBest:()=>v});module.exports=q(Q);var y=u(require("fs"),1),$=u(require("path"),1);var M=u(require("fs"),1),b=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 h=u(require("fs"),1),P=u(require("path"),1),F=u(require("os"),1);function I(o){h.default.mkdirSync(o,{recursive:!0,mode:511});try{h.default.chmodSync(o,511)}catch{}h.default.accessSync(o,h.default.constants.R_OK|h.default.constants.W_OK)}function E(o){let t=o?.trim()?o:P.default.join(F.default.tmpdir(),"yt-play"),e=P.default.resolve(t),r=P.default.join(e);return I(e),I(r),{baseDir:e,cacheDir:r}}var C=require("child_process"),d=u(require("path"),1),p=u(require("fs"),1),W={},m;try{m=d.default.dirname(new URL(W.url).pathname)}catch{m=typeof m<"u"?m:process.cwd()}var f=class{binaryPath;ffmpegPath;aria2cPath;timeoutMs;useAria2c;concurrentFragments;cookiesPath;cookiesFromBrowser;constructor(t={}){this.binaryPath=t.binaryPath||this.detectYtDlp(),this.ffmpegPath=t.ffmpegPath,this.timeoutMs=t.timeoutMs??3e5,this.concurrentFragments=t.concurrentFragments??5,this.cookiesPath=t.cookiesPath,this.cookiesFromBrowser=t.cookiesFromBrowser,this.aria2cPath=t.aria2cPath||this.detectAria2c(),this.useAria2c=t.useAria2c??!!this.aria2cPath}detectYtDlp(){let t=d.default.resolve(m,"../.."),e=[d.default.join(t,"bin","yt-dlp"),d.default.join(t,"bin","yt-dlp.exe")];for(let r of e)if(p.default.existsSync(r))return r;try{let{execSync:r}=require("child_process"),i=process.platform==="win32"?"where yt-dlp":"which yt-dlp",n=r(i,{encoding:"utf-8"}).trim();if(n)return n.split(`
2
+ `)[0]}catch{}return"yt-dlp"}detectAria2c(){let t=d.default.resolve(m,"../.."),e=[d.default.join(t,"bin","aria2c"),d.default.join(t,"bin","aria2c.exe")];for(let r of e)if(p.default.existsSync(r))return r;try{let{execSync:r}=require("child_process"),i=process.platform==="win32"?"where aria2c":"which aria2c",n=r(i,{encoding:"utf-8"}).trim();if(n)return n.split(`
3
+ `)[0]}catch{}}async exec(t){return new Promise((e,r)=>{let i=[...t];this.ffmpegPath&&(i=["--ffmpeg-location",this.ffmpegPath,...i]),this.cookiesPath&&p.default.existsSync(this.cookiesPath)&&(i=["--cookies",this.cookiesPath,...i]),this.cookiesFromBrowser&&(i=["--cookies-from-browser",this.cookiesFromBrowser,...i]);let n=(0,C.spawn)(this.binaryPath,i,{stdio:["ignore","pipe","pipe"]}),s="",a="";n.stdout.on("data",l=>{s+=l.toString()}),n.stderr.on("data",l=>{a+=l.toString()});let c=setTimeout(()=>{n.kill("SIGKILL"),r(new Error(`yt-dlp timeout after ${this.timeoutMs}ms`))},this.timeoutMs);n.on("close",l=>{clearTimeout(c),l===0?e(s):r(new Error(`yt-dlp exited with code ${l}. stderr: ${a.slice(0,500)}`))}),n.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),!p.default.existsSync(r))throw new Error(`yt-dlp failed to create audio file: ${r}`);let a=this.formatDuration(i.duration);return{title:i.title,author:i.uploader,duration:a,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),!p.default.existsSync(r))throw new Error(`yt-dlp failed to create video file: ${r}`);let a=this.formatDuration(i.duration);return{title:i.title,author:i.uploader,duration:a,quality:`${e}p`,filename:d.default.basename(r),downloadUrl:r}}formatDuration(t){if(!t)return"0:00";let e=Math.floor(t/3600),r=Math.floor(t%3600/60),i=Math.floor(t%60);return e>0?`${e}:${r.toString().padStart(2,"0")}:${i.toString().padStart(2,"0")}`:`${r}:${i.toString().padStart(2,"0")}`}};var O=u(require("yt-search"),1);function J(o){let t=(o||"").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(o){let t=/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=|shorts\/)|youtu\.be\/)([^"&?\/\s]{11})/i,e=(o||"").match(t);return e?e[1]:null}function g(o){let t=J(o),e=t.match(/https?:\/\/[^\s)]+/i)?.[0]??t,r=S(e);return r?`https://www.youtube.com/watch?v=${r}`:null}async function v(o){let e=(await(0,O.default)(o))?.videos?.[0];if(!e)return null;let r=e.duration?.seconds??0,i=g(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 B=[320,256,192,128,96,64],_=[1080,720,480,360];function x(o,t){return t.includes(o)?o:t[0]}function z(o){return(o||"").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=E(t.cacheDir),this.cache=new b({cleanupIntervalMs:this.opts.cleanupIntervalMs}),this.cache.start(),this.ytdlp=new f({binaryPath:t.ytdlpBinaryPath,ffmpegPath:t.ffmpegPath,aria2cPath:t.aria2cPath,useAria2c:this.opts.useAria2c,concurrentFragments:this.opts.concurrentFragments,timeoutMs:t.ytdlpTimeoutMs??3e5,cookiesPath:t.cookiesPath,cookiesFromBrowser:t.cookiesFromBrowser})}generateRequestId(t="play"){return`${t}_${Date.now()}_${Math.random().toString(36).slice(2,8)}`}async search(t){return v(t)}getFromCache(t){return this.cache.get(t)}async preload(t,e){let r=g(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 n={...t,url:r};this.cache.set(e,{metadata:n,audio:null,video:null,expiresAt:Date.now()+this.opts.ttlMs,loading:!0});let s=i?96:x(this.opts.preferredAudioKbps,B),a=this.preloadOne(e,"audio",r,s),c=i?[a]:[a,this.preloadOne(e,"video",r,x(this.opts.preferredVideoP,_))];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&&y.default.existsSync(i.path)&&i.size>0)return{metadata:r.metadata,file:i,direct:!1};let n=g(r.metadata.url);if(!n)throw new Error("Invalid YouTube URL.");let s=await this.downloadDirect(e,n);return{metadata:r.metadata,file:s,direct:!0}}async waitCache(t,e,r=8e3,i=500){let n=Date.now();for(;Date.now()-n<r;){let a=this.cache.get(t)?.[e];if(a?.path&&y.default.existsSync(a.path)&&a.size>0)return a;await new Promise(c=>setTimeout(c,i))}return null}cleanup(t){this.cache.delete(t)}async preloadOne(t,e,r,i){try{let n=z(`temp_${Date.now()}`),a=`${e}_${t}_${n}.${e==="audio"?"m4a":"mp4"}`,c=$.default.join(this.paths.cacheDir,a),l=e==="audio"?await this.ytdlp.getAudio(r,i,c):await this.ytdlp.getVideo(r,i,c),k=y.default.statSync(c).size,T;this.opts.preloadBuffer&&(T=await y.default.promises.readFile(c));let V={path:c,size:k,info:{quality:l.quality},buffer:T};this.cache.setFile(t,e,V),this.opts.logger?.debug?.(`preloaded ${e} ${k} bytes: ${a}`)}catch(n){this.opts.logger?.error?.(`preload ${e} failed`,n)}}async downloadDirect(t,e){let r=x(this.opts.preferredAudioKbps,B),i=x(this.opts.preferredVideoP,_),n=t==="audio"?"m4a":"mp4",s=z(`direct_${Date.now()}`),a=$.default.join(this.paths.cacheDir,`${t}_${s}.${n}`),c=t==="audio"?await this.ytdlp.getAudio(e,r,a):await this.ytdlp.getVideo(e,i,a),l=y.default.statSync(a);return{path:a,size:l.size,info:{quality:c.quality}}}};0&&(module.exports={PlayEngine,YtDlpClient,getYouTubeVideoId,normalizeYoutubeUrl,searchBest});
2
4
  //# sourceMappingURL=index.cjs.map