@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 +342 -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 +57 -2
- package/dist/index.d.ts +57 -2
- 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,380 @@
|
|
|
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
|
|
31
136
|
|
|
32
|
-
|
|
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
|
-
|
|
146
|
+
#### `generateRequestId(prefix?: string): string`
|
|
36
147
|
|
|
37
|
-
|
|
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
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
150
|
+
```typescript
|
|
151
|
+
const requestId = engine.generateRequestId("audio"); // "audio_1234567890_abc123"
|
|
152
|
+
```
|
|
45
153
|
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
163
|
+
#### `getOrDownload(requestId: string, type: 'audio' | 'video'): Promise<Result>`
|
|
51
164
|
|
|
52
|
-
|
|
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
|
-
|
|
57
|
-
console.log("cache pronto", cached.path);
|
|
184
|
+
console.log("Cache ready:", cached.path);
|
|
58
185
|
} else {
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
211
|
+
## Advanced Usage
|
|
68
212
|
|
|
69
|
-
|
|
213
|
+
### Handle Long Videos (>1h)
|
|
70
214
|
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
218
|
+
// Automatically uses:
|
|
219
|
+
// - Audio: 96kbps (reduced quality)
|
|
220
|
+
// - Video: skipped (audio only)
|
|
221
|
+
await engine.preload(metadata, requestId);
|
|
80
222
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
|
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
|
package/dist/index.cjs.map
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
package/dist/index.mjs.map
CHANGED
|
@@ -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.
|
|
4
|
-
"description": "YouTube search +
|
|
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
|
+
});
|