@irithell-js/yt-play 0.2.3 → 0.2.6
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 +142 -7
- package/dist/index.cjs +5 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -9
- package/dist/index.d.ts +37 -9
- package/dist/index.mjs +5 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -2
- package/scripts/check-ytdlp-update.mjs +150 -0
- package/scripts/setup-binaries.mjs +93 -24
- package/bin/.platform +0 -1
- package/bin/aria2c +0 -0
- package/bin/yt-dlp +0 -0
package/README.md
CHANGED
|
@@ -4,11 +4,13 @@ High-performance YouTube audio/video download engine with intelligent caching, b
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
-
|
|
7
|
+
- **Bundled Binaries** - yt-dlp and aria2c included (no system dependencies)
|
|
8
|
+
- **Auto-Update System** - yt-dlp updates automatically when needed
|
|
8
9
|
- **Ultra Fast Downloads** - aria2c acceleration (up to 5x faster)
|
|
9
10
|
- **Intelligent Caching** - TTL-based cache with automatic cleanup
|
|
10
11
|
- **Smart Quality** - Auto-reduces quality for long videos (>1h)
|
|
11
|
-
- **Container Ready** - Works in Docker/isolated environments
|
|
12
|
+
- **Container Ready** - Works in Docker/isolated environments with cookie support
|
|
13
|
+
- **Auto-Configuration** - Creates yt-dlp.conf with optimal settings
|
|
12
14
|
- **Cross-Platform** - Linux (x64/arm64), macOS, Windows
|
|
13
15
|
- **Zero Config** - Auto-detects binaries and optimizes settings
|
|
14
16
|
- **TypeScript** - Full type definitions included
|
|
@@ -71,6 +73,21 @@ async function download() {
|
|
|
71
73
|
download();
|
|
72
74
|
```
|
|
73
75
|
|
|
76
|
+
### Direct URL Support
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// Search by name
|
|
80
|
+
const metadata1 = await engine.search("never gonna give you up");
|
|
81
|
+
|
|
82
|
+
// Or use direct YouTube URL
|
|
83
|
+
const metadata2 = await engine.search(
|
|
84
|
+
"https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// Both return the same metadata format
|
|
88
|
+
console.log(metadata2.title); // "Rick Astley - Never Gonna Give You Up"
|
|
89
|
+
```
|
|
90
|
+
|
|
74
91
|
## Configuration
|
|
75
92
|
|
|
76
93
|
### Constructor Options
|
|
@@ -98,11 +115,48 @@ const engine = new PlayEngine({
|
|
|
98
115
|
aria2cPath: "./bin/aria2c",
|
|
99
116
|
ffmpegPath: "/usr/bin/ffmpeg", // Optional
|
|
100
117
|
|
|
118
|
+
// Cookies (required for VPS/Docker environments)
|
|
119
|
+
cookiesPath: "./cookies.txt", // Path to Netscape cookies file
|
|
120
|
+
// OR
|
|
121
|
+
cookiesFromBrowser: "firefox", // Extract from browser: chrome, firefox, edge, safari
|
|
122
|
+
|
|
101
123
|
// Logging
|
|
102
124
|
logger: console, // Logger instance (optional)
|
|
103
125
|
});
|
|
104
126
|
```
|
|
105
127
|
|
|
128
|
+
### Cookie Configuration (VPS/Docker)
|
|
129
|
+
|
|
130
|
+
For headless environments (VPS, Docker), YouTube authentication is required:
|
|
131
|
+
|
|
132
|
+
**Option 1: Export cookies from browser**
|
|
133
|
+
|
|
134
|
+
1. Install browser extension: "Get cookies.txt LOCALLY"
|
|
135
|
+
2. Visit youtube.com and login
|
|
136
|
+
3. Export cookies to `cookies.txt`
|
|
137
|
+
4. Use in PlayEngine:
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
const engine = new PlayEngine({
|
|
141
|
+
cookiesPath: "/path/to/cookies.txt",
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Option 2: Extract directly from browser** (requires browser installed)
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
const engine = new PlayEngine({
|
|
149
|
+
cookiesFromBrowser: "firefox", // chrome, firefox, edge, safari
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The engine automatically creates `yt-dlp.conf` with:
|
|
154
|
+
|
|
155
|
+
- Node.js runtime detection
|
|
156
|
+
- EJS remote components
|
|
157
|
+
- Cookie authentication
|
|
158
|
+
- Optimal download settings
|
|
159
|
+
|
|
106
160
|
### Quality Presets
|
|
107
161
|
|
|
108
162
|
```typescript
|
|
@@ -131,10 +185,17 @@ const lq = new PlayEngine({
|
|
|
131
185
|
|
|
132
186
|
#### `search(query: string): Promise<PlayMetadata | null>`
|
|
133
187
|
|
|
134
|
-
Search for a video on YouTube.
|
|
188
|
+
Search for a video on YouTube or get metadata from URL.
|
|
135
189
|
|
|
136
190
|
```typescript
|
|
191
|
+
// Search by name
|
|
137
192
|
const metadata = await engine.search("artist - song name");
|
|
193
|
+
|
|
194
|
+
// Or use direct URL
|
|
195
|
+
const metadata = await engine.search(
|
|
196
|
+
"https://www.youtube.com/watch?v=VIDEO_ID",
|
|
197
|
+
);
|
|
198
|
+
|
|
138
199
|
// Returns: { title, author, duration, durationSeconds, thumb, videoId, url }
|
|
139
200
|
```
|
|
140
201
|
|
|
@@ -203,6 +264,37 @@ if (entry) {
|
|
|
203
264
|
}
|
|
204
265
|
```
|
|
205
266
|
|
|
267
|
+
## Auto-Update System
|
|
268
|
+
|
|
269
|
+
The engine includes an intelligent auto-update system for yt-dlp:
|
|
270
|
+
|
|
271
|
+
### How it works
|
|
272
|
+
|
|
273
|
+
1. **Constant Background Checks** - Checks GitHub for new yt-dlp releases
|
|
274
|
+
2. **Instant Updates on Failure** - If a download fails, immediately checks and updates yt-dlp
|
|
275
|
+
3. **Automatic Retry** - After updating, automatically retries the failed download
|
|
276
|
+
4. **Version Detection** - Compares installed binary version with latest GitHub release
|
|
277
|
+
5. **Zero Downtime** - Updates happen in background without blocking your application
|
|
278
|
+
|
|
279
|
+
### Update Behavior
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
const engine = new PlayEngine();
|
|
283
|
+
// ✓ Checks for updates in background
|
|
284
|
+
|
|
285
|
+
// If download fails
|
|
286
|
+
await engine.getOrDownload(requestId, "audio");
|
|
287
|
+
// <!> Download failed. Forcing yt-dlp update check...
|
|
288
|
+
// >> Updating yt-dlp to 2026.02.04...
|
|
289
|
+
// ✓ Updated to 2026.02.04
|
|
290
|
+
// >> Retrying download after update...
|
|
291
|
+
// ✓ Success
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Manual Update Check
|
|
295
|
+
|
|
296
|
+
To force an update check, simply trigger a download. The system will automatically update if needed.
|
|
297
|
+
|
|
206
298
|
## Advanced Usage
|
|
207
299
|
|
|
208
300
|
### Handle Long Videos (>1h)
|
|
@@ -255,6 +347,7 @@ try {
|
|
|
255
347
|
console.log("Success:", file.path);
|
|
256
348
|
} catch (error) {
|
|
257
349
|
console.error("Download failed:", error.message);
|
|
350
|
+
// Engine automatically tries to update yt-dlp and retry
|
|
258
351
|
}
|
|
259
352
|
```
|
|
260
353
|
|
|
@@ -270,14 +363,14 @@ With aria2c enabled (default):
|
|
|
270
363
|
|
|
271
364
|
_Times may vary based on network speed and YouTube throttling_
|
|
272
365
|
|
|
273
|
-
_The values
|
|
366
|
+
_The values are based on local tests with optimized caching, for downloading long videos use direct download_
|
|
274
367
|
|
|
275
368
|
## File Formats
|
|
276
369
|
|
|
277
370
|
- **Audio**: M4A (native format, no conversion needed)
|
|
278
371
|
- **Video**: MP4 (with audio merged)
|
|
279
372
|
|
|
280
|
-
M4A provides better quality-to-size ratio and downloads
|
|
373
|
+
M4A provides better quality-to-size ratio and downloads 2-5x faster (no re-encoding).
|
|
281
374
|
|
|
282
375
|
## Requirements
|
|
283
376
|
|
|
@@ -287,13 +380,19 @@ M4A provides better quality-to-size ratio and downloads 10-20x faster (no re-enc
|
|
|
287
380
|
|
|
288
381
|
## Binaries
|
|
289
382
|
|
|
290
|
-
The package automatically downloads:
|
|
383
|
+
The package automatically downloads and manages:
|
|
291
384
|
|
|
292
|
-
- **yt-dlp**
|
|
385
|
+
- **yt-dlp** (auto-updates to latest version) (~35 MB)
|
|
293
386
|
- **aria2c** v1.37.0 (12 MB)
|
|
294
387
|
|
|
295
388
|
Binaries are platform-specific and downloaded on first `npm install`.
|
|
296
389
|
|
|
390
|
+
### Auto-Update Schedule
|
|
391
|
+
|
|
392
|
+
- **Background Check**: Constant version check
|
|
393
|
+
- **On-Demand**: Immediately when download fails
|
|
394
|
+
- **Version Source**: GitHub Releases API
|
|
395
|
+
|
|
297
396
|
### Supported Platforms
|
|
298
397
|
|
|
299
398
|
- Linux x64 / arm64
|
|
@@ -337,6 +436,25 @@ Binaries are auto-downloaded to `node_modules/@irithell-js/yt-play/bin/`. If mis
|
|
|
337
436
|
npm rebuild @irithell-js/yt-play
|
|
338
437
|
```
|
|
339
438
|
|
|
439
|
+
### Cookie Authentication Issues
|
|
440
|
+
|
|
441
|
+
If downloads fail with "Sign in to confirm you're not a bot":
|
|
442
|
+
|
|
443
|
+
1. Export fresh cookies from your browser
|
|
444
|
+
2. Ensure cookies.txt is in Netscape format
|
|
445
|
+
3. Verify cookie file path is correct
|
|
446
|
+
4. Try `cookiesFromBrowser` option if browser is installed
|
|
447
|
+
|
|
448
|
+
### Update Check Failures
|
|
449
|
+
|
|
450
|
+
If auto-update fails:
|
|
451
|
+
|
|
452
|
+
```bash
|
|
453
|
+
# Manually update binaries
|
|
454
|
+
cd node_modules/@irithell-js/yt-play
|
|
455
|
+
node scripts/setup-binaries.mjs
|
|
456
|
+
```
|
|
457
|
+
|
|
340
458
|
## License
|
|
341
459
|
|
|
342
460
|
MIT
|
|
@@ -349,6 +467,23 @@ Issues and PRs welcome!
|
|
|
349
467
|
|
|
350
468
|
Deprecated versions have been removed to prevent errors during use.
|
|
351
469
|
|
|
470
|
+
### 0.2.6 (Latest)
|
|
471
|
+
|
|
472
|
+
- Added auto-update system for yt-dlp
|
|
473
|
+
- Direct YouTube URL support in search()
|
|
474
|
+
- Auto-configuration system (creates yt-dlp.conf)
|
|
475
|
+
- Cookie authentication support (cookiesPath/cookiesFromBrowser)
|
|
476
|
+
- Fixed URL search returning wrong video
|
|
477
|
+
- Improved error handling with automatic retry after update
|
|
478
|
+
|
|
479
|
+
### 0.2.5
|
|
480
|
+
|
|
481
|
+
- Added support to direct cookies extraction in pre-built browsers
|
|
482
|
+
|
|
483
|
+
### 0.2.4
|
|
484
|
+
|
|
485
|
+
- Added support to cookies.txt
|
|
486
|
+
|
|
352
487
|
### 0.2.3
|
|
353
488
|
|
|
354
489
|
- Updated documentation
|
package/dist/index.cjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
"use strict";var L=Object.create;var
|
|
2
|
-
`)[0]}catch{}return"yt-dlp"}detectAria2c(){let t=
|
|
3
|
-
`)[0]}catch{}}async exec(t){return new Promise((e
|
|
1
|
+
"use strict";var L=Object.create;var b=Object.defineProperty;var R=Object.getOwnPropertyDescriptor;var K=Object.getOwnPropertyNames;var q=Object.getPrototypeOf,N=Object.prototype.hasOwnProperty;var W=(n,t)=>{for(var r in t)b(n,r,{get:t[r],enumerable:!0})},j=(n,t,r,e)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of K(t))!N.call(n,i)&&i!==r&&b(n,i,{get:()=>t[i],enumerable:!(e=R(t,i))||e.enumerable});return n};var u=(n,t,r)=>(r=n!=null?L(q(n)):{},j(t||!n||!n.__esModule?b(r,"default",{value:n,enumerable:!0}):r,n)),J=n=>j(b({},"__esModule",{value:!0}),n);var tt={};W(tt,{PlayEngine:()=>S,YtDlpClient:()=>w,getYouTubeVideoId:()=>x,normalizeYoutubeUrl:()=>f,searchBest:()=>D});module.exports=J(tt);var p=u(require("fs"),1),h=u(require("path"),1),_=require("child_process");var M=u(require("fs"),1),v=class{constructor(t){this.opts=t}store=new Map;cleanupTimer;get(t){return this.store.get(t)}set(t,r){this.store.set(t,r)}has(t){return this.store.has(t)}delete(t){this.cleanupEntry(t),this.store.delete(t)}markLoading(t,r){let e=this.store.get(t);e&&(e.loading=r)}setFile(t,r,e){let i=this.store.get(t);i&&(i[r]=e)}cleanupExpired(t=Date.now()){let r=0;for(let[e,i]of this.store.entries())t>i.expiresAt&&(this.delete(e),r++);return r}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 r=this.store.get(t);r&&["audio","video"].forEach(e=>{let i=r[e];if(i?.path&&M.default.existsSync(i.path))try{M.default.unlinkSync(i.path)}catch{}})}};var m=u(require("fs"),1),k=u(require("path"),1),B=u(require("os"),1);function z(n){m.default.mkdirSync(n,{recursive:!0,mode:511});try{m.default.chmodSync(n,511)}catch{}m.default.accessSync(n,m.default.constants.R_OK|m.default.constants.W_OK)}function O(n){let t=n?.trim()?n:k.default.join(B.default.tmpdir(),"yt-play"),r=k.default.resolve(t),e=k.default.join(r);return z(r),z(e),{baseDir:r,cacheDir:e}}var V=require("child_process"),l=u(require("path"),1),g=u(require("fs"),1),Q={},y;try{y=l.default.dirname(new URL(Q.url).pathname)}catch{y=typeof y<"u"?y:process.cwd()}var w=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=l.default.resolve(y,"../.."),r=[l.default.join(t,"bin","yt-dlp"),l.default.join(t,"bin","yt-dlp.exe")];for(let e of r)if(g.default.existsSync(e))return e;try{let{execSync:e}=require("child_process"),i=process.platform==="win32"?"where yt-dlp":"which yt-dlp",o=e(i,{encoding:"utf-8"}).trim();if(o)return o.split(`
|
|
2
|
+
`)[0]}catch{}return"yt-dlp"}detectAria2c(){let t=l.default.resolve(y,"../.."),r=[l.default.join(t,"bin","aria2c"),l.default.join(t,"bin","aria2c.exe")];for(let e of r)if(g.default.existsSync(e))return e;try{let{execSync:e}=require("child_process"),i=process.platform==="win32"?"where aria2c":"which aria2c",o=e(i,{encoding:"utf-8"}).trim();if(o)return o.split(`
|
|
3
|
+
`)[0]}catch{}}async exec(t){return new Promise((r,e)=>{let i=[...t];this.ffmpegPath&&(i=["--ffmpeg-location",this.ffmpegPath,...i]),this.cookiesPath&&g.default.existsSync(this.cookiesPath)&&(i=["--cookies",this.cookiesPath,...i]),this.cookiesFromBrowser&&(i=["--cookies-from-browser",this.cookiesFromBrowser,...i]);let o=(0,V.spawn)(this.binaryPath,i,{stdio:["ignore","pipe","pipe"]}),a="",s="";o.stdout.on("data",d=>{a+=d.toString()}),o.stderr.on("data",d=>{s+=d.toString()});let c=setTimeout(()=>{o.kill("SIGKILL"),e(new Error(`yt-dlp timeout after ${this.timeoutMs}ms`))},this.timeoutMs);o.on("close",d=>{clearTimeout(c),d===0?r(a):e(new Error(`yt-dlp exited with code ${d}. stderr: ${s.slice(0,500)}`))}),o.on("error",d=>{clearTimeout(c),e(d)})})}async getInfo(t){let r=await this.exec(["-J","--no-warnings","--no-playlist",t]);return JSON.parse(r)}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,r,e){let i=await this.getInfo(t),a=["-f","bestaudio[ext=m4a]/bestaudio/best","-o",e,...this.buildOptimizationArgs(),t];if(await this.exec(a),!g.default.existsSync(e))throw new Error(`yt-dlp failed to create audio file: ${e}`);let s=this.formatDuration(i.duration);return{title:i.title,author:i.uploader,duration:s,quality:`${r}kbps m4a`,filename:l.default.basename(e),downloadUrl:e}}async getVideo(t,r,e){let i=await this.getInfo(t),a=["-f",`bestvideo[height<=${r}][ext=mp4]+bestaudio[ext=m4a]/best[height<=${r}]`,"--merge-output-format","mp4","-o",e,...this.buildOptimizationArgs(),t];if(await this.exec(a),!g.default.existsSync(e))throw new Error(`yt-dlp failed to create video file: ${e}`);let s=this.formatDuration(i.duration);return{title:i.title,author:i.uploader,duration:s,quality:`${r}p`,filename:l.default.basename(e),downloadUrl:e}}formatDuration(t){if(!t)return"0:00";let r=Math.floor(t/3600),e=Math.floor(t%3600/60),i=Math.floor(t%60);return r>0?`${r}:${e.toString().padStart(2,"0")}:${i.toString().padStart(2,"0")}`:`${e}:${i.toString().padStart(2,"0")}`}};var $=u(require("yt-search"),1);function G(n){let t=(n||"").trim(),r=[...t.matchAll(/\[[^\]]*\]\((https?:\/\/[^)\s]+)\)/gi)];return r.length>0?r[0][1].trim():(t=t.replace(/^<([^>]+)>$/,"$1").trim(),t=t.replace(/^["'`](.*)["'`]$/,"$1").trim(),t)}function x(n){let t=/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=|shorts\/)|youtu\.be\/)([^"&?\/\s]{11})/i,r=(n||"").match(t);return r?r[1]:null}function f(n){let t=G(n),r=t.match(/https?:\/\/[^\s)]+/i)?.[0]??t,e=x(r);return e?`https://www.youtube.com/watch?v=${e}`:null}async function D(n){let t=x(n);if(t){let a=await(0,$.default)({videoId:t});if(!a)return null;let s=a.duration?.seconds??0,c=f(a.url)??a.url;return{title:a.title||"Untitled",author:a.author?.name||void 0,duration:a.duration?.timestamp||void 0,thumb:a.image||a.thumbnail||void 0,videoId:a.videoId,url:c,durationSeconds:s}}let e=(await(0,$.default)(n))?.videos?.[0];if(!e)return null;let i=e.duration?.seconds??0,o=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:o,durationSeconds:i}}var C={},A=[320,256,192,128,96,64],T=[1080,720,480,360],H=36e5;function P(n,t){return t.includes(n)?n:t[0]}function I(n){return(n||"").replace(/[\\/:*?"<>|]/g,"").replace(/[^\w\s-]/gi,"").trim().replace(/\s+/g," ").substring(0,100)}function X(){try{return(0,_.execSync)("which node",{encoding:"utf-8"}).trim()||null}catch{return process.execPath||null}}function Z(n,t,r){try{let e;if(n)e=h.default.dirname(n);else try{let s=new URL(C.url);e=h.default.join(h.default.dirname(s.pathname),"..","bin")}catch{e=h.default.join(__dirname,"..","bin")}if(!p.default.existsSync(e))return;let i=h.default.join(e,"yt-dlp.conf"),o=[],a=X();a&&o.push(`--js-runtimes node:${a}`),o.push("--remote-components ejs:npm"),t&&p.default.existsSync(t)?o.push(`--cookies ${t}`):r&&o.push(`--cookies-from-browser ${r}`),p.default.writeFileSync(i,o.join(`
|
|
4
|
+
`)+`
|
|
5
|
+
`,"utf-8")}catch(e){console.warn("Failed to create yt-dlp.conf:",e)}}var S=class n{opts;paths;cache;ytdlp;static lastUpdateCheck=0;static isUpdating=!1;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=O(t.cacheDir),this.cache=new v({cleanupIntervalMs:this.opts.cleanupIntervalMs}),this.cache.start(),Z(t.ytdlpBinaryPath,t.cookiesPath,t.cookiesFromBrowser),this.ytdlp=new w({binaryPath:t.ytdlpBinaryPath,ffmpegPath:t.ffmpegPath,aria2cPath:t.aria2cPath,useAria2c:this.opts.useAria2c,concurrentFragments:this.opts.concurrentFragments,timeoutMs:t.ytdlpTimeoutMs??3e5,cookiesPath:t.cookiesPath,cookiesFromBrowser:t.cookiesFromBrowser}),this.backgroundUpdateCheck()}backgroundUpdateCheck(){let t=Date.now();t-n.lastUpdateCheck<H||(async()=>{try{n.lastUpdateCheck=t;let r=new URL("../scripts/check-ytdlp-update.mjs",C.url),{checkAndUpdate:e}=await import(r.href);await e()&&this.opts.logger?.info?.("\u2713 yt-dlp updated to latest version")}catch{this.opts.logger?.debug?.("Update check failed (will retry later)")}})()}async forceUpdateCheck(){if(n.isUpdating){this.opts.logger?.info?.("Update already in progress, skipping...");return}try{n.isUpdating=!0,this.opts.logger?.warn?.("<!> Download failed. Forcing yt-dlp update check...");let t=new URL("../scripts/check-ytdlp-update.mjs",C.url),{checkAndUpdate:r}=await import(t.href);await r()?(this.opts.logger?.info?.("\u2713 yt-dlp updated successfully"),n.lastUpdateCheck=Date.now()):this.opts.logger?.info?.("yt-dlp is already up to date")}catch(t){this.opts.logger?.error?.("Failed to update yt-dlp:",t)}finally{n.isUpdating=!1}}generateRequestId(t="play"){return`${t}_${Date.now()}_${Math.random().toString(36).slice(2,8)}`}async search(t){return D(t)}getFromCache(t){return this.cache.get(t)}async preload(t,r){let e=f(t.url);if(!e)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:e};this.cache.set(r,{metadata:o,audio:null,video:null,expiresAt:Date.now()+this.opts.ttlMs,loading:!0});let a=i?96:P(this.opts.preferredAudioKbps,A),s=this.preloadOne(r,"audio",e,a),c=i?[s]:[s,this.preloadOne(r,"video",e,P(this.opts.preferredVideoP,T))];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(r,!1)}async getOrDownload(t,r){let e=this.cache.get(t);if(!e)throw new Error("Request not found (cache miss).");let i=e[r];if(i?.path&&p.default.existsSync(i.path)&&i.size>0)return{metadata:e.metadata,file:i,direct:!1};let o=f(e.metadata.url);if(!o)throw new Error("Invalid YouTube URL.");let a=await this.downloadDirect(r,o);return{metadata:e.metadata,file:a,direct:!0}}async waitCache(t,r,e=8e3,i=500){let o=Date.now();for(;Date.now()-o<e;){let s=this.cache.get(t)?.[r];if(s?.path&&p.default.existsSync(s.path)&&s.size>0)return s;await new Promise(c=>setTimeout(c,i))}return null}cleanup(t){this.cache.delete(t)}async preloadOne(t,r,e,i){try{let o=I(`temp_${Date.now()}`),s=`${r}_${t}_${o}.${r==="audio"?"m4a":"mp4"}`,c=h.default.join(this.paths.cacheDir,s),d=r==="audio"?await this.ytdlp.getAudio(e,i,c):await this.ytdlp.getVideo(e,i,c),U=p.default.statSync(c).size,E;this.opts.preloadBuffer&&(E=await p.default.promises.readFile(c));let Y={path:c,size:U,info:{quality:d.quality},buffer:E};this.cache.setFile(t,r,Y),this.opts.logger?.debug?.(`preloaded ${r} ${U} bytes: ${s}`)}catch(o){throw this.opts.logger?.error?.(`preload ${r} failed`,o),await this.forceUpdateCheck(),o}}async downloadDirect(t,r){try{let e=P(this.opts.preferredAudioKbps,A),i=P(this.opts.preferredVideoP,T),o=t==="audio"?"m4a":"mp4",a=I(`direct_${Date.now()}`),s=h.default.join(this.paths.cacheDir,`${t}_${a}.${o}`),c=t==="audio"?await this.ytdlp.getAudio(r,e,s):await this.ytdlp.getVideo(r,i,s),d=p.default.statSync(s);return{path:s,size:d.size,info:{quality:c.quality}}}catch(e){this.opts.logger?.error?.("Direct download failed:",e),await this.forceUpdateCheck(),this.opts.logger?.info?.(">> Retrying download after update...");let i=P(this.opts.preferredAudioKbps,A),o=P(this.opts.preferredVideoP,T),a=t==="audio"?"m4a":"mp4",s=I(`direct_retry_${Date.now()}`),c=h.default.join(this.paths.cacheDir,`${t}_${s}.${a}`),d=t==="audio"?await this.ytdlp.getAudio(r,i,c):await this.ytdlp.getVideo(r,o,c),F=p.default.statSync(c);return{path:c,size:F.size,info:{quality:d.quality}}}}};0&&(module.exports={PlayEngine,YtDlpClient,getYouTubeVideoId,normalizeYoutubeUrl,searchBest});
|
|
4
6
|
//# 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/ytdlp-client.ts","../src/core/youtube.ts"],"sourcesContent":["export type {\n PlayMetadata,\n CachedFile,\n PlayEngineOptions,\n MediaType,\n DownloadInfo,\n CacheEntry,\n} from \"./core/types.js\";\n\nexport { PlayEngine } from \"./core/play-engine.js\";\nexport {\n searchBest,\n normalizeYoutubeUrl,\n getYouTubeVideoId,\n} from \"./core/youtube.js\";\nexport { YtDlpClient } from \"./core/ytdlp-client.js\";\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type {\n CacheEntry,\n CachedFile,\n DownloadInfo,\n MediaType,\n PlayEngineOptions,\n PlayMetadata,\n} from \"./types.js\";\nimport { CacheStore } from \"./cache.js\";\nimport { resolvePaths } from \"./paths.js\";\nimport { YtDlpClient } from \"./ytdlp-client.js\";\nimport { normalizeYoutubeUrl, searchBest } from \"./youtube.js\";\n\nconst AUDIO_QUALITIES = [320, 256, 192, 128, 96, 64] as const;\nconst VIDEO_QUALITIES = [1080, 720, 480, 360] as const;\n\nfunction pickQuality<T extends number>(\n requested: T,\n available: readonly T[],\n): T {\n return (available as readonly number[]).includes(requested)\n ? requested\n : available[0];\n}\n\nfunction sanitizeFilename(filename: string): string {\n return (filename || \"\")\n .replace(/[\\\\/:*?\"<>|]/g, \"\")\n .replace(/[^\\w\\s-]/gi, \"\")\n .trim()\n .replace(/\\s+/g, \" \")\n .substring(0, 100);\n}\n\nexport class PlayEngine {\n private readonly opts: Required<\n Pick<\n PlayEngineOptions,\n | \"ttlMs\"\n | \"maxPreloadDurationSeconds\"\n | \"preferredAudioKbps\"\n | \"preferredVideoP\"\n | \"preloadBuffer\"\n | \"cleanupIntervalMs\"\n | \"concurrentFragments\"\n >\n > &\n Pick<PlayEngineOptions, \"logger\" | \"useAria2c\">;\n\n private readonly paths: { baseDir: string; cacheDir: string };\n private readonly cache: CacheStore;\n private readonly ytdlp: YtDlpClient;\n\n constructor(options: PlayEngineOptions = {}) {\n this.opts = {\n ttlMs: options.ttlMs ?? 3 * 60_000,\n maxPreloadDurationSeconds: options.maxPreloadDurationSeconds ?? 20 * 60,\n preferredAudioKbps: options.preferredAudioKbps ?? 128,\n preferredVideoP: options.preferredVideoP ?? 720,\n preloadBuffer: options.preloadBuffer ?? true,\n cleanupIntervalMs: options.cleanupIntervalMs ?? 30_000,\n concurrentFragments: options.concurrentFragments ?? 5,\n useAria2c: options.useAria2c,\n logger: options.logger,\n };\n\n this.paths = resolvePaths(options.cacheDir);\n this.cache = new CacheStore({\n cleanupIntervalMs: this.opts.cleanupIntervalMs,\n });\n this.cache.start();\n\n this.ytdlp = new YtDlpClient({\n binaryPath: options.ytdlpBinaryPath,\n ffmpegPath: options.ffmpegPath,\n useAria2c: this.opts.useAria2c,\n concurrentFragments: this.opts.concurrentFragments,\n timeoutMs: options.ytdlpTimeoutMs ?? 300_000, // 5min default\n });\n }\n\n generateRequestId(prefix = \"play\"): string {\n return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;\n }\n\n async search(query: string): Promise<PlayMetadata | null> {\n return searchBest(query);\n }\n\n getFromCache(requestId: string): CacheEntry | undefined {\n return this.cache.get(requestId);\n }\n\n async preload(metadata: PlayMetadata, requestId: string): Promise<void> {\n const normalized = normalizeYoutubeUrl(metadata.url);\n if (!normalized) throw new Error(\"Invalid YouTube URL.\");\n\n const isLongVideo = metadata.durationSeconds > 3600; // >1h\n\n if (metadata.durationSeconds > this.opts.maxPreloadDurationSeconds) {\n this.opts.logger?.warn?.(\n `Video too long for preload (${Math.floor(metadata.durationSeconds / 60)}min). Will use direct download with reduced quality.`,\n );\n }\n\n const normalizedMeta: PlayMetadata = { ...metadata, url: normalized };\n this.cache.set(requestId, {\n metadata: normalizedMeta,\n audio: null,\n video: null,\n expiresAt: Date.now() + this.opts.ttlMs,\n loading: true,\n });\n\n // Vídeos longos (>1h): áudio 96kbps, sem vídeo\n const audioKbps = isLongVideo\n ? 96\n : pickQuality(this.opts.preferredAudioKbps, AUDIO_QUALITIES);\n\n const audioTask = this.preloadOne(\n requestId,\n \"audio\",\n normalized,\n audioKbps,\n );\n\n // Só baixa vídeo se for menor que 1h\n const tasks = isLongVideo\n ? [audioTask]\n : [\n audioTask,\n this.preloadOne(\n requestId,\n \"video\",\n normalized,\n pickQuality(this.opts.preferredVideoP, VIDEO_QUALITIES),\n ),\n ];\n\n if (isLongVideo) {\n this.opts.logger?.info?.(\n `Long video detected (${Math.floor(metadata.durationSeconds / 60)}min). Audio only mode (96kbps).`,\n );\n }\n\n await Promise.allSettled(tasks);\n this.cache.markLoading(requestId, false);\n }\n\n async getOrDownload(\n requestId: string,\n type: MediaType,\n ): Promise<{ metadata: PlayMetadata; file: CachedFile; direct: boolean }> {\n const entry = this.cache.get(requestId);\n if (!entry) throw new Error(\"Request not found (cache miss).\");\n\n const cached = entry[type];\n if (cached?.path && fs.existsSync(cached.path) && cached.size > 0) {\n return { metadata: entry.metadata, file: cached, direct: false };\n }\n\n const normalized = normalizeYoutubeUrl(entry.metadata.url);\n if (!normalized) throw new Error(\"Invalid YouTube URL.\");\n\n const directFile = await this.downloadDirect(type, normalized);\n return { metadata: entry.metadata, file: directFile, direct: true };\n }\n\n async waitCache(\n requestId: string,\n type: MediaType,\n timeoutMs = 8_000,\n intervalMs = 500,\n ): Promise<CachedFile | null> {\n const started = Date.now();\n while (Date.now() - started < timeoutMs) {\n const entry = this.cache.get(requestId);\n const f = entry?.[type];\n if (f?.path && fs.existsSync(f.path) && f.size > 0) return f;\n await new Promise((r) => setTimeout(r, intervalMs));\n }\n return null;\n }\n\n cleanup(requestId: string): void {\n this.cache.delete(requestId);\n }\n\n private async preloadOne(\n requestId: string,\n type: MediaType,\n youtubeUrl: string,\n quality: number,\n ): Promise<void> {\n try {\n const safeTitle = sanitizeFilename(`temp_${Date.now()}`);\n const ext = type === \"audio\" ? \"m4a\" : \"mp4\"; // Mudou de mp3 para m4a\n const filename = `${type}_${requestId}_${safeTitle}.${ext}`;\n const filePath = path.join(this.paths.cacheDir, filename);\n\n const info: DownloadInfo =\n type === \"audio\"\n ? await this.ytdlp.getAudio(youtubeUrl, quality, filePath)\n : await this.ytdlp.getVideo(youtubeUrl, quality, filePath);\n\n const stats = fs.statSync(filePath);\n const size = stats.size;\n\n let buffer: Buffer | undefined;\n if (this.opts.preloadBuffer) {\n buffer = await fs.promises.readFile(filePath);\n }\n\n const cached: CachedFile = {\n path: filePath,\n size,\n info: { quality: info.quality },\n buffer,\n };\n\n this.cache.setFile(requestId, type, cached);\n this.opts.logger?.debug?.(`preloaded ${type} ${size} bytes: ${filename}`);\n } catch (err) {\n this.opts.logger?.error?.(`preload ${type} failed`, err);\n }\n }\n\n private async downloadDirect(\n type: MediaType,\n youtubeUrl: string,\n ): Promise<CachedFile> {\n const audioKbps = pickQuality(\n this.opts.preferredAudioKbps,\n AUDIO_QUALITIES,\n );\n const videoP = pickQuality(this.opts.preferredVideoP, VIDEO_QUALITIES);\n const ext = type === \"audio\" ? \"m4a\" : \"mp4\"; // Mudou de mp3 para m4a\n const safeTitle = sanitizeFilename(`direct_${Date.now()}`);\n const filePath = path.join(\n this.paths.cacheDir,\n `${type}_${safeTitle}.${ext}`,\n );\n\n const info =\n type === \"audio\"\n ? await this.ytdlp.getAudio(youtubeUrl, audioKbps, filePath)\n : await this.ytdlp.getVideo(youtubeUrl, videoP, filePath);\n\n const stats = fs.statSync(filePath);\n return {\n path: filePath,\n size: stats.size,\n info: { quality: info.quality },\n };\n }\n}\n","import fs from \"node:fs\";\n\nimport type { CacheEntry, MediaType } from \"./types.js\";\n\nexport class CacheStore {\n private readonly store = new Map<string, CacheEntry>();\n private cleanupTimer?: NodeJS.Timeout;\n\n constructor(\n private readonly opts: {\n cleanupIntervalMs: number;\n }\n ) {}\n\n get(requestId: string): CacheEntry | undefined {\n return this.store.get(requestId);\n }\n\n set(requestId: string, entry: CacheEntry): void {\n this.store.set(requestId, entry);\n }\n\n has(requestId: string): boolean {\n return this.store.has(requestId);\n }\n\n delete(requestId: string): void {\n this.cleanupEntry(requestId);\n this.store.delete(requestId);\n }\n\n markLoading(requestId: string, loading: boolean): void {\n const e = this.store.get(requestId);\n if (e) e.loading = loading;\n }\n\n setFile(\n requestId: string,\n type: MediaType,\n file: CacheEntry[MediaType]\n ): void {\n const e = this.store.get(requestId);\n if (!e) return;\n e[type] = file as any;\n }\n\n cleanupExpired(now = Date.now()): number {\n let removed = 0;\n for (const [requestId, entry] of this.store.entries()) {\n if (now > entry.expiresAt) {\n this.delete(requestId);\n removed++;\n }\n }\n return removed;\n }\n\n start(): void {\n if (this.cleanupTimer) return;\n\n this.cleanupTimer = setInterval(() => {\n this.cleanupExpired(Date.now());\n }, this.opts.cleanupIntervalMs);\n\n this.cleanupTimer.unref();\n }\n\n stop(): void {\n if (!this.cleanupTimer) return;\n clearInterval(this.cleanupTimer);\n this.cleanupTimer = undefined;\n }\n\n private cleanupEntry(requestId: string) {\n const entry = this.store.get(requestId);\n if (!entry) return;\n\n ([\"audio\", \"video\"] as const).forEach((type) => {\n const f = entry[type];\n if (f?.path && fs.existsSync(f.path)) {\n try {\n fs.unlinkSync(f.path);\n } catch {\n // ignore\n }\n }\n });\n }\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport os from \"node:os\";\n\nexport interface ResolvedPaths {\n baseDir: string;\n cacheDir: string;\n}\n\nexport function ensureDirSync(dirPath: string) {\n fs.mkdirSync(dirPath, { recursive: true, mode: 0o777 });\n\n try {\n fs.chmodSync(dirPath, 0o777);\n } catch {\n // ignore\n }\n\n fs.accessSync(dirPath, fs.constants.R_OK | fs.constants.W_OK);\n}\n\nexport function resolvePaths(cacheDir?: string): ResolvedPaths {\n const baseDir = cacheDir?.trim()\n ? cacheDir\n : path.join(os.tmpdir(), \"yt-play\");\n const resolvedBase = path.resolve(baseDir);\n\n const resolvedCache = path.join(resolvedBase);\n\n ensureDirSync(resolvedBase);\n ensureDirSync(resolvedCache);\n\n return {\n baseDir: resolvedBase,\n cacheDir: resolvedCache,\n };\n}\n","import { spawn } from \"node:child_process\";\nimport path from \"node:path\";\nimport fs from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport type { DownloadInfo } from \"./types.js\";\n\nlet __dirname: string;\ntry {\n // @ts-ignore\n __dirname = path.dirname(fileURLToPath(import.meta.url));\n} catch {\n // @ts-ignore\n __dirname = typeof __dirname !== \"undefined\" ? __dirname : process.cwd();\n}\n\nexport interface YtDlpClientOptions {\n binaryPath?: string;\n ffmpegPath?: string;\n aria2cPath?: string;\n timeoutMs?: number;\n useAria2c?: boolean;\n concurrentFragments?: number;\n}\n\ninterface YtDlpVideoInfo {\n id: string;\n title: string;\n uploader?: string;\n duration: number;\n thumbnail?: string;\n}\n\nexport class YtDlpClient {\n private readonly binaryPath: string;\n private readonly ffmpegPath?: string;\n private readonly aria2cPath?: string;\n private readonly timeoutMs: number;\n private readonly useAria2c: boolean;\n private readonly concurrentFragments: number;\n\n constructor(opts: YtDlpClientOptions = {}) {\n this.binaryPath = opts.binaryPath || this.detectYtDlp();\n this.ffmpegPath = opts.ffmpegPath;\n this.timeoutMs = opts.timeoutMs ?? 300_000;\n this.concurrentFragments = opts.concurrentFragments ?? 5;\n\n this.aria2cPath = opts.aria2cPath || this.detectAria2c();\n this.useAria2c = opts.useAria2c ?? !!this.aria2cPath;\n }\n\n private detectYtDlp(): string {\n const packageRoot = path.resolve(__dirname, \"../..\");\n const bundledPaths = [\n path.join(packageRoot, \"bin\", \"yt-dlp\"),\n path.join(packageRoot, \"bin\", \"yt-dlp.exe\"),\n ];\n\n for (const p of bundledPaths) {\n if (fs.existsSync(p)) {\n return p;\n }\n }\n\n try {\n const { execSync } = require(\"node:child_process\");\n const cmd =\n process.platform === \"win32\" ? \"where yt-dlp\" : \"which yt-dlp\";\n const result = execSync(cmd, { encoding: \"utf-8\" }).trim();\n if (result) return result.split(\"\\n\")[0];\n } catch {\n // yt-dlp not found\n }\n\n return \"yt-dlp\";\n }\n\n private detectAria2c(): string | undefined {\n const packageRoot = path.resolve(__dirname, \"../..\");\n const bundledPaths = [\n path.join(packageRoot, \"bin\", \"aria2c\"),\n path.join(packageRoot, \"bin\", \"aria2c.exe\"),\n ];\n\n for (const p of bundledPaths) {\n if (fs.existsSync(p)) {\n return p;\n }\n }\n\n try {\n const { execSync } = require(\"node:child_process\");\n const cmd =\n process.platform === \"win32\" ? \"where aria2c\" : \"which aria2c\";\n const result = execSync(cmd, { encoding: \"utf-8\" }).trim();\n if (result) return result.split(\"\\n\")[0];\n } catch {\n // aria2c not found\n }\n\n return undefined;\n }\n\n private async exec(args: string[]): Promise<string> {\n return new Promise((resolve, reject) => {\n const allArgs = this.ffmpegPath\n ? [\"--ffmpeg-location\", this.ffmpegPath, ...args]\n : args;\n\n const proc = spawn(this.binaryPath, allArgs, {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n let stdout = \"\";\n let stderr = \"\";\n\n proc.stdout.on(\"data\", (chunk) => {\n stdout += chunk.toString();\n });\n\n proc.stderr.on(\"data\", (chunk) => {\n stderr += chunk.toString();\n });\n\n const timer = setTimeout(() => {\n proc.kill(\"SIGKILL\");\n reject(new Error(`yt-dlp timeout after ${this.timeoutMs}ms`));\n }, this.timeoutMs);\n\n proc.on(\"close\", (code) => {\n clearTimeout(timer);\n if (code === 0) {\n resolve(stdout);\n } else {\n reject(\n new Error(\n `yt-dlp exited with code ${code}. stderr: ${stderr.slice(0, 500)}`,\n ),\n );\n }\n });\n\n proc.on(\"error\", (err) => {\n clearTimeout(timer);\n reject(err);\n });\n });\n }\n\n async getInfo(youtubeUrl: string): Promise<YtDlpVideoInfo> {\n const stdout = await this.exec([\n \"-J\",\n \"--no-warnings\",\n \"--no-playlist\",\n youtubeUrl,\n ]);\n const info = JSON.parse(stdout) as YtDlpVideoInfo;\n return info;\n }\n\n private buildOptimizationArgs(): string[] {\n const args: string[] = [\n \"--no-warnings\",\n \"--no-playlist\",\n \"--no-check-certificates\",\n \"--concurrent-fragments\",\n String(this.concurrentFragments),\n ];\n\n if (this.useAria2c && this.aria2cPath) {\n args.push(\"--downloader\", this.aria2cPath);\n args.push(\"--downloader-args\", \"aria2c:-x 16 -s 16 -k 1M\");\n }\n\n return args;\n }\n\n async getAudio(\n youtubeUrl: string,\n qualityKbps: number,\n outputPath: string,\n ): Promise<DownloadInfo> {\n const info = await this.getInfo(youtubeUrl);\n const format = \"bestaudio[ext=m4a]/bestaudio/best\";\n\n const args = [\n \"-f\",\n format,\n \"-o\",\n outputPath,\n ...this.buildOptimizationArgs(),\n youtubeUrl,\n ];\n\n await this.exec(args);\n\n if (!fs.existsSync(outputPath)) {\n throw new Error(`yt-dlp failed to create audio file: ${outputPath}`);\n }\n\n const duration = this.formatDuration(info.duration);\n\n return {\n title: info.title,\n author: info.uploader,\n duration,\n quality: `${qualityKbps}kbps m4a`,\n filename: path.basename(outputPath),\n downloadUrl: outputPath,\n };\n }\n\n async getVideo(\n youtubeUrl: string,\n qualityP: number,\n outputPath: string,\n ): Promise<DownloadInfo> {\n const info = await this.getInfo(youtubeUrl);\n const format = `bestvideo[height<=${qualityP}][ext=mp4]+bestaudio[ext=m4a]/best[height<=${qualityP}]`;\n\n const args = [\n \"-f\",\n format,\n \"--merge-output-format\",\n \"mp4\",\n \"-o\",\n outputPath,\n ...this.buildOptimizationArgs(),\n youtubeUrl,\n ];\n\n await this.exec(args);\n\n if (!fs.existsSync(outputPath)) {\n throw new Error(`yt-dlp failed to create video file: ${outputPath}`);\n }\n\n const duration = this.formatDuration(info.duration);\n\n return {\n title: info.title,\n author: info.uploader,\n duration,\n quality: `${qualityP}p`,\n filename: path.basename(outputPath),\n downloadUrl: outputPath,\n };\n }\n\n private mapAudioQuality(kbps: number): string {\n if (kbps >= 320) return \"0\";\n if (kbps >= 256) return \"2\";\n if (kbps >= 192) return \"3\";\n if (kbps >= 128) return \"5\";\n if (kbps >= 96) return \"7\";\n return \"9\";\n }\n\n private formatDuration(seconds: number): string {\n if (!seconds) return \"0:00\";\n const h = Math.floor(seconds / 3600);\n const m = Math.floor((seconds % 3600) / 60);\n const s = Math.floor(seconds % 60);\n if (h > 0) {\n return `${h}:${m.toString().padStart(2, \"0\")}:${s.toString().padStart(2, \"0\")}`;\n }\n return `${m}:${s.toString().padStart(2, \"0\")}`;\n }\n}\n","import yts from \"yt-search\";\n\nimport type { PlayMetadata } from \"./types.js\";\n\nexport function stripWeirdUrlWrappers(input: string): string {\n let s = (input || \"\").trim();\n const mdAll = [...s.matchAll(/\\[[^\\]]*\\]\\((https?:\\/\\/[^)\\s]+)\\)/gi)];\n if (mdAll.length > 0) return mdAll[0][1].trim();\n s = s.replace(/^<([^>]+)>$/, \"$1\").trim();\n s = s.replace(/^[\"'`](.*)[\"'`]$/, \"$1\").trim();\n\n return s;\n}\n\nexport function getYouTubeVideoId(input: string): string | null {\n const regex =\n /(?:https?:\\/\\/)?(?:www\\.)?(?:youtube\\.com\\/(?:[^\\/]+\\/.+\\/|(?:v|e(?:mbed)?)\\/|.*[?&]v=|shorts\\/)|youtu\\.be\\/)([^\"&?\\/\\s]{11})/i;\n\n const match = (input || \"\").match(regex);\n return match ? match[1] : null;\n}\n\nexport function normalizeYoutubeUrl(input: string): string | null {\n const cleaned0 = stripWeirdUrlWrappers(input);\n\n const firstUrl = cleaned0.match(/https?:\\/\\/[^\\s)]+/i)?.[0] ?? cleaned0;\n\n const id = getYouTubeVideoId(firstUrl);\n if (!id) return null;\n\n return `https://www.youtube.com/watch?v=${id}`;\n}\n\nexport async function searchBest(query: string): Promise<PlayMetadata | null> {\n const result = await yts(query);\n const v = result?.videos?.[0];\n if (!v) return null;\n\n const durationSeconds = v.duration?.seconds ?? 0;\n\n const normalizedUrl = normalizeYoutubeUrl(v.url) ?? v.url;\n\n return {\n title: v.title || \"Untitled\",\n author: v.author?.name || undefined,\n duration: v.duration?.timestamp || undefined,\n thumb: v.image || v.thumbnail || undefined,\n videoId: v.videoId,\n url: normalizedUrl,\n durationSeconds,\n };\n}\n"],"mappings":"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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_node_url","import_meta","__dirname","path","YtDlpClient","opts","packageRoot","bundledPaths","p","fs","execSync","cmd","result","args","resolve","reject","allArgs","proc","stdout","stderr","chunk","timer","code","err","youtubeUrl","qualityKbps","outputPath","info","duration","qualityP","kbps","seconds","h","m","s","import_yt_search","stripWeirdUrlWrappers","input","s","mdAll","getYouTubeVideoId","regex","match","normalizeYoutubeUrl","cleaned0","firstUrl","id","searchBest","query","v","yts","durationSeconds","normalizedUrl","AUDIO_QUALITIES","VIDEO_QUALITIES","pickQuality","requested","available","sanitizeFilename","filename","PlayEngine","options","resolvePaths","CacheStore","YtDlpClient","prefix","query","searchBest","requestId","metadata","normalized","normalizeYoutubeUrl","isLongVideo","normalizedMeta","audioKbps","audioTask","tasks","type","entry","cached","fs","directFile","timeoutMs","intervalMs","started","f","r","youtubeUrl","quality","safeTitle","filePath","path","info","size","buffer","err","videoP","ext","stats"]}
|
|
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 { execSync } from \"node:child_process\";\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;\nconst UPDATE_CHECK_INTERVAL = 3600000;\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\nfunction getNodeBinaryPath(): string | null {\n try {\n const nodePath = execSync(\"which node\", { encoding: \"utf-8\" }).trim();\n return nodePath || null;\n } catch {\n return process.execPath || null;\n }\n}\n\nfunction setupYtDlpConfig(\n ytdlpBinaryPath: string | undefined,\n cookiesPath: string | undefined,\n cookiesFromBrowser: string | undefined,\n): void {\n try {\n let binaryDir: string;\n\n if (ytdlpBinaryPath) {\n binaryDir = path.dirname(ytdlpBinaryPath);\n } else {\n try {\n const moduleUrl = new URL(import.meta.url);\n binaryDir = path.join(path.dirname(moduleUrl.pathname), \"..\", \"bin\");\n } catch {\n binaryDir = path.join(__dirname, \"..\", \"bin\");\n }\n }\n\n if (!fs.existsSync(binaryDir)) {\n return;\n }\n\n const configPath = path.join(binaryDir, \"yt-dlp.conf\");\n\n const configLines: string[] = [];\n\n const nodePath = getNodeBinaryPath();\n if (nodePath) {\n configLines.push(`--js-runtimes node:${nodePath}`);\n }\n\n configLines.push(\"--remote-components ejs:npm\");\n\n if (cookiesPath && fs.existsSync(cookiesPath)) {\n configLines.push(`--cookies ${cookiesPath}`);\n } else if (cookiesFromBrowser) {\n configLines.push(`--cookies-from-browser ${cookiesFromBrowser}`);\n }\n\n fs.writeFileSync(configPath, configLines.join(\"\\n\") + \"\\n\", \"utf-8\");\n } catch (error) {\n console.warn(\"Failed to create yt-dlp.conf:\", error);\n }\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, \"useAria2c\" | \"logger\">;\n\n private readonly paths: { baseDir: string; cacheDir: string };\n readonly cache: CacheStore;\n private readonly ytdlp: YtDlpClient;\n\n private static lastUpdateCheck: number = 0;\n private static isUpdating: boolean = false;\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 setupYtDlpConfig(\n options.ytdlpBinaryPath,\n options.cookiesPath,\n options.cookiesFromBrowser,\n );\n\n this.ytdlp = new YtDlpClient({\n binaryPath: options.ytdlpBinaryPath,\n ffmpegPath: options.ffmpegPath,\n aria2cPath: options.aria2cPath,\n useAria2c: this.opts.useAria2c,\n concurrentFragments: this.opts.concurrentFragments,\n timeoutMs: options.ytdlpTimeoutMs ?? 300_000,\n cookiesPath: options.cookiesPath,\n cookiesFromBrowser: options.cookiesFromBrowser,\n });\n\n this.backgroundUpdateCheck();\n }\n\n private backgroundUpdateCheck(): void {\n const now = Date.now();\n\n if (now - PlayEngine.lastUpdateCheck < UPDATE_CHECK_INTERVAL) {\n return;\n }\n\n (async () => {\n try {\n PlayEngine.lastUpdateCheck = now;\n const scriptPath = new URL(\n \"../scripts/check-ytdlp-update.mjs\",\n import.meta.url,\n );\n const { checkAndUpdate } = await import(scriptPath.href);\n const updated = await checkAndUpdate();\n\n if (updated) {\n this.opts.logger?.info?.(\"✓ yt-dlp updated to latest version\");\n }\n } catch (error) {\n this.opts.logger?.debug?.(\"Update check failed (will retry later)\");\n }\n })();\n }\n\n private async forceUpdateCheck(): Promise<void> {\n if (PlayEngine.isUpdating) {\n this.opts.logger?.info?.(\"Update already in progress, skipping...\");\n return;\n }\n\n try {\n PlayEngine.isUpdating = true;\n this.opts.logger?.warn?.(\n \"<!> Download failed. Forcing yt-dlp update check...\",\n );\n\n const scriptPath = new URL(\n \"../scripts/check-ytdlp-update.mjs\",\n import.meta.url,\n );\n const { checkAndUpdate } = await import(scriptPath.href);\n const updated = await checkAndUpdate();\n\n if (updated) {\n this.opts.logger?.info?.(\"✓ yt-dlp updated successfully\");\n PlayEngine.lastUpdateCheck = Date.now();\n } else {\n this.opts.logger?.info?.(\"yt-dlp is already up to date\");\n }\n } catch (error) {\n this.opts.logger?.error?.(\"Failed to update yt-dlp:\", error);\n } finally {\n PlayEngine.isUpdating = false;\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;\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\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 = 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 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\";\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 await this.forceUpdateCheck();\n throw err;\n }\n }\n\n private async downloadDirect(\n type: MediaType,\n youtubeUrl: string,\n ): Promise<CachedFile> {\n try {\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\";\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 } catch (err) {\n this.opts.logger?.error?.(\"Direct download failed:\", err);\n await this.forceUpdateCheck();\n\n this.opts.logger?.info?.(\">> Retrying download after update...\");\n\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\";\n const safeTitle = sanitizeFilename(`direct_retry_${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}\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 // Se já é uma URL válida, extrair o ID e buscar direto\n const videoId = getYouTubeVideoId(query);\n\n if (videoId) {\n // É URL - buscar pelo videoId específico (retorna VideoMetadataResult)\n const video = await yts({ videoId });\n\n if (!video) return null;\n\n const durationSeconds = video.duration?.seconds ?? 0;\n const normalizedUrl = normalizeYoutubeUrl(video.url) ?? video.url;\n\n return {\n title: video.title || \"Untitled\",\n author: video.author?.name || undefined,\n duration: video.duration?.timestamp || undefined,\n thumb: video.image || video.thumbnail || undefined,\n videoId: video.videoId,\n url: normalizedUrl,\n durationSeconds,\n };\n }\n\n // Não é URL - buscar normalmente (retorna SearchResult)\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 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_child_process","import_node_fs","CacheStore","opts","requestId","entry","loading","type","file","e","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","videoId","video","yts","durationSeconds","normalizedUrl","v","import_meta","AUDIO_QUALITIES","VIDEO_QUALITIES","UPDATE_CHECK_INTERVAL","pickQuality","requested","available","sanitizeFilename","filename","getNodeBinaryPath","setupYtDlpConfig","ytdlpBinaryPath","cookiesPath","cookiesFromBrowser","binaryDir","path","moduleUrl","fs","configPath","configLines","nodePath","error","PlayEngine","_PlayEngine","options","resolvePaths","CacheStore","YtDlpClient","now","scriptPath","checkAndUpdate","prefix","query","searchBest","requestId","metadata","normalized","normalizeYoutubeUrl","isLongVideo","normalizedMeta","audioKbps","audioTask","tasks","type","entry","cached","directFile","timeoutMs","intervalMs","started","f","r","youtubeUrl","quality","safeTitle","filePath","info","size","buffer","err","videoP","ext","stats"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -33,32 +33,57 @@ interface CacheEntry {
|
|
|
33
33
|
}
|
|
34
34
|
interface PlayEngineOptions {
|
|
35
35
|
cacheDir?: string;
|
|
36
|
+
ytdlpBinaryPath?: string;
|
|
37
|
+
ffmpegPath?: string;
|
|
38
|
+
aria2cPath?: string;
|
|
39
|
+
ytdlpTimeoutMs?: number;
|
|
36
40
|
ttlMs?: number;
|
|
37
41
|
maxPreloadDurationSeconds?: number;
|
|
38
|
-
preferredAudioKbps?:
|
|
39
|
-
preferredVideoP?:
|
|
42
|
+
preferredAudioKbps?: number;
|
|
43
|
+
preferredVideoP?: number;
|
|
40
44
|
preloadBuffer?: boolean;
|
|
41
45
|
cleanupIntervalMs?: number;
|
|
42
|
-
ytdlpBinaryPath?: string;
|
|
43
|
-
ffmpegPath?: string;
|
|
44
|
-
aria2cPath?: string;
|
|
45
46
|
useAria2c?: boolean;
|
|
46
47
|
concurrentFragments?: number;
|
|
47
|
-
|
|
48
|
+
cookiesPath?: string;
|
|
49
|
+
cookiesFromBrowser?: string;
|
|
48
50
|
logger?: {
|
|
51
|
+
debug?: (...args: any[]) => void;
|
|
49
52
|
info?: (...args: any[]) => void;
|
|
50
53
|
warn?: (...args: any[]) => void;
|
|
51
54
|
error?: (...args: any[]) => void;
|
|
52
|
-
debug?: (...args: any[]) => void;
|
|
53
55
|
};
|
|
54
56
|
}
|
|
55
57
|
|
|
58
|
+
declare class CacheStore {
|
|
59
|
+
private readonly opts;
|
|
60
|
+
private readonly store;
|
|
61
|
+
private cleanupTimer?;
|
|
62
|
+
constructor(opts: {
|
|
63
|
+
cleanupIntervalMs: number;
|
|
64
|
+
});
|
|
65
|
+
get(requestId: string): CacheEntry | undefined;
|
|
66
|
+
set(requestId: string, entry: CacheEntry): void;
|
|
67
|
+
has(requestId: string): boolean;
|
|
68
|
+
delete(requestId: string): void;
|
|
69
|
+
markLoading(requestId: string, loading: boolean): void;
|
|
70
|
+
setFile(requestId: string, type: MediaType, file: CacheEntry[MediaType]): void;
|
|
71
|
+
cleanupExpired(now?: number): number;
|
|
72
|
+
start(): void;
|
|
73
|
+
stop(): void;
|
|
74
|
+
private cleanupEntry;
|
|
75
|
+
}
|
|
76
|
+
|
|
56
77
|
declare class PlayEngine {
|
|
57
78
|
private readonly opts;
|
|
58
79
|
private readonly paths;
|
|
59
|
-
|
|
80
|
+
readonly cache: CacheStore;
|
|
60
81
|
private readonly ytdlp;
|
|
82
|
+
private static lastUpdateCheck;
|
|
83
|
+
private static isUpdating;
|
|
61
84
|
constructor(options?: PlayEngineOptions);
|
|
85
|
+
private backgroundUpdateCheck;
|
|
86
|
+
private forceUpdateCheck;
|
|
62
87
|
generateRequestId(prefix?: string): string;
|
|
63
88
|
search(query: string): Promise<PlayMetadata | null>;
|
|
64
89
|
getFromCache(requestId: string): CacheEntry | undefined;
|
|
@@ -85,6 +110,8 @@ interface YtDlpClientOptions {
|
|
|
85
110
|
timeoutMs?: number;
|
|
86
111
|
useAria2c?: boolean;
|
|
87
112
|
concurrentFragments?: number;
|
|
113
|
+
cookiesPath?: string;
|
|
114
|
+
cookiesFromBrowser?: string;
|
|
88
115
|
}
|
|
89
116
|
interface YtDlpVideoInfo {
|
|
90
117
|
id: string;
|
|
@@ -100,6 +127,8 @@ declare class YtDlpClient {
|
|
|
100
127
|
private readonly timeoutMs;
|
|
101
128
|
private readonly useAria2c;
|
|
102
129
|
private readonly concurrentFragments;
|
|
130
|
+
private readonly cookiesPath?;
|
|
131
|
+
private readonly cookiesFromBrowser?;
|
|
103
132
|
constructor(opts?: YtDlpClientOptions);
|
|
104
133
|
private detectYtDlp;
|
|
105
134
|
private detectAria2c;
|
|
@@ -108,7 +137,6 @@ declare class YtDlpClient {
|
|
|
108
137
|
private buildOptimizationArgs;
|
|
109
138
|
getAudio(youtubeUrl: string, qualityKbps: number, outputPath: string): Promise<DownloadInfo>;
|
|
110
139
|
getVideo(youtubeUrl: string, qualityP: number, outputPath: string): Promise<DownloadInfo>;
|
|
111
|
-
private mapAudioQuality;
|
|
112
140
|
private formatDuration;
|
|
113
141
|
}
|
|
114
142
|
|