@irithell-js/yt-play 0.2.8 → 0.3.2
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 +107 -1
- package/dist/index.cjs +5 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +31 -2
- package/dist/index.d.ts +31 -2
- package/dist/index.mjs +5 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/scripts/check-ytdlp-update.mjs +10 -12
- package/scripts/setup-binaries.mjs +23 -18
package/README.md
CHANGED
|
@@ -7,6 +7,8 @@ High-performance YouTube audio/video download engine with intelligent caching, b
|
|
|
7
7
|
- **Bundled Binaries** - yt-dlp and aria2c included (no system dependencies)
|
|
8
8
|
- **Auto-Update System** - yt-dlp updates automatically when needed
|
|
9
9
|
- **Ultra Fast Downloads** - aria2c acceleration (up to 5x faster)
|
|
10
|
+
- **Playlist Support** - Search, extract, and resolve direct media URLs from entire playlists in batches
|
|
11
|
+
- **Shorts Support** - Download and convertion for more compatibility
|
|
10
12
|
- **Intelligent Caching** - TTL-based cache with automatic cleanup
|
|
11
13
|
- **Smart Quality** - Auto-reduces quality for long videos (>1h)
|
|
12
14
|
- **Container Ready** - Works in Docker/isolated environments with cookie support
|
|
@@ -88,6 +90,86 @@ const metadata2 = await engine.search(
|
|
|
88
90
|
console.log(metadata2.title); // "Rick Astley - Never Gonna Give You Up"
|
|
89
91
|
```
|
|
90
92
|
|
|
93
|
+
### Playlist Support
|
|
94
|
+
|
|
95
|
+
You can search and extract direct media URLs from playlists with automatic batch processing:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { YtDlpClient, searchPlaylistBest } from "@irithell-js/yt-play";
|
|
99
|
+
|
|
100
|
+
// 1. Find a playlist (by search term or direct URL)
|
|
101
|
+
const metadata = await searchPlaylistBest("Lofi hip hop");
|
|
102
|
+
|
|
103
|
+
if (metadata) {
|
|
104
|
+
const ytdlp = new YtDlpClient();
|
|
105
|
+
|
|
106
|
+
// 2. Extract and resolve direct streaming URLs
|
|
107
|
+
const playlistInfo = await ytdlp.getPlaylistInfo(metadata.url, {
|
|
108
|
+
limit: 5, // Get exactly 5 valid tracks
|
|
109
|
+
resolveLinks: true, // Automatically resolve direct media URLs
|
|
110
|
+
type: "audio", // "audio", "video", or "both"
|
|
111
|
+
batchSize: 5, // Process 5 tracks concurrently for maximum speed
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
playlistInfo.entries.forEach((track: any) => {
|
|
115
|
+
console.log(`Title: ${track.title}`);
|
|
116
|
+
console.log(`Direct URL: ${track.directUrl}\n`);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Example Output:**
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"id": "PLjNlQ2vXx1xbt30X8TcUfNzw_akVISXEu",
|
|
126
|
+
"title": "BEST Anime Openings and Endings Songs *Playlist*",
|
|
127
|
+
"uploader": "by - AnimeEnfermos -",
|
|
128
|
+
"entries": [
|
|
129
|
+
{
|
|
130
|
+
"id": "jIfogFtgV-o",
|
|
131
|
+
"title": "Noragami / Opening 2",
|
|
132
|
+
"url": "https://www.youtube.com/watch?v=jIfogFtgV-o",
|
|
133
|
+
"duration": 103,
|
|
134
|
+
"uploader": "reeaaas",
|
|
135
|
+
"directUrl": "https://rr2---sn-103oxu-bg0l.go..."
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"id": "0YF8vecQWYs",
|
|
139
|
+
"title": "Crying for Rain - 美波 (Minami) MV",
|
|
140
|
+
"url": "https://www.youtube.com/watch?v=0YF8vecQWYs",
|
|
141
|
+
"duration": 254,
|
|
142
|
+
"uploader": "Minami",
|
|
143
|
+
"directUrl": "https://rr1---sn-103oxu-bg0l.go..."
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"id": "pmanD_s7G3U",
|
|
147
|
+
"title": "Demon Slayer | OP | Gurenge by LiSA HD",
|
|
148
|
+
"url": "https://www.youtube.com/watch?v=pmanD_s7G3U",
|
|
149
|
+
"duration": 90,
|
|
150
|
+
"uploader": "animelab",
|
|
151
|
+
"directUrl": "https://rr1---sn-103oxu-bg0l.go"
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"id": "atxYe-nOa9w",
|
|
155
|
+
"title": "One Punch Man - Official Opening - The Hero!! Set Fire to the Furious Fist",
|
|
156
|
+
"url": "https://www.youtube.com/watch?v=atxYe-nOa9w",
|
|
157
|
+
"duration": 90,
|
|
158
|
+
"uploader": "animelab",
|
|
159
|
+
"directUrl": "https://rr1---sn-103oxu-bg0l.go..."
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"id": "792vg0amsuQ",
|
|
163
|
+
"title": "Steins;Gate 0 - op pt-br legendado",
|
|
164
|
+
"url": "https://www.youtube.com/watch?v=792vg0amsuQ",
|
|
165
|
+
"duration": 243,
|
|
166
|
+
"uploader": "Animes Nii-san",
|
|
167
|
+
"directUrl": "https://rr1---sn-103oxu-bg0l.go..."
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
91
173
|
## Configuration
|
|
92
174
|
|
|
93
175
|
### Constructor Options
|
|
@@ -181,6 +263,16 @@ const lq = new PlayEngine({
|
|
|
181
263
|
|
|
182
264
|
## API Reference
|
|
183
265
|
|
|
266
|
+
### Exported Functions
|
|
267
|
+
|
|
268
|
+
#### `searchBest(query: string): Promise<PlayMetadata | null>`
|
|
269
|
+
|
|
270
|
+
Search for a video on YouTube or get metadata from a URL.
|
|
271
|
+
|
|
272
|
+
#### `searchPlaylistBest(query: string): Promise<PlayMetadata | null>`
|
|
273
|
+
|
|
274
|
+
Search for a playlist on YouTube or get metadata from a direct playlist URL. Returns the list ID and base URL.
|
|
275
|
+
|
|
184
276
|
### PlayEngine Methods
|
|
185
277
|
|
|
186
278
|
#### `search(query: string): Promise<PlayMetadata | null>`
|
|
@@ -264,6 +356,12 @@ if (entry) {
|
|
|
264
356
|
}
|
|
265
357
|
```
|
|
266
358
|
|
|
359
|
+
### YtDlpClient Methods
|
|
360
|
+
|
|
361
|
+
#### `getPlaylistInfo(url: string, opts?: YtDlpPlaylistOptions): Promise<YtDlpPlaylistInfo>`
|
|
362
|
+
|
|
363
|
+
Fetches all entries from a playlist efficiently. If `resolveLinks: true` is provided in options, it will batch-resolve the direct playable streaming URLs for each valid track up to the provided `limit`.
|
|
364
|
+
|
|
267
365
|
## Auto-Update System
|
|
268
366
|
|
|
269
367
|
The engine includes an intelligent auto-update system for yt-dlp:
|
|
@@ -467,7 +565,15 @@ Issues and PRs welcome!
|
|
|
467
565
|
|
|
468
566
|
Deprecated versions have been removed to prevent errors during use.
|
|
469
567
|
|
|
470
|
-
### 0.2
|
|
568
|
+
### 0.3.2 (Latest)
|
|
569
|
+
|
|
570
|
+
- Added full support for YouTube Playlists (search by name or URL via `searchPlaylistBest`)
|
|
571
|
+
- Added fast batch processing to resolve direct media URLs from playlists (`resolveLinks`) with auto-replenishment
|
|
572
|
+
- Fixed race conditions in the `PlayEngine` auto-update system during concurrent downloads
|
|
573
|
+
- Fixed binary setup scripts to use atomic downloads (temp files), preventing missing or corrupted binaries on connection drops
|
|
574
|
+
- Added automatic cleanup of leftover `.part` and `.ytdl` temporary files on download failure or timeout
|
|
575
|
+
|
|
576
|
+
### 0.2.8
|
|
471
577
|
|
|
472
578
|
- Fixed Codec for shorts to work in all plataforms
|
|
473
579
|
|
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
"use strict";var
|
|
2
|
-
`)[0]}catch{}return"yt-dlp"}detectAria2c(){let t=
|
|
3
|
-
`)[0]}catch{}}async exec(t){return new Promise((r,e)=>{let i=[...t];this.ffmpegPath&&(i=["--ffmpeg-location",this.ffmpegPath,...i]),this.cookiesPath&&
|
|
1
|
+
"use strict";var K=Object.create;var S=Object.defineProperty;var J=Object.getOwnPropertyDescriptor;var N=Object.getOwnPropertyNames;var W=Object.getPrototypeOf,Q=Object.prototype.hasOwnProperty;var H=(n,t)=>{for(var r in t)S(n,r,{get:t[r],enumerable:!0})},F=(n,t,r,e)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of N(t))!Q.call(n,i)&&i!==r&&S(n,i,{get:()=>t[i],enumerable:!(e=J(t,i))||e.enumerable});return n};var h=(n,t,r)=>(r=n!=null?K(W(n)):{},F(t||!n||!n.__esModule?S(r,"default",{value:n,enumerable:!0}):r,n)),Z=n=>F(S({},"__esModule",{value:!0}),n);var it={};H(it,{PlayEngine:()=>A,YtDlpClient:()=>P,getYouTubeVideoId:()=>$,normalizeYoutubeUrl:()=>y,searchBest:()=>M,searchPlaylistBest:()=>R});module.exports=Z(it);var m=h(require("fs"),1),g=h(require("path"),1),L=require("child_process");var C=h(require("fs"),1),k=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&&C.default.existsSync(i.path))try{C.default.unlinkSync(i.path)}catch{}})}};var w=h(require("fs"),1),I=h(require("path"),1),Y=h(require("os"),1);function E(n){w.default.mkdirSync(n,{recursive:!0,mode:511});try{w.default.chmodSync(n,511)}catch{}w.default.accessSync(n,w.default.constants.R_OK|w.default.constants.W_OK)}function z(n){let t=n?.trim()?n:I.default.join(Y.default.tmpdir(),"yt-play"),r=I.default.resolve(t),e=I.default.join(r);return E(r),E(e),{baseDir:r,cacheDir:e}}var O=require("child_process"),p=h(require("path"),1),u=h(require("fs"),1),q={},v;try{v=p.default.dirname(new URL(q.url).pathname)}catch{v=typeof v<"u"?v:process.cwd()}var P=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=p.default.resolve(v,"../.."),r=[p.default.join(t,"bin","yt-dlp"),p.default.join(t,"bin","yt-dlp.exe")];for(let e of r)if(u.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=p.default.resolve(v,"../.."),r=[p.default.join(t,"bin","aria2c"),p.default.join(t,"bin","aria2c.exe")];for(let e of r)if(u.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&&u.default.existsSync(this.cookiesPath)&&(i=["--cookies",this.cookiesPath,...i]),this.cookiesFromBrowser&&(i=["--cookies-from-browser",this.cookiesFromBrowser,...i]);let o=(0,O.spawn)(this.binaryPath,i,{stdio:["ignore","pipe","pipe"]}),a="",s="";o.stdout.on("data",c=>{a+=c.toString()}),o.stderr.on("data",c=>{s+=c.toString()});let l=setTimeout(()=>{o.kill("SIGKILL"),e(new Error(`yt-dlp timeout after ${this.timeoutMs}ms`))},this.timeoutMs);o.on("close",c=>{clearTimeout(l),c===0?r(a):e(new Error(`yt-dlp exited with code ${c}. stderr: ${s.slice(0,500)}`))}),o.on("error",c=>{clearTimeout(l),e(c)})})}async getInfo(t){let r=await this.exec(["-J","--no-warnings","--no-playlist",t]);return JSON.parse(r)}async getPlaylistInfo(t,r={}){let e=await this.exec(["-J","--flat-playlist","--no-warnings",t]),i=JSON.parse(e),o=(i.entries||[]).filter(a=>{let s=a.title||"";return a.id&&s&&!s.includes("[Deleted video]")&&!s.includes("[Private video]")}).map(a=>({id:a.id,title:a.title,url:`https://www.youtube.com/watch?v=${a.id}`,duration:a.duration,uploader:a.uploader||i.uploader}));return r.resolveLinks?o=await this.resolvePlaylistItems(o,r.limit&&r.limit>0?r.limit:o.length,r.type||"audio",r.batchSize||5):r.limit&&r.limit>0&&(o=o.slice(0,r.limit)),{id:i.id,title:i.title,uploader:i.uploader,entries:o}}async getDirectUrl(t,r="audio"){let e=r==="audio"?"bestaudio[ext=m4a]/bestaudio/best":"best[ext=mp4]/best";return(await this.exec(["-f",e,"-g","--no-warnings",t])).trim().split(`
|
|
4
|
+
`)[0]}async resolvePlaylistItems(t,r,e="audio",i=5){let o=[],a=0;for(;o.length<r&&a<t.length;){let s=r-o.length,l=Math.min(i,s,t.length-a),c=t.slice(a,a+l);a+=l;let f=c.map(async d=>{try{let b=await this.getDirectUrl(d.url,e);return{...d,directUrl:b}}catch{return null}}),D=await Promise.all(f);for(let d of D)d&&o.length<r&&o.push(d)}return o}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];try{await this.exec(a)}catch(l){throw[e,`${e}.part`,`${e}.ytdl`].forEach(c=>{if(u.default.existsSync(c))try{u.default.unlinkSync(c)}catch{}}),l}if(!u.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:p.default.basename(e),downloadUrl:e}}async getVideo(t,r,e){let i=await this.getInfo(t),a=["-f",`bestvideo[height<=${r}][ext=mp4][vcodec^=avc]+bestaudio[ext=m4a]/bestvideo[height<=${r}][vcodec!=av1][vcodec!=vp9]+bestaudio/best[height<=${r}]`,"--merge-output-format","mp4","-o",e,...this.buildOptimizationArgs(),t];try{await this.exec(a)}catch(l){throw[e,`${e}.part`,`${e}.ytdl`].forEach(c=>{if(u.default.existsSync(c))try{u.default.unlinkSync(c)}catch{}}),l}if(!u.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 H.264`,filename:p.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 x=h(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 $(n){let t=/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})(?:[?&]|$)/i,r=(n||"").match(t);return r?r[1]:null}function X(n){let t=/[?&]list=([a-zA-Z0-9_-]+)/i,r=(n||"").match(t);return r?r[1]:null}function y(n){let t=G(n),r=t.match(/https?:\/\/[^\s)]+/i)?.[0]??t,e=$(r);return e?`https://www.youtube.com/watch?v=${e}`:null}async function M(n){let t=$(n);if(t){let a=await(0,x.default)({videoId:t});if(!a)return null;let s=a.duration?.seconds??0,l=y(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:l,durationSeconds:s}}let e=(await(0,x.default)(n))?.videos?.[0];if(!e)return null;let i=e.duration?.seconds??0,o=y(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}}async function R(n){let t=X(n);if(t)try{let i=await(0,x.default)({listId:t});return i?{title:i.title||"Untitled",author:i.author?.name||void 0,duration:void 0,durationSeconds:0,thumb:i.image||i.thumbnail||void 0,videoId:t,url:i.url||`https://www.youtube.com/playlist?list=${t}`}:null}catch{}let e=(await(0,x.default)(n))?.playlists?.[0];return e?{title:e.title||"Untitled",author:e.author?.name||void 0,duration:void 0,durationSeconds:0,thumb:e.image||e.thumbnail||void 0,videoId:e.listId,url:e.url||`https://www.youtube.com/playlist?list=${e.listId}`}:null}var U={},_=[320,256,192,128,96,64],B=[1080,720,480,360],tt=36e5;function T(n,t){return t.includes(n)?n:t[0]}function j(n){return(n||"").replace(/[\\/:*?"<>|]/g,"").replace(/[^\w\s-]/gi,"").trim().replace(/\s+/g," ").substring(0,100)}function et(){try{return(0,L.execSync)("which node",{encoding:"utf-8"}).trim()||null}catch{return process.execPath||null}}function rt(n,t,r){try{let e;if(n)e=g.default.dirname(n);else try{let s=new URL(U.url);e=g.default.join(g.default.dirname(s.pathname),"..","bin")}catch{e=g.default.join(__dirname,"..","bin")}if(!m.default.existsSync(e))return;let i=g.default.join(e,"yt-dlp.conf"),o=[],a=et();a&&o.push(`--js-runtimes node:${a}`),o.push("--remote-components ejs:npm"),t&&m.default.existsSync(t)?o.push(`--cookies ${t}`):r&&o.push(`--cookies-from-browser ${r}`),m.default.writeFileSync(i,o.join(`
|
|
4
5
|
`)+`
|
|
5
|
-
`,"utf-8")}catch
|
|
6
|
+
`,"utf-8")}catch{}}var A=class n{opts;paths;cache;ytdlp;static lastUpdateCheck=0;static updatePromise=null;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=z(t.cacheDir),this.cache=new k({cleanupIntervalMs:this.opts.cleanupIntervalMs}),this.cache.start(),rt(t.ytdlpBinaryPath,t.cookiesPath,t.cookiesFromBrowser),this.ytdlp=new P({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<tt||n.updatePromise||(n.updatePromise=(async()=>{try{n.lastUpdateCheck=t;let r=new URL("../scripts/check-ytdlp-update.mjs",U.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)")}finally{n.updatePromise=null}})())}async forceUpdateCheck(){return n.updatePromise?(this.opts.logger?.info?.("Update already in progress, waiting..."),n.updatePromise):(n.updatePromise=(async()=>{try{this.opts.logger?.warn?.("<!> Download failed. Forcing yt-dlp update check...");let t=new URL("../scripts/check-ytdlp-update.mjs",U.url),{checkAndUpdate:r}=await import(t.href);await r()&&(this.opts.logger?.info?.("\u2713 yt-dlp updated successfully"),n.lastUpdateCheck=Date.now())}catch(t){this.opts.logger?.error?.("Failed to update yt-dlp:",t)}finally{n.updatePromise=null}})(),n.updatePromise)}generateRequestId(t="play"){return`${t}_${Date.now()}_${Math.random().toString(36).slice(2,8)}`}async search(t){return M(t)}getFromCache(t){return this.cache.get(t)}async preload(t,r){let e=y(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:T(this.opts.preferredAudioKbps,_),s=this.preloadOne(r,"audio",e,a),l=i?[s]:[s,this.preloadOne(r,"video",e,T(this.opts.preferredVideoP,B))];i&&this.opts.logger?.info?.(`Long video detected (${Math.floor(t.durationSeconds/60)}min). Audio only mode (96kbps).`),await Promise.allSettled(l),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&&m.default.existsSync(i.path)&&i.size>0)return{metadata:e.metadata,file:i,direct:!1};let o=y(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&&m.default.existsSync(s.path)&&s.size>0)return s;await new Promise(l=>setTimeout(l,i))}return null}cleanup(t){this.cache.delete(t)}async preloadOne(t,r,e,i){try{await this._executePreload(t,r,e,i)}catch(o){this.opts.logger?.error?.(`preload ${r} failed, forcing update...`,o),await this.forceUpdateCheck(),this.opts.logger?.info?.(">> Retrying preload after update..."),await this._executePreload(t,r,e,i)}}async _executePreload(t,r,e,i){let o=Math.random().toString(36).slice(2,8),a=j(`temp_${Date.now()}_${o}`),l=`${r}_${t}_${a}.${r==="audio"?"m4a":"mp4"}`,c=g.default.join(this.paths.cacheDir,l),f=r==="audio"?await this.ytdlp.getAudio(e,i,c):await this.ytdlp.getVideo(e,i,c),d=m.default.statSync(c).size,b;this.opts.preloadBuffer&&(b=await m.default.promises.readFile(c));let V={path:c,size:d,info:{quality:f.quality},buffer:b};this.cache.setFile(t,r,V),this.opts.logger?.debug?.(`preloaded ${r} ${d} bytes: ${l}`)}async downloadDirect(t,r){try{return await this._executeDownloadDirect(t,r,!1)}catch(e){return this.opts.logger?.error?.("Direct download failed:",e),await this.forceUpdateCheck(),this.opts.logger?.info?.(">> Retrying download after update..."),await this._executeDownloadDirect(t,r,!0)}}async _executeDownloadDirect(t,r,e){let i=T(this.opts.preferredAudioKbps,_),o=T(this.opts.preferredVideoP,B),a=t==="audio"?"m4a":"mp4",s=e?"direct_retry":"direct",l=Math.random().toString(36).slice(2,8),c=j(`${s}_${Date.now()}_${l}`),f=g.default.join(this.paths.cacheDir,`${t}_${c}.${a}`),D=t==="audio"?await this.ytdlp.getAudio(r,i,f):await this.ytdlp.getVideo(r,o,f),d=m.default.statSync(f);return{path:f,size:d.size,info:{quality:D.quality}}}};0&&(module.exports={PlayEngine,YtDlpClient,getYouTubeVideoId,normalizeYoutubeUrl,searchBest,searchPlaylistBest});
|
|
6
7
|
//# 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 { 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\n // Força H.264 (avc) e evita AV1/VP9 que WhatsApp não suporta\n const format = `bestvideo[height<=${qualityP}][ext=mp4][vcodec^=avc]+bestaudio[ext=m4a]/bestvideo[height<=${qualityP}][vcodec!=av1][vcodec!=vp9]+bestaudio/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 H.264`,\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\\/)([a-zA-Z0-9_-]{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"]}
|
|
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 searchPlaylistBest,\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 const configLines: string[] = [];\n const nodePath = getNodeBinaryPath();\n\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}\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 updatePromise: Promise<void> | null = null;\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 if (PlayEngine.updatePromise) {\n return;\n }\n\n PlayEngine.updatePromise = (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 } finally {\n PlayEngine.updatePromise = null;\n }\n })();\n }\n\n private async forceUpdateCheck(): Promise<void> {\n if (PlayEngine.updatePromise) {\n this.opts.logger?.info?.(\"Update already in progress, waiting...\");\n return PlayEngine.updatePromise;\n }\n\n PlayEngine.updatePromise = (async () => {\n try {\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 }\n } catch (error) {\n this.opts.logger?.error?.(\"Failed to update yt-dlp:\", error);\n } finally {\n PlayEngine.updatePromise = null;\n }\n })();\n\n return PlayEngine.updatePromise;\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 await this._executePreload(requestId, type, youtubeUrl, quality);\n } catch (err) {\n this.opts.logger?.error?.(\n `preload ${type} failed, forcing update...`,\n err,\n );\n await this.forceUpdateCheck();\n\n this.opts.logger?.info?.(\">> Retrying preload after update...\");\n await this._executePreload(requestId, type, youtubeUrl, quality);\n }\n }\n\n private async _executePreload(\n requestId: string,\n type: MediaType,\n youtubeUrl: string,\n quality: number,\n ): Promise<void> {\n const uniqueSuffix = Math.random().toString(36).slice(2, 8);\n const safeTitle = sanitizeFilename(`temp_${Date.now()}_${uniqueSuffix}`);\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 }\n\n private async downloadDirect(\n type: MediaType,\n youtubeUrl: string,\n ): Promise<CachedFile> {\n try {\n return await this._executeDownloadDirect(type, youtubeUrl, false);\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 return await this._executeDownloadDirect(type, youtubeUrl, true);\n }\n }\n\n private async _executeDownloadDirect(\n type: MediaType,\n youtubeUrl: string,\n isRetry: boolean,\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\";\n const prefix = isRetry ? \"direct_retry\" : \"direct\";\n const uniqueSuffix = Math.random().toString(36).slice(2, 8);\n const safeTitle = sanitizeFilename(\n `${prefix}_${Date.now()}_${uniqueSuffix}`,\n );\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\nexport interface YtDlpVideoInfo {\n id: string;\n title: string;\n uploader?: string;\n duration: number;\n thumbnail?: string;\n}\n\nexport interface YtDlpPlaylistItem {\n id: string;\n title: string;\n url: string;\n duration?: number;\n uploader?: string;\n directUrl?: string;\n}\n\nexport interface YtDlpResolvedItem extends YtDlpPlaylistItem {\n directUrl: string;\n}\n\nexport interface YtDlpPlaylistInfo {\n id: string;\n title: string;\n uploader?: string;\n entries: YtDlpPlaylistItem[] | YtDlpResolvedItem[];\n}\n\nexport interface YtDlpPlaylistOptions {\n limit?: number;\n resolveLinks?: boolean;\n type?: \"audio\" | \"video\" | \"both\";\n batchSize?: number;\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 async getPlaylistInfo(\n youtubeUrl: string,\n opts: YtDlpPlaylistOptions = {},\n ): Promise<YtDlpPlaylistInfo> {\n const stdout = await this.exec([\n \"-J\",\n \"--flat-playlist\",\n \"--no-warnings\",\n youtubeUrl,\n ]);\n\n const info = JSON.parse(stdout);\n\n let entries: YtDlpPlaylistItem[] = (info.entries || [])\n .filter((entry: any) => {\n const t = entry.title || \"\";\n return (\n entry.id &&\n t &&\n !t.includes(\"[Deleted video]\") &&\n !t.includes(\"[Private video]\")\n );\n })\n .map((entry: any) => ({\n id: entry.id,\n title: entry.title,\n url: `https://www.youtube.com/watch?v=${entry.id}`,\n duration: entry.duration,\n uploader: entry.uploader || info.uploader,\n }));\n\n if (opts.resolveLinks) {\n entries = await this.resolvePlaylistItems(\n entries,\n opts.limit && opts.limit > 0 ? opts.limit : entries.length,\n opts.type || \"audio\",\n opts.batchSize || 5,\n );\n } else if (opts.limit && opts.limit > 0) {\n entries = entries.slice(0, opts.limit);\n }\n\n return {\n id: info.id,\n title: info.title,\n uploader: info.uploader,\n entries,\n };\n }\n\n async getDirectUrl(\n youtubeUrl: string,\n type: \"audio\" | \"video\" | \"both\" = \"audio\",\n ): Promise<string> {\n const format =\n type === \"audio\"\n ? \"bestaudio[ext=m4a]/bestaudio/best\"\n : \"best[ext=mp4]/best\";\n\n const stdout = await this.exec([\n \"-f\",\n format,\n \"-g\",\n \"--no-warnings\",\n youtubeUrl,\n ]);\n\n return stdout.trim().split(\"\\n\")[0];\n }\n\n async resolvePlaylistItems(\n entries: YtDlpPlaylistItem[],\n limit: number,\n type: \"audio\" | \"video\" | \"both\" = \"audio\",\n batchSize: number = 5,\n ): Promise<YtDlpResolvedItem[]> {\n const resolved: YtDlpResolvedItem[] = [];\n let currentIndex = 0;\n\n // Continua processando até atingir o limite solicitado ou a playlist acabar\n while (resolved.length < limit && currentIndex < entries.length) {\n const needed = limit - resolved.length;\n const takeCount = Math.min(\n batchSize,\n needed,\n entries.length - currentIndex,\n );\n\n const batch = entries.slice(currentIndex, currentIndex + takeCount);\n currentIndex += takeCount;\n\n const promises = batch.map(async (item) => {\n try {\n const directUrl = await this.getDirectUrl(item.url, type);\n return { ...item, directUrl } as YtDlpResolvedItem;\n } catch (err) {\n return null; // Ignora os que falharem\n }\n });\n\n const results = await Promise.all(promises);\n\n for (const res of results) {\n if (res && resolved.length < limit) {\n resolved.push(res);\n }\n }\n }\n\n return resolved;\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 try {\n await this.exec(args);\n } catch (err) {\n [outputPath, `${outputPath}.part`, `${outputPath}.ytdl`].forEach((f) => {\n if (fs.existsSync(f)) {\n try {\n fs.unlinkSync(f);\n } catch {}\n }\n });\n throw err;\n }\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\n const format = `bestvideo[height<=${qualityP}][ext=mp4][vcodec^=avc]+bestaudio[ext=m4a]/bestvideo[height<=${qualityP}][vcodec!=av1][vcodec!=vp9]+bestaudio/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 try {\n await this.exec(args);\n } catch (err) {\n [outputPath, `${outputPath}.part`, `${outputPath}.ytdl`].forEach((f) => {\n if (fs.existsSync(f)) {\n try {\n fs.unlinkSync(f);\n } catch {}\n }\n });\n throw err;\n }\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 H.264`,\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\\/)([a-zA-Z0-9_-]{11})(?:[?&]|$)/i;\n\n const match = (input || \"\").match(regex);\n return match ? match[1] : null;\n}\n\nexport function getYouTubePlaylistId(input: string): string | null {\n const regex = /[?&]list=([a-zA-Z0-9_-]+)/i;\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 videoId = getYouTubeVideoId(query);\n\n if (videoId) {\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 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\nexport async function searchPlaylistBest(\n query: string,\n): Promise<PlayMetadata | null> {\n const listId = getYouTubePlaylistId(query);\n\n if (listId) {\n try {\n const list = await yts({ listId });\n\n if (!list) return null;\n\n return {\n title: list.title || \"Untitled\",\n author: list.author?.name || undefined,\n duration: undefined,\n durationSeconds: 0,\n thumb: list.image || list.thumbnail || undefined,\n videoId: listId,\n url: list.url || `https://www.youtube.com/playlist?list=${listId}`,\n };\n } catch (err) {}\n }\n\n const result = await yts(query);\n const p = result?.playlists?.[0];\n if (!p) return null;\n\n return {\n title: p.title || \"Untitled\",\n author: p.author?.name || undefined,\n duration: undefined,\n durationSeconds: 0,\n thumb: p.image || p.thumbnail || undefined,\n videoId: p.listId,\n url: p.url || `https://www.youtube.com/playlist?list=${p.listId}`,\n };\n}\n"],"mappings":"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","names":["index_exports","__export","PlayEngine","YtDlpClient","getYouTubeVideoId","normalizeYoutubeUrl","searchBest","searchPlaylistBest","__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","info","entries","entry","t","type","format","limit","batchSize","resolved","currentIndex","needed","takeCount","batch","promises","item","directUrl","results","res","qualityKbps","outputPath","f","duration","qualityP","seconds","h","m","s","import_yt_search","stripWeirdUrlWrappers","input","s","mdAll","getYouTubeVideoId","regex","match","getYouTubePlaylistId","normalizeYoutubeUrl","cleaned0","firstUrl","id","searchBest","query","videoId","video","yts","durationSeconds","normalizedUrl","v","searchPlaylistBest","listId","list","p","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","PlayEngine","_PlayEngine","options","resolvePaths","CacheStore","YtDlpClient","now","scriptPath","checkAndUpdate","error","prefix","query","searchBest","requestId","metadata","normalized","normalizeYoutubeUrl","isLongVideo","normalizedMeta","audioKbps","audioTask","tasks","type","entry","cached","directFile","timeoutMs","intervalMs","started","f","r","youtubeUrl","quality","err","uniqueSuffix","safeTitle","filePath","info","size","buffer","isRetry","videoP","ext","stats"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -80,7 +80,7 @@ declare class PlayEngine {
|
|
|
80
80
|
readonly cache: CacheStore;
|
|
81
81
|
private readonly ytdlp;
|
|
82
82
|
private static lastUpdateCheck;
|
|
83
|
-
private static
|
|
83
|
+
private static updatePromise;
|
|
84
84
|
constructor(options?: PlayEngineOptions);
|
|
85
85
|
private backgroundUpdateCheck;
|
|
86
86
|
private forceUpdateCheck;
|
|
@@ -96,12 +96,15 @@ declare class PlayEngine {
|
|
|
96
96
|
waitCache(requestId: string, type: MediaType, timeoutMs?: number, intervalMs?: number): Promise<CachedFile | null>;
|
|
97
97
|
cleanup(requestId: string): void;
|
|
98
98
|
private preloadOne;
|
|
99
|
+
private _executePreload;
|
|
99
100
|
private downloadDirect;
|
|
101
|
+
private _executeDownloadDirect;
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
declare function getYouTubeVideoId(input: string): string | null;
|
|
103
105
|
declare function normalizeYoutubeUrl(input: string): string | null;
|
|
104
106
|
declare function searchBest(query: string): Promise<PlayMetadata | null>;
|
|
107
|
+
declare function searchPlaylistBest(query: string): Promise<PlayMetadata | null>;
|
|
105
108
|
|
|
106
109
|
interface YtDlpClientOptions {
|
|
107
110
|
binaryPath?: string;
|
|
@@ -120,6 +123,29 @@ interface YtDlpVideoInfo {
|
|
|
120
123
|
duration: number;
|
|
121
124
|
thumbnail?: string;
|
|
122
125
|
}
|
|
126
|
+
interface YtDlpPlaylistItem {
|
|
127
|
+
id: string;
|
|
128
|
+
title: string;
|
|
129
|
+
url: string;
|
|
130
|
+
duration?: number;
|
|
131
|
+
uploader?: string;
|
|
132
|
+
directUrl?: string;
|
|
133
|
+
}
|
|
134
|
+
interface YtDlpResolvedItem extends YtDlpPlaylistItem {
|
|
135
|
+
directUrl: string;
|
|
136
|
+
}
|
|
137
|
+
interface YtDlpPlaylistInfo {
|
|
138
|
+
id: string;
|
|
139
|
+
title: string;
|
|
140
|
+
uploader?: string;
|
|
141
|
+
entries: YtDlpPlaylistItem[] | YtDlpResolvedItem[];
|
|
142
|
+
}
|
|
143
|
+
interface YtDlpPlaylistOptions {
|
|
144
|
+
limit?: number;
|
|
145
|
+
resolveLinks?: boolean;
|
|
146
|
+
type?: "audio" | "video" | "both";
|
|
147
|
+
batchSize?: number;
|
|
148
|
+
}
|
|
123
149
|
declare class YtDlpClient {
|
|
124
150
|
private readonly binaryPath;
|
|
125
151
|
private readonly ffmpegPath?;
|
|
@@ -134,10 +160,13 @@ declare class YtDlpClient {
|
|
|
134
160
|
private detectAria2c;
|
|
135
161
|
private exec;
|
|
136
162
|
getInfo(youtubeUrl: string): Promise<YtDlpVideoInfo>;
|
|
163
|
+
getPlaylistInfo(youtubeUrl: string, opts?: YtDlpPlaylistOptions): Promise<YtDlpPlaylistInfo>;
|
|
164
|
+
getDirectUrl(youtubeUrl: string, type?: "audio" | "video" | "both"): Promise<string>;
|
|
165
|
+
resolvePlaylistItems(entries: YtDlpPlaylistItem[], limit: number, type?: "audio" | "video" | "both", batchSize?: number): Promise<YtDlpResolvedItem[]>;
|
|
137
166
|
private buildOptimizationArgs;
|
|
138
167
|
getAudio(youtubeUrl: string, qualityKbps: number, outputPath: string): Promise<DownloadInfo>;
|
|
139
168
|
getVideo(youtubeUrl: string, qualityP: number, outputPath: string): Promise<DownloadInfo>;
|
|
140
169
|
private formatDuration;
|
|
141
170
|
}
|
|
142
171
|
|
|
143
|
-
export { type CacheEntry, type CachedFile, type DownloadInfo, type MediaType, PlayEngine, type PlayEngineOptions, type PlayMetadata, YtDlpClient, getYouTubeVideoId, normalizeYoutubeUrl, searchBest };
|
|
172
|
+
export { type CacheEntry, type CachedFile, type DownloadInfo, type MediaType, PlayEngine, type PlayEngineOptions, type PlayMetadata, YtDlpClient, getYouTubeVideoId, normalizeYoutubeUrl, searchBest, searchPlaylistBest };
|
package/dist/index.d.ts
CHANGED
|
@@ -80,7 +80,7 @@ declare class PlayEngine {
|
|
|
80
80
|
readonly cache: CacheStore;
|
|
81
81
|
private readonly ytdlp;
|
|
82
82
|
private static lastUpdateCheck;
|
|
83
|
-
private static
|
|
83
|
+
private static updatePromise;
|
|
84
84
|
constructor(options?: PlayEngineOptions);
|
|
85
85
|
private backgroundUpdateCheck;
|
|
86
86
|
private forceUpdateCheck;
|
|
@@ -96,12 +96,15 @@ declare class PlayEngine {
|
|
|
96
96
|
waitCache(requestId: string, type: MediaType, timeoutMs?: number, intervalMs?: number): Promise<CachedFile | null>;
|
|
97
97
|
cleanup(requestId: string): void;
|
|
98
98
|
private preloadOne;
|
|
99
|
+
private _executePreload;
|
|
99
100
|
private downloadDirect;
|
|
101
|
+
private _executeDownloadDirect;
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
declare function getYouTubeVideoId(input: string): string | null;
|
|
103
105
|
declare function normalizeYoutubeUrl(input: string): string | null;
|
|
104
106
|
declare function searchBest(query: string): Promise<PlayMetadata | null>;
|
|
107
|
+
declare function searchPlaylistBest(query: string): Promise<PlayMetadata | null>;
|
|
105
108
|
|
|
106
109
|
interface YtDlpClientOptions {
|
|
107
110
|
binaryPath?: string;
|
|
@@ -120,6 +123,29 @@ interface YtDlpVideoInfo {
|
|
|
120
123
|
duration: number;
|
|
121
124
|
thumbnail?: string;
|
|
122
125
|
}
|
|
126
|
+
interface YtDlpPlaylistItem {
|
|
127
|
+
id: string;
|
|
128
|
+
title: string;
|
|
129
|
+
url: string;
|
|
130
|
+
duration?: number;
|
|
131
|
+
uploader?: string;
|
|
132
|
+
directUrl?: string;
|
|
133
|
+
}
|
|
134
|
+
interface YtDlpResolvedItem extends YtDlpPlaylistItem {
|
|
135
|
+
directUrl: string;
|
|
136
|
+
}
|
|
137
|
+
interface YtDlpPlaylistInfo {
|
|
138
|
+
id: string;
|
|
139
|
+
title: string;
|
|
140
|
+
uploader?: string;
|
|
141
|
+
entries: YtDlpPlaylistItem[] | YtDlpResolvedItem[];
|
|
142
|
+
}
|
|
143
|
+
interface YtDlpPlaylistOptions {
|
|
144
|
+
limit?: number;
|
|
145
|
+
resolveLinks?: boolean;
|
|
146
|
+
type?: "audio" | "video" | "both";
|
|
147
|
+
batchSize?: number;
|
|
148
|
+
}
|
|
123
149
|
declare class YtDlpClient {
|
|
124
150
|
private readonly binaryPath;
|
|
125
151
|
private readonly ffmpegPath?;
|
|
@@ -134,10 +160,13 @@ declare class YtDlpClient {
|
|
|
134
160
|
private detectAria2c;
|
|
135
161
|
private exec;
|
|
136
162
|
getInfo(youtubeUrl: string): Promise<YtDlpVideoInfo>;
|
|
163
|
+
getPlaylistInfo(youtubeUrl: string, opts?: YtDlpPlaylistOptions): Promise<YtDlpPlaylistInfo>;
|
|
164
|
+
getDirectUrl(youtubeUrl: string, type?: "audio" | "video" | "both"): Promise<string>;
|
|
165
|
+
resolvePlaylistItems(entries: YtDlpPlaylistItem[], limit: number, type?: "audio" | "video" | "both", batchSize?: number): Promise<YtDlpResolvedItem[]>;
|
|
137
166
|
private buildOptimizationArgs;
|
|
138
167
|
getAudio(youtubeUrl: string, qualityKbps: number, outputPath: string): Promise<DownloadInfo>;
|
|
139
168
|
getVideo(youtubeUrl: string, qualityP: number, outputPath: string): Promise<DownloadInfo>;
|
|
140
169
|
private formatDuration;
|
|
141
170
|
}
|
|
142
171
|
|
|
143
|
-
export { type CacheEntry, type CachedFile, type DownloadInfo, type MediaType, PlayEngine, type PlayEngineOptions, type PlayMetadata, YtDlpClient, getYouTubeVideoId, normalizeYoutubeUrl, searchBest };
|
|
172
|
+
export { type CacheEntry, type CachedFile, type DownloadInfo, type MediaType, PlayEngine, type PlayEngineOptions, type PlayMetadata, YtDlpClient, getYouTubeVideoId, normalizeYoutubeUrl, searchBest, searchPlaylistBest };
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
var
|
|
2
|
-
`)[0]}catch{}return"yt-dlp"}detectAria2c(){let t=
|
|
3
|
-
`)[0]}catch{}}async exec(t){return new Promise((r,e)=>{let i=[...t];this.ffmpegPath&&(i=["--ffmpeg-location",this.ffmpegPath,...i]),this.cookiesPath&&
|
|
1
|
+
var T=(n=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(n,{get:(t,r)=>(typeof require<"u"?require:t)[r]}):n)(function(n){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+n+'" is not supported')});import m from"fs";import f from"path";import{execSync as L}from"child_process";import A from"fs";var x=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&&A.existsSync(i.path))try{A.unlinkSync(i.path)}catch{}})}};import v from"fs";import k from"path";import O from"os";function C(n){v.mkdirSync(n,{recursive:!0,mode:511});try{v.chmodSync(n,511)}catch{}v.accessSync(n,v.constants.R_OK|v.constants.W_OK)}function U(n){let t=n?.trim()?n:k.join(O.tmpdir(),"yt-play"),r=k.resolve(t),e=k.join(r);return C(r),C(e),{baseDir:r,cacheDir:e}}import{spawn as R}from"child_process";import p from"path";import u from"fs";var g;try{g=p.dirname(new URL(import.meta.url).pathname)}catch{g=typeof g<"u"?g:process.cwd()}var P=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=p.resolve(g,"../.."),r=[p.join(t,"bin","yt-dlp"),p.join(t,"bin","yt-dlp.exe")];for(let e of r)if(u.existsSync(e))return e;try{let{execSync:e}=T("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=p.resolve(g,"../.."),r=[p.join(t,"bin","aria2c"),p.join(t,"bin","aria2c.exe")];for(let e of r)if(u.existsSync(e))return e;try{let{execSync:e}=T("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&&u.existsSync(this.cookiesPath)&&(i=["--cookies",this.cookiesPath,...i]),this.cookiesFromBrowser&&(i=["--cookies-from-browser",this.cookiesFromBrowser,...i]);let o=R(this.binaryPath,i,{stdio:["ignore","pipe","pipe"]}),a="",s="";o.stdout.on("data",c=>{a+=c.toString()}),o.stderr.on("data",c=>{s+=c.toString()});let l=setTimeout(()=>{o.kill("SIGKILL"),e(new Error(`yt-dlp timeout after ${this.timeoutMs}ms`))},this.timeoutMs);o.on("close",c=>{clearTimeout(l),c===0?r(a):e(new Error(`yt-dlp exited with code ${c}. stderr: ${s.slice(0,500)}`))}),o.on("error",c=>{clearTimeout(l),e(c)})})}async getInfo(t){let r=await this.exec(["-J","--no-warnings","--no-playlist",t]);return JSON.parse(r)}async getPlaylistInfo(t,r={}){let e=await this.exec(["-J","--flat-playlist","--no-warnings",t]),i=JSON.parse(e),o=(i.entries||[]).filter(a=>{let s=a.title||"";return a.id&&s&&!s.includes("[Deleted video]")&&!s.includes("[Private video]")}).map(a=>({id:a.id,title:a.title,url:`https://www.youtube.com/watch?v=${a.id}`,duration:a.duration,uploader:a.uploader||i.uploader}));return r.resolveLinks?o=await this.resolvePlaylistItems(o,r.limit&&r.limit>0?r.limit:o.length,r.type||"audio",r.batchSize||5):r.limit&&r.limit>0&&(o=o.slice(0,r.limit)),{id:i.id,title:i.title,uploader:i.uploader,entries:o}}async getDirectUrl(t,r="audio"){let e=r==="audio"?"bestaudio[ext=m4a]/bestaudio/best":"best[ext=mp4]/best";return(await this.exec(["-f",e,"-g","--no-warnings",t])).trim().split(`
|
|
4
|
+
`)[0]}async resolvePlaylistItems(t,r,e="audio",i=5){let o=[],a=0;for(;o.length<r&&a<t.length;){let s=r-o.length,l=Math.min(i,s,t.length-a),c=t.slice(a,a+l);a+=l;let h=c.map(async d=>{try{let w=await this.getDirectUrl(d.url,e);return{...d,directUrl:w}}catch{return null}}),b=await Promise.all(h);for(let d of b)d&&o.length<r&&o.push(d)}return o}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];try{await this.exec(a)}catch(l){throw[e,`${e}.part`,`${e}.ytdl`].forEach(c=>{if(u.existsSync(c))try{u.unlinkSync(c)}catch{}}),l}if(!u.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:p.basename(e),downloadUrl:e}}async getVideo(t,r,e){let i=await this.getInfo(t),a=["-f",`bestvideo[height<=${r}][ext=mp4][vcodec^=avc]+bestaudio[ext=m4a]/bestvideo[height<=${r}][vcodec!=av1][vcodec!=vp9]+bestaudio/best[height<=${r}]`,"--merge-output-format","mp4","-o",e,...this.buildOptimizationArgs(),t];try{await this.exec(a)}catch(l){throw[e,`${e}.part`,`${e}.ytdl`].forEach(c=>{if(u.existsSync(c))try{u.unlinkSync(c)}catch{}}),l}if(!u.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 H.264`,filename:p.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")}`}};import D from"yt-search";function _(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 I(n){let t=/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})(?:[?&]|$)/i,r=(n||"").match(t);return r?r[1]:null}function B(n){let t=/[?&]list=([a-zA-Z0-9_-]+)/i,r=(n||"").match(t);return r?r[1]:null}function y(n){let t=_(n),r=t.match(/https?:\/\/[^\s)]+/i)?.[0]??t,e=I(r);return e?`https://www.youtube.com/watch?v=${e}`:null}async function $(n){let t=I(n);if(t){let a=await D({videoId:t});if(!a)return null;let s=a.duration?.seconds??0,l=y(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:l,durationSeconds:s}}let e=(await D(n))?.videos?.[0];if(!e)return null;let i=e.duration?.seconds??0,o=y(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}}async function j(n){let t=B(n);if(t)try{let i=await D({listId:t});return i?{title:i.title||"Untitled",author:i.author?.name||void 0,duration:void 0,durationSeconds:0,thumb:i.image||i.thumbnail||void 0,videoId:t,url:i.url||`https://www.youtube.com/playlist?list=${t}`}:null}catch{}let e=(await D(n))?.playlists?.[0];return e?{title:e.title||"Untitled",author:e.author?.name||void 0,duration:void 0,durationSeconds:0,thumb:e.image||e.thumbnail||void 0,videoId:e.listId,url:e.url||`https://www.youtube.com/playlist?list=${e.listId}`}:null}var F=[320,256,192,128,96,64],E=[1080,720,480,360],V=36e5;function S(n,t){return t.includes(n)?n:t[0]}function Y(n){return(n||"").replace(/[\\/:*?"<>|]/g,"").replace(/[^\w\s-]/gi,"").trim().replace(/\s+/g," ").substring(0,100)}function K(){try{return L("which node",{encoding:"utf-8"}).trim()||null}catch{return process.execPath||null}}function J(n,t,r){try{let e;if(n)e=f.dirname(n);else try{let s=new URL(import.meta.url);e=f.join(f.dirname(s.pathname),"..","bin")}catch{e=f.join(__dirname,"..","bin")}if(!m.existsSync(e))return;let i=f.join(e,"yt-dlp.conf"),o=[],a=K();a&&o.push(`--js-runtimes node:${a}`),o.push("--remote-components ejs:npm"),t&&m.existsSync(t)?o.push(`--cookies ${t}`):r&&o.push(`--cookies-from-browser ${r}`),m.writeFileSync(i,o.join(`
|
|
4
5
|
`)+`
|
|
5
|
-
`,"utf-8")}catch
|
|
6
|
+
`,"utf-8")}catch{}}var M=class n{opts;paths;cache;ytdlp;static lastUpdateCheck=0;static updatePromise=null;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=U(t.cacheDir),this.cache=new x({cleanupIntervalMs:this.opts.cleanupIntervalMs}),this.cache.start(),J(t.ytdlpBinaryPath,t.cookiesPath,t.cookiesFromBrowser),this.ytdlp=new P({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<V||n.updatePromise||(n.updatePromise=(async()=>{try{n.lastUpdateCheck=t;let r=new URL("../scripts/check-ytdlp-update.mjs",import.meta.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)")}finally{n.updatePromise=null}})())}async forceUpdateCheck(){return n.updatePromise?(this.opts.logger?.info?.("Update already in progress, waiting..."),n.updatePromise):(n.updatePromise=(async()=>{try{this.opts.logger?.warn?.("<!> Download failed. Forcing yt-dlp update check...");let t=new URL("../scripts/check-ytdlp-update.mjs",import.meta.url),{checkAndUpdate:r}=await import(t.href);await r()&&(this.opts.logger?.info?.("\u2713 yt-dlp updated successfully"),n.lastUpdateCheck=Date.now())}catch(t){this.opts.logger?.error?.("Failed to update yt-dlp:",t)}finally{n.updatePromise=null}})(),n.updatePromise)}generateRequestId(t="play"){return`${t}_${Date.now()}_${Math.random().toString(36).slice(2,8)}`}async search(t){return $(t)}getFromCache(t){return this.cache.get(t)}async preload(t,r){let e=y(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:S(this.opts.preferredAudioKbps,F),s=this.preloadOne(r,"audio",e,a),l=i?[s]:[s,this.preloadOne(r,"video",e,S(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(l),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&&m.existsSync(i.path)&&i.size>0)return{metadata:e.metadata,file:i,direct:!1};let o=y(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&&m.existsSync(s.path)&&s.size>0)return s;await new Promise(l=>setTimeout(l,i))}return null}cleanup(t){this.cache.delete(t)}async preloadOne(t,r,e,i){try{await this._executePreload(t,r,e,i)}catch(o){this.opts.logger?.error?.(`preload ${r} failed, forcing update...`,o),await this.forceUpdateCheck(),this.opts.logger?.info?.(">> Retrying preload after update..."),await this._executePreload(t,r,e,i)}}async _executePreload(t,r,e,i){let o=Math.random().toString(36).slice(2,8),a=Y(`temp_${Date.now()}_${o}`),l=`${r}_${t}_${a}.${r==="audio"?"m4a":"mp4"}`,c=f.join(this.paths.cacheDir,l),h=r==="audio"?await this.ytdlp.getAudio(e,i,c):await this.ytdlp.getVideo(e,i,c),d=m.statSync(c).size,w;this.opts.preloadBuffer&&(w=await m.promises.readFile(c));let z={path:c,size:d,info:{quality:h.quality},buffer:w};this.cache.setFile(t,r,z),this.opts.logger?.debug?.(`preloaded ${r} ${d} bytes: ${l}`)}async downloadDirect(t,r){try{return await this._executeDownloadDirect(t,r,!1)}catch(e){return this.opts.logger?.error?.("Direct download failed:",e),await this.forceUpdateCheck(),this.opts.logger?.info?.(">> Retrying download after update..."),await this._executeDownloadDirect(t,r,!0)}}async _executeDownloadDirect(t,r,e){let i=S(this.opts.preferredAudioKbps,F),o=S(this.opts.preferredVideoP,E),a=t==="audio"?"m4a":"mp4",s=e?"direct_retry":"direct",l=Math.random().toString(36).slice(2,8),c=Y(`${s}_${Date.now()}_${l}`),h=f.join(this.paths.cacheDir,`${t}_${c}.${a}`),b=t==="audio"?await this.ytdlp.getAudio(r,i,h):await this.ytdlp.getVideo(r,o,h),d=m.statSync(h);return{path:h,size:d.size,info:{quality:b.quality}}}};export{M as PlayEngine,P as YtDlpClient,I as getYouTubeVideoId,y as normalizeYoutubeUrl,$ as searchBest,j as searchPlaylistBest};
|
|
6
7
|
//# 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/ytdlp-client.ts","../src/core/youtube.ts"],"sourcesContent":["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\n // Força H.264 (avc) e evita AV1/VP9 que WhatsApp não suporta\n const format = `bestvideo[height<=${qualityP}][ext=mp4][vcodec^=avc]+bestaudio[ext=m4a]/bestvideo[height<=${qualityP}][vcodec!=av1][vcodec!=vp9]+bestaudio/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 H.264`,\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\\/)([a-zA-Z0-9_-]{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":["fs","path","execSync","fs","CacheStore","opts","requestId","entry","loading","type","file","e","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","videoId","video","durationSeconds","normalizedUrl","v","AUDIO_QUALITIES","VIDEO_QUALITIES","UPDATE_CHECK_INTERVAL","pickQuality","requested","available","sanitizeFilename","filename","getNodeBinaryPath","execSync","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"]}
|
|
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 { 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 const configLines: string[] = [];\n const nodePath = getNodeBinaryPath();\n\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}\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 updatePromise: Promise<void> | null = null;\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 if (PlayEngine.updatePromise) {\n return;\n }\n\n PlayEngine.updatePromise = (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 } finally {\n PlayEngine.updatePromise = null;\n }\n })();\n }\n\n private async forceUpdateCheck(): Promise<void> {\n if (PlayEngine.updatePromise) {\n this.opts.logger?.info?.(\"Update already in progress, waiting...\");\n return PlayEngine.updatePromise;\n }\n\n PlayEngine.updatePromise = (async () => {\n try {\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 }\n } catch (error) {\n this.opts.logger?.error?.(\"Failed to update yt-dlp:\", error);\n } finally {\n PlayEngine.updatePromise = null;\n }\n })();\n\n return PlayEngine.updatePromise;\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 await this._executePreload(requestId, type, youtubeUrl, quality);\n } catch (err) {\n this.opts.logger?.error?.(\n `preload ${type} failed, forcing update...`,\n err,\n );\n await this.forceUpdateCheck();\n\n this.opts.logger?.info?.(\">> Retrying preload after update...\");\n await this._executePreload(requestId, type, youtubeUrl, quality);\n }\n }\n\n private async _executePreload(\n requestId: string,\n type: MediaType,\n youtubeUrl: string,\n quality: number,\n ): Promise<void> {\n const uniqueSuffix = Math.random().toString(36).slice(2, 8);\n const safeTitle = sanitizeFilename(`temp_${Date.now()}_${uniqueSuffix}`);\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 }\n\n private async downloadDirect(\n type: MediaType,\n youtubeUrl: string,\n ): Promise<CachedFile> {\n try {\n return await this._executeDownloadDirect(type, youtubeUrl, false);\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 return await this._executeDownloadDirect(type, youtubeUrl, true);\n }\n }\n\n private async _executeDownloadDirect(\n type: MediaType,\n youtubeUrl: string,\n isRetry: boolean,\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\";\n const prefix = isRetry ? \"direct_retry\" : \"direct\";\n const uniqueSuffix = Math.random().toString(36).slice(2, 8);\n const safeTitle = sanitizeFilename(\n `${prefix}_${Date.now()}_${uniqueSuffix}`,\n );\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\nexport interface YtDlpVideoInfo {\n id: string;\n title: string;\n uploader?: string;\n duration: number;\n thumbnail?: string;\n}\n\nexport interface YtDlpPlaylistItem {\n id: string;\n title: string;\n url: string;\n duration?: number;\n uploader?: string;\n directUrl?: string;\n}\n\nexport interface YtDlpResolvedItem extends YtDlpPlaylistItem {\n directUrl: string;\n}\n\nexport interface YtDlpPlaylistInfo {\n id: string;\n title: string;\n uploader?: string;\n entries: YtDlpPlaylistItem[] | YtDlpResolvedItem[];\n}\n\nexport interface YtDlpPlaylistOptions {\n limit?: number;\n resolveLinks?: boolean;\n type?: \"audio\" | \"video\" | \"both\";\n batchSize?: number;\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 async getPlaylistInfo(\n youtubeUrl: string,\n opts: YtDlpPlaylistOptions = {},\n ): Promise<YtDlpPlaylistInfo> {\n const stdout = await this.exec([\n \"-J\",\n \"--flat-playlist\",\n \"--no-warnings\",\n youtubeUrl,\n ]);\n\n const info = JSON.parse(stdout);\n\n let entries: YtDlpPlaylistItem[] = (info.entries || [])\n .filter((entry: any) => {\n const t = entry.title || \"\";\n return (\n entry.id &&\n t &&\n !t.includes(\"[Deleted video]\") &&\n !t.includes(\"[Private video]\")\n );\n })\n .map((entry: any) => ({\n id: entry.id,\n title: entry.title,\n url: `https://www.youtube.com/watch?v=${entry.id}`,\n duration: entry.duration,\n uploader: entry.uploader || info.uploader,\n }));\n\n if (opts.resolveLinks) {\n entries = await this.resolvePlaylistItems(\n entries,\n opts.limit && opts.limit > 0 ? opts.limit : entries.length,\n opts.type || \"audio\",\n opts.batchSize || 5,\n );\n } else if (opts.limit && opts.limit > 0) {\n entries = entries.slice(0, opts.limit);\n }\n\n return {\n id: info.id,\n title: info.title,\n uploader: info.uploader,\n entries,\n };\n }\n\n async getDirectUrl(\n youtubeUrl: string,\n type: \"audio\" | \"video\" | \"both\" = \"audio\",\n ): Promise<string> {\n const format =\n type === \"audio\"\n ? \"bestaudio[ext=m4a]/bestaudio/best\"\n : \"best[ext=mp4]/best\";\n\n const stdout = await this.exec([\n \"-f\",\n format,\n \"-g\",\n \"--no-warnings\",\n youtubeUrl,\n ]);\n\n return stdout.trim().split(\"\\n\")[0];\n }\n\n async resolvePlaylistItems(\n entries: YtDlpPlaylistItem[],\n limit: number,\n type: \"audio\" | \"video\" | \"both\" = \"audio\",\n batchSize: number = 5,\n ): Promise<YtDlpResolvedItem[]> {\n const resolved: YtDlpResolvedItem[] = [];\n let currentIndex = 0;\n\n // Continua processando até atingir o limite solicitado ou a playlist acabar\n while (resolved.length < limit && currentIndex < entries.length) {\n const needed = limit - resolved.length;\n const takeCount = Math.min(\n batchSize,\n needed,\n entries.length - currentIndex,\n );\n\n const batch = entries.slice(currentIndex, currentIndex + takeCount);\n currentIndex += takeCount;\n\n const promises = batch.map(async (item) => {\n try {\n const directUrl = await this.getDirectUrl(item.url, type);\n return { ...item, directUrl } as YtDlpResolvedItem;\n } catch (err) {\n return null; // Ignora os que falharem\n }\n });\n\n const results = await Promise.all(promises);\n\n for (const res of results) {\n if (res && resolved.length < limit) {\n resolved.push(res);\n }\n }\n }\n\n return resolved;\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 try {\n await this.exec(args);\n } catch (err) {\n [outputPath, `${outputPath}.part`, `${outputPath}.ytdl`].forEach((f) => {\n if (fs.existsSync(f)) {\n try {\n fs.unlinkSync(f);\n } catch {}\n }\n });\n throw err;\n }\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\n const format = `bestvideo[height<=${qualityP}][ext=mp4][vcodec^=avc]+bestaudio[ext=m4a]/bestvideo[height<=${qualityP}][vcodec!=av1][vcodec!=vp9]+bestaudio/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 try {\n await this.exec(args);\n } catch (err) {\n [outputPath, `${outputPath}.part`, `${outputPath}.ytdl`].forEach((f) => {\n if (fs.existsSync(f)) {\n try {\n fs.unlinkSync(f);\n } catch {}\n }\n });\n throw err;\n }\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 H.264`,\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\\/)([a-zA-Z0-9_-]{11})(?:[?&]|$)/i;\n\n const match = (input || \"\").match(regex);\n return match ? match[1] : null;\n}\n\nexport function getYouTubePlaylistId(input: string): string | null {\n const regex = /[?&]list=([a-zA-Z0-9_-]+)/i;\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 videoId = getYouTubeVideoId(query);\n\n if (videoId) {\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 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\nexport async function searchPlaylistBest(\n query: string,\n): Promise<PlayMetadata | null> {\n const listId = getYouTubePlaylistId(query);\n\n if (listId) {\n try {\n const list = await yts({ listId });\n\n if (!list) return null;\n\n return {\n title: list.title || \"Untitled\",\n author: list.author?.name || undefined,\n duration: undefined,\n durationSeconds: 0,\n thumb: list.image || list.thumbnail || undefined,\n videoId: listId,\n url: list.url || `https://www.youtube.com/playlist?list=${listId}`,\n };\n } catch (err) {}\n }\n\n const result = await yts(query);\n const p = result?.playlists?.[0];\n if (!p) return null;\n\n return {\n title: p.title || \"Untitled\",\n author: p.author?.name || undefined,\n duration: undefined,\n durationSeconds: 0,\n thumb: p.image || p.thumbnail || undefined,\n videoId: p.listId,\n url: p.url || `https://www.youtube.com/playlist?list=${p.listId}`,\n };\n}\n"],"mappings":"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","names":["fs","path","execSync","fs","CacheStore","opts","requestId","entry","loading","type","file","e","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","info","entries","entry","t","type","format","limit","batchSize","resolved","currentIndex","needed","takeCount","batch","promises","item","directUrl","results","res","qualityKbps","outputPath","f","duration","qualityP","seconds","h","m","s","yts","stripWeirdUrlWrappers","input","s","mdAll","getYouTubeVideoId","regex","match","getYouTubePlaylistId","normalizeYoutubeUrl","cleaned0","firstUrl","id","searchBest","query","videoId","video","durationSeconds","normalizedUrl","v","searchPlaylistBest","listId","list","p","AUDIO_QUALITIES","VIDEO_QUALITIES","UPDATE_CHECK_INTERVAL","pickQuality","requested","available","sanitizeFilename","filename","getNodeBinaryPath","execSync","setupYtDlpConfig","ytdlpBinaryPath","cookiesPath","cookiesFromBrowser","binaryDir","path","moduleUrl","fs","configPath","configLines","nodePath","PlayEngine","_PlayEngine","options","resolvePaths","CacheStore","YtDlpClient","now","scriptPath","checkAndUpdate","error","prefix","query","searchBest","requestId","metadata","normalized","normalizeYoutubeUrl","isLongVideo","normalizedMeta","audioKbps","audioTask","tasks","type","entry","cached","directFile","timeoutMs","intervalMs","started","f","r","youtubeUrl","quality","err","uniqueSuffix","safeTitle","filePath","info","size","buffer","isRetry","videoP","ext","stats"]}
|
package/package.json
CHANGED
|
@@ -86,16 +86,8 @@ function saveVersion(version) {
|
|
|
86
86
|
async function runSetup() {
|
|
87
87
|
try {
|
|
88
88
|
const setupPath = path.join(__dirname, "setup-binaries.mjs");
|
|
89
|
-
|
|
90
|
-
const platform = process.platform;
|
|
91
|
-
const binaryName = platform === "win32" ? "yt-dlp.exe" : "yt-dlp";
|
|
92
|
-
const binaryPath = path.join(BINS_DIR, binaryName);
|
|
93
|
-
|
|
94
|
-
if (fs.existsSync(binaryPath)) {
|
|
95
|
-
fs.unlinkSync(binaryPath);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
89
|
const { execSync } = await import("child_process");
|
|
90
|
+
|
|
99
91
|
execSync(`node "${setupPath}"`, {
|
|
100
92
|
encoding: "utf-8",
|
|
101
93
|
stdio: "inherit",
|
|
@@ -110,9 +102,15 @@ async function runSetup() {
|
|
|
110
102
|
|
|
111
103
|
async function checkAndUpdate() {
|
|
112
104
|
try {
|
|
113
|
-
|
|
114
|
-
|
|
105
|
+
let latestVersion;
|
|
106
|
+
try {
|
|
107
|
+
latestVersion = await getLatestVersion();
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.warn("<!> Could not check latest version from GitHub API");
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
115
112
|
|
|
113
|
+
const binaryVersion = getCurrentBinaryVersion();
|
|
116
114
|
const needsUpdate = !binaryVersion || latestVersion !== binaryVersion;
|
|
117
115
|
|
|
118
116
|
if (needsUpdate) {
|
|
@@ -134,7 +132,7 @@ async function checkAndUpdate() {
|
|
|
134
132
|
saveVersion(latestVersion);
|
|
135
133
|
return false;
|
|
136
134
|
} catch (error) {
|
|
137
|
-
console.error("<!>
|
|
135
|
+
console.error("<!> Update check failed:", error.message);
|
|
138
136
|
return false;
|
|
139
137
|
}
|
|
140
138
|
}
|
|
@@ -29,7 +29,6 @@ const ARIA2_BINARIES = {
|
|
|
29
29
|
},
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
// Função para buscar a versão mais recente do yt-dlp
|
|
33
32
|
async function getLatestYtDlpVersion() {
|
|
34
33
|
return new Promise((resolve, reject) => {
|
|
35
34
|
const options = {
|
|
@@ -52,7 +51,7 @@ async function getLatestYtDlpVersion() {
|
|
|
52
51
|
response.on("end", () => {
|
|
53
52
|
try {
|
|
54
53
|
const release = JSON.parse(data);
|
|
55
|
-
const version = release.tag_name;
|
|
54
|
+
const version = release.tag_name;
|
|
56
55
|
console.log(`✓ Latest yt-dlp version found: ${version}`);
|
|
57
56
|
resolve(version);
|
|
58
57
|
} catch (error) {
|
|
@@ -68,7 +67,6 @@ async function getLatestYtDlpVersion() {
|
|
|
68
67
|
});
|
|
69
68
|
}
|
|
70
69
|
|
|
71
|
-
// Função para gerar URLs dos binários
|
|
72
70
|
function getYtDlpBinaries(version) {
|
|
73
71
|
return {
|
|
74
72
|
linux: {
|
|
@@ -89,21 +87,36 @@ function getYtDlpBinaries(version) {
|
|
|
89
87
|
|
|
90
88
|
function download(url, dest) {
|
|
91
89
|
return new Promise((resolve, reject) => {
|
|
92
|
-
const
|
|
90
|
+
const tmpDest = `${dest}.tmp`;
|
|
91
|
+
const file = fs.createWriteStream(tmpDest);
|
|
92
|
+
|
|
93
93
|
https
|
|
94
94
|
.get(url, (response) => {
|
|
95
95
|
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
96
96
|
file.close();
|
|
97
|
-
fs.unlinkSync(
|
|
97
|
+
if (fs.existsSync(tmpDest)) fs.unlinkSync(tmpDest);
|
|
98
98
|
return download(response.headers.location, dest)
|
|
99
99
|
.then(resolve)
|
|
100
100
|
.catch(reject);
|
|
101
101
|
}
|
|
102
|
+
|
|
102
103
|
response.pipe(file);
|
|
103
|
-
|
|
104
|
+
|
|
105
|
+
file.on("finish", () => {
|
|
106
|
+
file.close(() => {
|
|
107
|
+
try {
|
|
108
|
+
if (fs.existsSync(dest)) fs.unlinkSync(dest);
|
|
109
|
+
fs.renameSync(tmpDest, dest);
|
|
110
|
+
resolve();
|
|
111
|
+
} catch (err) {
|
|
112
|
+
reject(err);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
});
|
|
104
116
|
})
|
|
105
117
|
.on("error", (err) => {
|
|
106
|
-
|
|
118
|
+
file.close();
|
|
119
|
+
if (fs.existsSync(tmpDest)) fs.unlinkSync(tmpDest);
|
|
107
120
|
reject(err);
|
|
108
121
|
});
|
|
109
122
|
});
|
|
@@ -174,12 +187,6 @@ async function setupYtDlp(platform, arch, ytdlpBinaries) {
|
|
|
174
187
|
platform === "win32" ? "yt-dlp.exe" : "yt-dlp",
|
|
175
188
|
);
|
|
176
189
|
|
|
177
|
-
// Sempre remove o binário antigo para forçar atualização
|
|
178
|
-
if (fs.existsSync(ytdlpPath)) {
|
|
179
|
-
console.log("⚠ Removing old yt-dlp binary...");
|
|
180
|
-
fs.unlinkSync(ytdlpPath);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
190
|
try {
|
|
184
191
|
console.log("⬇ Downloading yt-dlp...");
|
|
185
192
|
const url = ytdlpBinaries[platform][arch];
|
|
@@ -192,6 +199,7 @@ async function setupYtDlp(platform, arch, ytdlpBinaries) {
|
|
|
192
199
|
console.log("✓ yt-dlp installed successfully");
|
|
193
200
|
} catch (error) {
|
|
194
201
|
console.error("✗ Failed to install yt-dlp:", error.message);
|
|
202
|
+
throw error;
|
|
195
203
|
}
|
|
196
204
|
}
|
|
197
205
|
|
|
@@ -212,20 +220,17 @@ async function setup() {
|
|
|
212
220
|
fs.unlinkSync(path.join(BINS_DIR, file));
|
|
213
221
|
}
|
|
214
222
|
}
|
|
215
|
-
} catch (err) {
|
|
216
|
-
// Ignore cleanup errors
|
|
217
|
-
}
|
|
223
|
+
} catch (err) {}
|
|
218
224
|
}
|
|
219
225
|
|
|
220
226
|
fs.mkdirSync(BINS_DIR, { recursive: true });
|
|
221
227
|
|
|
222
|
-
// Busca a versão mais recente do yt-dlp
|
|
223
228
|
let ytdlpVersion;
|
|
224
229
|
try {
|
|
225
230
|
ytdlpVersion = await getLatestYtDlpVersion();
|
|
226
231
|
} catch (error) {
|
|
227
232
|
console.error("✗ Failed to fetch latest version, using fallback");
|
|
228
|
-
ytdlpVersion = "2026.02.04";
|
|
233
|
+
ytdlpVersion = "2026.02.04";
|
|
229
234
|
}
|
|
230
235
|
|
|
231
236
|
const ytdlpBinaries = getYtDlpBinaries(ytdlpVersion);
|