@irithell-js/yt-play 0.1.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +346 -52
- package/bin/.platform +1 -0
- package/bin/aria2c +0 -0
- package/bin/yt-dlp +0 -0
- package/dist/index.cjs +3 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +82 -6
- package/dist/index.d.ts +82 -6
- package/dist/index.mjs +3 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +18 -4
- package/scripts/setup-binaries.mjs +177 -0
package/README.md
CHANGED
|
@@ -1,90 +1,384 @@
|
|
|
1
|
-
# @irithell/yt-play
|
|
1
|
+
# @irithell-js/yt-play
|
|
2
2
|
|
|
3
|
-
YouTube
|
|
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
|
-
##
|
|
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
|
|
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
|
-
|
|
53
|
+
### Basic Usage (CommonJS)
|
|
12
54
|
|
|
13
|
-
|
|
55
|
+
```javascript
|
|
56
|
+
const { PlayEngine } = require("@irithell-js/yt-play");
|
|
14
57
|
|
|
15
|
-
|
|
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
|
-
//
|
|
20
|
-
|
|
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
|
-
|
|
27
|
-
|
|
126
|
+
// Low quality (faster, smaller)
|
|
127
|
+
const lq = new PlayEngine({
|
|
128
|
+
preferredAudioKbps: 96,
|
|
129
|
+
preferredVideoP: 480,
|
|
130
|
+
});
|
|
131
|
+
```
|
|
28
132
|
|
|
29
|
-
|
|
30
|
-
|
|
133
|
+
## API Reference
|
|
134
|
+
|
|
135
|
+
### PlayEngine Methods
|
|
136
|
+
|
|
137
|
+
#### `search(query: string): Promise<PlayMetadata | null>`
|
|
31
138
|
|
|
32
|
-
|
|
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
|
-
|
|
146
|
+
#### `generateRequestId(prefix?: string): string`
|
|
147
|
+
|
|
148
|
+
Generate unique request ID for caching.
|
|
36
149
|
|
|
37
|
-
```
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
console.log(audio.metadata.title, audio.file.info.quality, audio.file.path);
|
|
150
|
+
```typescript
|
|
151
|
+
const requestId = engine.generateRequestId("audio"); // "audio_1234567890_abc123"
|
|
152
|
+
```
|
|
41
153
|
|
|
42
|
-
|
|
43
|
-
const video = await engine.getOrDownload(requestId, "video");
|
|
44
|
-
console.log(video.file.info.quality, video.file.path);
|
|
154
|
+
#### `preload(metadata: PlayMetadata, requestId: string): Promise<void>`
|
|
45
155
|
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
163
|
+
#### `getOrDownload(requestId: string, type: 'audio' | 'video'): Promise<Result>`
|
|
51
164
|
|
|
52
|
-
|
|
53
|
-
|
|
165
|
+
Get file from cache or download directly.
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
const result = await engine.getOrDownload(requestId, "audio");
|
|
169
|
+
// Returns: { metadata, file: { path, size, info, buffer? }, direct: boolean }
|
|
54
170
|
|
|
171
|
+
console.log(result.file.path); // "/tmp/cache/audio_xxx.m4a"
|
|
172
|
+
console.log(result.file.size); // 8457234 (bytes)
|
|
173
|
+
console.log(result.file.info.quality); // "128kbps m4a"
|
|
174
|
+
console.log(result.direct); // false if from cache, true if direct download
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
#### `waitCache(requestId: string, type: 'audio' | 'video', timeoutMs?: number, intervalMs?: number): Promise<CachedFile | null>`
|
|
178
|
+
|
|
179
|
+
Wait for cache to be ready (useful for checking preload status).
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
const cached = await engine.waitCache(requestId, "audio", 8000, 500);
|
|
55
183
|
if (cached) {
|
|
56
|
-
|
|
57
|
-
console.log("cache pronto", cached.path);
|
|
184
|
+
console.log("Cache ready:", cached.path);
|
|
58
185
|
} else {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
186
|
+
console.log("Timeout - falling back to direct download");
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
#### `cleanup(requestId: string): void`
|
|
191
|
+
|
|
192
|
+
Remove cached files for a request.
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
engine.cleanup(requestId); // Deletes audio + video from cache
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
#### `getFromCache(requestId: string): CacheEntry | undefined`
|
|
199
|
+
|
|
200
|
+
Get cache entry metadata (without downloading).
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
const entry = engine.getFromCache(requestId);
|
|
204
|
+
if (entry) {
|
|
205
|
+
console.log(entry.metadata.title);
|
|
206
|
+
console.log(entry.audio?.path);
|
|
207
|
+
console.log(entry.loading); // true if preload in progress
|
|
62
208
|
}
|
|
63
209
|
```
|
|
64
210
|
|
|
65
|
-
##
|
|
211
|
+
## Advanced Usage
|
|
66
212
|
|
|
67
|
-
###
|
|
213
|
+
### Handle Long Videos (>1h)
|
|
68
214
|
|
|
69
|
-
|
|
215
|
+
```typescript
|
|
216
|
+
const metadata = await engine.search("2h music mix");
|
|
70
217
|
|
|
71
|
-
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
|
|
75
|
-
- `preloadBuffer?: boolean` Se true, lê o arquivo e deixa `buffer` pronto (mais RAM, envio mais rápido).
|
|
76
|
-
- `cleanupIntervalMs?: number` Intervalo do GC do cache.
|
|
77
|
-
- `logger?: { info|warn|error|debug }` Logger opcional.
|
|
218
|
+
// Automatically uses:
|
|
219
|
+
// - Audio: 96kbps (reduced quality)
|
|
220
|
+
// - Video: skipped (audio only)
|
|
221
|
+
await engine.preload(metadata, requestId);
|
|
78
222
|
|
|
79
|
-
|
|
223
|
+
const { file } = await engine.getOrDownload(requestId, "audio");
|
|
224
|
+
// Fast download with reduced quality
|
|
225
|
+
```
|
|
80
226
|
|
|
81
|
-
|
|
82
|
-
- `generateRequestId(prefix?): string`
|
|
83
|
-
- `preload(metadata, requestId): Promise<void>`
|
|
84
|
-
- `getOrDownload(requestId, 'audio'|'video'): Promise<{ metadata; file; direct }>`
|
|
85
|
-
- `waitCache(requestId, 'audio'|'video', timeoutMs?, intervalMs?): Promise<CachedFile | null>`
|
|
86
|
-
- `cleanup(requestId): void`
|
|
227
|
+
### Custom Cache Directory
|
|
87
228
|
|
|
88
|
-
|
|
229
|
+
```typescript
|
|
230
|
+
import path from "path";
|
|
231
|
+
|
|
232
|
+
const engine = new PlayEngine({
|
|
233
|
+
cacheDir: path.join(process.cwd(), "downloads"),
|
|
234
|
+
ttlMs: 10 * 60_000, // 10 minutes
|
|
235
|
+
});
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Performance Monitoring
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
const startTime = Date.now();
|
|
242
|
+
await engine.preload(metadata, requestId);
|
|
243
|
+
const preloadTime = Date.now() - startTime;
|
|
244
|
+
|
|
245
|
+
console.log(`Preload took ${(preloadTime / 1000).toFixed(2)}s`);
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Error Handling
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
try {
|
|
252
|
+
const metadata = await engine.search("non-existent-video");
|
|
253
|
+
if (!metadata) {
|
|
254
|
+
console.error("Video not found");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
await engine.preload(metadata, requestId);
|
|
259
|
+
const { file } = await engine.getOrDownload(requestId, "audio");
|
|
260
|
+
console.log("Success:", file.path);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.error("Download failed:", error.message);
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Performance
|
|
267
|
+
|
|
268
|
+
With aria2c enabled (default):
|
|
269
|
+
|
|
270
|
+
| Video Length | Audio Download | Video Download | Total Time |
|
|
271
|
+
| ------------ | -------------- | -------------- | ---------- |
|
|
272
|
+
| 5 min | ~3-5s | ~6-8s | ~8s |
|
|
273
|
+
| 1 hour | ~15-20s | Audio only | ~20s |
|
|
274
|
+
| 2 hours | ~25-30s | Audio only | ~30s |
|
|
275
|
+
|
|
276
|
+
_Times may vary based on network speed and YouTube throttling_
|
|
277
|
+
|
|
278
|
+
_The values are based on local tests with optimized caching, for downloading long videos use direct download_
|
|
279
|
+
|
|
280
|
+
## File Formats
|
|
281
|
+
|
|
282
|
+
- **Audio**: M4A (native format, no conversion needed)
|
|
283
|
+
- **Video**: MP4 (with audio merged)
|
|
284
|
+
|
|
285
|
+
M4A provides better quality-to-size ratio and downloads 10-20x faster (no re-encoding).
|
|
286
|
+
|
|
287
|
+
## Requirements
|
|
288
|
+
|
|
289
|
+
- Node.js >= 18.0.0
|
|
290
|
+
- ~50MB disk space for binaries (auto-downloaded)
|
|
291
|
+
- Optional: ffmpeg for advanced features
|
|
292
|
+
|
|
293
|
+
## Binaries
|
|
294
|
+
|
|
295
|
+
The package automatically downloads:
|
|
296
|
+
|
|
297
|
+
- **yt-dlp** v2025.12.08 (35 MB)
|
|
298
|
+
- **aria2c** v1.37.0 (12 MB)
|
|
299
|
+
|
|
300
|
+
Binaries are platform-specific and downloaded on first `npm install`.
|
|
301
|
+
|
|
302
|
+
### Supported Platforms
|
|
303
|
+
|
|
304
|
+
- Linux x64 / arm64
|
|
305
|
+
- macOS x64 / arm64 (Apple Silicon)
|
|
306
|
+
- Windows x64
|
|
307
|
+
|
|
308
|
+
### Manual Binary Paths
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
const engine = new PlayEngine({
|
|
312
|
+
ytdlpBinaryPath: "/custom/path/yt-dlp",
|
|
313
|
+
aria2cPath: "/custom/path/aria2c",
|
|
314
|
+
});
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Troubleshooting
|
|
318
|
+
|
|
319
|
+
### Slow Downloads
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
// Enable aria2c explicitly
|
|
323
|
+
const engine = new PlayEngine({
|
|
324
|
+
useAria2c: true,
|
|
325
|
+
concurrentFragments: 10, // Increase parallelism
|
|
326
|
+
});
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Cache Issues
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
// Clear cache directory manually
|
|
333
|
+
import fs from "fs";
|
|
334
|
+
fs.rmSync("./cache", { recursive: true, force: true });
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Binary Not Found
|
|
338
|
+
|
|
339
|
+
Binaries are auto-downloaded to `node_modules/@irithell-js/yt-play/bin/`. If missing:
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
npm rebuild @irithell-js/yt-play
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## License
|
|
89
346
|
|
|
90
347
|
MIT
|
|
348
|
+
|
|
349
|
+
## Contributing
|
|
350
|
+
|
|
351
|
+
Issues and PRs welcome!
|
|
352
|
+
|
|
353
|
+
## Changelog
|
|
354
|
+
|
|
355
|
+
Deprecated versions have been removed to prevent errors during use.
|
|
356
|
+
|
|
357
|
+
### 0.2.5
|
|
358
|
+
|
|
359
|
+
- Added support to direct cookies extraction in pre built browsers
|
|
360
|
+
|
|
361
|
+
### 0.2.4
|
|
362
|
+
|
|
363
|
+
- Added support to cookies.txt
|
|
364
|
+
|
|
365
|
+
### 0.2.3
|
|
366
|
+
|
|
367
|
+
- Updated documentation
|
|
368
|
+
- Improved error messages
|
|
369
|
+
|
|
370
|
+
### 0.2.2
|
|
371
|
+
|
|
372
|
+
- Many syntax errors fixed
|
|
373
|
+
|
|
374
|
+
### 0.2.1
|
|
375
|
+
|
|
376
|
+
- Added auto-detection for yt-dlp and aria2c binaries
|
|
377
|
+
- Fixed CommonJS compatibility
|
|
378
|
+
- Improved error handling for long videos
|
|
379
|
+
|
|
380
|
+
### 0.2.0
|
|
381
|
+
|
|
382
|
+
- Initial release with bundled binaries
|
|
383
|
+
- aria2c acceleration support
|
|
384
|
+
- Intelligent caching system
|
package/bin/.platform
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
linux-x64
|
package/bin/aria2c
ADDED
|
Binary file
|
package/bin/yt-dlp
ADDED
|
Binary file
|
package/dist/index.cjs
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var Y=Object.create;var w=Object.defineProperty;var j=Object.getOwnPropertyDescriptor;var L=Object.getOwnPropertyNames;var R=Object.getPrototypeOf,U=Object.prototype.hasOwnProperty;var K=(o,t)=>{for(var e in t)w(o,e,{get:t[e],enumerable:!0})},A=(o,t,e,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of L(t))!U.call(o,i)&&i!==e&&w(o,i,{get:()=>t[i],enumerable:!(r=j(t,i))||r.enumerable});return o};var u=(o,t,e)=>(e=o!=null?Y(R(o)):{},A(t||!o||!o.__esModule?w(e,"default",{value:o,enumerable:!0}):e,o)),q=o=>A(w({},"__esModule",{value:!0}),o);var Q={};K(Q,{PlayEngine:()=>D,YtDlpClient:()=>f,getYouTubeVideoId:()=>S,normalizeYoutubeUrl:()=>g,searchBest:()=>v});module.exports=q(Q);var y=u(require("fs"),1),$=u(require("path"),1);var M=u(require("fs"),1),b=class{constructor(t){this.opts=t}store=new Map;cleanupTimer;get(t){return this.store.get(t)}set(t,e){this.store.set(t,e)}has(t){return this.store.has(t)}delete(t){this.cleanupEntry(t),this.store.delete(t)}markLoading(t,e){let r=this.store.get(t);r&&(r.loading=e)}setFile(t,e,r){let i=this.store.get(t);i&&(i[e]=r)}cleanupExpired(t=Date.now()){let e=0;for(let[r,i]of this.store.entries())t>i.expiresAt&&(this.delete(r),e++);return e}start(){this.cleanupTimer||(this.cleanupTimer=setInterval(()=>{this.cleanupExpired(Date.now())},this.opts.cleanupIntervalMs),this.cleanupTimer.unref())}stop(){this.cleanupTimer&&(clearInterval(this.cleanupTimer),this.cleanupTimer=void 0)}cleanupEntry(t){let e=this.store.get(t);e&&["audio","video"].forEach(r=>{let i=e[r];if(i?.path&&M.default.existsSync(i.path))try{M.default.unlinkSync(i.path)}catch{}})}};var h=u(require("fs"),1),P=u(require("path"),1),F=u(require("os"),1);function I(o){h.default.mkdirSync(o,{recursive:!0,mode:511});try{h.default.chmodSync(o,511)}catch{}h.default.accessSync(o,h.default.constants.R_OK|h.default.constants.W_OK)}function E(o){let t=o?.trim()?o:P.default.join(F.default.tmpdir(),"yt-play"),e=P.default.resolve(t),r=P.default.join(e);return I(e),I(r),{baseDir:e,cacheDir:r}}var C=require("child_process"),d=u(require("path"),1),p=u(require("fs"),1),W={},m;try{m=d.default.dirname(new URL(W.url).pathname)}catch{m=typeof m<"u"?m:process.cwd()}var f=class{binaryPath;ffmpegPath;aria2cPath;timeoutMs;useAria2c;concurrentFragments;cookiesPath;cookiesFromBrowser;constructor(t={}){this.binaryPath=t.binaryPath||this.detectYtDlp(),this.ffmpegPath=t.ffmpegPath,this.timeoutMs=t.timeoutMs??3e5,this.concurrentFragments=t.concurrentFragments??5,this.cookiesPath=t.cookiesPath,this.cookiesFromBrowser=t.cookiesFromBrowser,this.aria2cPath=t.aria2cPath||this.detectAria2c(),this.useAria2c=t.useAria2c??!!this.aria2cPath}detectYtDlp(){let t=d.default.resolve(m,"../.."),e=[d.default.join(t,"bin","yt-dlp"),d.default.join(t,"bin","yt-dlp.exe")];for(let r of e)if(p.default.existsSync(r))return r;try{let{execSync:r}=require("child_process"),i=process.platform==="win32"?"where yt-dlp":"which yt-dlp",n=r(i,{encoding:"utf-8"}).trim();if(n)return n.split(`
|
|
2
|
+
`)[0]}catch{}return"yt-dlp"}detectAria2c(){let t=d.default.resolve(m,"../.."),e=[d.default.join(t,"bin","aria2c"),d.default.join(t,"bin","aria2c.exe")];for(let r of e)if(p.default.existsSync(r))return r;try{let{execSync:r}=require("child_process"),i=process.platform==="win32"?"where aria2c":"which aria2c",n=r(i,{encoding:"utf-8"}).trim();if(n)return n.split(`
|
|
3
|
+
`)[0]}catch{}}async exec(t){return new Promise((e,r)=>{let i=[...t];this.ffmpegPath&&(i=["--ffmpeg-location",this.ffmpegPath,...i]),this.cookiesPath&&p.default.existsSync(this.cookiesPath)&&(i=["--cookies",this.cookiesPath,...i]),this.cookiesFromBrowser&&(i=["--cookies-from-browser",this.cookiesFromBrowser,...i]);let n=(0,C.spawn)(this.binaryPath,i,{stdio:["ignore","pipe","pipe"]}),s="",a="";n.stdout.on("data",l=>{s+=l.toString()}),n.stderr.on("data",l=>{a+=l.toString()});let c=setTimeout(()=>{n.kill("SIGKILL"),r(new Error(`yt-dlp timeout after ${this.timeoutMs}ms`))},this.timeoutMs);n.on("close",l=>{clearTimeout(c),l===0?e(s):r(new Error(`yt-dlp exited with code ${l}. stderr: ${a.slice(0,500)}`))}),n.on("error",l=>{clearTimeout(c),r(l)})})}async getInfo(t){let e=await this.exec(["-J","--no-warnings","--no-playlist",t]);return JSON.parse(e)}buildOptimizationArgs(){let t=["--no-warnings","--no-playlist","--no-check-certificates","--concurrent-fragments",String(this.concurrentFragments)];return this.useAria2c&&this.aria2cPath&&(t.push("--downloader",this.aria2cPath),t.push("--downloader-args","aria2c:-x 16 -s 16 -k 1M")),t}async getAudio(t,e,r){let i=await this.getInfo(t),s=["-f","bestaudio[ext=m4a]/bestaudio/best","-o",r,...this.buildOptimizationArgs(),t];if(await this.exec(s),!p.default.existsSync(r))throw new Error(`yt-dlp failed to create audio file: ${r}`);let a=this.formatDuration(i.duration);return{title:i.title,author:i.uploader,duration:a,quality:`${e}kbps m4a`,filename:d.default.basename(r),downloadUrl:r}}async getVideo(t,e,r){let i=await this.getInfo(t),s=["-f",`bestvideo[height<=${e}][ext=mp4]+bestaudio[ext=m4a]/best[height<=${e}]`,"--merge-output-format","mp4","-o",r,...this.buildOptimizationArgs(),t];if(await this.exec(s),!p.default.existsSync(r))throw new Error(`yt-dlp failed to create video file: ${r}`);let a=this.formatDuration(i.duration);return{title:i.title,author:i.uploader,duration:a,quality:`${e}p`,filename:d.default.basename(r),downloadUrl:r}}formatDuration(t){if(!t)return"0:00";let e=Math.floor(t/3600),r=Math.floor(t%3600/60),i=Math.floor(t%60);return e>0?`${e}:${r.toString().padStart(2,"0")}:${i.toString().padStart(2,"0")}`:`${r}:${i.toString().padStart(2,"0")}`}};var O=u(require("yt-search"),1);function J(o){let t=(o||"").trim(),e=[...t.matchAll(/\[[^\]]*\]\((https?:\/\/[^)\s]+)\)/gi)];return e.length>0?e[0][1].trim():(t=t.replace(/^<([^>]+)>$/,"$1").trim(),t=t.replace(/^["'`](.*)["'`]$/,"$1").trim(),t)}function S(o){let t=/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=|shorts\/)|youtu\.be\/)([^"&?\/\s]{11})/i,e=(o||"").match(t);return e?e[1]:null}function g(o){let t=J(o),e=t.match(/https?:\/\/[^\s)]+/i)?.[0]??t,r=S(e);return r?`https://www.youtube.com/watch?v=${r}`:null}async function v(o){let e=(await(0,O.default)(o))?.videos?.[0];if(!e)return null;let r=e.duration?.seconds??0,i=g(e.url)??e.url;return{title:e.title||"Untitled",author:e.author?.name||void 0,duration:e.duration?.timestamp||void 0,thumb:e.image||e.thumbnail||void 0,videoId:e.videoId,url:i,durationSeconds:r}}var B=[320,256,192,128,96,64],_=[1080,720,480,360];function x(o,t){return t.includes(o)?o:t[0]}function z(o){return(o||"").replace(/[\\/:*?"<>|]/g,"").replace(/[^\w\s-]/gi,"").trim().replace(/\s+/g," ").substring(0,100)}var D=class{opts;paths;cache;ytdlp;constructor(t={}){this.opts={ttlMs:t.ttlMs??3*6e4,maxPreloadDurationSeconds:t.maxPreloadDurationSeconds??1200,preferredAudioKbps:t.preferredAudioKbps??128,preferredVideoP:t.preferredVideoP??720,preloadBuffer:t.preloadBuffer??!0,cleanupIntervalMs:t.cleanupIntervalMs??3e4,concurrentFragments:t.concurrentFragments??5,useAria2c:t.useAria2c,logger:t.logger},this.paths=E(t.cacheDir),this.cache=new b({cleanupIntervalMs:this.opts.cleanupIntervalMs}),this.cache.start(),this.ytdlp=new f({binaryPath:t.ytdlpBinaryPath,ffmpegPath:t.ffmpegPath,aria2cPath:t.aria2cPath,useAria2c:this.opts.useAria2c,concurrentFragments:this.opts.concurrentFragments,timeoutMs:t.ytdlpTimeoutMs??3e5,cookiesPath:t.cookiesPath,cookiesFromBrowser:t.cookiesFromBrowser})}generateRequestId(t="play"){return`${t}_${Date.now()}_${Math.random().toString(36).slice(2,8)}`}async search(t){return v(t)}getFromCache(t){return this.cache.get(t)}async preload(t,e){let r=g(t.url);if(!r)throw new Error("Invalid YouTube URL.");let i=t.durationSeconds>3600;t.durationSeconds>this.opts.maxPreloadDurationSeconds&&this.opts.logger?.warn?.(`Video too long for preload (${Math.floor(t.durationSeconds/60)}min). Will use direct download with reduced quality.`);let n={...t,url:r};this.cache.set(e,{metadata:n,audio:null,video:null,expiresAt:Date.now()+this.opts.ttlMs,loading:!0});let s=i?96:x(this.opts.preferredAudioKbps,B),a=this.preloadOne(e,"audio",r,s),c=i?[a]:[a,this.preloadOne(e,"video",r,x(this.opts.preferredVideoP,_))];i&&this.opts.logger?.info?.(`Long video detected (${Math.floor(t.durationSeconds/60)}min). Audio only mode (96kbps).`),await Promise.allSettled(c),this.cache.markLoading(e,!1)}async getOrDownload(t,e){let r=this.cache.get(t);if(!r)throw new Error("Request not found (cache miss).");let i=r[e];if(i?.path&&y.default.existsSync(i.path)&&i.size>0)return{metadata:r.metadata,file:i,direct:!1};let n=g(r.metadata.url);if(!n)throw new Error("Invalid YouTube URL.");let s=await this.downloadDirect(e,n);return{metadata:r.metadata,file:s,direct:!0}}async waitCache(t,e,r=8e3,i=500){let n=Date.now();for(;Date.now()-n<r;){let a=this.cache.get(t)?.[e];if(a?.path&&y.default.existsSync(a.path)&&a.size>0)return a;await new Promise(c=>setTimeout(c,i))}return null}cleanup(t){this.cache.delete(t)}async preloadOne(t,e,r,i){try{let n=z(`temp_${Date.now()}`),a=`${e}_${t}_${n}.${e==="audio"?"m4a":"mp4"}`,c=$.default.join(this.paths.cacheDir,a),l=e==="audio"?await this.ytdlp.getAudio(r,i,c):await this.ytdlp.getVideo(r,i,c),k=y.default.statSync(c).size,T;this.opts.preloadBuffer&&(T=await y.default.promises.readFile(c));let V={path:c,size:k,info:{quality:l.quality},buffer:T};this.cache.setFile(t,e,V),this.opts.logger?.debug?.(`preloaded ${e} ${k} bytes: ${a}`)}catch(n){this.opts.logger?.error?.(`preload ${e} failed`,n)}}async downloadDirect(t,e){let r=x(this.opts.preferredAudioKbps,B),i=x(this.opts.preferredVideoP,_),n=t==="audio"?"m4a":"mp4",s=z(`direct_${Date.now()}`),a=$.default.join(this.paths.cacheDir,`${t}_${s}.${n}`),c=t==="audio"?await this.ytdlp.getAudio(e,r,a):await this.ytdlp.getVideo(e,i,a),l=y.default.statSync(a);return{path:a,size:l.size,info:{quality:c.quality}}}};0&&(module.exports={PlayEngine,YtDlpClient,getYouTubeVideoId,normalizeYoutubeUrl,searchBest});
|
|
2
4
|
//# sourceMappingURL=index.cjs.map
|