@irithell-js/yt-play 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -100,6 +100,11 @@ const engine = new PlayEngine({
100
100
 
101
101
  // Logging
102
102
  logger: console, // Logger instance (optional)
103
+
104
+ // cookies (optional for VPS and dockers)
105
+ cookiesPath: "./cookies.txt",
106
+ // or
107
+ cookiesFromBrowser: "firefox", // extract from browser
103
108
  });
104
109
  ```
105
110
 
@@ -349,6 +354,14 @@ Issues and PRs welcome!
349
354
 
350
355
  Deprecated versions have been removed to prevent errors during use.
351
356
 
357
+ ### 0.2.5
358
+
359
+ - Added support to direct cookies extraction in pre built browsers
360
+
361
+ ### 0.2.4
362
+
363
+ - Added support to cookies.txt
364
+
352
365
  ### 0.2.3
353
366
 
354
367
  - Updated documentation
package/dist/index.cjs CHANGED
@@ -1,4 +1,4 @@
1
- "use strict";var L=Object.create;var w=Object.defineProperty;var R=Object.getOwnPropertyDescriptor;var U=Object.getOwnPropertyNames;var B=Object.getPrototypeOf,K=Object.prototype.hasOwnProperty;var q=(n,t)=>{for(var e in t)w(n,e,{get:t[e],enumerable:!0})},I=(n,t,e,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of U(t))!K.call(n,i)&&i!==e&&w(n,i,{get:()=>t[i],enumerable:!(r=R(t,i))||r.enumerable});return n};var u=(n,t,e)=>(e=n!=null?L(B(n)):{},I(t||!n||!n.__esModule?w(e,"default",{value:n,enumerable:!0}):e,n)),Q=n=>I(w({},"__esModule",{value:!0}),n);var N={};q(N,{PlayEngine:()=>D,YtDlpClient:()=>m,getYouTubeVideoId:()=>S,normalizeYoutubeUrl:()=>f,searchBest:()=>P});module.exports=Q(N);var g=u(require("fs"),1),$=u(require("path"),1);var M=u(require("fs"),1),v=class{constructor(t){this.opts=t}store=new Map;cleanupTimer;get(t){return this.store.get(t)}set(t,e){this.store.set(t,e)}has(t){return this.store.has(t)}delete(t){this.cleanupEntry(t),this.store.delete(t)}markLoading(t,e){let r=this.store.get(t);r&&(r.loading=e)}setFile(t,e,r){let i=this.store.get(t);i&&(i[e]=r)}cleanupExpired(t=Date.now()){let e=0;for(let[r,i]of this.store.entries())t>i.expiresAt&&(this.delete(r),e++);return e}start(){this.cleanupTimer||(this.cleanupTimer=setInterval(()=>{this.cleanupExpired(Date.now())},this.opts.cleanupIntervalMs),this.cleanupTimer.unref())}stop(){this.cleanupTimer&&(clearInterval(this.cleanupTimer),this.cleanupTimer=void 0)}cleanupEntry(t){let e=this.store.get(t);e&&["audio","video"].forEach(r=>{let i=e[r];if(i?.path&&M.default.existsSync(i.path))try{M.default.unlinkSync(i.path)}catch{}})}};var p=u(require("fs"),1),b=u(require("path"),1),C=u(require("os"),1);function E(n){p.default.mkdirSync(n,{recursive:!0,mode:511});try{p.default.chmodSync(n,511)}catch{}p.default.accessSync(n,p.default.constants.R_OK|p.default.constants.W_OK)}function F(n){let t=n?.trim()?n:b.default.join(C.default.tmpdir(),"yt-play"),e=b.default.resolve(t),r=b.default.join(e);return E(e),E(r),{baseDir:e,cacheDir:r}}var O=require("child_process"),d=u(require("path"),1),y=u(require("fs"),1),_=require("url"),W={},h;try{h=d.default.dirname((0,_.fileURLToPath)(W.url))}catch{h=typeof h<"u"?h:process.cwd()}var m=class{binaryPath;ffmpegPath;aria2cPath;timeoutMs;useAria2c;concurrentFragments;constructor(t={}){this.binaryPath=t.binaryPath||this.detectYtDlp(),this.ffmpegPath=t.ffmpegPath,this.timeoutMs=t.timeoutMs??3e5,this.concurrentFragments=t.concurrentFragments??5,this.aria2cPath=t.aria2cPath||this.detectAria2c(),this.useAria2c=t.useAria2c??!!this.aria2cPath}detectYtDlp(){let t=d.default.resolve(h,"../.."),e=[d.default.join(t,"bin","yt-dlp"),d.default.join(t,"bin","yt-dlp.exe")];for(let r of e)if(y.default.existsSync(r))return r;try{let{execSync:r}=require("child_process"),i=process.platform==="win32"?"where yt-dlp":"which yt-dlp",a=r(i,{encoding:"utf-8"}).trim();if(a)return a.split(`
2
- `)[0]}catch{}return"yt-dlp"}detectAria2c(){let t=d.default.resolve(h,"../.."),e=[d.default.join(t,"bin","aria2c"),d.default.join(t,"bin","aria2c.exe")];for(let r of e)if(y.default.existsSync(r))return r;try{let{execSync:r}=require("child_process"),i=process.platform==="win32"?"where aria2c":"which aria2c",a=r(i,{encoding:"utf-8"}).trim();if(a)return a.split(`
3
- `)[0]}catch{}}async exec(t){return new Promise((e,r)=>{let i=this.ffmpegPath?["--ffmpeg-location",this.ffmpegPath,...t]:t,a=(0,O.spawn)(this.binaryPath,i,{stdio:["ignore","pipe","pipe"]}),s="",o="";a.stdout.on("data",l=>{s+=l.toString()}),a.stderr.on("data",l=>{o+=l.toString()});let c=setTimeout(()=>{a.kill("SIGKILL"),r(new Error(`yt-dlp timeout after ${this.timeoutMs}ms`))},this.timeoutMs);a.on("close",l=>{clearTimeout(c),l===0?e(s):r(new Error(`yt-dlp exited with code ${l}. stderr: ${o.slice(0,500)}`))}),a.on("error",l=>{clearTimeout(c),r(l)})})}async getInfo(t){let e=await this.exec(["-J","--no-warnings","--no-playlist",t]);return JSON.parse(e)}buildOptimizationArgs(){let t=["--no-warnings","--no-playlist","--no-check-certificates","--concurrent-fragments",String(this.concurrentFragments)];return this.useAria2c&&this.aria2cPath&&(t.push("--downloader",this.aria2cPath),t.push("--downloader-args","aria2c:-x 16 -s 16 -k 1M")),t}async getAudio(t,e,r){let i=await this.getInfo(t),s=["-f","bestaudio[ext=m4a]/bestaudio/best","-o",r,...this.buildOptimizationArgs(),t];if(await this.exec(s),!y.default.existsSync(r))throw new Error(`yt-dlp failed to create audio file: ${r}`);let o=this.formatDuration(i.duration);return{title:i.title,author:i.uploader,duration:o,quality:`${e}kbps m4a`,filename:d.default.basename(r),downloadUrl:r}}async getVideo(t,e,r){let i=await this.getInfo(t),s=["-f",`bestvideo[height<=${e}][ext=mp4]+bestaudio[ext=m4a]/best[height<=${e}]`,"--merge-output-format","mp4","-o",r,...this.buildOptimizationArgs(),t];if(await this.exec(s),!y.default.existsSync(r))throw new Error(`yt-dlp failed to create video file: ${r}`);let o=this.formatDuration(i.duration);return{title:i.title,author:i.uploader,duration:o,quality:`${e}p`,filename:d.default.basename(r),downloadUrl:r}}mapAudioQuality(t){return t>=320?"0":t>=256?"2":t>=192?"3":t>=128?"5":t>=96?"7":"9"}formatDuration(t){if(!t)return"0:00";let e=Math.floor(t/3600),r=Math.floor(t%3600/60),i=Math.floor(t%60);return e>0?`${e}:${r.toString().padStart(2,"0")}:${i.toString().padStart(2,"0")}`:`${r}:${i.toString().padStart(2,"0")}`}};var z=u(require("yt-search"),1);function J(n){let t=(n||"").trim(),e=[...t.matchAll(/\[[^\]]*\]\((https?:\/\/[^)\s]+)\)/gi)];return e.length>0?e[0][1].trim():(t=t.replace(/^<([^>]+)>$/,"$1").trim(),t=t.replace(/^["'`](.*)["'`]$/,"$1").trim(),t)}function S(n){let t=/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=|shorts\/)|youtu\.be\/)([^"&?\/\s]{11})/i,e=(n||"").match(t);return e?e[1]:null}function f(n){let t=J(n),e=t.match(/https?:\/\/[^\s)]+/i)?.[0]??t,r=S(e);return r?`https://www.youtube.com/watch?v=${r}`:null}async function P(n){let e=(await(0,z.default)(n))?.videos?.[0];if(!e)return null;let r=e.duration?.seconds??0,i=f(e.url)??e.url;return{title:e.title||"Untitled",author:e.author?.name||void 0,duration:e.duration?.timestamp||void 0,thumb:e.image||e.thumbnail||void 0,videoId:e.videoId,url:i,durationSeconds:r}}var V=[320,256,192,128,96,64],Y=[1080,720,480,360];function x(n,t){return t.includes(n)?n:t[0]}function k(n){return(n||"").replace(/[\\/:*?"<>|]/g,"").replace(/[^\w\s-]/gi,"").trim().replace(/\s+/g," ").substring(0,100)}var D=class{opts;paths;cache;ytdlp;constructor(t={}){this.opts={ttlMs:t.ttlMs??3*6e4,maxPreloadDurationSeconds:t.maxPreloadDurationSeconds??1200,preferredAudioKbps:t.preferredAudioKbps??128,preferredVideoP:t.preferredVideoP??720,preloadBuffer:t.preloadBuffer??!0,cleanupIntervalMs:t.cleanupIntervalMs??3e4,concurrentFragments:t.concurrentFragments??5,useAria2c:t.useAria2c,logger:t.logger},this.paths=F(t.cacheDir),this.cache=new v({cleanupIntervalMs:this.opts.cleanupIntervalMs}),this.cache.start(),this.ytdlp=new m({binaryPath:t.ytdlpBinaryPath,ffmpegPath:t.ffmpegPath,useAria2c:this.opts.useAria2c,concurrentFragments:this.opts.concurrentFragments,timeoutMs:t.ytdlpTimeoutMs??3e5})}generateRequestId(t="play"){return`${t}_${Date.now()}_${Math.random().toString(36).slice(2,8)}`}async search(t){return P(t)}getFromCache(t){return this.cache.get(t)}async preload(t,e){let r=f(t.url);if(!r)throw new Error("Invalid YouTube URL.");let i=t.durationSeconds>3600;t.durationSeconds>this.opts.maxPreloadDurationSeconds&&this.opts.logger?.warn?.(`Video too long for preload (${Math.floor(t.durationSeconds/60)}min). Will use direct download with reduced quality.`);let a={...t,url:r};this.cache.set(e,{metadata:a,audio:null,video:null,expiresAt:Date.now()+this.opts.ttlMs,loading:!0});let s=i?96:x(this.opts.preferredAudioKbps,V),o=this.preloadOne(e,"audio",r,s),c=i?[o]:[o,this.preloadOne(e,"video",r,x(this.opts.preferredVideoP,Y))];i&&this.opts.logger?.info?.(`Long video detected (${Math.floor(t.durationSeconds/60)}min). Audio only mode (96kbps).`),await Promise.allSettled(c),this.cache.markLoading(e,!1)}async getOrDownload(t,e){let r=this.cache.get(t);if(!r)throw new Error("Request not found (cache miss).");let i=r[e];if(i?.path&&g.default.existsSync(i.path)&&i.size>0)return{metadata:r.metadata,file:i,direct:!1};let a=f(r.metadata.url);if(!a)throw new Error("Invalid YouTube URL.");let s=await this.downloadDirect(e,a);return{metadata:r.metadata,file:s,direct:!0}}async waitCache(t,e,r=8e3,i=500){let a=Date.now();for(;Date.now()-a<r;){let o=this.cache.get(t)?.[e];if(o?.path&&g.default.existsSync(o.path)&&o.size>0)return o;await new Promise(c=>setTimeout(c,i))}return null}cleanup(t){this.cache.delete(t)}async preloadOne(t,e,r,i){try{let a=k(`temp_${Date.now()}`),o=`${e}_${t}_${a}.${e==="audio"?"m4a":"mp4"}`,c=$.default.join(this.paths.cacheDir,o),l=e==="audio"?await this.ytdlp.getAudio(r,i,c):await this.ytdlp.getVideo(r,i,c),T=g.default.statSync(c).size,A;this.opts.preloadBuffer&&(A=await g.default.promises.readFile(c));let j={path:c,size:T,info:{quality:l.quality},buffer:A};this.cache.setFile(t,e,j),this.opts.logger?.debug?.(`preloaded ${e} ${T} bytes: ${o}`)}catch(a){this.opts.logger?.error?.(`preload ${e} failed`,a)}}async downloadDirect(t,e){let r=x(this.opts.preferredAudioKbps,V),i=x(this.opts.preferredVideoP,Y),a=t==="audio"?"m4a":"mp4",s=k(`direct_${Date.now()}`),o=$.default.join(this.paths.cacheDir,`${t}_${s}.${a}`),c=t==="audio"?await this.ytdlp.getAudio(e,r,o):await this.ytdlp.getVideo(e,i,o),l=g.default.statSync(o);return{path:o,size:l.size,info:{quality:c.quality}}}};0&&(module.exports={PlayEngine,YtDlpClient,getYouTubeVideoId,normalizeYoutubeUrl,searchBest});
1
+ "use strict";var Y=Object.create;var w=Object.defineProperty;var j=Object.getOwnPropertyDescriptor;var L=Object.getOwnPropertyNames;var R=Object.getPrototypeOf,U=Object.prototype.hasOwnProperty;var K=(o,t)=>{for(var e in t)w(o,e,{get:t[e],enumerable:!0})},A=(o,t,e,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of L(t))!U.call(o,i)&&i!==e&&w(o,i,{get:()=>t[i],enumerable:!(r=j(t,i))||r.enumerable});return o};var u=(o,t,e)=>(e=o!=null?Y(R(o)):{},A(t||!o||!o.__esModule?w(e,"default",{value:o,enumerable:!0}):e,o)),q=o=>A(w({},"__esModule",{value:!0}),o);var Q={};K(Q,{PlayEngine:()=>D,YtDlpClient:()=>f,getYouTubeVideoId:()=>S,normalizeYoutubeUrl:()=>g,searchBest:()=>v});module.exports=q(Q);var y=u(require("fs"),1),$=u(require("path"),1);var M=u(require("fs"),1),b=class{constructor(t){this.opts=t}store=new Map;cleanupTimer;get(t){return this.store.get(t)}set(t,e){this.store.set(t,e)}has(t){return this.store.has(t)}delete(t){this.cleanupEntry(t),this.store.delete(t)}markLoading(t,e){let r=this.store.get(t);r&&(r.loading=e)}setFile(t,e,r){let i=this.store.get(t);i&&(i[e]=r)}cleanupExpired(t=Date.now()){let e=0;for(let[r,i]of this.store.entries())t>i.expiresAt&&(this.delete(r),e++);return e}start(){this.cleanupTimer||(this.cleanupTimer=setInterval(()=>{this.cleanupExpired(Date.now())},this.opts.cleanupIntervalMs),this.cleanupTimer.unref())}stop(){this.cleanupTimer&&(clearInterval(this.cleanupTimer),this.cleanupTimer=void 0)}cleanupEntry(t){let e=this.store.get(t);e&&["audio","video"].forEach(r=>{let i=e[r];if(i?.path&&M.default.existsSync(i.path))try{M.default.unlinkSync(i.path)}catch{}})}};var h=u(require("fs"),1),P=u(require("path"),1),F=u(require("os"),1);function I(o){h.default.mkdirSync(o,{recursive:!0,mode:511});try{h.default.chmodSync(o,511)}catch{}h.default.accessSync(o,h.default.constants.R_OK|h.default.constants.W_OK)}function E(o){let t=o?.trim()?o:P.default.join(F.default.tmpdir(),"yt-play"),e=P.default.resolve(t),r=P.default.join(e);return I(e),I(r),{baseDir:e,cacheDir:r}}var C=require("child_process"),d=u(require("path"),1),p=u(require("fs"),1),W={},m;try{m=d.default.dirname(new URL(W.url).pathname)}catch{m=typeof m<"u"?m:process.cwd()}var f=class{binaryPath;ffmpegPath;aria2cPath;timeoutMs;useAria2c;concurrentFragments;cookiesPath;cookiesFromBrowser;constructor(t={}){this.binaryPath=t.binaryPath||this.detectYtDlp(),this.ffmpegPath=t.ffmpegPath,this.timeoutMs=t.timeoutMs??3e5,this.concurrentFragments=t.concurrentFragments??5,this.cookiesPath=t.cookiesPath,this.cookiesFromBrowser=t.cookiesFromBrowser,this.aria2cPath=t.aria2cPath||this.detectAria2c(),this.useAria2c=t.useAria2c??!!this.aria2cPath}detectYtDlp(){let t=d.default.resolve(m,"../.."),e=[d.default.join(t,"bin","yt-dlp"),d.default.join(t,"bin","yt-dlp.exe")];for(let r of e)if(p.default.existsSync(r))return r;try{let{execSync:r}=require("child_process"),i=process.platform==="win32"?"where yt-dlp":"which yt-dlp",n=r(i,{encoding:"utf-8"}).trim();if(n)return n.split(`
2
+ `)[0]}catch{}return"yt-dlp"}detectAria2c(){let t=d.default.resolve(m,"../.."),e=[d.default.join(t,"bin","aria2c"),d.default.join(t,"bin","aria2c.exe")];for(let r of e)if(p.default.existsSync(r))return r;try{let{execSync:r}=require("child_process"),i=process.platform==="win32"?"where aria2c":"which aria2c",n=r(i,{encoding:"utf-8"}).trim();if(n)return n.split(`
3
+ `)[0]}catch{}}async exec(t){return new Promise((e,r)=>{let i=[...t];this.ffmpegPath&&(i=["--ffmpeg-location",this.ffmpegPath,...i]),this.cookiesPath&&p.default.existsSync(this.cookiesPath)&&(i=["--cookies",this.cookiesPath,...i]),this.cookiesFromBrowser&&(i=["--cookies-from-browser",this.cookiesFromBrowser,...i]);let n=(0,C.spawn)(this.binaryPath,i,{stdio:["ignore","pipe","pipe"]}),s="",a="";n.stdout.on("data",l=>{s+=l.toString()}),n.stderr.on("data",l=>{a+=l.toString()});let c=setTimeout(()=>{n.kill("SIGKILL"),r(new Error(`yt-dlp timeout after ${this.timeoutMs}ms`))},this.timeoutMs);n.on("close",l=>{clearTimeout(c),l===0?e(s):r(new Error(`yt-dlp exited with code ${l}. stderr: ${a.slice(0,500)}`))}),n.on("error",l=>{clearTimeout(c),r(l)})})}async getInfo(t){let e=await this.exec(["-J","--no-warnings","--no-playlist",t]);return JSON.parse(e)}buildOptimizationArgs(){let t=["--no-warnings","--no-playlist","--no-check-certificates","--concurrent-fragments",String(this.concurrentFragments)];return this.useAria2c&&this.aria2cPath&&(t.push("--downloader",this.aria2cPath),t.push("--downloader-args","aria2c:-x 16 -s 16 -k 1M")),t}async getAudio(t,e,r){let i=await this.getInfo(t),s=["-f","bestaudio[ext=m4a]/bestaudio/best","-o",r,...this.buildOptimizationArgs(),t];if(await this.exec(s),!p.default.existsSync(r))throw new Error(`yt-dlp failed to create audio file: ${r}`);let a=this.formatDuration(i.duration);return{title:i.title,author:i.uploader,duration:a,quality:`${e}kbps m4a`,filename:d.default.basename(r),downloadUrl:r}}async getVideo(t,e,r){let i=await this.getInfo(t),s=["-f",`bestvideo[height<=${e}][ext=mp4]+bestaudio[ext=m4a]/best[height<=${e}]`,"--merge-output-format","mp4","-o",r,...this.buildOptimizationArgs(),t];if(await this.exec(s),!p.default.existsSync(r))throw new Error(`yt-dlp failed to create video file: ${r}`);let a=this.formatDuration(i.duration);return{title:i.title,author:i.uploader,duration:a,quality:`${e}p`,filename:d.default.basename(r),downloadUrl:r}}formatDuration(t){if(!t)return"0:00";let e=Math.floor(t/3600),r=Math.floor(t%3600/60),i=Math.floor(t%60);return e>0?`${e}:${r.toString().padStart(2,"0")}:${i.toString().padStart(2,"0")}`:`${r}:${i.toString().padStart(2,"0")}`}};var O=u(require("yt-search"),1);function J(o){let t=(o||"").trim(),e=[...t.matchAll(/\[[^\]]*\]\((https?:\/\/[^)\s]+)\)/gi)];return e.length>0?e[0][1].trim():(t=t.replace(/^<([^>]+)>$/,"$1").trim(),t=t.replace(/^["'`](.*)["'`]$/,"$1").trim(),t)}function S(o){let t=/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=|shorts\/)|youtu\.be\/)([^"&?\/\s]{11})/i,e=(o||"").match(t);return e?e[1]:null}function g(o){let t=J(o),e=t.match(/https?:\/\/[^\s)]+/i)?.[0]??t,r=S(e);return r?`https://www.youtube.com/watch?v=${r}`:null}async function v(o){let e=(await(0,O.default)(o))?.videos?.[0];if(!e)return null;let r=e.duration?.seconds??0,i=g(e.url)??e.url;return{title:e.title||"Untitled",author:e.author?.name||void 0,duration:e.duration?.timestamp||void 0,thumb:e.image||e.thumbnail||void 0,videoId:e.videoId,url:i,durationSeconds:r}}var B=[320,256,192,128,96,64],_=[1080,720,480,360];function x(o,t){return t.includes(o)?o:t[0]}function z(o){return(o||"").replace(/[\\/:*?"<>|]/g,"").replace(/[^\w\s-]/gi,"").trim().replace(/\s+/g," ").substring(0,100)}var D=class{opts;paths;cache;ytdlp;constructor(t={}){this.opts={ttlMs:t.ttlMs??3*6e4,maxPreloadDurationSeconds:t.maxPreloadDurationSeconds??1200,preferredAudioKbps:t.preferredAudioKbps??128,preferredVideoP:t.preferredVideoP??720,preloadBuffer:t.preloadBuffer??!0,cleanupIntervalMs:t.cleanupIntervalMs??3e4,concurrentFragments:t.concurrentFragments??5,useAria2c:t.useAria2c,logger:t.logger},this.paths=E(t.cacheDir),this.cache=new b({cleanupIntervalMs:this.opts.cleanupIntervalMs}),this.cache.start(),this.ytdlp=new f({binaryPath:t.ytdlpBinaryPath,ffmpegPath:t.ffmpegPath,aria2cPath:t.aria2cPath,useAria2c:this.opts.useAria2c,concurrentFragments:this.opts.concurrentFragments,timeoutMs:t.ytdlpTimeoutMs??3e5,cookiesPath:t.cookiesPath,cookiesFromBrowser:t.cookiesFromBrowser})}generateRequestId(t="play"){return`${t}_${Date.now()}_${Math.random().toString(36).slice(2,8)}`}async search(t){return v(t)}getFromCache(t){return this.cache.get(t)}async preload(t,e){let r=g(t.url);if(!r)throw new Error("Invalid YouTube URL.");let i=t.durationSeconds>3600;t.durationSeconds>this.opts.maxPreloadDurationSeconds&&this.opts.logger?.warn?.(`Video too long for preload (${Math.floor(t.durationSeconds/60)}min). Will use direct download with reduced quality.`);let n={...t,url:r};this.cache.set(e,{metadata:n,audio:null,video:null,expiresAt:Date.now()+this.opts.ttlMs,loading:!0});let s=i?96:x(this.opts.preferredAudioKbps,B),a=this.preloadOne(e,"audio",r,s),c=i?[a]:[a,this.preloadOne(e,"video",r,x(this.opts.preferredVideoP,_))];i&&this.opts.logger?.info?.(`Long video detected (${Math.floor(t.durationSeconds/60)}min). Audio only mode (96kbps).`),await Promise.allSettled(c),this.cache.markLoading(e,!1)}async getOrDownload(t,e){let r=this.cache.get(t);if(!r)throw new Error("Request not found (cache miss).");let i=r[e];if(i?.path&&y.default.existsSync(i.path)&&i.size>0)return{metadata:r.metadata,file:i,direct:!1};let n=g(r.metadata.url);if(!n)throw new Error("Invalid YouTube URL.");let s=await this.downloadDirect(e,n);return{metadata:r.metadata,file:s,direct:!0}}async waitCache(t,e,r=8e3,i=500){let n=Date.now();for(;Date.now()-n<r;){let a=this.cache.get(t)?.[e];if(a?.path&&y.default.existsSync(a.path)&&a.size>0)return a;await new Promise(c=>setTimeout(c,i))}return null}cleanup(t){this.cache.delete(t)}async preloadOne(t,e,r,i){try{let n=z(`temp_${Date.now()}`),a=`${e}_${t}_${n}.${e==="audio"?"m4a":"mp4"}`,c=$.default.join(this.paths.cacheDir,a),l=e==="audio"?await this.ytdlp.getAudio(r,i,c):await this.ytdlp.getVideo(r,i,c),k=y.default.statSync(c).size,T;this.opts.preloadBuffer&&(T=await y.default.promises.readFile(c));let V={path:c,size:k,info:{quality:l.quality},buffer:T};this.cache.setFile(t,e,V),this.opts.logger?.debug?.(`preloaded ${e} ${k} bytes: ${a}`)}catch(n){this.opts.logger?.error?.(`preload ${e} failed`,n)}}async downloadDirect(t,e){let r=x(this.opts.preferredAudioKbps,B),i=x(this.opts.preferredVideoP,_),n=t==="audio"?"m4a":"mp4",s=z(`direct_${Date.now()}`),a=$.default.join(this.paths.cacheDir,`${t}_${s}.${n}`),c=t==="audio"?await this.ytdlp.getAudio(e,r,a):await this.ytdlp.getVideo(e,i,a),l=y.default.statSync(a);return{path:a,size:l.size,info:{quality:c.quality}}}};0&&(module.exports={PlayEngine,YtDlpClient,getYouTubeVideoId,normalizeYoutubeUrl,searchBest});
4
4
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/core/play-engine.ts","../src/core/cache.ts","../src/core/paths.ts","../src/core/ytdlp-client.ts","../src/core/youtube.ts"],"sourcesContent":["export type {\n PlayMetadata,\n CachedFile,\n PlayEngineOptions,\n MediaType,\n DownloadInfo,\n CacheEntry,\n} from \"./core/types.js\";\n\nexport { PlayEngine } from \"./core/play-engine.js\";\nexport {\n searchBest,\n normalizeYoutubeUrl,\n getYouTubeVideoId,\n} from \"./core/youtube.js\";\nexport { YtDlpClient } from \"./core/ytdlp-client.js\";\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type {\n CacheEntry,\n CachedFile,\n DownloadInfo,\n MediaType,\n PlayEngineOptions,\n PlayMetadata,\n} from \"./types.js\";\nimport { CacheStore } from \"./cache.js\";\nimport { resolvePaths } from \"./paths.js\";\nimport { YtDlpClient } from \"./ytdlp-client.js\";\nimport { normalizeYoutubeUrl, searchBest } from \"./youtube.js\";\n\nconst AUDIO_QUALITIES = [320, 256, 192, 128, 96, 64] as const;\nconst VIDEO_QUALITIES = [1080, 720, 480, 360] as const;\n\nfunction pickQuality<T extends number>(\n requested: T,\n available: readonly T[],\n): T {\n return (available as readonly number[]).includes(requested)\n ? requested\n : available[0];\n}\n\nfunction sanitizeFilename(filename: string): string {\n return (filename || \"\")\n .replace(/[\\\\/:*?\"<>|]/g, \"\")\n .replace(/[^\\w\\s-]/gi, \"\")\n .trim()\n .replace(/\\s+/g, \" \")\n .substring(0, 100);\n}\n\nexport class PlayEngine {\n private readonly opts: Required<\n Pick<\n PlayEngineOptions,\n | \"ttlMs\"\n | \"maxPreloadDurationSeconds\"\n | \"preferredAudioKbps\"\n | \"preferredVideoP\"\n | \"preloadBuffer\"\n | \"cleanupIntervalMs\"\n | \"concurrentFragments\"\n >\n > &\n Pick<PlayEngineOptions, \"logger\" | \"useAria2c\">;\n\n private readonly paths: { baseDir: string; cacheDir: string };\n private readonly cache: CacheStore;\n private readonly ytdlp: YtDlpClient;\n\n constructor(options: PlayEngineOptions = {}) {\n this.opts = {\n ttlMs: options.ttlMs ?? 3 * 60_000,\n maxPreloadDurationSeconds: options.maxPreloadDurationSeconds ?? 20 * 60,\n preferredAudioKbps: options.preferredAudioKbps ?? 128,\n preferredVideoP: options.preferredVideoP ?? 720,\n preloadBuffer: options.preloadBuffer ?? true,\n cleanupIntervalMs: options.cleanupIntervalMs ?? 30_000,\n concurrentFragments: options.concurrentFragments ?? 5,\n useAria2c: options.useAria2c,\n logger: options.logger,\n };\n\n this.paths = resolvePaths(options.cacheDir);\n this.cache = new CacheStore({\n cleanupIntervalMs: this.opts.cleanupIntervalMs,\n });\n this.cache.start();\n\n this.ytdlp = new YtDlpClient({\n binaryPath: options.ytdlpBinaryPath,\n ffmpegPath: options.ffmpegPath,\n useAria2c: this.opts.useAria2c,\n concurrentFragments: this.opts.concurrentFragments,\n timeoutMs: options.ytdlpTimeoutMs ?? 300_000, // 5min default\n });\n }\n\n generateRequestId(prefix = \"play\"): string {\n return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;\n }\n\n async search(query: string): Promise<PlayMetadata | null> {\n return searchBest(query);\n }\n\n getFromCache(requestId: string): CacheEntry | undefined {\n return this.cache.get(requestId);\n }\n\n async preload(metadata: PlayMetadata, requestId: string): Promise<void> {\n const normalized = normalizeYoutubeUrl(metadata.url);\n if (!normalized) throw new Error(\"Invalid YouTube URL.\");\n\n const isLongVideo = metadata.durationSeconds > 3600; // >1h\n\n if (metadata.durationSeconds > this.opts.maxPreloadDurationSeconds) {\n this.opts.logger?.warn?.(\n `Video too long for preload (${Math.floor(metadata.durationSeconds / 60)}min). Will use direct download with reduced quality.`,\n );\n }\n\n const normalizedMeta: PlayMetadata = { ...metadata, url: normalized };\n this.cache.set(requestId, {\n metadata: normalizedMeta,\n audio: null,\n video: null,\n expiresAt: Date.now() + this.opts.ttlMs,\n loading: true,\n });\n\n // Vídeos longos (>1h): áudio 96kbps, sem vídeo\n const audioKbps = isLongVideo\n ? 96\n : pickQuality(this.opts.preferredAudioKbps, AUDIO_QUALITIES);\n\n const audioTask = this.preloadOne(\n requestId,\n \"audio\",\n normalized,\n audioKbps,\n );\n\n // Só baixa vídeo se for menor que 1h\n const tasks = isLongVideo\n ? [audioTask]\n : [\n audioTask,\n this.preloadOne(\n requestId,\n \"video\",\n normalized,\n pickQuality(this.opts.preferredVideoP, VIDEO_QUALITIES),\n ),\n ];\n\n if (isLongVideo) {\n this.opts.logger?.info?.(\n `Long video detected (${Math.floor(metadata.durationSeconds / 60)}min). Audio only mode (96kbps).`,\n );\n }\n\n await Promise.allSettled(tasks);\n this.cache.markLoading(requestId, false);\n }\n\n async getOrDownload(\n requestId: string,\n type: MediaType,\n ): Promise<{ metadata: PlayMetadata; file: CachedFile; direct: boolean }> {\n const entry = this.cache.get(requestId);\n if (!entry) throw new Error(\"Request not found (cache miss).\");\n\n const cached = entry[type];\n if (cached?.path && fs.existsSync(cached.path) && cached.size > 0) {\n return { metadata: entry.metadata, file: cached, direct: false };\n }\n\n const normalized = normalizeYoutubeUrl(entry.metadata.url);\n if (!normalized) throw new Error(\"Invalid YouTube URL.\");\n\n const directFile = await this.downloadDirect(type, normalized);\n return { metadata: entry.metadata, file: directFile, direct: true };\n }\n\n async waitCache(\n requestId: string,\n type: MediaType,\n timeoutMs = 8_000,\n intervalMs = 500,\n ): Promise<CachedFile | null> {\n const started = Date.now();\n while (Date.now() - started < timeoutMs) {\n const entry = this.cache.get(requestId);\n const f = entry?.[type];\n if (f?.path && fs.existsSync(f.path) && f.size > 0) return f;\n await new Promise((r) => setTimeout(r, intervalMs));\n }\n return null;\n }\n\n cleanup(requestId: string): void {\n this.cache.delete(requestId);\n }\n\n private async preloadOne(\n requestId: string,\n type: MediaType,\n youtubeUrl: string,\n quality: number,\n ): Promise<void> {\n try {\n const safeTitle = sanitizeFilename(`temp_${Date.now()}`);\n const ext = type === \"audio\" ? \"m4a\" : \"mp4\"; // Mudou de mp3 para m4a\n const filename = `${type}_${requestId}_${safeTitle}.${ext}`;\n const filePath = path.join(this.paths.cacheDir, filename);\n\n const info: DownloadInfo =\n type === \"audio\"\n ? await this.ytdlp.getAudio(youtubeUrl, quality, filePath)\n : await this.ytdlp.getVideo(youtubeUrl, quality, filePath);\n\n const stats = fs.statSync(filePath);\n const size = stats.size;\n\n let buffer: Buffer | undefined;\n if (this.opts.preloadBuffer) {\n buffer = await fs.promises.readFile(filePath);\n }\n\n const cached: CachedFile = {\n path: filePath,\n size,\n info: { quality: info.quality },\n buffer,\n };\n\n this.cache.setFile(requestId, type, cached);\n this.opts.logger?.debug?.(`preloaded ${type} ${size} bytes: ${filename}`);\n } catch (err) {\n this.opts.logger?.error?.(`preload ${type} failed`, err);\n }\n }\n\n private async downloadDirect(\n type: MediaType,\n youtubeUrl: string,\n ): Promise<CachedFile> {\n const audioKbps = pickQuality(\n this.opts.preferredAudioKbps,\n AUDIO_QUALITIES,\n );\n const videoP = pickQuality(this.opts.preferredVideoP, VIDEO_QUALITIES);\n const ext = type === \"audio\" ? \"m4a\" : \"mp4\"; // Mudou de mp3 para m4a\n const safeTitle = sanitizeFilename(`direct_${Date.now()}`);\n const filePath = path.join(\n this.paths.cacheDir,\n `${type}_${safeTitle}.${ext}`,\n );\n\n const info =\n type === \"audio\"\n ? await this.ytdlp.getAudio(youtubeUrl, audioKbps, filePath)\n : await this.ytdlp.getVideo(youtubeUrl, videoP, filePath);\n\n const stats = fs.statSync(filePath);\n return {\n path: filePath,\n size: stats.size,\n info: { quality: info.quality },\n };\n }\n}\n","import fs from \"node:fs\";\n\nimport type { CacheEntry, MediaType } from \"./types.js\";\n\nexport class CacheStore {\n private readonly store = new Map<string, CacheEntry>();\n private cleanupTimer?: NodeJS.Timeout;\n\n constructor(\n private readonly opts: {\n cleanupIntervalMs: number;\n }\n ) {}\n\n get(requestId: string): CacheEntry | undefined {\n return this.store.get(requestId);\n }\n\n set(requestId: string, entry: CacheEntry): void {\n this.store.set(requestId, entry);\n }\n\n has(requestId: string): boolean {\n return this.store.has(requestId);\n }\n\n delete(requestId: string): void {\n this.cleanupEntry(requestId);\n this.store.delete(requestId);\n }\n\n markLoading(requestId: string, loading: boolean): void {\n const e = this.store.get(requestId);\n if (e) e.loading = loading;\n }\n\n setFile(\n requestId: string,\n type: MediaType,\n file: CacheEntry[MediaType]\n ): void {\n const e = this.store.get(requestId);\n if (!e) return;\n e[type] = file as any;\n }\n\n cleanupExpired(now = Date.now()): number {\n let removed = 0;\n for (const [requestId, entry] of this.store.entries()) {\n if (now > entry.expiresAt) {\n this.delete(requestId);\n removed++;\n }\n }\n return removed;\n }\n\n start(): void {\n if (this.cleanupTimer) return;\n\n this.cleanupTimer = setInterval(() => {\n this.cleanupExpired(Date.now());\n }, this.opts.cleanupIntervalMs);\n\n this.cleanupTimer.unref();\n }\n\n stop(): void {\n if (!this.cleanupTimer) return;\n clearInterval(this.cleanupTimer);\n this.cleanupTimer = undefined;\n }\n\n private cleanupEntry(requestId: string) {\n const entry = this.store.get(requestId);\n if (!entry) return;\n\n ([\"audio\", \"video\"] as const).forEach((type) => {\n const f = entry[type];\n if (f?.path && fs.existsSync(f.path)) {\n try {\n fs.unlinkSync(f.path);\n } catch {\n // ignore\n }\n }\n });\n }\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport os from \"node:os\";\n\nexport interface ResolvedPaths {\n baseDir: string;\n cacheDir: string;\n}\n\nexport function ensureDirSync(dirPath: string) {\n fs.mkdirSync(dirPath, { recursive: true, mode: 0o777 });\n\n try {\n fs.chmodSync(dirPath, 0o777);\n } catch {\n // ignore\n }\n\n fs.accessSync(dirPath, fs.constants.R_OK | fs.constants.W_OK);\n}\n\nexport function resolvePaths(cacheDir?: string): ResolvedPaths {\n const baseDir = cacheDir?.trim()\n ? cacheDir\n : path.join(os.tmpdir(), \"yt-play\");\n const resolvedBase = path.resolve(baseDir);\n\n const resolvedCache = path.join(resolvedBase);\n\n ensureDirSync(resolvedBase);\n ensureDirSync(resolvedCache);\n\n return {\n baseDir: resolvedBase,\n cacheDir: resolvedCache,\n };\n}\n","import { spawn } from \"node:child_process\";\nimport path from \"node:path\";\nimport fs from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport type { DownloadInfo } from \"./types.js\";\n\nlet __dirname: string;\ntry {\n // @ts-ignore\n __dirname = path.dirname(fileURLToPath(import.meta.url));\n} catch {\n // @ts-ignore\n __dirname = typeof __dirname !== \"undefined\" ? __dirname : process.cwd();\n}\n\nexport interface YtDlpClientOptions {\n binaryPath?: string;\n ffmpegPath?: string;\n aria2cPath?: string;\n timeoutMs?: number;\n useAria2c?: boolean;\n concurrentFragments?: number;\n}\n\ninterface YtDlpVideoInfo {\n id: string;\n title: string;\n uploader?: string;\n duration: number;\n thumbnail?: string;\n}\n\nexport class YtDlpClient {\n private readonly binaryPath: string;\n private readonly ffmpegPath?: string;\n private readonly aria2cPath?: string;\n private readonly timeoutMs: number;\n private readonly useAria2c: boolean;\n private readonly concurrentFragments: number;\n\n constructor(opts: YtDlpClientOptions = {}) {\n this.binaryPath = opts.binaryPath || this.detectYtDlp();\n this.ffmpegPath = opts.ffmpegPath;\n this.timeoutMs = opts.timeoutMs ?? 300_000;\n this.concurrentFragments = opts.concurrentFragments ?? 5;\n\n this.aria2cPath = opts.aria2cPath || this.detectAria2c();\n this.useAria2c = opts.useAria2c ?? !!this.aria2cPath;\n }\n\n private detectYtDlp(): string {\n const packageRoot = path.resolve(__dirname, \"../..\");\n const bundledPaths = [\n path.join(packageRoot, \"bin\", \"yt-dlp\"),\n path.join(packageRoot, \"bin\", \"yt-dlp.exe\"),\n ];\n\n for (const p of bundledPaths) {\n if (fs.existsSync(p)) {\n return p;\n }\n }\n\n try {\n const { execSync } = require(\"node:child_process\");\n const cmd =\n process.platform === \"win32\" ? \"where yt-dlp\" : \"which yt-dlp\";\n const result = execSync(cmd, { encoding: \"utf-8\" }).trim();\n if (result) return result.split(\"\\n\")[0];\n } catch {\n // yt-dlp not found\n }\n\n return \"yt-dlp\";\n }\n\n private detectAria2c(): string | undefined {\n const packageRoot = path.resolve(__dirname, \"../..\");\n const bundledPaths = [\n path.join(packageRoot, \"bin\", \"aria2c\"),\n path.join(packageRoot, \"bin\", \"aria2c.exe\"),\n ];\n\n for (const p of bundledPaths) {\n if (fs.existsSync(p)) {\n return p;\n }\n }\n\n try {\n const { execSync } = require(\"node:child_process\");\n const cmd =\n process.platform === \"win32\" ? \"where aria2c\" : \"which aria2c\";\n const result = execSync(cmd, { encoding: \"utf-8\" }).trim();\n if (result) return result.split(\"\\n\")[0];\n } catch {\n // aria2c not found\n }\n\n return undefined;\n }\n\n private async exec(args: string[]): Promise<string> {\n return new Promise((resolve, reject) => {\n const allArgs = this.ffmpegPath\n ? [\"--ffmpeg-location\", this.ffmpegPath, ...args]\n : args;\n\n const proc = spawn(this.binaryPath, allArgs, {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n let stdout = \"\";\n let stderr = \"\";\n\n proc.stdout.on(\"data\", (chunk) => {\n stdout += chunk.toString();\n });\n\n proc.stderr.on(\"data\", (chunk) => {\n stderr += chunk.toString();\n });\n\n const timer = setTimeout(() => {\n proc.kill(\"SIGKILL\");\n reject(new Error(`yt-dlp timeout after ${this.timeoutMs}ms`));\n }, this.timeoutMs);\n\n proc.on(\"close\", (code) => {\n clearTimeout(timer);\n if (code === 0) {\n resolve(stdout);\n } else {\n reject(\n new Error(\n `yt-dlp exited with code ${code}. stderr: ${stderr.slice(0, 500)}`,\n ),\n );\n }\n });\n\n proc.on(\"error\", (err) => {\n clearTimeout(timer);\n reject(err);\n });\n });\n }\n\n async getInfo(youtubeUrl: string): Promise<YtDlpVideoInfo> {\n const stdout = await this.exec([\n \"-J\",\n \"--no-warnings\",\n \"--no-playlist\",\n youtubeUrl,\n ]);\n const info = JSON.parse(stdout) as YtDlpVideoInfo;\n return info;\n }\n\n private buildOptimizationArgs(): string[] {\n const args: string[] = [\n \"--no-warnings\",\n \"--no-playlist\",\n \"--no-check-certificates\",\n \"--concurrent-fragments\",\n String(this.concurrentFragments),\n ];\n\n if (this.useAria2c && this.aria2cPath) {\n args.push(\"--downloader\", this.aria2cPath);\n args.push(\"--downloader-args\", \"aria2c:-x 16 -s 16 -k 1M\");\n }\n\n return args;\n }\n\n async getAudio(\n youtubeUrl: string,\n qualityKbps: number,\n outputPath: string,\n ): Promise<DownloadInfo> {\n const info = await this.getInfo(youtubeUrl);\n const format = \"bestaudio[ext=m4a]/bestaudio/best\";\n\n const args = [\n \"-f\",\n format,\n \"-o\",\n outputPath,\n ...this.buildOptimizationArgs(),\n youtubeUrl,\n ];\n\n await this.exec(args);\n\n if (!fs.existsSync(outputPath)) {\n throw new Error(`yt-dlp failed to create audio file: ${outputPath}`);\n }\n\n const duration = this.formatDuration(info.duration);\n\n return {\n title: info.title,\n author: info.uploader,\n duration,\n quality: `${qualityKbps}kbps m4a`,\n filename: path.basename(outputPath),\n downloadUrl: outputPath,\n };\n }\n\n async getVideo(\n youtubeUrl: string,\n qualityP: number,\n outputPath: string,\n ): Promise<DownloadInfo> {\n const info = await this.getInfo(youtubeUrl);\n const format = `bestvideo[height<=${qualityP}][ext=mp4]+bestaudio[ext=m4a]/best[height<=${qualityP}]`;\n\n const args = [\n \"-f\",\n format,\n \"--merge-output-format\",\n \"mp4\",\n \"-o\",\n outputPath,\n ...this.buildOptimizationArgs(),\n youtubeUrl,\n ];\n\n await this.exec(args);\n\n if (!fs.existsSync(outputPath)) {\n throw new Error(`yt-dlp failed to create video file: ${outputPath}`);\n }\n\n const duration = this.formatDuration(info.duration);\n\n return {\n title: info.title,\n author: info.uploader,\n duration,\n quality: `${qualityP}p`,\n filename: path.basename(outputPath),\n downloadUrl: outputPath,\n };\n }\n\n private mapAudioQuality(kbps: number): string {\n if (kbps >= 320) return \"0\";\n if (kbps >= 256) return \"2\";\n if (kbps >= 192) return \"3\";\n if (kbps >= 128) return \"5\";\n if (kbps >= 96) return \"7\";\n return \"9\";\n }\n\n private formatDuration(seconds: number): string {\n if (!seconds) return \"0:00\";\n const h = Math.floor(seconds / 3600);\n const m = Math.floor((seconds % 3600) / 60);\n const s = Math.floor(seconds % 60);\n if (h > 0) {\n return `${h}:${m.toString().padStart(2, \"0\")}:${s.toString().padStart(2, \"0\")}`;\n }\n return `${m}:${s.toString().padStart(2, \"0\")}`;\n }\n}\n","import yts from \"yt-search\";\n\nimport type { PlayMetadata } from \"./types.js\";\n\nexport function stripWeirdUrlWrappers(input: string): string {\n let s = (input || \"\").trim();\n const mdAll = [...s.matchAll(/\\[[^\\]]*\\]\\((https?:\\/\\/[^)\\s]+)\\)/gi)];\n if (mdAll.length > 0) return mdAll[0][1].trim();\n s = s.replace(/^<([^>]+)>$/, \"$1\").trim();\n s = s.replace(/^[\"'`](.*)[\"'`]$/, \"$1\").trim();\n\n return s;\n}\n\nexport function getYouTubeVideoId(input: string): string | null {\n const regex =\n /(?:https?:\\/\\/)?(?:www\\.)?(?:youtube\\.com\\/(?:[^\\/]+\\/.+\\/|(?:v|e(?:mbed)?)\\/|.*[?&]v=|shorts\\/)|youtu\\.be\\/)([^\"&?\\/\\s]{11})/i;\n\n const match = (input || \"\").match(regex);\n return match ? match[1] : null;\n}\n\nexport function normalizeYoutubeUrl(input: string): string | null {\n const cleaned0 = stripWeirdUrlWrappers(input);\n\n const firstUrl = cleaned0.match(/https?:\\/\\/[^\\s)]+/i)?.[0] ?? cleaned0;\n\n const id = getYouTubeVideoId(firstUrl);\n if (!id) return null;\n\n return `https://www.youtube.com/watch?v=${id}`;\n}\n\nexport async function searchBest(query: string): Promise<PlayMetadata | null> {\n const result = await yts(query);\n const v = result?.videos?.[0];\n if (!v) return null;\n\n const durationSeconds = v.duration?.seconds ?? 0;\n\n const normalizedUrl = normalizeYoutubeUrl(v.url) ?? v.url;\n\n return {\n title: v.title || \"Untitled\",\n author: v.author?.name || undefined,\n duration: v.duration?.timestamp || undefined,\n thumb: v.image || v.thumbnail || undefined,\n videoId: v.videoId,\n url: normalizedUrl,\n durationSeconds,\n };\n}\n"],"mappings":"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","names":["index_exports","__export","PlayEngine","YtDlpClient","getYouTubeVideoId","normalizeYoutubeUrl","searchBest","__toCommonJS","import_node_fs","import_node_path","import_node_fs","CacheStore","opts","requestId","entry","loading","e","type","file","now","removed","f","fs","import_node_fs","import_node_path","import_node_os","ensureDirSync","dirPath","fs","resolvePaths","cacheDir","baseDir","path","os","resolvedBase","resolvedCache","import_node_child_process","import_node_path","import_node_fs","import_node_url","import_meta","__dirname","path","YtDlpClient","opts","packageRoot","bundledPaths","p","fs","execSync","cmd","result","args","resolve","reject","allArgs","proc","stdout","stderr","chunk","timer","code","err","youtubeUrl","qualityKbps","outputPath","info","duration","qualityP","kbps","seconds","h","m","s","import_yt_search","stripWeirdUrlWrappers","input","s","mdAll","getYouTubeVideoId","regex","match","normalizeYoutubeUrl","cleaned0","firstUrl","id","searchBest","query","v","yts","durationSeconds","normalizedUrl","AUDIO_QUALITIES","VIDEO_QUALITIES","pickQuality","requested","available","sanitizeFilename","filename","PlayEngine","options","resolvePaths","CacheStore","YtDlpClient","prefix","query","searchBest","requestId","metadata","normalized","normalizeYoutubeUrl","isLongVideo","normalizedMeta","audioKbps","audioTask","tasks","type","entry","cached","fs","directFile","timeoutMs","intervalMs","started","f","r","youtubeUrl","quality","safeTitle","filePath","path","info","size","buffer","err","videoP","ext","stats"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/core/play-engine.ts","../src/core/cache.ts","../src/core/paths.ts","../src/core/ytdlp-client.ts","../src/core/youtube.ts"],"sourcesContent":["export type {\n PlayMetadata,\n CachedFile,\n PlayEngineOptions,\n MediaType,\n DownloadInfo,\n CacheEntry,\n} from \"./core/types.js\";\n\nexport { PlayEngine } from \"./core/play-engine.js\";\nexport {\n searchBest,\n normalizeYoutubeUrl,\n getYouTubeVideoId,\n} from \"./core/youtube.js\";\nexport { YtDlpClient } from \"./core/ytdlp-client.js\";\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type {\n CacheEntry,\n CachedFile,\n DownloadInfo,\n MediaType,\n PlayEngineOptions,\n PlayMetadata,\n} from \"./types.js\";\nimport { CacheStore } from \"./cache.js\";\nimport { resolvePaths } from \"./paths.js\";\nimport { YtDlpClient } from \"./ytdlp-client.js\";\nimport { normalizeYoutubeUrl, searchBest } from \"./youtube.js\";\n\nconst AUDIO_QUALITIES = [320, 256, 192, 128, 96, 64] as const;\nconst VIDEO_QUALITIES = [1080, 720, 480, 360] as const;\n\nfunction pickQuality<T extends number>(\n requested: T,\n available: readonly T[],\n): T {\n return (available as readonly number[]).includes(requested)\n ? requested\n : available[0];\n}\n\nfunction sanitizeFilename(filename: string): string {\n return (filename || \"\")\n .replace(/[\\\\/:*?\"<>|]/g, \"\")\n .replace(/[^\\w\\s-]/gi, \"\")\n .trim()\n .replace(/\\s+/g, \" \")\n .substring(0, 100);\n}\n\nexport class PlayEngine {\n private readonly opts: Required<\n Pick<\n PlayEngineOptions,\n | \"ttlMs\"\n | \"maxPreloadDurationSeconds\"\n | \"preferredAudioKbps\"\n | \"preferredVideoP\"\n | \"preloadBuffer\"\n | \"cleanupIntervalMs\"\n | \"concurrentFragments\"\n >\n > &\n Pick<PlayEngineOptions, \"useAria2c\" | \"logger\">;\n\n private readonly paths: { baseDir: string; cacheDir: string };\n readonly cache: CacheStore;\n private readonly ytdlp: YtDlpClient;\n\n constructor(options: PlayEngineOptions = {}) {\n this.opts = {\n ttlMs: options.ttlMs ?? 3 * 60_000,\n maxPreloadDurationSeconds: options.maxPreloadDurationSeconds ?? 20 * 60,\n preferredAudioKbps: options.preferredAudioKbps ?? 128,\n preferredVideoP: options.preferredVideoP ?? 720,\n preloadBuffer: options.preloadBuffer ?? true,\n cleanupIntervalMs: options.cleanupIntervalMs ?? 30_000,\n concurrentFragments: options.concurrentFragments ?? 5,\n useAria2c: options.useAria2c,\n logger: options.logger,\n };\n\n this.paths = resolvePaths(options.cacheDir);\n this.cache = new CacheStore({\n cleanupIntervalMs: this.opts.cleanupIntervalMs,\n });\n this.cache.start();\n\n this.ytdlp = new YtDlpClient({\n binaryPath: options.ytdlpBinaryPath,\n ffmpegPath: options.ffmpegPath,\n 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\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 }\n }\n\n private async downloadDirect(\n type: MediaType,\n youtubeUrl: string,\n ): Promise<CachedFile> {\n const audioKbps = pickQuality(\n this.opts.preferredAudioKbps,\n AUDIO_QUALITIES,\n );\n const videoP = pickQuality(this.opts.preferredVideoP, VIDEO_QUALITIES);\n const ext = type === \"audio\" ? \"m4a\" : \"mp4\";\n const safeTitle = sanitizeFilename(`direct_${Date.now()}`);\n const filePath = path.join(\n this.paths.cacheDir,\n `${type}_${safeTitle}.${ext}`,\n );\n\n const info =\n type === \"audio\"\n ? await this.ytdlp.getAudio(youtubeUrl, audioKbps, filePath)\n : await this.ytdlp.getVideo(youtubeUrl, videoP, filePath);\n\n const stats = fs.statSync(filePath);\n return {\n path: filePath,\n size: stats.size,\n info: { quality: info.quality },\n };\n }\n}\n","import fs from \"node:fs\";\n\nimport type { CacheEntry, MediaType } from \"./types.js\";\n\nexport class CacheStore {\n private readonly store = new Map<string, CacheEntry>();\n private cleanupTimer?: NodeJS.Timeout;\n\n constructor(\n private readonly opts: {\n cleanupIntervalMs: number;\n }\n ) {}\n\n get(requestId: string): CacheEntry | undefined {\n return this.store.get(requestId);\n }\n\n set(requestId: string, entry: CacheEntry): void {\n this.store.set(requestId, entry);\n }\n\n has(requestId: string): boolean {\n return this.store.has(requestId);\n }\n\n delete(requestId: string): void {\n this.cleanupEntry(requestId);\n this.store.delete(requestId);\n }\n\n markLoading(requestId: string, loading: boolean): void {\n const e = this.store.get(requestId);\n if (e) e.loading = loading;\n }\n\n setFile(\n requestId: string,\n type: MediaType,\n file: CacheEntry[MediaType]\n ): void {\n const e = this.store.get(requestId);\n if (!e) return;\n e[type] = file as any;\n }\n\n cleanupExpired(now = Date.now()): number {\n let removed = 0;\n for (const [requestId, entry] of this.store.entries()) {\n if (now > entry.expiresAt) {\n this.delete(requestId);\n removed++;\n }\n }\n return removed;\n }\n\n start(): void {\n if (this.cleanupTimer) return;\n\n this.cleanupTimer = setInterval(() => {\n this.cleanupExpired(Date.now());\n }, this.opts.cleanupIntervalMs);\n\n this.cleanupTimer.unref();\n }\n\n stop(): void {\n if (!this.cleanupTimer) return;\n clearInterval(this.cleanupTimer);\n this.cleanupTimer = undefined;\n }\n\n private cleanupEntry(requestId: string) {\n const entry = this.store.get(requestId);\n if (!entry) return;\n\n ([\"audio\", \"video\"] as const).forEach((type) => {\n const f = entry[type];\n if (f?.path && fs.existsSync(f.path)) {\n try {\n fs.unlinkSync(f.path);\n } catch {\n // ignore\n }\n }\n });\n }\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport os from \"node:os\";\n\nexport interface ResolvedPaths {\n baseDir: string;\n cacheDir: string;\n}\n\nexport function ensureDirSync(dirPath: string) {\n fs.mkdirSync(dirPath, { recursive: true, mode: 0o777 });\n\n try {\n fs.chmodSync(dirPath, 0o777);\n } catch {\n // ignore\n }\n\n fs.accessSync(dirPath, fs.constants.R_OK | fs.constants.W_OK);\n}\n\nexport function resolvePaths(cacheDir?: string): ResolvedPaths {\n const baseDir = cacheDir?.trim()\n ? cacheDir\n : path.join(os.tmpdir(), \"yt-play\");\n const resolvedBase = path.resolve(baseDir);\n\n const resolvedCache = path.join(resolvedBase);\n\n ensureDirSync(resolvedBase);\n ensureDirSync(resolvedCache);\n\n return {\n baseDir: resolvedBase,\n cacheDir: resolvedCache,\n };\n}\n","import { spawn } from \"node:child_process\";\nimport path from \"node:path\";\nimport fs from \"node:fs\";\nimport type { DownloadInfo } from \"./types.js\";\n\nlet __dirname: string;\ntry {\n // @ts-ignore\n __dirname = path.dirname(new URL(import.meta.url).pathname);\n} catch {\n // @ts-ignore\n __dirname = typeof __dirname !== \"undefined\" ? __dirname : process.cwd();\n}\n\nexport interface YtDlpClientOptions {\n binaryPath?: string;\n ffmpegPath?: string;\n aria2cPath?: string;\n timeoutMs?: number;\n useAria2c?: boolean;\n concurrentFragments?: number;\n cookiesPath?: string;\n cookiesFromBrowser?: string;\n}\n\ninterface YtDlpVideoInfo {\n id: string;\n title: string;\n uploader?: string;\n duration: number;\n thumbnail?: string;\n}\n\nexport class YtDlpClient {\n private readonly binaryPath: string;\n private readonly ffmpegPath?: string;\n private readonly aria2cPath?: string;\n private readonly timeoutMs: number;\n private readonly useAria2c: boolean;\n private readonly concurrentFragments: number;\n private readonly cookiesPath?: string;\n private readonly cookiesFromBrowser?: string;\n\n constructor(opts: YtDlpClientOptions = {}) {\n this.binaryPath = opts.binaryPath || this.detectYtDlp();\n this.ffmpegPath = opts.ffmpegPath;\n this.timeoutMs = opts.timeoutMs ?? 300_000;\n this.concurrentFragments = opts.concurrentFragments ?? 5;\n this.cookiesPath = opts.cookiesPath;\n this.cookiesFromBrowser = opts.cookiesFromBrowser;\n\n this.aria2cPath = opts.aria2cPath || this.detectAria2c();\n this.useAria2c = opts.useAria2c ?? !!this.aria2cPath;\n }\n\n private detectYtDlp(): string {\n const packageRoot = path.resolve(__dirname, \"../..\");\n const bundledPaths = [\n path.join(packageRoot, \"bin\", \"yt-dlp\"),\n path.join(packageRoot, \"bin\", \"yt-dlp.exe\"),\n ];\n\n for (const p of bundledPaths) {\n if (fs.existsSync(p)) {\n return p;\n }\n }\n\n try {\n const { execSync } = require(\"node:child_process\");\n const cmd =\n process.platform === \"win32\" ? \"where yt-dlp\" : \"which yt-dlp\";\n const result = execSync(cmd, { encoding: \"utf-8\" }).trim();\n if (result) return result.split(\"\\n\")[0];\n } catch {}\n\n return \"yt-dlp\";\n }\n\n private detectAria2c(): string | undefined {\n const packageRoot = path.resolve(__dirname, \"../..\");\n const bundledPaths = [\n path.join(packageRoot, \"bin\", \"aria2c\"),\n path.join(packageRoot, \"bin\", \"aria2c.exe\"),\n ];\n\n for (const p of bundledPaths) {\n if (fs.existsSync(p)) {\n return p;\n }\n }\n\n try {\n const { execSync } = require(\"node:child_process\");\n const cmd =\n process.platform === \"win32\" ? \"where aria2c\" : \"which aria2c\";\n const result = execSync(cmd, { encoding: \"utf-8\" }).trim();\n if (result) return result.split(\"\\n\")[0];\n } catch {}\n\n return undefined;\n }\n\n private async exec(args: string[]): Promise<string> {\n return new Promise((resolve, reject) => {\n let allArgs = [...args];\n\n if (this.ffmpegPath) {\n allArgs = [\"--ffmpeg-location\", this.ffmpegPath, ...allArgs];\n }\n\n if (this.cookiesPath && fs.existsSync(this.cookiesPath)) {\n allArgs = [\"--cookies\", this.cookiesPath, ...allArgs];\n }\n\n if (this.cookiesFromBrowser) {\n allArgs = [\n \"--cookies-from-browser\",\n this.cookiesFromBrowser,\n ...allArgs,\n ];\n }\n\n const proc = spawn(this.binaryPath, allArgs, {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n let stdout = \"\";\n let stderr = \"\";\n\n proc.stdout.on(\"data\", (chunk) => {\n stdout += chunk.toString();\n });\n\n proc.stderr.on(\"data\", (chunk) => {\n stderr += chunk.toString();\n });\n\n const timer = setTimeout(() => {\n proc.kill(\"SIGKILL\");\n reject(new Error(`yt-dlp timeout after ${this.timeoutMs}ms`));\n }, this.timeoutMs);\n\n proc.on(\"close\", (code) => {\n clearTimeout(timer);\n if (code === 0) {\n resolve(stdout);\n } else {\n reject(\n new Error(\n `yt-dlp exited with code ${code}. stderr: ${stderr.slice(0, 500)}`,\n ),\n );\n }\n });\n\n proc.on(\"error\", (err) => {\n clearTimeout(timer);\n reject(err);\n });\n });\n }\n\n async getInfo(youtubeUrl: string): Promise<YtDlpVideoInfo> {\n const stdout = await this.exec([\n \"-J\",\n \"--no-warnings\",\n \"--no-playlist\",\n youtubeUrl,\n ]);\n const info = JSON.parse(stdout) as YtDlpVideoInfo;\n return info;\n }\n\n private buildOptimizationArgs(): string[] {\n const args: string[] = [\n \"--no-warnings\",\n \"--no-playlist\",\n \"--no-check-certificates\",\n \"--concurrent-fragments\",\n String(this.concurrentFragments),\n ];\n\n if (this.useAria2c && this.aria2cPath) {\n args.push(\"--downloader\", this.aria2cPath);\n args.push(\"--downloader-args\", \"aria2c:-x 16 -s 16 -k 1M\");\n }\n\n return args;\n }\n\n async getAudio(\n youtubeUrl: string,\n qualityKbps: number,\n outputPath: string,\n ): Promise<DownloadInfo> {\n const info = await this.getInfo(youtubeUrl);\n const format = \"bestaudio[ext=m4a]/bestaudio/best\";\n\n const args = [\n \"-f\",\n format,\n \"-o\",\n outputPath,\n ...this.buildOptimizationArgs(),\n youtubeUrl,\n ];\n\n await this.exec(args);\n\n if (!fs.existsSync(outputPath)) {\n throw new Error(`yt-dlp failed to create audio file: ${outputPath}`);\n }\n\n const duration = this.formatDuration(info.duration);\n\n return {\n title: info.title,\n author: info.uploader,\n duration,\n quality: `${qualityKbps}kbps m4a`,\n filename: path.basename(outputPath),\n downloadUrl: outputPath,\n };\n }\n\n async getVideo(\n youtubeUrl: string,\n qualityP: number,\n outputPath: string,\n ): Promise<DownloadInfo> {\n const info = await this.getInfo(youtubeUrl);\n const format = `bestvideo[height<=${qualityP}][ext=mp4]+bestaudio[ext=m4a]/best[height<=${qualityP}]`;\n\n const args = [\n \"-f\",\n format,\n \"--merge-output-format\",\n \"mp4\",\n \"-o\",\n outputPath,\n ...this.buildOptimizationArgs(),\n youtubeUrl,\n ];\n\n await this.exec(args);\n\n if (!fs.existsSync(outputPath)) {\n throw new Error(`yt-dlp failed to create video file: ${outputPath}`);\n }\n\n const duration = this.formatDuration(info.duration);\n\n return {\n title: info.title,\n author: info.uploader,\n duration,\n quality: `${qualityP}p`,\n filename: path.basename(outputPath),\n downloadUrl: outputPath,\n };\n }\n\n private formatDuration(seconds: number): string {\n if (!seconds) return \"0:00\";\n const h = Math.floor(seconds / 3600);\n const m = Math.floor((seconds % 3600) / 60);\n const s = Math.floor(seconds % 60);\n if (h > 0) {\n return `${h}:${m.toString().padStart(2, \"0\")}:${s.toString().padStart(2, \"0\")}`;\n }\n return `${m}:${s.toString().padStart(2, \"0\")}`;\n }\n}\n","import yts from \"yt-search\";\n\nimport type { PlayMetadata } from \"./types.js\";\n\nexport function stripWeirdUrlWrappers(input: string): string {\n let s = (input || \"\").trim();\n const mdAll = [...s.matchAll(/\\[[^\\]]*\\]\\((https?:\\/\\/[^)\\s]+)\\)/gi)];\n if (mdAll.length > 0) return mdAll[0][1].trim();\n s = s.replace(/^<([^>]+)>$/, \"$1\").trim();\n s = s.replace(/^[\"'`](.*)[\"'`]$/, \"$1\").trim();\n\n return s;\n}\n\nexport function getYouTubeVideoId(input: string): string | null {\n const regex =\n /(?:https?:\\/\\/)?(?:www\\.)?(?:youtube\\.com\\/(?:[^\\/]+\\/.+\\/|(?:v|e(?:mbed)?)\\/|.*[?&]v=|shorts\\/)|youtu\\.be\\/)([^\"&?\\/\\s]{11})/i;\n\n const match = (input || \"\").match(regex);\n return match ? match[1] : null;\n}\n\nexport function normalizeYoutubeUrl(input: string): string | null {\n const cleaned0 = stripWeirdUrlWrappers(input);\n\n const firstUrl = cleaned0.match(/https?:\\/\\/[^\\s)]+/i)?.[0] ?? cleaned0;\n\n const id = getYouTubeVideoId(firstUrl);\n if (!id) return null;\n\n return `https://www.youtube.com/watch?v=${id}`;\n}\n\nexport async function searchBest(query: string): Promise<PlayMetadata | null> {\n const result = await yts(query);\n const v = result?.videos?.[0];\n if (!v) return null;\n\n const durationSeconds = v.duration?.seconds ?? 0;\n\n const normalizedUrl = normalizeYoutubeUrl(v.url) ?? v.url;\n\n return {\n title: v.title || \"Untitled\",\n author: v.author?.name || undefined,\n duration: v.duration?.timestamp || undefined,\n thumb: v.image || v.thumbnail || undefined,\n videoId: v.videoId,\n url: normalizedUrl,\n durationSeconds,\n };\n}\n"],"mappings":"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","names":["index_exports","__export","PlayEngine","YtDlpClient","getYouTubeVideoId","normalizeYoutubeUrl","searchBest","__toCommonJS","import_node_fs","import_node_path","import_node_fs","CacheStore","opts","requestId","entry","loading","e","type","file","now","removed","f","fs","import_node_fs","import_node_path","import_node_os","ensureDirSync","dirPath","fs","resolvePaths","cacheDir","baseDir","path","os","resolvedBase","resolvedCache","import_node_child_process","import_node_path","import_node_fs","import_meta","__dirname","path","YtDlpClient","opts","packageRoot","bundledPaths","p","fs","execSync","cmd","result","args","resolve","reject","allArgs","proc","stdout","stderr","chunk","timer","code","err","youtubeUrl","qualityKbps","outputPath","info","duration","qualityP","seconds","h","m","s","import_yt_search","stripWeirdUrlWrappers","input","s","mdAll","getYouTubeVideoId","regex","match","normalizeYoutubeUrl","cleaned0","firstUrl","id","searchBest","query","v","yts","durationSeconds","normalizedUrl","AUDIO_QUALITIES","VIDEO_QUALITIES","pickQuality","requested","available","sanitizeFilename","filename","PlayEngine","options","resolvePaths","CacheStore","YtDlpClient","prefix","query","searchBest","requestId","metadata","normalized","normalizeYoutubeUrl","isLongVideo","normalizedMeta","audioKbps","audioTask","tasks","type","entry","cached","fs","directFile","timeoutMs","intervalMs","started","f","r","youtubeUrl","quality","safeTitle","filePath","path","info","size","buffer","err","videoP","ext","stats"]}
package/dist/index.d.cts CHANGED
@@ -33,30 +33,51 @@ interface CacheEntry {
33
33
  }
34
34
  interface PlayEngineOptions {
35
35
  cacheDir?: string;
36
+ ytdlpBinaryPath?: string;
37
+ ffmpegPath?: string;
38
+ aria2cPath?: string;
39
+ ytdlpTimeoutMs?: number;
36
40
  ttlMs?: number;
37
41
  maxPreloadDurationSeconds?: number;
38
- preferredAudioKbps?: 320 | 256 | 192 | 128 | 96 | 64;
39
- preferredVideoP?: 1080 | 720 | 480 | 360;
42
+ preferredAudioKbps?: number;
43
+ preferredVideoP?: number;
40
44
  preloadBuffer?: boolean;
41
45
  cleanupIntervalMs?: number;
42
- ytdlpBinaryPath?: string;
43
- ffmpegPath?: string;
44
- aria2cPath?: string;
45
46
  useAria2c?: boolean;
46
47
  concurrentFragments?: number;
47
- ytdlpTimeoutMs?: number;
48
+ cookiesPath?: string;
49
+ cookiesFromBrowser?: string;
48
50
  logger?: {
51
+ debug?: (...args: any[]) => void;
49
52
  info?: (...args: any[]) => void;
50
53
  warn?: (...args: any[]) => void;
51
54
  error?: (...args: any[]) => void;
52
- debug?: (...args: any[]) => void;
53
55
  };
54
56
  }
55
57
 
58
+ declare class CacheStore {
59
+ private readonly opts;
60
+ private readonly store;
61
+ private cleanupTimer?;
62
+ constructor(opts: {
63
+ cleanupIntervalMs: number;
64
+ });
65
+ get(requestId: string): CacheEntry | undefined;
66
+ set(requestId: string, entry: CacheEntry): void;
67
+ has(requestId: string): boolean;
68
+ delete(requestId: string): void;
69
+ markLoading(requestId: string, loading: boolean): void;
70
+ setFile(requestId: string, type: MediaType, file: CacheEntry[MediaType]): void;
71
+ cleanupExpired(now?: number): number;
72
+ start(): void;
73
+ stop(): void;
74
+ private cleanupEntry;
75
+ }
76
+
56
77
  declare class PlayEngine {
57
78
  private readonly opts;
58
79
  private readonly paths;
59
- private readonly cache;
80
+ readonly cache: CacheStore;
60
81
  private readonly ytdlp;
61
82
  constructor(options?: PlayEngineOptions);
62
83
  generateRequestId(prefix?: string): string;
@@ -85,6 +106,8 @@ interface YtDlpClientOptions {
85
106
  timeoutMs?: number;
86
107
  useAria2c?: boolean;
87
108
  concurrentFragments?: number;
109
+ cookiesPath?: string;
110
+ cookiesFromBrowser?: string;
88
111
  }
89
112
  interface YtDlpVideoInfo {
90
113
  id: string;
@@ -100,6 +123,8 @@ declare class YtDlpClient {
100
123
  private readonly timeoutMs;
101
124
  private readonly useAria2c;
102
125
  private readonly concurrentFragments;
126
+ private readonly cookiesPath?;
127
+ private readonly cookiesFromBrowser?;
103
128
  constructor(opts?: YtDlpClientOptions);
104
129
  private detectYtDlp;
105
130
  private detectAria2c;
@@ -108,7 +133,6 @@ declare class YtDlpClient {
108
133
  private buildOptimizationArgs;
109
134
  getAudio(youtubeUrl: string, qualityKbps: number, outputPath: string): Promise<DownloadInfo>;
110
135
  getVideo(youtubeUrl: string, qualityP: number, outputPath: string): Promise<DownloadInfo>;
111
- private mapAudioQuality;
112
136
  private formatDuration;
113
137
  }
114
138
 
package/dist/index.d.ts CHANGED
@@ -33,30 +33,51 @@ interface CacheEntry {
33
33
  }
34
34
  interface PlayEngineOptions {
35
35
  cacheDir?: string;
36
+ ytdlpBinaryPath?: string;
37
+ ffmpegPath?: string;
38
+ aria2cPath?: string;
39
+ ytdlpTimeoutMs?: number;
36
40
  ttlMs?: number;
37
41
  maxPreloadDurationSeconds?: number;
38
- preferredAudioKbps?: 320 | 256 | 192 | 128 | 96 | 64;
39
- preferredVideoP?: 1080 | 720 | 480 | 360;
42
+ preferredAudioKbps?: number;
43
+ preferredVideoP?: number;
40
44
  preloadBuffer?: boolean;
41
45
  cleanupIntervalMs?: number;
42
- ytdlpBinaryPath?: string;
43
- ffmpegPath?: string;
44
- aria2cPath?: string;
45
46
  useAria2c?: boolean;
46
47
  concurrentFragments?: number;
47
- ytdlpTimeoutMs?: number;
48
+ cookiesPath?: string;
49
+ cookiesFromBrowser?: string;
48
50
  logger?: {
51
+ debug?: (...args: any[]) => void;
49
52
  info?: (...args: any[]) => void;
50
53
  warn?: (...args: any[]) => void;
51
54
  error?: (...args: any[]) => void;
52
- debug?: (...args: any[]) => void;
53
55
  };
54
56
  }
55
57
 
58
+ declare class CacheStore {
59
+ private readonly opts;
60
+ private readonly store;
61
+ private cleanupTimer?;
62
+ constructor(opts: {
63
+ cleanupIntervalMs: number;
64
+ });
65
+ get(requestId: string): CacheEntry | undefined;
66
+ set(requestId: string, entry: CacheEntry): void;
67
+ has(requestId: string): boolean;
68
+ delete(requestId: string): void;
69
+ markLoading(requestId: string, loading: boolean): void;
70
+ setFile(requestId: string, type: MediaType, file: CacheEntry[MediaType]): void;
71
+ cleanupExpired(now?: number): number;
72
+ start(): void;
73
+ stop(): void;
74
+ private cleanupEntry;
75
+ }
76
+
56
77
  declare class PlayEngine {
57
78
  private readonly opts;
58
79
  private readonly paths;
59
- private readonly cache;
80
+ readonly cache: CacheStore;
60
81
  private readonly ytdlp;
61
82
  constructor(options?: PlayEngineOptions);
62
83
  generateRequestId(prefix?: string): string;
@@ -85,6 +106,8 @@ interface YtDlpClientOptions {
85
106
  timeoutMs?: number;
86
107
  useAria2c?: boolean;
87
108
  concurrentFragments?: number;
109
+ cookiesPath?: string;
110
+ cookiesFromBrowser?: string;
88
111
  }
89
112
  interface YtDlpVideoInfo {
90
113
  id: string;
@@ -100,6 +123,8 @@ declare class YtDlpClient {
100
123
  private readonly timeoutMs;
101
124
  private readonly useAria2c;
102
125
  private readonly concurrentFragments;
126
+ private readonly cookiesPath?;
127
+ private readonly cookiesFromBrowser?;
103
128
  constructor(opts?: YtDlpClientOptions);
104
129
  private detectYtDlp;
105
130
  private detectAria2c;
@@ -108,7 +133,6 @@ declare class YtDlpClient {
108
133
  private buildOptimizationArgs;
109
134
  getAudio(youtubeUrl: string, qualityKbps: number, outputPath: string): Promise<DownloadInfo>;
110
135
  getVideo(youtubeUrl: string, qualityP: number, outputPath: string): Promise<DownloadInfo>;
111
- private mapAudioQuality;
112
136
  private formatDuration;
113
137
  }
114
138
 
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- var M=(n=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(n,{get:(t,e)=>(typeof require<"u"?require:t)[e]}):n)(function(n){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+n+'" is not supported')});import f from"fs";import I from"path";import S from"fs";var g=class{constructor(t){this.opts=t}store=new Map;cleanupTimer;get(t){return this.store.get(t)}set(t,e){this.store.set(t,e)}has(t){return this.store.has(t)}delete(t){this.cleanupEntry(t),this.store.delete(t)}markLoading(t,e){let r=this.store.get(t);r&&(r.loading=e)}setFile(t,e,r){let i=this.store.get(t);i&&(i[e]=r)}cleanupExpired(t=Date.now()){let e=0;for(let[r,i]of this.store.entries())t>i.expiresAt&&(this.delete(r),e++);return e}start(){this.cleanupTimer||(this.cleanupTimer=setInterval(()=>{this.cleanupExpired(Date.now())},this.opts.cleanupIntervalMs),this.cleanupTimer.unref())}stop(){this.cleanupTimer&&(clearInterval(this.cleanupTimer),this.cleanupTimer=void 0)}cleanupEntry(t){let e=this.store.get(t);e&&["audio","video"].forEach(r=>{let i=e[r];if(i?.path&&S.existsSync(i.path))try{S.unlinkSync(i.path)}catch{}})}};import p from"fs";import v from"path";import _ from"os";function $(n){p.mkdirSync(n,{recursive:!0,mode:511});try{p.chmodSync(n,511)}catch{}p.accessSync(n,p.constants.R_OK|p.constants.W_OK)}function T(n){let t=n?.trim()?n:v.join(_.tmpdir(),"yt-play"),e=v.resolve(t),r=v.join(e);return $(e),$(r),{baseDir:e,cacheDir:r}}import{spawn as z}from"child_process";import d from"path";import y from"fs";import{fileURLToPath as V}from"url";var u;try{u=d.dirname(V(import.meta.url))}catch{u=typeof u<"u"?u:process.cwd()}var h=class{binaryPath;ffmpegPath;aria2cPath;timeoutMs;useAria2c;concurrentFragments;constructor(t={}){this.binaryPath=t.binaryPath||this.detectYtDlp(),this.ffmpegPath=t.ffmpegPath,this.timeoutMs=t.timeoutMs??3e5,this.concurrentFragments=t.concurrentFragments??5,this.aria2cPath=t.aria2cPath||this.detectAria2c(),this.useAria2c=t.useAria2c??!!this.aria2cPath}detectYtDlp(){let t=d.resolve(u,"../.."),e=[d.join(t,"bin","yt-dlp"),d.join(t,"bin","yt-dlp.exe")];for(let r of e)if(y.existsSync(r))return r;try{let{execSync:r}=M("child_process"),i=process.platform==="win32"?"where yt-dlp":"which yt-dlp",a=r(i,{encoding:"utf-8"}).trim();if(a)return a.split(`
2
- `)[0]}catch{}return"yt-dlp"}detectAria2c(){let t=d.resolve(u,"../.."),e=[d.join(t,"bin","aria2c"),d.join(t,"bin","aria2c.exe")];for(let r of e)if(y.existsSync(r))return r;try{let{execSync:r}=M("child_process"),i=process.platform==="win32"?"where aria2c":"which aria2c",a=r(i,{encoding:"utf-8"}).trim();if(a)return a.split(`
3
- `)[0]}catch{}}async exec(t){return new Promise((e,r)=>{let i=this.ffmpegPath?["--ffmpeg-location",this.ffmpegPath,...t]:t,a=z(this.binaryPath,i,{stdio:["ignore","pipe","pipe"]}),s="",o="";a.stdout.on("data",l=>{s+=l.toString()}),a.stderr.on("data",l=>{o+=l.toString()});let c=setTimeout(()=>{a.kill("SIGKILL"),r(new Error(`yt-dlp timeout after ${this.timeoutMs}ms`))},this.timeoutMs);a.on("close",l=>{clearTimeout(c),l===0?e(s):r(new Error(`yt-dlp exited with code ${l}. stderr: ${o.slice(0,500)}`))}),a.on("error",l=>{clearTimeout(c),r(l)})})}async getInfo(t){let e=await this.exec(["-J","--no-warnings","--no-playlist",t]);return JSON.parse(e)}buildOptimizationArgs(){let t=["--no-warnings","--no-playlist","--no-check-certificates","--concurrent-fragments",String(this.concurrentFragments)];return this.useAria2c&&this.aria2cPath&&(t.push("--downloader",this.aria2cPath),t.push("--downloader-args","aria2c:-x 16 -s 16 -k 1M")),t}async getAudio(t,e,r){let i=await this.getInfo(t),s=["-f","bestaudio[ext=m4a]/bestaudio/best","-o",r,...this.buildOptimizationArgs(),t];if(await this.exec(s),!y.existsSync(r))throw new Error(`yt-dlp failed to create audio file: ${r}`);let o=this.formatDuration(i.duration);return{title:i.title,author:i.uploader,duration:o,quality:`${e}kbps m4a`,filename:d.basename(r),downloadUrl:r}}async getVideo(t,e,r){let i=await this.getInfo(t),s=["-f",`bestvideo[height<=${e}][ext=mp4]+bestaudio[ext=m4a]/best[height<=${e}]`,"--merge-output-format","mp4","-o",r,...this.buildOptimizationArgs(),t];if(await this.exec(s),!y.existsSync(r))throw new Error(`yt-dlp failed to create video file: ${r}`);let o=this.formatDuration(i.duration);return{title:i.title,author:i.uploader,duration:o,quality:`${e}p`,filename:d.basename(r),downloadUrl:r}}mapAudioQuality(t){return t>=320?"0":t>=256?"2":t>=192?"3":t>=128?"5":t>=96?"7":"9"}formatDuration(t){if(!t)return"0:00";let e=Math.floor(t/3600),r=Math.floor(t%3600/60),i=Math.floor(t%60);return e>0?`${e}:${r.toString().padStart(2,"0")}:${i.toString().padStart(2,"0")}`:`${r}:${i.toString().padStart(2,"0")}`}};import Y from"yt-search";function k(n){let t=(n||"").trim(),e=[...t.matchAll(/\[[^\]]*\]\((https?:\/\/[^)\s]+)\)/gi)];return e.length>0?e[0][1].trim():(t=t.replace(/^<([^>]+)>$/,"$1").trim(),t=t.replace(/^["'`](.*)["'`]$/,"$1").trim(),t)}function A(n){let t=/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=|shorts\/)|youtu\.be\/)([^"&?\/\s]{11})/i,e=(n||"").match(t);return e?e[1]:null}function m(n){let t=k(n),e=t.match(/https?:\/\/[^\s)]+/i)?.[0]??t,r=A(e);return r?`https://www.youtube.com/watch?v=${r}`:null}async function b(n){let e=(await Y(n))?.videos?.[0];if(!e)return null;let r=e.duration?.seconds??0,i=m(e.url)??e.url;return{title:e.title||"Untitled",author:e.author?.name||void 0,duration:e.duration?.timestamp||void 0,thumb:e.image||e.thumbnail||void 0,videoId:e.videoId,url:i,durationSeconds:r}}var E=[320,256,192,128,96,64],C=[1080,720,480,360];function w(n,t){return t.includes(n)?n:t[0]}function F(n){return(n||"").replace(/[\\/:*?"<>|]/g,"").replace(/[^\w\s-]/gi,"").trim().replace(/\s+/g," ").substring(0,100)}var P=class{opts;paths;cache;ytdlp;constructor(t={}){this.opts={ttlMs:t.ttlMs??3*6e4,maxPreloadDurationSeconds:t.maxPreloadDurationSeconds??1200,preferredAudioKbps:t.preferredAudioKbps??128,preferredVideoP:t.preferredVideoP??720,preloadBuffer:t.preloadBuffer??!0,cleanupIntervalMs:t.cleanupIntervalMs??3e4,concurrentFragments:t.concurrentFragments??5,useAria2c:t.useAria2c,logger:t.logger},this.paths=T(t.cacheDir),this.cache=new g({cleanupIntervalMs:this.opts.cleanupIntervalMs}),this.cache.start(),this.ytdlp=new h({binaryPath:t.ytdlpBinaryPath,ffmpegPath:t.ffmpegPath,useAria2c:this.opts.useAria2c,concurrentFragments:this.opts.concurrentFragments,timeoutMs:t.ytdlpTimeoutMs??3e5})}generateRequestId(t="play"){return`${t}_${Date.now()}_${Math.random().toString(36).slice(2,8)}`}async search(t){return b(t)}getFromCache(t){return this.cache.get(t)}async preload(t,e){let r=m(t.url);if(!r)throw new Error("Invalid YouTube URL.");let i=t.durationSeconds>3600;t.durationSeconds>this.opts.maxPreloadDurationSeconds&&this.opts.logger?.warn?.(`Video too long for preload (${Math.floor(t.durationSeconds/60)}min). Will use direct download with reduced quality.`);let a={...t,url:r};this.cache.set(e,{metadata:a,audio:null,video:null,expiresAt:Date.now()+this.opts.ttlMs,loading:!0});let s=i?96:w(this.opts.preferredAudioKbps,E),o=this.preloadOne(e,"audio",r,s),c=i?[o]:[o,this.preloadOne(e,"video",r,w(this.opts.preferredVideoP,C))];i&&this.opts.logger?.info?.(`Long video detected (${Math.floor(t.durationSeconds/60)}min). Audio only mode (96kbps).`),await Promise.allSettled(c),this.cache.markLoading(e,!1)}async getOrDownload(t,e){let r=this.cache.get(t);if(!r)throw new Error("Request not found (cache miss).");let i=r[e];if(i?.path&&f.existsSync(i.path)&&i.size>0)return{metadata:r.metadata,file:i,direct:!1};let a=m(r.metadata.url);if(!a)throw new Error("Invalid YouTube URL.");let s=await this.downloadDirect(e,a);return{metadata:r.metadata,file:s,direct:!0}}async waitCache(t,e,r=8e3,i=500){let a=Date.now();for(;Date.now()-a<r;){let o=this.cache.get(t)?.[e];if(o?.path&&f.existsSync(o.path)&&o.size>0)return o;await new Promise(c=>setTimeout(c,i))}return null}cleanup(t){this.cache.delete(t)}async preloadOne(t,e,r,i){try{let a=F(`temp_${Date.now()}`),o=`${e}_${t}_${a}.${e==="audio"?"m4a":"mp4"}`,c=I.join(this.paths.cacheDir,o),l=e==="audio"?await this.ytdlp.getAudio(r,i,c):await this.ytdlp.getVideo(r,i,c),x=f.statSync(c).size,D;this.opts.preloadBuffer&&(D=await f.promises.readFile(c));let O={path:c,size:x,info:{quality:l.quality},buffer:D};this.cache.setFile(t,e,O),this.opts.logger?.debug?.(`preloaded ${e} ${x} bytes: ${o}`)}catch(a){this.opts.logger?.error?.(`preload ${e} failed`,a)}}async downloadDirect(t,e){let r=w(this.opts.preferredAudioKbps,E),i=w(this.opts.preferredVideoP,C),a=t==="audio"?"m4a":"mp4",s=F(`direct_${Date.now()}`),o=I.join(this.paths.cacheDir,`${t}_${s}.${a}`),c=t==="audio"?await this.ytdlp.getAudio(e,r,o):await this.ytdlp.getVideo(e,i,o),l=f.statSync(o);return{path:o,size:l.size,info:{quality:c.quality}}}};export{P as PlayEngine,h as YtDlpClient,A as getYouTubeVideoId,m as normalizeYoutubeUrl,b as searchBest};
1
+ var M=(o=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(o,{get:(t,e)=>(typeof require<"u"?require:t)[e]}):o)(function(o){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+o+'" is not supported')});import g from"fs";import A from"path";import S from"fs";var y=class{constructor(t){this.opts=t}store=new Map;cleanupTimer;get(t){return this.store.get(t)}set(t,e){this.store.set(t,e)}has(t){return this.store.has(t)}delete(t){this.cleanupEntry(t),this.store.delete(t)}markLoading(t,e){let r=this.store.get(t);r&&(r.loading=e)}setFile(t,e,r){let i=this.store.get(t);i&&(i[e]=r)}cleanupExpired(t=Date.now()){let e=0;for(let[r,i]of this.store.entries())t>i.expiresAt&&(this.delete(r),e++);return e}start(){this.cleanupTimer||(this.cleanupTimer=setInterval(()=>{this.cleanupExpired(Date.now())},this.opts.cleanupIntervalMs),this.cleanupTimer.unref())}stop(){this.cleanupTimer&&(clearInterval(this.cleanupTimer),this.cleanupTimer=void 0)}cleanupEntry(t){let e=this.store.get(t);e&&["audio","video"].forEach(r=>{let i=e[r];if(i?.path&&S.existsSync(i.path))try{S.unlinkSync(i.path)}catch{}})}};import h from"fs";import b from"path";import O from"os";function $(o){h.mkdirSync(o,{recursive:!0,mode:511});try{h.chmodSync(o,511)}catch{}h.accessSync(o,h.constants.R_OK|h.constants.W_OK)}function k(o){let t=o?.trim()?o:b.join(O.tmpdir(),"yt-play"),e=b.resolve(t),r=b.join(e);return $(e),$(r),{baseDir:e,cacheDir:r}}import{spawn as B}from"child_process";import d from"path";import p from"fs";var u;try{u=d.dirname(new URL(import.meta.url).pathname)}catch{u=typeof u<"u"?u:process.cwd()}var m=class{binaryPath;ffmpegPath;aria2cPath;timeoutMs;useAria2c;concurrentFragments;cookiesPath;cookiesFromBrowser;constructor(t={}){this.binaryPath=t.binaryPath||this.detectYtDlp(),this.ffmpegPath=t.ffmpegPath,this.timeoutMs=t.timeoutMs??3e5,this.concurrentFragments=t.concurrentFragments??5,this.cookiesPath=t.cookiesPath,this.cookiesFromBrowser=t.cookiesFromBrowser,this.aria2cPath=t.aria2cPath||this.detectAria2c(),this.useAria2c=t.useAria2c??!!this.aria2cPath}detectYtDlp(){let t=d.resolve(u,"../.."),e=[d.join(t,"bin","yt-dlp"),d.join(t,"bin","yt-dlp.exe")];for(let r of e)if(p.existsSync(r))return r;try{let{execSync:r}=M("child_process"),i=process.platform==="win32"?"where yt-dlp":"which yt-dlp",n=r(i,{encoding:"utf-8"}).trim();if(n)return n.split(`
2
+ `)[0]}catch{}return"yt-dlp"}detectAria2c(){let t=d.resolve(u,"../.."),e=[d.join(t,"bin","aria2c"),d.join(t,"bin","aria2c.exe")];for(let r of e)if(p.existsSync(r))return r;try{let{execSync:r}=M("child_process"),i=process.platform==="win32"?"where aria2c":"which aria2c",n=r(i,{encoding:"utf-8"}).trim();if(n)return n.split(`
3
+ `)[0]}catch{}}async exec(t){return new Promise((e,r)=>{let i=[...t];this.ffmpegPath&&(i=["--ffmpeg-location",this.ffmpegPath,...i]),this.cookiesPath&&p.existsSync(this.cookiesPath)&&(i=["--cookies",this.cookiesPath,...i]),this.cookiesFromBrowser&&(i=["--cookies-from-browser",this.cookiesFromBrowser,...i]);let n=B(this.binaryPath,i,{stdio:["ignore","pipe","pipe"]}),s="",a="";n.stdout.on("data",l=>{s+=l.toString()}),n.stderr.on("data",l=>{a+=l.toString()});let c=setTimeout(()=>{n.kill("SIGKILL"),r(new Error(`yt-dlp timeout after ${this.timeoutMs}ms`))},this.timeoutMs);n.on("close",l=>{clearTimeout(c),l===0?e(s):r(new Error(`yt-dlp exited with code ${l}. stderr: ${a.slice(0,500)}`))}),n.on("error",l=>{clearTimeout(c),r(l)})})}async getInfo(t){let e=await this.exec(["-J","--no-warnings","--no-playlist",t]);return JSON.parse(e)}buildOptimizationArgs(){let t=["--no-warnings","--no-playlist","--no-check-certificates","--concurrent-fragments",String(this.concurrentFragments)];return this.useAria2c&&this.aria2cPath&&(t.push("--downloader",this.aria2cPath),t.push("--downloader-args","aria2c:-x 16 -s 16 -k 1M")),t}async getAudio(t,e,r){let i=await this.getInfo(t),s=["-f","bestaudio[ext=m4a]/bestaudio/best","-o",r,...this.buildOptimizationArgs(),t];if(await this.exec(s),!p.existsSync(r))throw new Error(`yt-dlp failed to create audio file: ${r}`);let a=this.formatDuration(i.duration);return{title:i.title,author:i.uploader,duration:a,quality:`${e}kbps m4a`,filename:d.basename(r),downloadUrl:r}}async getVideo(t,e,r){let i=await this.getInfo(t),s=["-f",`bestvideo[height<=${e}][ext=mp4]+bestaudio[ext=m4a]/best[height<=${e}]`,"--merge-output-format","mp4","-o",r,...this.buildOptimizationArgs(),t];if(await this.exec(s),!p.existsSync(r))throw new Error(`yt-dlp failed to create video file: ${r}`);let a=this.formatDuration(i.duration);return{title:i.title,author:i.uploader,duration:a,quality:`${e}p`,filename:d.basename(r),downloadUrl:r}}formatDuration(t){if(!t)return"0:00";let e=Math.floor(t/3600),r=Math.floor(t%3600/60),i=Math.floor(t%60);return e>0?`${e}:${r.toString().padStart(2,"0")}:${i.toString().padStart(2,"0")}`:`${r}:${i.toString().padStart(2,"0")}`}};import _ from"yt-search";function z(o){let t=(o||"").trim(),e=[...t.matchAll(/\[[^\]]*\]\((https?:\/\/[^)\s]+)\)/gi)];return e.length>0?e[0][1].trim():(t=t.replace(/^<([^>]+)>$/,"$1").trim(),t=t.replace(/^["'`](.*)["'`]$/,"$1").trim(),t)}function T(o){let t=/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=|shorts\/)|youtu\.be\/)([^"&?\/\s]{11})/i,e=(o||"").match(t);return e?e[1]:null}function f(o){let t=z(o),e=t.match(/https?:\/\/[^\s)]+/i)?.[0]??t,r=T(e);return r?`https://www.youtube.com/watch?v=${r}`:null}async function P(o){let e=(await _(o))?.videos?.[0];if(!e)return null;let r=e.duration?.seconds??0,i=f(e.url)??e.url;return{title:e.title||"Untitled",author:e.author?.name||void 0,duration:e.duration?.timestamp||void 0,thumb:e.image||e.thumbnail||void 0,videoId:e.videoId,url:i,durationSeconds:r}}var I=[320,256,192,128,96,64],F=[1080,720,480,360];function w(o,t){return t.includes(o)?o:t[0]}function E(o){return(o||"").replace(/[\\/:*?"<>|]/g,"").replace(/[^\w\s-]/gi,"").trim().replace(/\s+/g," ").substring(0,100)}var v=class{opts;paths;cache;ytdlp;constructor(t={}){this.opts={ttlMs:t.ttlMs??3*6e4,maxPreloadDurationSeconds:t.maxPreloadDurationSeconds??1200,preferredAudioKbps:t.preferredAudioKbps??128,preferredVideoP:t.preferredVideoP??720,preloadBuffer:t.preloadBuffer??!0,cleanupIntervalMs:t.cleanupIntervalMs??3e4,concurrentFragments:t.concurrentFragments??5,useAria2c:t.useAria2c,logger:t.logger},this.paths=k(t.cacheDir),this.cache=new y({cleanupIntervalMs:this.opts.cleanupIntervalMs}),this.cache.start(),this.ytdlp=new m({binaryPath:t.ytdlpBinaryPath,ffmpegPath:t.ffmpegPath,aria2cPath:t.aria2cPath,useAria2c:this.opts.useAria2c,concurrentFragments:this.opts.concurrentFragments,timeoutMs:t.ytdlpTimeoutMs??3e5,cookiesPath:t.cookiesPath,cookiesFromBrowser:t.cookiesFromBrowser})}generateRequestId(t="play"){return`${t}_${Date.now()}_${Math.random().toString(36).slice(2,8)}`}async search(t){return P(t)}getFromCache(t){return this.cache.get(t)}async preload(t,e){let r=f(t.url);if(!r)throw new Error("Invalid YouTube URL.");let i=t.durationSeconds>3600;t.durationSeconds>this.opts.maxPreloadDurationSeconds&&this.opts.logger?.warn?.(`Video too long for preload (${Math.floor(t.durationSeconds/60)}min). Will use direct download with reduced quality.`);let n={...t,url:r};this.cache.set(e,{metadata:n,audio:null,video:null,expiresAt:Date.now()+this.opts.ttlMs,loading:!0});let s=i?96:w(this.opts.preferredAudioKbps,I),a=this.preloadOne(e,"audio",r,s),c=i?[a]:[a,this.preloadOne(e,"video",r,w(this.opts.preferredVideoP,F))];i&&this.opts.logger?.info?.(`Long video detected (${Math.floor(t.durationSeconds/60)}min). Audio only mode (96kbps).`),await Promise.allSettled(c),this.cache.markLoading(e,!1)}async getOrDownload(t,e){let r=this.cache.get(t);if(!r)throw new Error("Request not found (cache miss).");let i=r[e];if(i?.path&&g.existsSync(i.path)&&i.size>0)return{metadata:r.metadata,file:i,direct:!1};let n=f(r.metadata.url);if(!n)throw new Error("Invalid YouTube URL.");let s=await this.downloadDirect(e,n);return{metadata:r.metadata,file:s,direct:!0}}async waitCache(t,e,r=8e3,i=500){let n=Date.now();for(;Date.now()-n<r;){let a=this.cache.get(t)?.[e];if(a?.path&&g.existsSync(a.path)&&a.size>0)return a;await new Promise(c=>setTimeout(c,i))}return null}cleanup(t){this.cache.delete(t)}async preloadOne(t,e,r,i){try{let n=E(`temp_${Date.now()}`),a=`${e}_${t}_${n}.${e==="audio"?"m4a":"mp4"}`,c=A.join(this.paths.cacheDir,a),l=e==="audio"?await this.ytdlp.getAudio(r,i,c):await this.ytdlp.getVideo(r,i,c),x=g.statSync(c).size,D;this.opts.preloadBuffer&&(D=await g.promises.readFile(c));let C={path:c,size:x,info:{quality:l.quality},buffer:D};this.cache.setFile(t,e,C),this.opts.logger?.debug?.(`preloaded ${e} ${x} bytes: ${a}`)}catch(n){this.opts.logger?.error?.(`preload ${e} failed`,n)}}async downloadDirect(t,e){let r=w(this.opts.preferredAudioKbps,I),i=w(this.opts.preferredVideoP,F),n=t==="audio"?"m4a":"mp4",s=E(`direct_${Date.now()}`),a=A.join(this.paths.cacheDir,`${t}_${s}.${n}`),c=t==="audio"?await this.ytdlp.getAudio(e,r,a):await this.ytdlp.getVideo(e,i,a),l=g.statSync(a);return{path:a,size:l.size,info:{quality:c.quality}}}};export{v as PlayEngine,m as YtDlpClient,T as getYouTubeVideoId,f as normalizeYoutubeUrl,P as searchBest};
4
4
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core/play-engine.ts","../src/core/cache.ts","../src/core/paths.ts","../src/core/ytdlp-client.ts","../src/core/youtube.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type {\n CacheEntry,\n CachedFile,\n DownloadInfo,\n MediaType,\n PlayEngineOptions,\n PlayMetadata,\n} from \"./types.js\";\nimport { CacheStore } from \"./cache.js\";\nimport { resolvePaths } from \"./paths.js\";\nimport { YtDlpClient } from \"./ytdlp-client.js\";\nimport { normalizeYoutubeUrl, searchBest } from \"./youtube.js\";\n\nconst AUDIO_QUALITIES = [320, 256, 192, 128, 96, 64] as const;\nconst VIDEO_QUALITIES = [1080, 720, 480, 360] as const;\n\nfunction pickQuality<T extends number>(\n requested: T,\n available: readonly T[],\n): T {\n return (available as readonly number[]).includes(requested)\n ? requested\n : available[0];\n}\n\nfunction sanitizeFilename(filename: string): string {\n return (filename || \"\")\n .replace(/[\\\\/:*?\"<>|]/g, \"\")\n .replace(/[^\\w\\s-]/gi, \"\")\n .trim()\n .replace(/\\s+/g, \" \")\n .substring(0, 100);\n}\n\nexport class PlayEngine {\n private readonly opts: Required<\n Pick<\n PlayEngineOptions,\n | \"ttlMs\"\n | \"maxPreloadDurationSeconds\"\n | \"preferredAudioKbps\"\n | \"preferredVideoP\"\n | \"preloadBuffer\"\n | \"cleanupIntervalMs\"\n | \"concurrentFragments\"\n >\n > &\n Pick<PlayEngineOptions, \"logger\" | \"useAria2c\">;\n\n private readonly paths: { baseDir: string; cacheDir: string };\n private readonly cache: CacheStore;\n private readonly ytdlp: YtDlpClient;\n\n constructor(options: PlayEngineOptions = {}) {\n this.opts = {\n ttlMs: options.ttlMs ?? 3 * 60_000,\n maxPreloadDurationSeconds: options.maxPreloadDurationSeconds ?? 20 * 60,\n preferredAudioKbps: options.preferredAudioKbps ?? 128,\n preferredVideoP: options.preferredVideoP ?? 720,\n preloadBuffer: options.preloadBuffer ?? true,\n cleanupIntervalMs: options.cleanupIntervalMs ?? 30_000,\n concurrentFragments: options.concurrentFragments ?? 5,\n useAria2c: options.useAria2c,\n logger: options.logger,\n };\n\n this.paths = resolvePaths(options.cacheDir);\n this.cache = new CacheStore({\n cleanupIntervalMs: this.opts.cleanupIntervalMs,\n });\n this.cache.start();\n\n this.ytdlp = new YtDlpClient({\n binaryPath: options.ytdlpBinaryPath,\n ffmpegPath: options.ffmpegPath,\n useAria2c: this.opts.useAria2c,\n concurrentFragments: this.opts.concurrentFragments,\n timeoutMs: options.ytdlpTimeoutMs ?? 300_000, // 5min default\n });\n }\n\n generateRequestId(prefix = \"play\"): string {\n return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;\n }\n\n async search(query: string): Promise<PlayMetadata | null> {\n return searchBest(query);\n }\n\n getFromCache(requestId: string): CacheEntry | undefined {\n return this.cache.get(requestId);\n }\n\n async preload(metadata: PlayMetadata, requestId: string): Promise<void> {\n const normalized = normalizeYoutubeUrl(metadata.url);\n if (!normalized) throw new Error(\"Invalid YouTube URL.\");\n\n const isLongVideo = metadata.durationSeconds > 3600; // >1h\n\n if (metadata.durationSeconds > this.opts.maxPreloadDurationSeconds) {\n this.opts.logger?.warn?.(\n `Video too long for preload (${Math.floor(metadata.durationSeconds / 60)}min). Will use direct download with reduced quality.`,\n );\n }\n\n const normalizedMeta: PlayMetadata = { ...metadata, url: normalized };\n this.cache.set(requestId, {\n metadata: normalizedMeta,\n audio: null,\n video: null,\n expiresAt: Date.now() + this.opts.ttlMs,\n loading: true,\n });\n\n // Vídeos longos (>1h): áudio 96kbps, sem vídeo\n const audioKbps = isLongVideo\n ? 96\n : pickQuality(this.opts.preferredAudioKbps, AUDIO_QUALITIES);\n\n const audioTask = this.preloadOne(\n requestId,\n \"audio\",\n normalized,\n audioKbps,\n );\n\n // Só baixa vídeo se for menor que 1h\n const tasks = isLongVideo\n ? [audioTask]\n : [\n audioTask,\n this.preloadOne(\n requestId,\n \"video\",\n normalized,\n pickQuality(this.opts.preferredVideoP, VIDEO_QUALITIES),\n ),\n ];\n\n if (isLongVideo) {\n this.opts.logger?.info?.(\n `Long video detected (${Math.floor(metadata.durationSeconds / 60)}min). Audio only mode (96kbps).`,\n );\n }\n\n await Promise.allSettled(tasks);\n this.cache.markLoading(requestId, false);\n }\n\n async getOrDownload(\n requestId: string,\n type: MediaType,\n ): Promise<{ metadata: PlayMetadata; file: CachedFile; direct: boolean }> {\n const entry = this.cache.get(requestId);\n if (!entry) throw new Error(\"Request not found (cache miss).\");\n\n const cached = entry[type];\n if (cached?.path && fs.existsSync(cached.path) && cached.size > 0) {\n return { metadata: entry.metadata, file: cached, direct: false };\n }\n\n const normalized = normalizeYoutubeUrl(entry.metadata.url);\n if (!normalized) throw new Error(\"Invalid YouTube URL.\");\n\n const directFile = await this.downloadDirect(type, normalized);\n return { metadata: entry.metadata, file: directFile, direct: true };\n }\n\n async waitCache(\n requestId: string,\n type: MediaType,\n timeoutMs = 8_000,\n intervalMs = 500,\n ): Promise<CachedFile | null> {\n const started = Date.now();\n while (Date.now() - started < timeoutMs) {\n const entry = this.cache.get(requestId);\n const f = entry?.[type];\n if (f?.path && fs.existsSync(f.path) && f.size > 0) return f;\n await new Promise((r) => setTimeout(r, intervalMs));\n }\n return null;\n }\n\n cleanup(requestId: string): void {\n this.cache.delete(requestId);\n }\n\n private async preloadOne(\n requestId: string,\n type: MediaType,\n youtubeUrl: string,\n quality: number,\n ): Promise<void> {\n try {\n const safeTitle = sanitizeFilename(`temp_${Date.now()}`);\n const ext = type === \"audio\" ? \"m4a\" : \"mp4\"; // Mudou de mp3 para m4a\n const filename = `${type}_${requestId}_${safeTitle}.${ext}`;\n const filePath = path.join(this.paths.cacheDir, filename);\n\n const info: DownloadInfo =\n type === \"audio\"\n ? await this.ytdlp.getAudio(youtubeUrl, quality, filePath)\n : await this.ytdlp.getVideo(youtubeUrl, quality, filePath);\n\n const stats = fs.statSync(filePath);\n const size = stats.size;\n\n let buffer: Buffer | undefined;\n if (this.opts.preloadBuffer) {\n buffer = await fs.promises.readFile(filePath);\n }\n\n const cached: CachedFile = {\n path: filePath,\n size,\n info: { quality: info.quality },\n buffer,\n };\n\n this.cache.setFile(requestId, type, cached);\n this.opts.logger?.debug?.(`preloaded ${type} ${size} bytes: ${filename}`);\n } catch (err) {\n this.opts.logger?.error?.(`preload ${type} failed`, err);\n }\n }\n\n private async downloadDirect(\n type: MediaType,\n youtubeUrl: string,\n ): Promise<CachedFile> {\n const audioKbps = pickQuality(\n this.opts.preferredAudioKbps,\n AUDIO_QUALITIES,\n );\n const videoP = pickQuality(this.opts.preferredVideoP, VIDEO_QUALITIES);\n const ext = type === \"audio\" ? \"m4a\" : \"mp4\"; // Mudou de mp3 para m4a\n const safeTitle = sanitizeFilename(`direct_${Date.now()}`);\n const filePath = path.join(\n this.paths.cacheDir,\n `${type}_${safeTitle}.${ext}`,\n );\n\n const info =\n type === \"audio\"\n ? await this.ytdlp.getAudio(youtubeUrl, audioKbps, filePath)\n : await this.ytdlp.getVideo(youtubeUrl, videoP, filePath);\n\n const stats = fs.statSync(filePath);\n return {\n path: filePath,\n size: stats.size,\n info: { quality: info.quality },\n };\n }\n}\n","import fs from \"node:fs\";\n\nimport type { CacheEntry, MediaType } from \"./types.js\";\n\nexport class CacheStore {\n private readonly store = new Map<string, CacheEntry>();\n private cleanupTimer?: NodeJS.Timeout;\n\n constructor(\n private readonly opts: {\n cleanupIntervalMs: number;\n }\n ) {}\n\n get(requestId: string): CacheEntry | undefined {\n return this.store.get(requestId);\n }\n\n set(requestId: string, entry: CacheEntry): void {\n this.store.set(requestId, entry);\n }\n\n has(requestId: string): boolean {\n return this.store.has(requestId);\n }\n\n delete(requestId: string): void {\n this.cleanupEntry(requestId);\n this.store.delete(requestId);\n }\n\n markLoading(requestId: string, loading: boolean): void {\n const e = this.store.get(requestId);\n if (e) e.loading = loading;\n }\n\n setFile(\n requestId: string,\n type: MediaType,\n file: CacheEntry[MediaType]\n ): void {\n const e = this.store.get(requestId);\n if (!e) return;\n e[type] = file as any;\n }\n\n cleanupExpired(now = Date.now()): number {\n let removed = 0;\n for (const [requestId, entry] of this.store.entries()) {\n if (now > entry.expiresAt) {\n this.delete(requestId);\n removed++;\n }\n }\n return removed;\n }\n\n start(): void {\n if (this.cleanupTimer) return;\n\n this.cleanupTimer = setInterval(() => {\n this.cleanupExpired(Date.now());\n }, this.opts.cleanupIntervalMs);\n\n this.cleanupTimer.unref();\n }\n\n stop(): void {\n if (!this.cleanupTimer) return;\n clearInterval(this.cleanupTimer);\n this.cleanupTimer = undefined;\n }\n\n private cleanupEntry(requestId: string) {\n const entry = this.store.get(requestId);\n if (!entry) return;\n\n ([\"audio\", \"video\"] as const).forEach((type) => {\n const f = entry[type];\n if (f?.path && fs.existsSync(f.path)) {\n try {\n fs.unlinkSync(f.path);\n } catch {\n // ignore\n }\n }\n });\n }\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport os from \"node:os\";\n\nexport interface ResolvedPaths {\n baseDir: string;\n cacheDir: string;\n}\n\nexport function ensureDirSync(dirPath: string) {\n fs.mkdirSync(dirPath, { recursive: true, mode: 0o777 });\n\n try {\n fs.chmodSync(dirPath, 0o777);\n } catch {\n // ignore\n }\n\n fs.accessSync(dirPath, fs.constants.R_OK | fs.constants.W_OK);\n}\n\nexport function resolvePaths(cacheDir?: string): ResolvedPaths {\n const baseDir = cacheDir?.trim()\n ? cacheDir\n : path.join(os.tmpdir(), \"yt-play\");\n const resolvedBase = path.resolve(baseDir);\n\n const resolvedCache = path.join(resolvedBase);\n\n ensureDirSync(resolvedBase);\n ensureDirSync(resolvedCache);\n\n return {\n baseDir: resolvedBase,\n cacheDir: resolvedCache,\n };\n}\n","import { spawn } from \"node:child_process\";\nimport path from \"node:path\";\nimport fs from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport type { DownloadInfo } from \"./types.js\";\n\nlet __dirname: string;\ntry {\n // @ts-ignore\n __dirname = path.dirname(fileURLToPath(import.meta.url));\n} catch {\n // @ts-ignore\n __dirname = typeof __dirname !== \"undefined\" ? __dirname : process.cwd();\n}\n\nexport interface YtDlpClientOptions {\n binaryPath?: string;\n ffmpegPath?: string;\n aria2cPath?: string;\n timeoutMs?: number;\n useAria2c?: boolean;\n concurrentFragments?: number;\n}\n\ninterface YtDlpVideoInfo {\n id: string;\n title: string;\n uploader?: string;\n duration: number;\n thumbnail?: string;\n}\n\nexport class YtDlpClient {\n private readonly binaryPath: string;\n private readonly ffmpegPath?: string;\n private readonly aria2cPath?: string;\n private readonly timeoutMs: number;\n private readonly useAria2c: boolean;\n private readonly concurrentFragments: number;\n\n constructor(opts: YtDlpClientOptions = {}) {\n this.binaryPath = opts.binaryPath || this.detectYtDlp();\n this.ffmpegPath = opts.ffmpegPath;\n this.timeoutMs = opts.timeoutMs ?? 300_000;\n this.concurrentFragments = opts.concurrentFragments ?? 5;\n\n this.aria2cPath = opts.aria2cPath || this.detectAria2c();\n this.useAria2c = opts.useAria2c ?? !!this.aria2cPath;\n }\n\n private detectYtDlp(): string {\n const packageRoot = path.resolve(__dirname, \"../..\");\n const bundledPaths = [\n path.join(packageRoot, \"bin\", \"yt-dlp\"),\n path.join(packageRoot, \"bin\", \"yt-dlp.exe\"),\n ];\n\n for (const p of bundledPaths) {\n if (fs.existsSync(p)) {\n return p;\n }\n }\n\n try {\n const { execSync } = require(\"node:child_process\");\n const cmd =\n process.platform === \"win32\" ? \"where yt-dlp\" : \"which yt-dlp\";\n const result = execSync(cmd, { encoding: \"utf-8\" }).trim();\n if (result) return result.split(\"\\n\")[0];\n } catch {\n // yt-dlp not found\n }\n\n return \"yt-dlp\";\n }\n\n private detectAria2c(): string | undefined {\n const packageRoot = path.resolve(__dirname, \"../..\");\n const bundledPaths = [\n path.join(packageRoot, \"bin\", \"aria2c\"),\n path.join(packageRoot, \"bin\", \"aria2c.exe\"),\n ];\n\n for (const p of bundledPaths) {\n if (fs.existsSync(p)) {\n return p;\n }\n }\n\n try {\n const { execSync } = require(\"node:child_process\");\n const cmd =\n process.platform === \"win32\" ? \"where aria2c\" : \"which aria2c\";\n const result = execSync(cmd, { encoding: \"utf-8\" }).trim();\n if (result) return result.split(\"\\n\")[0];\n } catch {\n // aria2c not found\n }\n\n return undefined;\n }\n\n private async exec(args: string[]): Promise<string> {\n return new Promise((resolve, reject) => {\n const allArgs = this.ffmpegPath\n ? [\"--ffmpeg-location\", this.ffmpegPath, ...args]\n : args;\n\n const proc = spawn(this.binaryPath, allArgs, {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n let stdout = \"\";\n let stderr = \"\";\n\n proc.stdout.on(\"data\", (chunk) => {\n stdout += chunk.toString();\n });\n\n proc.stderr.on(\"data\", (chunk) => {\n stderr += chunk.toString();\n });\n\n const timer = setTimeout(() => {\n proc.kill(\"SIGKILL\");\n reject(new Error(`yt-dlp timeout after ${this.timeoutMs}ms`));\n }, this.timeoutMs);\n\n proc.on(\"close\", (code) => {\n clearTimeout(timer);\n if (code === 0) {\n resolve(stdout);\n } else {\n reject(\n new Error(\n `yt-dlp exited with code ${code}. stderr: ${stderr.slice(0, 500)}`,\n ),\n );\n }\n });\n\n proc.on(\"error\", (err) => {\n clearTimeout(timer);\n reject(err);\n });\n });\n }\n\n async getInfo(youtubeUrl: string): Promise<YtDlpVideoInfo> {\n const stdout = await this.exec([\n \"-J\",\n \"--no-warnings\",\n \"--no-playlist\",\n youtubeUrl,\n ]);\n const info = JSON.parse(stdout) as YtDlpVideoInfo;\n return info;\n }\n\n private buildOptimizationArgs(): string[] {\n const args: string[] = [\n \"--no-warnings\",\n \"--no-playlist\",\n \"--no-check-certificates\",\n \"--concurrent-fragments\",\n String(this.concurrentFragments),\n ];\n\n if (this.useAria2c && this.aria2cPath) {\n args.push(\"--downloader\", this.aria2cPath);\n args.push(\"--downloader-args\", \"aria2c:-x 16 -s 16 -k 1M\");\n }\n\n return args;\n }\n\n async getAudio(\n youtubeUrl: string,\n qualityKbps: number,\n outputPath: string,\n ): Promise<DownloadInfo> {\n const info = await this.getInfo(youtubeUrl);\n const format = \"bestaudio[ext=m4a]/bestaudio/best\";\n\n const args = [\n \"-f\",\n format,\n \"-o\",\n outputPath,\n ...this.buildOptimizationArgs(),\n youtubeUrl,\n ];\n\n await this.exec(args);\n\n if (!fs.existsSync(outputPath)) {\n throw new Error(`yt-dlp failed to create audio file: ${outputPath}`);\n }\n\n const duration = this.formatDuration(info.duration);\n\n return {\n title: info.title,\n author: info.uploader,\n duration,\n quality: `${qualityKbps}kbps m4a`,\n filename: path.basename(outputPath),\n downloadUrl: outputPath,\n };\n }\n\n async getVideo(\n youtubeUrl: string,\n qualityP: number,\n outputPath: string,\n ): Promise<DownloadInfo> {\n const info = await this.getInfo(youtubeUrl);\n const format = `bestvideo[height<=${qualityP}][ext=mp4]+bestaudio[ext=m4a]/best[height<=${qualityP}]`;\n\n const args = [\n \"-f\",\n format,\n \"--merge-output-format\",\n \"mp4\",\n \"-o\",\n outputPath,\n ...this.buildOptimizationArgs(),\n youtubeUrl,\n ];\n\n await this.exec(args);\n\n if (!fs.existsSync(outputPath)) {\n throw new Error(`yt-dlp failed to create video file: ${outputPath}`);\n }\n\n const duration = this.formatDuration(info.duration);\n\n return {\n title: info.title,\n author: info.uploader,\n duration,\n quality: `${qualityP}p`,\n filename: path.basename(outputPath),\n downloadUrl: outputPath,\n };\n }\n\n private mapAudioQuality(kbps: number): string {\n if (kbps >= 320) return \"0\";\n if (kbps >= 256) return \"2\";\n if (kbps >= 192) return \"3\";\n if (kbps >= 128) return \"5\";\n if (kbps >= 96) return \"7\";\n return \"9\";\n }\n\n private formatDuration(seconds: number): string {\n if (!seconds) return \"0:00\";\n const h = Math.floor(seconds / 3600);\n const m = Math.floor((seconds % 3600) / 60);\n const s = Math.floor(seconds % 60);\n if (h > 0) {\n return `${h}:${m.toString().padStart(2, \"0\")}:${s.toString().padStart(2, \"0\")}`;\n }\n return `${m}:${s.toString().padStart(2, \"0\")}`;\n }\n}\n","import yts from \"yt-search\";\n\nimport type { PlayMetadata } from \"./types.js\";\n\nexport function stripWeirdUrlWrappers(input: string): string {\n let s = (input || \"\").trim();\n const mdAll = [...s.matchAll(/\\[[^\\]]*\\]\\((https?:\\/\\/[^)\\s]+)\\)/gi)];\n if (mdAll.length > 0) return mdAll[0][1].trim();\n s = s.replace(/^<([^>]+)>$/, \"$1\").trim();\n s = s.replace(/^[\"'`](.*)[\"'`]$/, \"$1\").trim();\n\n return s;\n}\n\nexport function getYouTubeVideoId(input: string): string | null {\n const regex =\n /(?:https?:\\/\\/)?(?:www\\.)?(?:youtube\\.com\\/(?:[^\\/]+\\/.+\\/|(?:v|e(?:mbed)?)\\/|.*[?&]v=|shorts\\/)|youtu\\.be\\/)([^\"&?\\/\\s]{11})/i;\n\n const match = (input || \"\").match(regex);\n return match ? match[1] : null;\n}\n\nexport function normalizeYoutubeUrl(input: string): string | null {\n const cleaned0 = stripWeirdUrlWrappers(input);\n\n const firstUrl = cleaned0.match(/https?:\\/\\/[^\\s)]+/i)?.[0] ?? cleaned0;\n\n const id = getYouTubeVideoId(firstUrl);\n if (!id) return null;\n\n return `https://www.youtube.com/watch?v=${id}`;\n}\n\nexport async function searchBest(query: string): Promise<PlayMetadata | null> {\n const result = await yts(query);\n const v = result?.videos?.[0];\n if (!v) return null;\n\n const durationSeconds = v.duration?.seconds ?? 0;\n\n const normalizedUrl = normalizeYoutubeUrl(v.url) ?? v.url;\n\n return {\n title: v.title || \"Untitled\",\n author: v.author?.name || undefined,\n duration: v.duration?.timestamp || undefined,\n thumb: v.image || v.thumbnail || undefined,\n videoId: v.videoId,\n url: normalizedUrl,\n durationSeconds,\n };\n}\n"],"mappings":"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","names":["fs","path","fs","CacheStore","opts","requestId","entry","loading","e","type","file","now","removed","f","fs","path","os","ensureDirSync","dirPath","resolvePaths","cacheDir","baseDir","resolvedBase","resolvedCache","spawn","path","fs","fileURLToPath","__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","kbps","seconds","h","m","s","yts","stripWeirdUrlWrappers","input","s","mdAll","getYouTubeVideoId","regex","match","normalizeYoutubeUrl","cleaned0","firstUrl","id","searchBest","query","v","durationSeconds","normalizedUrl","AUDIO_QUALITIES","VIDEO_QUALITIES","pickQuality","requested","available","sanitizeFilename","filename","PlayEngine","options","resolvePaths","CacheStore","YtDlpClient","prefix","query","searchBest","requestId","metadata","normalized","normalizeYoutubeUrl","isLongVideo","normalizedMeta","audioKbps","audioTask","tasks","type","entry","cached","fs","directFile","timeoutMs","intervalMs","started","f","r","youtubeUrl","quality","safeTitle","filePath","path","info","size","buffer","err","videoP","ext","stats"]}
1
+ {"version":3,"sources":["../src/core/play-engine.ts","../src/core/cache.ts","../src/core/paths.ts","../src/core/ytdlp-client.ts","../src/core/youtube.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type {\n CacheEntry,\n CachedFile,\n DownloadInfo,\n MediaType,\n PlayEngineOptions,\n PlayMetadata,\n} from \"./types.js\";\nimport { CacheStore } from \"./cache.js\";\nimport { resolvePaths } from \"./paths.js\";\nimport { YtDlpClient } from \"./ytdlp-client.js\";\nimport { normalizeYoutubeUrl, searchBest } from \"./youtube.js\";\n\nconst AUDIO_QUALITIES = [320, 256, 192, 128, 96, 64] as const;\nconst VIDEO_QUALITIES = [1080, 720, 480, 360] as const;\n\nfunction pickQuality<T extends number>(\n requested: T,\n available: readonly T[],\n): T {\n return (available as readonly number[]).includes(requested)\n ? requested\n : available[0];\n}\n\nfunction sanitizeFilename(filename: string): string {\n return (filename || \"\")\n .replace(/[\\\\/:*?\"<>|]/g, \"\")\n .replace(/[^\\w\\s-]/gi, \"\")\n .trim()\n .replace(/\\s+/g, \" \")\n .substring(0, 100);\n}\n\nexport class PlayEngine {\n private readonly opts: Required<\n Pick<\n PlayEngineOptions,\n | \"ttlMs\"\n | \"maxPreloadDurationSeconds\"\n | \"preferredAudioKbps\"\n | \"preferredVideoP\"\n | \"preloadBuffer\"\n | \"cleanupIntervalMs\"\n | \"concurrentFragments\"\n >\n > &\n Pick<PlayEngineOptions, \"useAria2c\" | \"logger\">;\n\n private readonly paths: { baseDir: string; cacheDir: string };\n readonly cache: CacheStore;\n private readonly ytdlp: YtDlpClient;\n\n constructor(options: PlayEngineOptions = {}) {\n this.opts = {\n ttlMs: options.ttlMs ?? 3 * 60_000,\n maxPreloadDurationSeconds: options.maxPreloadDurationSeconds ?? 20 * 60,\n preferredAudioKbps: options.preferredAudioKbps ?? 128,\n preferredVideoP: options.preferredVideoP ?? 720,\n preloadBuffer: options.preloadBuffer ?? true,\n cleanupIntervalMs: options.cleanupIntervalMs ?? 30_000,\n concurrentFragments: options.concurrentFragments ?? 5,\n useAria2c: options.useAria2c,\n logger: options.logger,\n };\n\n this.paths = resolvePaths(options.cacheDir);\n this.cache = new CacheStore({\n cleanupIntervalMs: this.opts.cleanupIntervalMs,\n });\n this.cache.start();\n\n this.ytdlp = new YtDlpClient({\n binaryPath: options.ytdlpBinaryPath,\n ffmpegPath: options.ffmpegPath,\n 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\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 }\n }\n\n private async downloadDirect(\n type: MediaType,\n youtubeUrl: string,\n ): Promise<CachedFile> {\n const audioKbps = pickQuality(\n this.opts.preferredAudioKbps,\n AUDIO_QUALITIES,\n );\n const videoP = pickQuality(this.opts.preferredVideoP, VIDEO_QUALITIES);\n const ext = type === \"audio\" ? \"m4a\" : \"mp4\";\n const safeTitle = sanitizeFilename(`direct_${Date.now()}`);\n const filePath = path.join(\n this.paths.cacheDir,\n `${type}_${safeTitle}.${ext}`,\n );\n\n const info =\n type === \"audio\"\n ? await this.ytdlp.getAudio(youtubeUrl, audioKbps, filePath)\n : await this.ytdlp.getVideo(youtubeUrl, videoP, filePath);\n\n const stats = fs.statSync(filePath);\n return {\n path: filePath,\n size: stats.size,\n info: { quality: info.quality },\n };\n }\n}\n","import fs from \"node:fs\";\n\nimport type { CacheEntry, MediaType } from \"./types.js\";\n\nexport class CacheStore {\n private readonly store = new Map<string, CacheEntry>();\n private cleanupTimer?: NodeJS.Timeout;\n\n constructor(\n private readonly opts: {\n cleanupIntervalMs: number;\n }\n ) {}\n\n get(requestId: string): CacheEntry | undefined {\n return this.store.get(requestId);\n }\n\n set(requestId: string, entry: CacheEntry): void {\n this.store.set(requestId, entry);\n }\n\n has(requestId: string): boolean {\n return this.store.has(requestId);\n }\n\n delete(requestId: string): void {\n this.cleanupEntry(requestId);\n this.store.delete(requestId);\n }\n\n markLoading(requestId: string, loading: boolean): void {\n const e = this.store.get(requestId);\n if (e) e.loading = loading;\n }\n\n setFile(\n requestId: string,\n type: MediaType,\n file: CacheEntry[MediaType]\n ): void {\n const e = this.store.get(requestId);\n if (!e) return;\n e[type] = file as any;\n }\n\n cleanupExpired(now = Date.now()): number {\n let removed = 0;\n for (const [requestId, entry] of this.store.entries()) {\n if (now > entry.expiresAt) {\n this.delete(requestId);\n removed++;\n }\n }\n return removed;\n }\n\n start(): void {\n if (this.cleanupTimer) return;\n\n this.cleanupTimer = setInterval(() => {\n this.cleanupExpired(Date.now());\n }, this.opts.cleanupIntervalMs);\n\n this.cleanupTimer.unref();\n }\n\n stop(): void {\n if (!this.cleanupTimer) return;\n clearInterval(this.cleanupTimer);\n this.cleanupTimer = undefined;\n }\n\n private cleanupEntry(requestId: string) {\n const entry = this.store.get(requestId);\n if (!entry) return;\n\n ([\"audio\", \"video\"] as const).forEach((type) => {\n const f = entry[type];\n if (f?.path && fs.existsSync(f.path)) {\n try {\n fs.unlinkSync(f.path);\n } catch {\n // ignore\n }\n }\n });\n }\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport os from \"node:os\";\n\nexport interface ResolvedPaths {\n baseDir: string;\n cacheDir: string;\n}\n\nexport function ensureDirSync(dirPath: string) {\n fs.mkdirSync(dirPath, { recursive: true, mode: 0o777 });\n\n try {\n fs.chmodSync(dirPath, 0o777);\n } catch {\n // ignore\n }\n\n fs.accessSync(dirPath, fs.constants.R_OK | fs.constants.W_OK);\n}\n\nexport function resolvePaths(cacheDir?: string): ResolvedPaths {\n const baseDir = cacheDir?.trim()\n ? cacheDir\n : path.join(os.tmpdir(), \"yt-play\");\n const resolvedBase = path.resolve(baseDir);\n\n const resolvedCache = path.join(resolvedBase);\n\n ensureDirSync(resolvedBase);\n ensureDirSync(resolvedCache);\n\n return {\n baseDir: resolvedBase,\n cacheDir: resolvedCache,\n };\n}\n","import { spawn } from \"node:child_process\";\nimport path from \"node:path\";\nimport fs from \"node:fs\";\nimport type { DownloadInfo } from \"./types.js\";\n\nlet __dirname: string;\ntry {\n // @ts-ignore\n __dirname = path.dirname(new URL(import.meta.url).pathname);\n} catch {\n // @ts-ignore\n __dirname = typeof __dirname !== \"undefined\" ? __dirname : process.cwd();\n}\n\nexport interface YtDlpClientOptions {\n binaryPath?: string;\n ffmpegPath?: string;\n aria2cPath?: string;\n timeoutMs?: number;\n useAria2c?: boolean;\n concurrentFragments?: number;\n cookiesPath?: string;\n cookiesFromBrowser?: string;\n}\n\ninterface YtDlpVideoInfo {\n id: string;\n title: string;\n uploader?: string;\n duration: number;\n thumbnail?: string;\n}\n\nexport class YtDlpClient {\n private readonly binaryPath: string;\n private readonly ffmpegPath?: string;\n private readonly aria2cPath?: string;\n private readonly timeoutMs: number;\n private readonly useAria2c: boolean;\n private readonly concurrentFragments: number;\n private readonly cookiesPath?: string;\n private readonly cookiesFromBrowser?: string;\n\n constructor(opts: YtDlpClientOptions = {}) {\n this.binaryPath = opts.binaryPath || this.detectYtDlp();\n this.ffmpegPath = opts.ffmpegPath;\n this.timeoutMs = opts.timeoutMs ?? 300_000;\n this.concurrentFragments = opts.concurrentFragments ?? 5;\n this.cookiesPath = opts.cookiesPath;\n this.cookiesFromBrowser = opts.cookiesFromBrowser;\n\n this.aria2cPath = opts.aria2cPath || this.detectAria2c();\n this.useAria2c = opts.useAria2c ?? !!this.aria2cPath;\n }\n\n private detectYtDlp(): string {\n const packageRoot = path.resolve(__dirname, \"../..\");\n const bundledPaths = [\n path.join(packageRoot, \"bin\", \"yt-dlp\"),\n path.join(packageRoot, \"bin\", \"yt-dlp.exe\"),\n ];\n\n for (const p of bundledPaths) {\n if (fs.existsSync(p)) {\n return p;\n }\n }\n\n try {\n const { execSync } = require(\"node:child_process\");\n const cmd =\n process.platform === \"win32\" ? \"where yt-dlp\" : \"which yt-dlp\";\n const result = execSync(cmd, { encoding: \"utf-8\" }).trim();\n if (result) return result.split(\"\\n\")[0];\n } catch {}\n\n return \"yt-dlp\";\n }\n\n private detectAria2c(): string | undefined {\n const packageRoot = path.resolve(__dirname, \"../..\");\n const bundledPaths = [\n path.join(packageRoot, \"bin\", \"aria2c\"),\n path.join(packageRoot, \"bin\", \"aria2c.exe\"),\n ];\n\n for (const p of bundledPaths) {\n if (fs.existsSync(p)) {\n return p;\n }\n }\n\n try {\n const { execSync } = require(\"node:child_process\");\n const cmd =\n process.platform === \"win32\" ? \"where aria2c\" : \"which aria2c\";\n const result = execSync(cmd, { encoding: \"utf-8\" }).trim();\n if (result) return result.split(\"\\n\")[0];\n } catch {}\n\n return undefined;\n }\n\n private async exec(args: string[]): Promise<string> {\n return new Promise((resolve, reject) => {\n let allArgs = [...args];\n\n if (this.ffmpegPath) {\n allArgs = [\"--ffmpeg-location\", this.ffmpegPath, ...allArgs];\n }\n\n if (this.cookiesPath && fs.existsSync(this.cookiesPath)) {\n allArgs = [\"--cookies\", this.cookiesPath, ...allArgs];\n }\n\n if (this.cookiesFromBrowser) {\n allArgs = [\n \"--cookies-from-browser\",\n this.cookiesFromBrowser,\n ...allArgs,\n ];\n }\n\n const proc = spawn(this.binaryPath, allArgs, {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n let stdout = \"\";\n let stderr = \"\";\n\n proc.stdout.on(\"data\", (chunk) => {\n stdout += chunk.toString();\n });\n\n proc.stderr.on(\"data\", (chunk) => {\n stderr += chunk.toString();\n });\n\n const timer = setTimeout(() => {\n proc.kill(\"SIGKILL\");\n reject(new Error(`yt-dlp timeout after ${this.timeoutMs}ms`));\n }, this.timeoutMs);\n\n proc.on(\"close\", (code) => {\n clearTimeout(timer);\n if (code === 0) {\n resolve(stdout);\n } else {\n reject(\n new Error(\n `yt-dlp exited with code ${code}. stderr: ${stderr.slice(0, 500)}`,\n ),\n );\n }\n });\n\n proc.on(\"error\", (err) => {\n clearTimeout(timer);\n reject(err);\n });\n });\n }\n\n async getInfo(youtubeUrl: string): Promise<YtDlpVideoInfo> {\n const stdout = await this.exec([\n \"-J\",\n \"--no-warnings\",\n \"--no-playlist\",\n youtubeUrl,\n ]);\n const info = JSON.parse(stdout) as YtDlpVideoInfo;\n return info;\n }\n\n private buildOptimizationArgs(): string[] {\n const args: string[] = [\n \"--no-warnings\",\n \"--no-playlist\",\n \"--no-check-certificates\",\n \"--concurrent-fragments\",\n String(this.concurrentFragments),\n ];\n\n if (this.useAria2c && this.aria2cPath) {\n args.push(\"--downloader\", this.aria2cPath);\n args.push(\"--downloader-args\", \"aria2c:-x 16 -s 16 -k 1M\");\n }\n\n return args;\n }\n\n async getAudio(\n youtubeUrl: string,\n qualityKbps: number,\n outputPath: string,\n ): Promise<DownloadInfo> {\n const info = await this.getInfo(youtubeUrl);\n const format = \"bestaudio[ext=m4a]/bestaudio/best\";\n\n const args = [\n \"-f\",\n format,\n \"-o\",\n outputPath,\n ...this.buildOptimizationArgs(),\n youtubeUrl,\n ];\n\n await this.exec(args);\n\n if (!fs.existsSync(outputPath)) {\n throw new Error(`yt-dlp failed to create audio file: ${outputPath}`);\n }\n\n const duration = this.formatDuration(info.duration);\n\n return {\n title: info.title,\n author: info.uploader,\n duration,\n quality: `${qualityKbps}kbps m4a`,\n filename: path.basename(outputPath),\n downloadUrl: outputPath,\n };\n }\n\n async getVideo(\n youtubeUrl: string,\n qualityP: number,\n outputPath: string,\n ): Promise<DownloadInfo> {\n const info = await this.getInfo(youtubeUrl);\n const format = `bestvideo[height<=${qualityP}][ext=mp4]+bestaudio[ext=m4a]/best[height<=${qualityP}]`;\n\n const args = [\n \"-f\",\n format,\n \"--merge-output-format\",\n \"mp4\",\n \"-o\",\n outputPath,\n ...this.buildOptimizationArgs(),\n youtubeUrl,\n ];\n\n await this.exec(args);\n\n if (!fs.existsSync(outputPath)) {\n throw new Error(`yt-dlp failed to create video file: ${outputPath}`);\n }\n\n const duration = this.formatDuration(info.duration);\n\n return {\n title: info.title,\n author: info.uploader,\n duration,\n quality: `${qualityP}p`,\n filename: path.basename(outputPath),\n downloadUrl: outputPath,\n };\n }\n\n private formatDuration(seconds: number): string {\n if (!seconds) return \"0:00\";\n const h = Math.floor(seconds / 3600);\n const m = Math.floor((seconds % 3600) / 60);\n const s = Math.floor(seconds % 60);\n if (h > 0) {\n return `${h}:${m.toString().padStart(2, \"0\")}:${s.toString().padStart(2, \"0\")}`;\n }\n return `${m}:${s.toString().padStart(2, \"0\")}`;\n }\n}\n","import yts from \"yt-search\";\n\nimport type { PlayMetadata } from \"./types.js\";\n\nexport function stripWeirdUrlWrappers(input: string): string {\n let s = (input || \"\").trim();\n const mdAll = [...s.matchAll(/\\[[^\\]]*\\]\\((https?:\\/\\/[^)\\s]+)\\)/gi)];\n if (mdAll.length > 0) return mdAll[0][1].trim();\n s = s.replace(/^<([^>]+)>$/, \"$1\").trim();\n s = s.replace(/^[\"'`](.*)[\"'`]$/, \"$1\").trim();\n\n return s;\n}\n\nexport function getYouTubeVideoId(input: string): string | null {\n const regex =\n /(?:https?:\\/\\/)?(?:www\\.)?(?:youtube\\.com\\/(?:[^\\/]+\\/.+\\/|(?:v|e(?:mbed)?)\\/|.*[?&]v=|shorts\\/)|youtu\\.be\\/)([^\"&?\\/\\s]{11})/i;\n\n const match = (input || \"\").match(regex);\n return match ? match[1] : null;\n}\n\nexport function normalizeYoutubeUrl(input: string): string | null {\n const cleaned0 = stripWeirdUrlWrappers(input);\n\n const firstUrl = cleaned0.match(/https?:\\/\\/[^\\s)]+/i)?.[0] ?? cleaned0;\n\n const id = getYouTubeVideoId(firstUrl);\n if (!id) return null;\n\n return `https://www.youtube.com/watch?v=${id}`;\n}\n\nexport async function searchBest(query: string): Promise<PlayMetadata | null> {\n const result = await yts(query);\n const v = result?.videos?.[0];\n if (!v) return null;\n\n const durationSeconds = v.duration?.seconds ?? 0;\n\n const normalizedUrl = normalizeYoutubeUrl(v.url) ?? v.url;\n\n return {\n title: v.title || \"Untitled\",\n author: v.author?.name || undefined,\n duration: v.duration?.timestamp || undefined,\n thumb: v.image || v.thumbnail || undefined,\n videoId: v.videoId,\n url: normalizedUrl,\n durationSeconds,\n };\n}\n"],"mappings":"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","names":["fs","path","fs","CacheStore","opts","requestId","entry","loading","e","type","file","now","removed","f","fs","path","os","ensureDirSync","dirPath","resolvePaths","cacheDir","baseDir","resolvedBase","resolvedCache","spawn","path","fs","__dirname","YtDlpClient","opts","packageRoot","bundledPaths","p","execSync","cmd","result","args","resolve","reject","allArgs","proc","stdout","stderr","chunk","timer","code","err","youtubeUrl","qualityKbps","outputPath","info","duration","qualityP","seconds","h","m","s","yts","stripWeirdUrlWrappers","input","s","mdAll","getYouTubeVideoId","regex","match","normalizeYoutubeUrl","cleaned0","firstUrl","id","searchBest","query","v","durationSeconds","normalizedUrl","AUDIO_QUALITIES","VIDEO_QUALITIES","pickQuality","requested","available","sanitizeFilename","filename","PlayEngine","options","resolvePaths","CacheStore","YtDlpClient","prefix","query","searchBest","requestId","metadata","normalized","normalizeYoutubeUrl","isLongVideo","normalizedMeta","audioKbps","audioTask","tasks","type","entry","cached","fs","directFile","timeoutMs","intervalMs","started","f","r","youtubeUrl","quality","safeTitle","filePath","path","info","size","buffer","err","videoP","ext","stats"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@irithell-js/yt-play",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "YouTube search + download engine (audio/video) with optional caching.",
5
5
  "license": "MIT",
6
6
  "type": "module",