@irithell-js/yt-play 0.1.3 → 0.2.4

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,380 @@
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
31
136
 
32
- // depois você usa requestId pra pedir audio/video
137
+ #### `search(query: string): Promise<PlayMetadata | null>`
138
+
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`
36
147
 
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);
148
+ Generate unique request ID for caching.
41
149
 
42
- // video
43
- const video = await engine.getOrDownload(requestId, "video");
44
- console.log(video.file.info.quality, video.file.path);
150
+ ```typescript
151
+ const requestId = engine.generateRequestId("audio"); // "audio_1234567890_abc123"
152
+ ```
45
153
 
46
- // se você não quiser manter o arquivo, apague depois de enviar
47
- engine.cleanup(requestId);
154
+ #### `preload(metadata: PlayMetadata, requestId: string): Promise<void>`
155
+
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.
54
166
 
167
+ ```typescript
168
+ const result = await engine.getOrDownload(requestId, "audio");
169
+ // Returns: { metadata, file: { path, size, info, buffer? }, direct: boolean }
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");
62
187
  }
63
188
  ```
64
189
 
65
- ## API
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
208
+ }
209
+ ```
66
210
 
67
- ### `new PlayEngine(options?)`
211
+ ## Advanced Usage
68
212
 
69
- Opções principais:
213
+ ### Handle Long Videos (>1h)
70
214
 
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.
215
+ ```typescript
216
+ const metadata = await engine.search("2h music mix");
78
217
 
79
- ### Métodos
218
+ // Automatically uses:
219
+ // - Audio: 96kbps (reduced quality)
220
+ // - Video: skipped (audio only)
221
+ await engine.preload(metadata, requestId);
80
222
 
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`
223
+ const { file } = await engine.getOrDownload(requestId, "audio");
224
+ // Fast download with reduced quality
225
+ ```
87
226
 
88
- ## Licença
227
+ ### Custom Cache Directory
228
+
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.4
358
+
359
+ - added support to cookies.txt
360
+
361
+ ### 0.2.3
362
+
363
+ - Updated documentation
364
+ - Improved error messages
365
+
366
+ ### 0.2.2
367
+
368
+ - Many syntax errors fixed
369
+
370
+ ### 0.2.1
371
+
372
+ - Added auto-detection for yt-dlp and aria2c binaries
373
+ - Fixed CommonJS compatibility
374
+ - Improved error handling for long videos
375
+
376
+ ### 0.2.0
377
+
378
+ - Initial release with bundled binaries
379
+ - aria2c acceleration support
380
+ - 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=(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 L(t))!U.call(n,i)&&i!==e&&w(n,i,{get:()=>t[i],enumerable:!(r=j(t,i))||r.enumerable});return n};var u=(n,t,e)=>(e=n!=null?Y(R(n)):{},I(t||!n||!n.__esModule?w(e,"default",{value:n,enumerable:!0}):e,n)),q=n=>I(w({},"__esModule",{value:!0}),n);var Q={};K(Q,{PlayEngine:()=>D,YtDlpClient:()=>f,getYouTubeVideoId:()=>S,normalizeYoutubeUrl:()=>g,searchBest:()=>P});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 p=u(require("fs"),1),v=u(require("path"),1),E=u(require("os"),1);function k(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:v.default.join(E.default.tmpdir(),"yt-play"),e=v.default.resolve(t),r=v.default.join(e);return k(e),k(r),{baseDir:e,cacheDir:r}}var C=require("child_process"),d=u(require("path"),1),h=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(h.default.existsSync(r))return r;try{let{execSync:r}=require("child_process"),i=process.platform==="win32"?"where yt-dlp":"which yt-dlp",o=r(i,{encoding:"utf-8"}).trim();if(o)return o.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(h.default.existsSync(r))return r;try{let{execSync:r}=require("child_process"),i=process.platform==="win32"?"where aria2c":"which aria2c",o=r(i,{encoding:"utf-8"}).trim();if(o)return o.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&&h.default.existsSync(this.cookiesPath)&&(i=["--cookies",this.cookiesPath,...i]),this.cookiesFromBrowser&&(i=["--cookies-from-browser",this.cookiesFromBrowser,...i]);let o=(0,C.spawn)(this.binaryPath,i,{stdio:["ignore","pipe","pipe"]}),s="",a="";o.stdout.on("data",l=>{s+=l.toString()}),o.stderr.on("data",l=>{a+=l.toString()});let c=setTimeout(()=>{o.kill("SIGKILL"),r(new Error(`yt-dlp timeout after ${this.timeoutMs}ms`))},this.timeoutMs);o.on("close",l=>{clearTimeout(c),l===0?e(s):r(new Error(`yt-dlp exited with code ${l}. stderr: ${a.slice(0,500)}`))}),o.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),!h.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),!h.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(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 g(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,O.default)(n))?.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 _=[320,256,192,128,96,64],z=[1080,720,480,360];function x(n,t){return t.includes(n)?n:t[0]}function B(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 b({cleanupIntervalMs:this.opts.cleanupIntervalMs}),this.cache.start(),this.ytdlp=new f({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=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 o={...t,url:r};this.cache.set(e,{metadata:o,audio:null,video:null,expiresAt:Date.now()+this.opts.ttlMs,loading:!0});let s=i?96:x(this.opts.preferredAudioKbps,_),a=this.preloadOne(e,"audio",r,s),c=i?[a]:[a,this.preloadOne(e,"video",r,x(this.opts.preferredVideoP,z))];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 o=g(r.metadata.url);if(!o)throw new Error("Invalid YouTube URL.");let s=await this.downloadDirect(e,o);return{metadata:r.metadata,file:s,direct:!0}}async waitCache(t,e,r=8e3,i=500){let o=Date.now();for(;Date.now()-o<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 o=B(`temp_${Date.now()}`),a=`${e}_${t}_${o}.${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),T=y.default.statSync(c).size,A;this.opts.preloadBuffer&&(A=await y.default.promises.readFile(c));let V={path:c,size:T,info:{quality:l.quality},buffer:A};this.cache.setFile(t,e,V),this.opts.logger?.debug?.(`preloaded ${e} ${T} bytes: ${a}`)}catch(o){this.opts.logger?.error?.(`preload ${e} failed`,o)}}async downloadDirect(t,e){let r=x(this.opts.preferredAudioKbps,_),i=x(this.opts.preferredVideoP,z),o=t==="audio"?"m4a":"mp4",s=B(`direct_${Date.now()}`),a=$.default.join(this.paths.cacheDir,`${t}_${s}.${o}`),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
@@ -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":"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","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 type { DownloadInfo } from \"./types.js\";\n\nlet __dirname: string;\ntry {\n // @ts-ignore\n __dirname = path.dirname(new URL(import.meta.url).pathname);\n} catch {\n // @ts-ignore\n __dirname = typeof __dirname !== \"undefined\" ? __dirname : process.cwd();\n}\n\nexport interface YtDlpClientOptions {\n binaryPath?: string;\n ffmpegPath?: string;\n aria2cPath?: string;\n timeoutMs?: number;\n useAria2c?: boolean;\n concurrentFragments?: number;\n cookiesPath?: string;\n cookiesFromBrowser?: string;\n}\n\ninterface YtDlpVideoInfo {\n id: string;\n title: string;\n uploader?: string;\n duration: number;\n thumbnail?: string;\n}\n\nexport class YtDlpClient {\n private readonly binaryPath: string;\n private readonly ffmpegPath?: string;\n private readonly aria2cPath?: string;\n private readonly timeoutMs: number;\n private readonly useAria2c: boolean;\n private readonly concurrentFragments: number;\n private readonly cookiesPath?: string;\n private readonly cookiesFromBrowser?: string;\n\n constructor(opts: YtDlpClientOptions = {}) {\n this.binaryPath = opts.binaryPath || this.detectYtDlp();\n this.ffmpegPath = opts.ffmpegPath;\n this.timeoutMs = opts.timeoutMs ?? 300_000;\n this.concurrentFragments = opts.concurrentFragments ?? 5;\n this.cookiesPath = opts.cookiesPath;\n this.cookiesFromBrowser = opts.cookiesFromBrowser;\n\n this.aria2cPath = opts.aria2cPath || this.detectAria2c();\n this.useAria2c = opts.useAria2c ?? !!this.aria2cPath;\n }\n\n private detectYtDlp(): string {\n const packageRoot = path.resolve(__dirname, \"../..\");\n const bundledPaths = [\n path.join(packageRoot, \"bin\", \"yt-dlp\"),\n path.join(packageRoot, \"bin\", \"yt-dlp.exe\"),\n ];\n\n for (const p of bundledPaths) {\n if (fs.existsSync(p)) {\n return p;\n }\n }\n\n try {\n const { execSync } = require(\"node:child_process\");\n const cmd =\n process.platform === \"win32\" ? \"where yt-dlp\" : \"which yt-dlp\";\n const result = execSync(cmd, { encoding: \"utf-8\" }).trim();\n if (result) return result.split(\"\\n\")[0];\n } catch {}\n\n return \"yt-dlp\";\n }\n\n private detectAria2c(): string | undefined {\n const packageRoot = path.resolve(__dirname, \"../..\");\n const bundledPaths = [\n path.join(packageRoot, \"bin\", \"aria2c\"),\n path.join(packageRoot, \"bin\", \"aria2c.exe\"),\n ];\n\n for (const p of bundledPaths) {\n if (fs.existsSync(p)) {\n return p;\n }\n }\n\n try {\n const { execSync } = require(\"node:child_process\");\n const cmd =\n process.platform === \"win32\" ? \"where aria2c\" : \"which aria2c\";\n const result = execSync(cmd, { encoding: \"utf-8\" }).trim();\n if (result) return result.split(\"\\n\")[0];\n } catch {}\n\n return undefined;\n }\n\n private async exec(args: string[]): Promise<string> {\n return new Promise((resolve, reject) => {\n let allArgs = [...args];\n\n if (this.ffmpegPath) {\n allArgs = [\"--ffmpeg-location\", this.ffmpegPath, ...allArgs];\n }\n\n if (this.cookiesPath && fs.existsSync(this.cookiesPath)) {\n allArgs = [\"--cookies\", this.cookiesPath, ...allArgs];\n }\n\n if (this.cookiesFromBrowser) {\n allArgs = [\n \"--cookies-from-browser\",\n this.cookiesFromBrowser,\n ...allArgs,\n ];\n }\n\n const proc = spawn(this.binaryPath, allArgs, {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n let stdout = \"\";\n let stderr = \"\";\n\n proc.stdout.on(\"data\", (chunk) => {\n stdout += chunk.toString();\n });\n\n proc.stderr.on(\"data\", (chunk) => {\n stderr += chunk.toString();\n });\n\n const timer = setTimeout(() => {\n proc.kill(\"SIGKILL\");\n reject(new Error(`yt-dlp timeout after ${this.timeoutMs}ms`));\n }, this.timeoutMs);\n\n proc.on(\"close\", (code) => {\n clearTimeout(timer);\n if (code === 0) {\n resolve(stdout);\n } else {\n reject(\n new Error(\n `yt-dlp exited with code ${code}. stderr: ${stderr.slice(0, 500)}`,\n ),\n );\n }\n });\n\n proc.on(\"error\", (err) => {\n clearTimeout(timer);\n reject(err);\n });\n });\n }\n\n async getInfo(youtubeUrl: string): Promise<YtDlpVideoInfo> {\n const stdout = await this.exec([\n \"-J\",\n \"--no-warnings\",\n \"--no-playlist\",\n youtubeUrl,\n ]);\n const info = JSON.parse(stdout) as YtDlpVideoInfo;\n return info;\n }\n\n private buildOptimizationArgs(): string[] {\n const args: string[] = [\n \"--no-warnings\",\n \"--no-playlist\",\n \"--no-check-certificates\",\n \"--concurrent-fragments\",\n String(this.concurrentFragments),\n ];\n\n if (this.useAria2c && this.aria2cPath) {\n args.push(\"--downloader\", this.aria2cPath);\n args.push(\"--downloader-args\", \"aria2c:-x 16 -s 16 -k 1M\");\n }\n\n return args;\n }\n\n async getAudio(\n youtubeUrl: string,\n qualityKbps: number,\n outputPath: string,\n ): Promise<DownloadInfo> {\n const info = await this.getInfo(youtubeUrl);\n const format = \"bestaudio[ext=m4a]/bestaudio/best\";\n\n const args = [\n \"-f\",\n format,\n \"-o\",\n outputPath,\n ...this.buildOptimizationArgs(),\n youtubeUrl,\n ];\n\n await this.exec(args);\n\n if (!fs.existsSync(outputPath)) {\n throw new Error(`yt-dlp failed to create audio file: ${outputPath}`);\n }\n\n const duration = this.formatDuration(info.duration);\n\n return {\n title: info.title,\n author: info.uploader,\n duration,\n quality: `${qualityKbps}kbps m4a`,\n filename: path.basename(outputPath),\n downloadUrl: outputPath,\n };\n }\n\n async getVideo(\n youtubeUrl: string,\n qualityP: number,\n outputPath: string,\n ): Promise<DownloadInfo> {\n const info = await this.getInfo(youtubeUrl);\n const format = `bestvideo[height<=${qualityP}][ext=mp4]+bestaudio[ext=m4a]/best[height<=${qualityP}]`;\n\n const args = [\n \"-f\",\n format,\n \"--merge-output-format\",\n \"mp4\",\n \"-o\",\n outputPath,\n ...this.buildOptimizationArgs(),\n youtubeUrl,\n ];\n\n await this.exec(args);\n\n if (!fs.existsSync(outputPath)) {\n throw new Error(`yt-dlp failed to create video file: ${outputPath}`);\n }\n\n const duration = this.formatDuration(info.duration);\n\n return {\n title: info.title,\n author: info.uploader,\n duration,\n quality: `${qualityP}p`,\n filename: path.basename(outputPath),\n downloadUrl: outputPath,\n };\n }\n\n private formatDuration(seconds: number): string {\n if (!seconds) return \"0:00\";\n const h = Math.floor(seconds / 3600);\n const m = Math.floor((seconds % 3600) / 60);\n const s = Math.floor(seconds % 60);\n if (h > 0) {\n return `${h}:${m.toString().padStart(2, \"0\")}:${s.toString().padStart(2, \"0\")}`;\n }\n return `${m}:${s.toString().padStart(2, \"0\")}`;\n }\n}\n","import yts from \"yt-search\";\n\nimport type { PlayMetadata } from \"./types.js\";\n\nexport function stripWeirdUrlWrappers(input: string): string {\n let s = (input || \"\").trim();\n const mdAll = [...s.matchAll(/\\[[^\\]]*\\]\\((https?:\\/\\/[^)\\s]+)\\)/gi)];\n if (mdAll.length > 0) return mdAll[0][1].trim();\n s = s.replace(/^<([^>]+)>$/, \"$1\").trim();\n s = s.replace(/^[\"'`](.*)[\"'`]$/, \"$1\").trim();\n\n return s;\n}\n\nexport function getYouTubeVideoId(input: string): string | null {\n const regex =\n /(?:https?:\\/\\/)?(?:www\\.)?(?:youtube\\.com\\/(?:[^\\/]+\\/.+\\/|(?:v|e(?:mbed)?)\\/|.*[?&]v=|shorts\\/)|youtu\\.be\\/)([^\"&?\\/\\s]{11})/i;\n\n const match = (input || \"\").match(regex);\n return match ? match[1] : null;\n}\n\nexport function normalizeYoutubeUrl(input: string): string | null {\n const cleaned0 = stripWeirdUrlWrappers(input);\n\n const firstUrl = cleaned0.match(/https?:\\/\\/[^\\s)]+/i)?.[0] ?? cleaned0;\n\n const id = getYouTubeVideoId(firstUrl);\n if (!id) return null;\n\n return `https://www.youtube.com/watch?v=${id}`;\n}\n\nexport async function searchBest(query: string): Promise<PlayMetadata | null> {\n 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":["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_meta","__dirname","path","YtDlpClient","opts","packageRoot","bundledPaths","p","fs","execSync","cmd","result","args","resolve","reject","allArgs","proc","stdout","stderr","chunk","timer","code","err","youtubeUrl","qualityKbps","outputPath","info","duration","qualityP","seconds","h","m","s","import_yt_search","stripWeirdUrlWrappers","input","s","mdAll","getYouTubeVideoId","regex","match","normalizeYoutubeUrl","cleaned0","firstUrl","id","searchBest","query","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,45 @@ 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
+ cookiesPath?: string;
89
+ cookiesFromBrowser?: string;
90
+ }
91
+ interface YtDlpVideoInfo {
92
+ id: string;
93
+ title: string;
94
+ uploader?: string;
95
+ duration: number;
96
+ thumbnail?: string;
97
+ }
98
+ declare class YtDlpClient {
99
+ private readonly binaryPath;
100
+ private readonly ffmpegPath?;
101
+ private readonly aria2cPath?;
102
+ private readonly timeoutMs;
103
+ private readonly useAria2c;
104
+ private readonly concurrentFragments;
105
+ private readonly cookiesPath?;
106
+ private readonly cookiesFromBrowser?;
107
+ constructor(opts?: YtDlpClientOptions);
108
+ private detectYtDlp;
109
+ private detectAria2c;
110
+ private exec;
111
+ getInfo(youtubeUrl: string): Promise<YtDlpVideoInfo>;
112
+ private buildOptimizationArgs;
113
+ getAudio(youtubeUrl: string, qualityKbps: number, outputPath: string): Promise<DownloadInfo>;
114
+ getVideo(youtubeUrl: string, qualityP: number, outputPath: string): Promise<DownloadInfo>;
115
+ private formatDuration;
116
+ }
117
+
118
+ 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,45 @@ 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
+ cookiesPath?: string;
89
+ cookiesFromBrowser?: string;
90
+ }
91
+ interface YtDlpVideoInfo {
92
+ id: string;
93
+ title: string;
94
+ uploader?: string;
95
+ duration: number;
96
+ thumbnail?: string;
97
+ }
98
+ declare class YtDlpClient {
99
+ private readonly binaryPath;
100
+ private readonly ffmpegPath?;
101
+ private readonly aria2cPath?;
102
+ private readonly timeoutMs;
103
+ private readonly useAria2c;
104
+ private readonly concurrentFragments;
105
+ private readonly cookiesPath?;
106
+ private readonly cookiesFromBrowser?;
107
+ constructor(opts?: YtDlpClientOptions);
108
+ private detectYtDlp;
109
+ private detectAria2c;
110
+ private exec;
111
+ getInfo(youtubeUrl: string): Promise<YtDlpVideoInfo>;
112
+ private buildOptimizationArgs;
113
+ getAudio(youtubeUrl: string, qualityKbps: number, outputPath: string): Promise<DownloadInfo>;
114
+ getVideo(youtubeUrl: string, qualityP: number, outputPath: string): Promise<DownloadInfo>;
115
+ private formatDuration;
116
+ }
117
+
118
+ 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 g from"fs";import I from"path";import S from"fs";var y=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 b from"path";import O 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:b.join(O.tmpdir(),"yt-play"),e=b.resolve(t),r=b.join(e);return $(e),$(r),{baseDir:e,cacheDir:r}}import{spawn as _}from"child_process";import d from"path";import h from"fs";var u;try{u=d.dirname(new URL(import.meta.url).pathname)}catch{u=typeof u<"u"?u:process.cwd()}var m=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.resolve(u,"../.."),e=[d.join(t,"bin","yt-dlp"),d.join(t,"bin","yt-dlp.exe")];for(let r of e)if(h.existsSync(r))return r;try{let{execSync:r}=M("child_process"),i=process.platform==="win32"?"where yt-dlp":"which yt-dlp",o=r(i,{encoding:"utf-8"}).trim();if(o)return o.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(h.existsSync(r))return r;try{let{execSync:r}=M("child_process"),i=process.platform==="win32"?"where aria2c":"which aria2c",o=r(i,{encoding:"utf-8"}).trim();if(o)return o.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&&h.existsSync(this.cookiesPath)&&(i=["--cookies",this.cookiesPath,...i]),this.cookiesFromBrowser&&(i=["--cookies-from-browser",this.cookiesFromBrowser,...i]);let o=_(this.binaryPath,i,{stdio:["ignore","pipe","pipe"]}),s="",a="";o.stdout.on("data",l=>{s+=l.toString()}),o.stderr.on("data",l=>{a+=l.toString()});let c=setTimeout(()=>{o.kill("SIGKILL"),r(new Error(`yt-dlp timeout after ${this.timeoutMs}ms`))},this.timeoutMs);o.on("close",l=>{clearTimeout(c),l===0?e(s):r(new Error(`yt-dlp exited with code ${l}. stderr: ${a.slice(0,500)}`))}),o.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),!h.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.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),!h.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.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")}`}};import z from"yt-search";function B(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 f(n){let t=B(n),e=t.match(/https?:\/\/[^\s)]+/i)?.[0]??t,r=A(e);return r?`https://www.youtube.com/watch?v=${r}`:null}async function v(n){let e=(await z(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 k=[320,256,192,128,96,64],E=[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 y({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 v(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 o={...t,url:r};this.cache.set(e,{metadata:o,audio:null,video:null,expiresAt:Date.now()+this.opts.ttlMs,loading:!0});let s=i?96:w(this.opts.preferredAudioKbps,k),a=this.preloadOne(e,"audio",r,s),c=i?[a]:[a,this.preloadOne(e,"video",r,w(this.opts.preferredVideoP,E))];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.existsSync(i.path)&&i.size>0)return{metadata:r.metadata,file:i,direct:!1};let o=f(r.metadata.url);if(!o)throw new Error("Invalid YouTube URL.");let s=await this.downloadDirect(e,o);return{metadata:r.metadata,file:s,direct:!0}}async waitCache(t,e,r=8e3,i=500){let o=Date.now();for(;Date.now()-o<r;){let a=this.cache.get(t)?.[e];if(a?.path&&g.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 o=F(`temp_${Date.now()}`),a=`${e}_${t}_${o}.${e==="audio"?"m4a":"mp4"}`,c=I.join(this.paths.cacheDir,a),l=e==="audio"?await this.ytdlp.getAudio(r,i,c):await this.ytdlp.getVideo(r,i,c),x=g.statSync(c).size,D;this.opts.preloadBuffer&&(D=await g.promises.readFile(c));let C={path:c,size:x,info:{quality:l.quality},buffer:D};this.cache.setFile(t,e,C),this.opts.logger?.debug?.(`preloaded ${e} ${x} bytes: ${a}`)}catch(o){this.opts.logger?.error?.(`preload ${e} failed`,o)}}async downloadDirect(t,e){let r=w(this.opts.preferredAudioKbps,k),i=w(this.opts.preferredVideoP,E),o=t==="audio"?"m4a":"mp4",s=F(`direct_${Date.now()}`),a=I.join(this.paths.cacheDir,`${t}_${s}.${o}`),c=t==="audio"?await this.ytdlp.getAudio(e,r,a):await this.ytdlp.getVideo(e,i,a),l=g.statSync(a);return{path:a,size:l.size,info:{quality:c.quality}}}};export{P as PlayEngine,m as YtDlpClient,A as getYouTubeVideoId,f as normalizeYoutubeUrl,v 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 type { DownloadInfo } from \"./types.js\";\n\nlet __dirname: string;\ntry {\n // @ts-ignore\n __dirname = path.dirname(new URL(import.meta.url).pathname);\n} catch {\n // @ts-ignore\n __dirname = typeof __dirname !== \"undefined\" ? __dirname : process.cwd();\n}\n\nexport interface YtDlpClientOptions {\n binaryPath?: string;\n ffmpegPath?: string;\n aria2cPath?: string;\n timeoutMs?: number;\n useAria2c?: boolean;\n concurrentFragments?: number;\n cookiesPath?: string;\n cookiesFromBrowser?: string;\n}\n\ninterface YtDlpVideoInfo {\n id: string;\n title: string;\n uploader?: string;\n duration: number;\n thumbnail?: string;\n}\n\nexport class YtDlpClient {\n private readonly binaryPath: string;\n private readonly ffmpegPath?: string;\n private readonly aria2cPath?: string;\n private readonly timeoutMs: number;\n private readonly useAria2c: boolean;\n private readonly concurrentFragments: number;\n private readonly cookiesPath?: string;\n private readonly cookiesFromBrowser?: string;\n\n constructor(opts: YtDlpClientOptions = {}) {\n this.binaryPath = opts.binaryPath || this.detectYtDlp();\n this.ffmpegPath = opts.ffmpegPath;\n this.timeoutMs = opts.timeoutMs ?? 300_000;\n this.concurrentFragments = opts.concurrentFragments ?? 5;\n this.cookiesPath = opts.cookiesPath;\n this.cookiesFromBrowser = opts.cookiesFromBrowser;\n\n this.aria2cPath = opts.aria2cPath || this.detectAria2c();\n this.useAria2c = opts.useAria2c ?? !!this.aria2cPath;\n }\n\n private detectYtDlp(): string {\n const packageRoot = path.resolve(__dirname, \"../..\");\n const bundledPaths = [\n path.join(packageRoot, \"bin\", \"yt-dlp\"),\n path.join(packageRoot, \"bin\", \"yt-dlp.exe\"),\n ];\n\n for (const p of bundledPaths) {\n if (fs.existsSync(p)) {\n return p;\n }\n }\n\n try {\n const { execSync } = require(\"node:child_process\");\n const cmd =\n process.platform === \"win32\" ? \"where yt-dlp\" : \"which yt-dlp\";\n const result = execSync(cmd, { encoding: \"utf-8\" }).trim();\n if (result) return result.split(\"\\n\")[0];\n } catch {}\n\n return \"yt-dlp\";\n }\n\n private detectAria2c(): string | undefined {\n const packageRoot = path.resolve(__dirname, \"../..\");\n const bundledPaths = [\n path.join(packageRoot, \"bin\", \"aria2c\"),\n path.join(packageRoot, \"bin\", \"aria2c.exe\"),\n ];\n\n for (const p of bundledPaths) {\n if (fs.existsSync(p)) {\n return p;\n }\n }\n\n try {\n const { execSync } = require(\"node:child_process\");\n const cmd =\n process.platform === \"win32\" ? \"where aria2c\" : \"which aria2c\";\n const result = execSync(cmd, { encoding: \"utf-8\" }).trim();\n if (result) return result.split(\"\\n\")[0];\n } catch {}\n\n return undefined;\n }\n\n private async exec(args: string[]): Promise<string> {\n return new Promise((resolve, reject) => {\n let allArgs = [...args];\n\n if (this.ffmpegPath) {\n allArgs = [\"--ffmpeg-location\", this.ffmpegPath, ...allArgs];\n }\n\n if (this.cookiesPath && fs.existsSync(this.cookiesPath)) {\n allArgs = [\"--cookies\", this.cookiesPath, ...allArgs];\n }\n\n if (this.cookiesFromBrowser) {\n allArgs = [\n \"--cookies-from-browser\",\n this.cookiesFromBrowser,\n ...allArgs,\n ];\n }\n\n const proc = spawn(this.binaryPath, allArgs, {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n let stdout = \"\";\n let stderr = \"\";\n\n proc.stdout.on(\"data\", (chunk) => {\n stdout += chunk.toString();\n });\n\n proc.stderr.on(\"data\", (chunk) => {\n stderr += chunk.toString();\n });\n\n const timer = setTimeout(() => {\n proc.kill(\"SIGKILL\");\n reject(new Error(`yt-dlp timeout after ${this.timeoutMs}ms`));\n }, this.timeoutMs);\n\n proc.on(\"close\", (code) => {\n clearTimeout(timer);\n if (code === 0) {\n resolve(stdout);\n } else {\n reject(\n new Error(\n `yt-dlp exited with code ${code}. stderr: ${stderr.slice(0, 500)}`,\n ),\n );\n }\n });\n\n proc.on(\"error\", (err) => {\n clearTimeout(timer);\n reject(err);\n });\n });\n }\n\n async getInfo(youtubeUrl: string): Promise<YtDlpVideoInfo> {\n const stdout = await this.exec([\n \"-J\",\n \"--no-warnings\",\n \"--no-playlist\",\n youtubeUrl,\n ]);\n const info = JSON.parse(stdout) as YtDlpVideoInfo;\n return info;\n }\n\n private buildOptimizationArgs(): string[] {\n const args: string[] = [\n \"--no-warnings\",\n \"--no-playlist\",\n \"--no-check-certificates\",\n \"--concurrent-fragments\",\n String(this.concurrentFragments),\n ];\n\n if (this.useAria2c && this.aria2cPath) {\n args.push(\"--downloader\", this.aria2cPath);\n args.push(\"--downloader-args\", \"aria2c:-x 16 -s 16 -k 1M\");\n }\n\n return args;\n }\n\n async getAudio(\n youtubeUrl: string,\n qualityKbps: number,\n outputPath: string,\n ): Promise<DownloadInfo> {\n const info = await this.getInfo(youtubeUrl);\n const format = \"bestaudio[ext=m4a]/bestaudio/best\";\n\n const args = [\n \"-f\",\n format,\n \"-o\",\n outputPath,\n ...this.buildOptimizationArgs(),\n youtubeUrl,\n ];\n\n await this.exec(args);\n\n if (!fs.existsSync(outputPath)) {\n throw new Error(`yt-dlp failed to create audio file: ${outputPath}`);\n }\n\n const duration = this.formatDuration(info.duration);\n\n return {\n title: info.title,\n author: info.uploader,\n duration,\n quality: `${qualityKbps}kbps m4a`,\n filename: path.basename(outputPath),\n downloadUrl: outputPath,\n };\n }\n\n async getVideo(\n youtubeUrl: string,\n qualityP: number,\n outputPath: string,\n ): Promise<DownloadInfo> {\n const info = await this.getInfo(youtubeUrl);\n const format = `bestvideo[height<=${qualityP}][ext=mp4]+bestaudio[ext=m4a]/best[height<=${qualityP}]`;\n\n const args = [\n \"-f\",\n format,\n \"--merge-output-format\",\n \"mp4\",\n \"-o\",\n outputPath,\n ...this.buildOptimizationArgs(),\n youtubeUrl,\n ];\n\n await this.exec(args);\n\n if (!fs.existsSync(outputPath)) {\n throw new Error(`yt-dlp failed to create video file: ${outputPath}`);\n }\n\n const duration = this.formatDuration(info.duration);\n\n return {\n title: info.title,\n author: info.uploader,\n duration,\n quality: `${qualityP}p`,\n filename: path.basename(outputPath),\n downloadUrl: outputPath,\n };\n }\n\n private formatDuration(seconds: number): string {\n if (!seconds) return \"0:00\";\n const h = Math.floor(seconds / 3600);\n const m = Math.floor((seconds % 3600) / 60);\n const s = Math.floor(seconds % 60);\n if (h > 0) {\n return `${h}:${m.toString().padStart(2, \"0\")}:${s.toString().padStart(2, \"0\")}`;\n }\n return `${m}:${s.toString().padStart(2, \"0\")}`;\n }\n}\n","import yts from \"yt-search\";\n\nimport type { PlayMetadata } from \"./types.js\";\n\nexport function stripWeirdUrlWrappers(input: string): string {\n let s = (input || \"\").trim();\n const mdAll = [...s.matchAll(/\\[[^\\]]*\\]\\((https?:\\/\\/[^)\\s]+)\\)/gi)];\n if (mdAll.length > 0) return mdAll[0][1].trim();\n s = s.replace(/^<([^>]+)>$/, \"$1\").trim();\n s = s.replace(/^[\"'`](.*)[\"'`]$/, \"$1\").trim();\n\n return s;\n}\n\nexport function getYouTubeVideoId(input: string): string | null {\n const regex =\n /(?:https?:\\/\\/)?(?:www\\.)?(?:youtube\\.com\\/(?:[^\\/]+\\/.+\\/|(?:v|e(?:mbed)?)\\/|.*[?&]v=|shorts\\/)|youtu\\.be\\/)([^\"&?\\/\\s]{11})/i;\n\n const match = (input || \"\").match(regex);\n return match ? match[1] : null;\n}\n\nexport function normalizeYoutubeUrl(input: string): string | null {\n const cleaned0 = stripWeirdUrlWrappers(input);\n\n const firstUrl = cleaned0.match(/https?:\\/\\/[^\\s)]+/i)?.[0] ?? cleaned0;\n\n const id = getYouTubeVideoId(firstUrl);\n if (!id) return null;\n\n return `https://www.youtube.com/watch?v=${id}`;\n}\n\nexport async function searchBest(query: string): Promise<PlayMetadata | null> {\n 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","__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","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.4",
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
+ });