@irithell-js/yt-play 0.1.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Jean (Irithell)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # @irithell/yt-play
2
+
3
+ YouTube search + áudio/vídeo download engine (via SaveTube) com cache TTL opcional.
4
+
5
+ ## Instalação
6
+
7
+ ```bash
8
+ npm i @irithell-js/yt-play
9
+ ```
10
+
11
+ ## Uso básico
12
+
13
+ ### 1) Buscar e iniciar preload
14
+
15
+ ```ts
16
+ import { PlayEngine } from "@irithell/yt-play";
17
+
18
+ const engine = new PlayEngine({
19
+ // opcional
20
+ ttlMs: 3 * 60_000,
21
+ preferredAudioKbps: 128,
22
+ preferredVideoP: 720,
23
+ preloadBuffer: true,
24
+ });
25
+
26
+ const metadata = await engine.search("linkin park numb");
27
+ if (!metadata) throw new Error("Nada encontrado");
28
+
29
+ const requestId = engine.generateRequestId();
30
+ await engine.preload(metadata, requestId);
31
+
32
+ // depois você usa requestId pra pedir audio/video
33
+ ```
34
+
35
+ ### 2) Obter do cache (ou baixar direto)
36
+
37
+ ```ts
38
+ // audio
39
+ const audio = await engine.getOrDownload(requestId, "audio");
40
+ console.log(audio.metadata.title, audio.file.info.quality, audio.file.path);
41
+
42
+ // video
43
+ const video = await engine.getOrDownload(requestId, "video");
44
+ console.log(video.file.info.quality, video.file.path);
45
+
46
+ // se você não quiser manter o arquivo, apague depois de enviar
47
+ engine.cleanup(requestId);
48
+ ```
49
+
50
+ ### 3) Esperar o cache “aquecer” (opcional)
51
+
52
+ ```ts
53
+ const cached = await engine.waitCache(requestId, "audio", 8000, 500);
54
+
55
+ if (cached) {
56
+ // já está no disco (e possivelmente com buffer em RAM)
57
+ console.log("cache pronto", cached.path);
58
+ } else {
59
+ // não ficou pronto a tempo — você pode cair pro getOrDownload
60
+ const direct = await engine.getOrDownload(requestId, "audio");
61
+ console.log("fallback", direct.file.path);
62
+ }
63
+ ```
64
+
65
+ ## API
66
+
67
+ ### `new PlayEngine(options?)`
68
+
69
+ Opções principais:
70
+
71
+ - `cacheDir?: string` Diretório base do cache (default: pasta temporária do sistema).
72
+ - `ttlMs?: number` TTL dos arquivos em cache.
73
+ - `preferredAudioKbps?: 320 | 256 | 192 | 128 | 96 | 64`
74
+ - `preferredVideoP?: 1080 | 720 | 480 | 360`
75
+ - `preloadBuffer?: boolean` Se true, lê o arquivo e deixa `buffer` pronto (mais RAM, envio mais rápido).
76
+ - `cleanupIntervalMs?: number` Intervalo do GC do cache.
77
+ - `logger?: { info|warn|error|debug }` Logger opcional.
78
+
79
+ ### Métodos
80
+
81
+ - `search(query): Promise<PlayMetadata | null>`
82
+ - `generateRequestId(prefix?): string`
83
+ - `preload(metadata, requestId): Promise<void>`
84
+ - `getOrDownload(requestId, 'audio'|'video'): Promise<{ metadata; file; direct }>`
85
+ - `waitCache(requestId, 'audio'|'video', timeoutMs?, intervalMs?): Promise<CachedFile | null>`
86
+ - `cleanup(requestId): void`
87
+
88
+ ## Licença
89
+
90
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";var W=Object.create;var h=Object.defineProperty;var Y=Object.getOwnPropertyDescriptor;var q=Object.getOwnPropertyNames;var G=Object.getPrototypeOf,H=Object.prototype.hasOwnProperty;var N=(o,e)=>{for(var t in e)h(o,t,{get:e[t],enumerable:!0})},I=(o,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of q(e))!H.call(o,r)&&r!==t&&h(o,r,{get:()=>e[r],enumerable:!(n=Y(e,r))||n.enumerable});return o};var c=(o,e,t)=>(t=o!=null?W(G(o)):{},I(e||!o||!o.__esModule?h(t,"default",{value:o,enumerable:!0}):t,o)),Q=o=>I(h({},"__esModule",{value:!0}),o);var re={};N(re,{PlayEngine:()=>M});module.exports=Q(re);var x=c(require("fs"),1),P=c(require("path"),1);var S=c(require("fs"),1),m=class{constructor(e){this.opts=e}store=new Map;cleanupTimer;get(e){return this.store.get(e)}set(e,t){this.store.set(e,t)}has(e){return this.store.has(e)}delete(e){this.cleanupEntry(e),this.store.delete(e)}markLoading(e,t){let n=this.store.get(e);n&&(n.loading=t)}setFile(e,t,n){let r=this.store.get(e);r&&(r[t]=n)}cleanupExpired(e=Date.now()){let t=0;for(let[n,r]of this.store.entries())e>r.expiresAt&&(this.delete(n),t++);return t}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(e){let t=this.store.get(e);t&&["audio","video"].forEach(n=>{let r=t[n];if(r?.path&&S.default.existsSync(r.path))try{S.default.unlinkSync(r.path)}catch{}})}};var p=c(require("fs"),1),g=c(require("path"),1),B=c(require("os"),1);function y(o){p.default.mkdirSync(o,{recursive:!0,mode:511});try{p.default.chmodSync(o,511)}catch{}p.default.accessSync(o,p.default.constants.R_OK|p.default.constants.W_OK)}function $(o){let e=o?.trim()?o:g.default.join(B.default.tmpdir(),"yt-play"),t=g.default.resolve(e),n=g.default.join(t,"play-cache");return y(t),y(n),{baseDir:t,cacheDir:n}}var C=c(require("axios"),1),k=require("crypto"),O=c(require("http"),1),U=c(require("https"),1),J="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36";function X(o){let e="C5D58EF67A7584E4A29F6C35BBC4EB12",t=Buffer.from(o,"base64"),n=t.subarray(0,16),r=t.subarray(16),i=Buffer.from(e,"hex"),a=(0,k.createDecipheriv)("aes-128-cbc",i,n),s=Buffer.concat([a.update(r),a.final()]);return JSON.parse(s.toString("utf8"))}function Z(o){if(o.axios)return o.axios;let e=o.timeoutMs??6e4;return C.default.create({timeout:e,maxRedirects:0,validateStatus:t=>t>=200&&t<400,httpAgent:new O.Agent({keepAlive:!0}),httpsAgent:new U.Agent({keepAlive:!0}),headers:{"User-Agent":o.userAgent??J,"Accept-Language":"en-US,en;q=0.9",Referer:"https://www.youtube.com",Origin:"https://www.youtube.com"}})}var w=class{constructor(e={}){this.opts=e;this.axiosBase=Z(e)}axiosBase;async getCdnBase(){let t=(await this.axiosBase.get("https://media.savetube.me/api/random-cdn",{timeout:5e3})).data?.cdn;if(!t)throw new Error("SaveTube random-cdn returned no cdn host.");return/^https?:\/\//i.test(t)?t:`https://${t}`}async getInfo(e){let t=await this.getCdnBase(),r=(await this.axiosBase.post(`${t}/v2/info`,{url:e},{headers:{"Content-Type":"application/json",Accept:"application/json"}})).data;if(!r?.data)throw new Error("Invalid SaveTube /v2/info response (missing data).");let i=X(r.data);if(!i?.key)throw new Error("Invalid SaveTube decoded info (missing key).");return{cdnBase:t,info:i}}async getAudio(e,t){let{cdnBase:n,info:r}=await this.getInfo(e),a=(await this.axiosBase.post(`${n}/download`,{downloadType:"audio",quality:String(t),key:r.key},{headers:{"Content-Type":"application/json",Accept:"application/json"}})).data?.data?.downloadUrl;if(!a)throw new Error("Invalid SaveTube audio downloadUrl.");return{title:r.title,author:r.author,duration:r.duration,quality:`${t}kbps`,filename:`${r.title??"audio"} ${t}kbps.mp3`,downloadUrl:a}}async getVideo(e,t){let{cdnBase:n,info:r}=await this.getInfo(e),a=(await this.axiosBase.post(`${n}/download`,{downloadType:"video",quality:t,key:r.key},{headers:{"Content-Type":"application/json",Accept:"application/json"}})).data?.data?.downloadUrl;if(!a)throw new Error("Invalid SaveTube video downloadUrl.");return{title:r.title,author:r.author,duration:r.duration,quality:`${t}p`,filename:`${r.title??"video"} ${t}p.mp4`,downloadUrl:a}}};var v=c(require("fs"),1),F=c(require("path"),1),_=c(require("axios"),1);async function A(o,e,t={}){let n=t.timeoutMs??12e4,r=t.fileMode??438;y(F.default.dirname(e));let a=await(t.axios??_.default).request({url:o,method:"GET",responseType:"stream",timeout:n}),s=v.default.createWriteStream(e,{mode:r});await new Promise((l,f)=>{let u=!1,D=()=>{u||(u=!0,l())},E=L=>{u||(u=!0,f(L))};s.on("finish",D),s.on("error",E),a.data.on("error",E),a.data.pipe(s)});let d=v.default.statSync(e);if(d.size===0){try{v.default.unlinkSync(e)}catch{}throw new Error("Downloaded file is empty.")}return{size:d.size}}var z=c(require("yt-search"),1);function ee(o){let e=(o||"").trim(),t=[...e.matchAll(/\[[^\]]*\]\((https?:\/\/[^)\s]+)\)/gi)];return t.length>0?t[0][1].trim():(e=e.replace(/^<([^>]+)>$/,"$1").trim(),e=e.replace(/^["'`](.*)["'`]$/,"$1").trim(),e)}function te(o){let e=/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=|shorts\/)|youtu\.be\/)([^"&?\/\s]{11})/i,t=(o||"").match(e);return t?t[1]:null}function b(o){let e=ee(o),t=e.match(/https?:\/\/[^\s)]+/i)?.[0]??e,n=te(t);return n?`https://www.youtube.com/watch?v=${n}`:null}async function j(o){let t=(await(0,z.default)(o))?.videos?.[0];if(!t)return null;let n=t.duration?.seconds??0,r=b(t.url)??t.url;return{title:t.title||"Untitled",author:t.author?.name||void 0,duration:t.duration?.timestamp||void 0,thumb:t.image||t.thumbnail||void 0,videoId:t.videoId,url:r,durationSeconds:n}}var R=[320,256,192,128,96,64],V=[1080,720,480,360];function T(o,e){return e.includes(o)?o:e[0]}function K(o){return(o||"").replace(/[\\\/:*?"<>|]/g,"").replace(/[^\w\s-]/gi,"").trim().replace(/\s+/g," ").substring(0,100)}var M=class{opts;paths;cache;saveTube;constructor(e={}){this.opts={ttlMs:e.ttlMs??3*6e4,maxPreloadDurationSeconds:e.maxPreloadDurationSeconds??1200,preferredAudioKbps:e.preferredAudioKbps??128,preferredVideoP:e.preferredVideoP??720,preloadBuffer:e.preloadBuffer??!0,cleanupIntervalMs:e.cleanupIntervalMs??3e4,logger:e.logger},this.paths=$(e.cacheDir),this.cache=new m({cleanupIntervalMs:this.opts.cleanupIntervalMs}),this.cache.start(),this.saveTube=new w({})}generateRequestId(e="play"){return`${e}_${Date.now()}_${Math.random().toString(36).slice(2,8)}`}async search(e){return j(e)}getFromCache(e){return this.cache.get(e)}async preload(e,t){if(e.durationSeconds>this.opts.maxPreloadDurationSeconds)throw new Error("Video too long for preload.");let n=b(e.url);if(!n)throw new Error("Invalid YouTube URL.");let r={...e,url:n};this.cache.set(t,{metadata:r,audio:null,video:null,expiresAt:Date.now()+this.opts.ttlMs,loading:!0});let i=T(this.opts.preferredAudioKbps,R),a=T(this.opts.preferredVideoP,V),s=this.preloadOne(t,"audio",n,i),d=this.preloadOne(t,"video",n,a);Promise.allSettled([s,d]).finally(()=>this.cache.markLoading(t,!1)).catch(()=>this.cache.markLoading(t,!1))}async getOrDownload(e,t){let n=this.cache.get(e);if(!n)throw new Error("Request not found (cache miss).");let r=n[t];if(r?.path&&x.default.existsSync(r.path)&&r.size>0)return{metadata:n.metadata,file:r,direct:!1};let i=b(n.metadata.url);if(!i)throw new Error("Invalid YouTube URL.");let a=await this.downloadDirect(t,i);return{metadata:n.metadata,file:a,direct:!0}}async waitCache(e,t,n=8e3,r=500){let i=Date.now();for(;Date.now()-i<n;){let s=this.cache.get(e)?.[t];if(s?.path&&x.default.existsSync(s.path)&&s.size>0)return s;await new Promise(d=>setTimeout(d,r))}return null}cleanup(e){this.cache.delete(e)}async preloadOne(e,t,n,r){try{let i=t==="audio"?await this.saveTube.getAudio(n,r):await this.saveTube.getVideo(n,r),a=K(i.title??t),d=`${t}_${e}_${a}.${t==="audio"?"mp3":"mp4"}`,l=P.default.join(this.paths.cacheDir,d),{size:f}=await A(i.downloadUrl,l),u;this.opts.preloadBuffer&&(u=await x.default.promises.readFile(l));let D={path:l,size:f,info:{quality:i.quality},buffer:u};this.cache.setFile(e,t,D),this.opts.logger?.debug?.(`preloaded ${t} ${f} bytes: ${d}`)}catch(i){this.opts.logger?.error?.(`preload ${t} failed`,i)}}async downloadDirect(e,t){let n=T(this.opts.preferredAudioKbps,R),r=T(this.opts.preferredVideoP,V),i=e==="audio"?await this.saveTube.getAudio(t,n):await this.saveTube.getVideo(t,r),a=e==="audio"?"mp3":"mp4",s=K(i.title??e),d=P.default.join(this.paths.cacheDir,`${e}_direct_${Date.now()}_${s}.${a}`),{size:l}=await A(i.downloadUrl,d);return{path:d,size:l,info:{quality:i.quality}}}};0&&(module.exports={PlayEngine});
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/core/play-engine.ts","../src/core/cache.ts","../src/core/paths.ts","../src/core/savetube.ts","../src/core/download.ts","../src/core/youtube.ts"],"sourcesContent":["export type {\n PlayMetadata,\n CachedFile,\n PlayEngineOptions,\n} from \"./core/types.js\";\n\nexport { PlayEngine } from \"./core/play-engine.js\";\n","import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport type {\n CacheEntry,\n CachedFile,\n DownloadInfo,\n MediaType,\n PlayEngineOptions,\n PlayMetadata,\n} from \"./types.js\";\nimport { CacheStore } from \"./cache.js\";\nimport { resolvePaths } from \"./paths.js\";\nimport { SaveTubeClient } from \"./savetube.js\";\nimport { downloadToBuffer, downloadToFile } from \"./download.js\";\nimport { normalizeYoutubeUrl, searchBest } from \"./youtube.js\";\n\nconst AUDIO_QUALITIES = [320, 256, 192, 128, 96, 64] as const;\nconst VIDEO_QUALITIES = [1080, 720, 480, 360] as const;\n\nfunction pickQuality<T extends number>(\n requested: T,\n available: readonly T[]\n): T {\n return (available as readonly number[]).includes(requested)\n ? requested\n : available[0];\n}\n\nfunction sanitizeFilename(filename: string): string {\n return (filename || \"\")\n .replace(/[\\\\\\/:*?\"<>|]/g, \"\")\n .replace(/[^\\w\\s-]/gi, \"\")\n .trim()\n .replace(/\\s+/g, \" \")\n .substring(0, 100);\n}\n\nexport class PlayEngine {\n private readonly opts: Required<\n Pick<\n PlayEngineOptions,\n | \"ttlMs\"\n | \"maxPreloadDurationSeconds\"\n | \"preferredAudioKbps\"\n | \"preferredVideoP\"\n | \"preloadBuffer\"\n | \"cleanupIntervalMs\"\n >\n > &\n Pick<PlayEngineOptions, \"logger\">;\n\n private readonly paths: { baseDir: string; cacheDir: string };\n private readonly cache: CacheStore;\n private readonly saveTube: SaveTubeClient;\n\n constructor(options: PlayEngineOptions = {}) {\n this.opts = {\n ttlMs: options.ttlMs ?? 3 * 60_000,\n maxPreloadDurationSeconds: options.maxPreloadDurationSeconds ?? 20 * 60,\n preferredAudioKbps: options.preferredAudioKbps ?? 128,\n preferredVideoP: options.preferredVideoP ?? 720,\n preloadBuffer: options.preloadBuffer ?? true,\n cleanupIntervalMs: options.cleanupIntervalMs ?? 30_000,\n logger: options.logger,\n };\n\n this.paths = resolvePaths(options.cacheDir);\n this.cache = new CacheStore({\n cleanupIntervalMs: this.opts.cleanupIntervalMs,\n });\n this.cache.start();\n\n this.saveTube = new SaveTubeClient({\n // mantém axios interno\n });\n }\n\n generateRequestId(prefix = \"play\"): string {\n return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;\n }\n\n async search(query: string): Promise<PlayMetadata | null> {\n return searchBest(query);\n }\n\n getFromCache(requestId: string): CacheEntry | undefined {\n return this.cache.get(requestId);\n }\n\n async preload(metadata: PlayMetadata, requestId: string): Promise<void> {\n if (metadata.durationSeconds > this.opts.maxPreloadDurationSeconds) {\n throw new Error(\"Video too long for preload.\");\n }\n\n const normalized = normalizeYoutubeUrl(metadata.url);\n if (!normalized) throw new Error(\"Invalid YouTube URL.\");\n\n const normalizedMeta: PlayMetadata = { ...metadata, url: normalized };\n\n this.cache.set(requestId, {\n metadata: normalizedMeta,\n audio: null,\n video: null,\n expiresAt: Date.now() + this.opts.ttlMs,\n loading: true,\n });\n\n const audioKbps = pickQuality(\n this.opts.preferredAudioKbps,\n AUDIO_QUALITIES\n );\n const videoP = pickQuality(this.opts.preferredVideoP, VIDEO_QUALITIES);\n\n const audioTask = this.preloadOne(\n requestId,\n \"audio\",\n normalized,\n audioKbps\n );\n const videoTask = this.preloadOne(requestId, \"video\", normalized, videoP);\n\n Promise.allSettled([audioTask, videoTask])\n .finally(() => this.cache.markLoading(requestId, false))\n .catch(() => this.cache.markLoading(requestId, false));\n }\n\n async getOrDownload(\n requestId: string,\n type: MediaType\n ): Promise<{ metadata: PlayMetadata; file: CachedFile; direct: boolean }> {\n const entry = this.cache.get(requestId);\n if (!entry) throw new Error(\"Request not found (cache miss).\");\n\n const cached = entry[type];\n if (cached?.path && fs.existsSync(cached.path) && cached.size > 0) {\n return { metadata: entry.metadata, file: cached, direct: false };\n }\n\n const normalized = normalizeYoutubeUrl(entry.metadata.url);\n if (!normalized) throw new Error(\"Invalid YouTube URL.\");\n\n const directFile = await this.downloadDirect(type, normalized);\n return { metadata: entry.metadata, file: directFile, direct: true };\n }\n\n async waitCache(\n requestId: string,\n type: MediaType,\n timeoutMs = 8_000,\n intervalMs = 500\n ): Promise<CachedFile | null> {\n const started = Date.now();\n while (Date.now() - started < timeoutMs) {\n const entry = this.cache.get(requestId);\n const f = entry?.[type];\n if (f?.path && fs.existsSync(f.path) && f.size > 0) return f;\n await new Promise((r) => setTimeout(r, intervalMs));\n }\n return null;\n }\n\n cleanup(requestId: string): void {\n this.cache.delete(requestId);\n }\n\n private async preloadOne(\n requestId: string,\n type: MediaType,\n youtubeUrl: string,\n quality: number\n ): Promise<void> {\n try {\n const info: DownloadInfo =\n type === \"audio\"\n ? await this.saveTube.getAudio(youtubeUrl, quality)\n : await this.saveTube.getVideo(youtubeUrl, quality);\n\n const safeTitle = sanitizeFilename(info.title ?? type);\n const ext = type === \"audio\" ? \"mp3\" : \"mp4\";\n const filename = `${type}_${requestId}_${safeTitle}.${ext}`;\n const filePath = path.join(this.paths.cacheDir, filename);\n\n const { size } = await downloadToFile(info.downloadUrl, filePath);\n\n let buffer: Buffer | undefined;\n if (this.opts.preloadBuffer) {\n buffer = await fs.promises.readFile(filePath);\n }\n\n const cached: CachedFile = {\n path: filePath,\n size,\n info: { quality: info.quality },\n buffer,\n };\n\n this.cache.setFile(requestId, type, cached);\n\n this.opts.logger?.debug?.(`preloaded ${type} ${size} bytes: ${filename}`);\n } catch (err) {\n this.opts.logger?.error?.(`preload ${type} failed`, err);\n }\n }\n\n private async downloadDirect(\n type: MediaType,\n youtubeUrl: string\n ): Promise<CachedFile> {\n const audioKbps = pickQuality(\n this.opts.preferredAudioKbps,\n AUDIO_QUALITIES\n );\n const videoP = pickQuality(this.opts.preferredVideoP, VIDEO_QUALITIES);\n\n const info =\n type === \"audio\"\n ? await this.saveTube.getAudio(youtubeUrl, audioKbps)\n : await this.saveTube.getVideo(youtubeUrl, videoP);\n\n const ext = type === \"audio\" ? \"mp3\" : \"mp4\";\n const safeTitle = sanitizeFilename(info.title ?? type);\n const filePath = path.join(\n this.paths.cacheDir,\n `${type}_direct_${Date.now()}_${safeTitle}.${ext}`\n );\n\n const { size } = await downloadToFile(info.downloadUrl, filePath);\n\n return {\n path: filePath,\n size,\n info: { quality: info.quality },\n };\n }\n}\n","import fs from \"node:fs\";\n\nimport type { CacheEntry, MediaType } from \"./types.js\";\n\nexport class CacheStore {\n private readonly store = new Map<string, CacheEntry>();\n private cleanupTimer?: NodeJS.Timeout;\n\n constructor(\n private readonly opts: {\n cleanupIntervalMs: number;\n }\n ) {}\n\n get(requestId: string): CacheEntry | undefined {\n return this.store.get(requestId);\n }\n\n set(requestId: string, entry: CacheEntry): void {\n this.store.set(requestId, entry);\n }\n\n has(requestId: string): boolean {\n return this.store.has(requestId);\n }\n\n delete(requestId: string): void {\n this.cleanupEntry(requestId);\n this.store.delete(requestId);\n }\n\n markLoading(requestId: string, loading: boolean): void {\n const e = this.store.get(requestId);\n if (e) e.loading = loading;\n }\n\n setFile(\n requestId: string,\n type: MediaType,\n file: CacheEntry[MediaType]\n ): void {\n const e = this.store.get(requestId);\n if (!e) return;\n e[type] = file as any;\n }\n\n cleanupExpired(now = Date.now()): number {\n let removed = 0;\n for (const [requestId, entry] of this.store.entries()) {\n if (now > entry.expiresAt) {\n this.delete(requestId);\n removed++;\n }\n }\n return removed;\n }\n\n start(): void {\n if (this.cleanupTimer) return;\n\n this.cleanupTimer = setInterval(() => {\n this.cleanupExpired(Date.now());\n }, this.opts.cleanupIntervalMs);\n\n this.cleanupTimer.unref();\n }\n\n stop(): void {\n if (!this.cleanupTimer) return;\n clearInterval(this.cleanupTimer);\n this.cleanupTimer = undefined;\n }\n\n private cleanupEntry(requestId: string) {\n const entry = this.store.get(requestId);\n if (!entry) return;\n\n ([\"audio\", \"video\"] as const).forEach((type) => {\n const f = entry[type];\n if (f?.path && fs.existsSync(f.path)) {\n try {\n fs.unlinkSync(f.path);\n } catch {\n // ignore\n }\n }\n });\n }\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport os from \"node:os\";\n\nexport interface ResolvedPaths {\n baseDir: string;\n cacheDir: string;\n}\n\nexport function ensureDirSync(dirPath: string) {\n fs.mkdirSync(dirPath, { recursive: true, mode: 0o777 });\n\n try {\n fs.chmodSync(dirPath, 0o777);\n } catch {\n // ignore\n }\n\n fs.accessSync(dirPath, fs.constants.R_OK | fs.constants.W_OK);\n}\n\nexport function resolvePaths(cacheDir?: string): ResolvedPaths {\n const baseDir = cacheDir?.trim()\n ? cacheDir\n : path.join(os.tmpdir(), \"yt-play\");\n const resolvedBase = path.resolve(baseDir);\n\n const resolvedCache = path.join(resolvedBase, \"play-cache\");\n\n ensureDirSync(resolvedBase);\n ensureDirSync(resolvedCache);\n\n return {\n baseDir: resolvedBase,\n cacheDir: resolvedCache,\n };\n}\n","import axios, { type AxiosInstance } from \"axios\";\nimport { createDecipheriv } from \"node:crypto\";\nimport * as http from \"node:http\";\nimport * as https from \"node:https\";\n\nimport type { DownloadInfo } from \"./types.js\";\n\nexport interface SaveTubeClientOptions {\n axios?: AxiosInstance;\n timeoutMs?: number;\n userAgent?: string;\n}\n\ntype SaveTubeInfo = {\n key: string;\n title?: string;\n author?: string;\n duration?: string;\n};\n\nconst DEFAULT_UA =\n \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36\";\n\nfunction decode(enc: string): any {\n const secretKeyHex = \"C5D58EF67A7584E4A29F6C35BBC4EB12\";\n const data = Buffer.from(enc, \"base64\");\n const iv = data.subarray(0, 16);\n const content = data.subarray(16);\n\n const key = Buffer.from(secretKeyHex, \"hex\");\n const decipher = createDecipheriv(\"aes-128-cbc\", key, iv);\n const decrypted = Buffer.concat([decipher.update(content), decipher.final()]);\n return JSON.parse(decrypted.toString(\"utf8\"));\n}\n\nfunction createAxiosBase(opts: SaveTubeClientOptions): AxiosInstance {\n if (opts.axios) return opts.axios;\n\n const timeout = opts.timeoutMs ?? 60_000;\n return axios.create({\n timeout,\n maxRedirects: 0,\n validateStatus: (s) => s >= 200 && s < 400,\n httpAgent: new http.Agent({ keepAlive: true }),\n httpsAgent: new https.Agent({ keepAlive: true }),\n headers: {\n \"User-Agent\": opts.userAgent ?? DEFAULT_UA,\n \"Accept-Language\": \"en-US,en;q=0.9\",\n Referer: \"https://www.youtube.com\",\n Origin: \"https://www.youtube.com\",\n },\n });\n}\n\nexport class SaveTubeClient {\n private readonly axiosBase: AxiosInstance;\n\n constructor(private readonly opts: SaveTubeClientOptions = {}) {\n this.axiosBase = createAxiosBase(opts);\n }\n\n private async getCdnBase(): Promise<string> {\n const cdnResponse = await this.axiosBase.get(\n \"https://media.savetube.me/api/random-cdn\",\n {\n timeout: 5_000,\n }\n );\n\n const cdnHost = cdnResponse.data?.cdn as string | undefined;\n if (!cdnHost) throw new Error(\"SaveTube random-cdn returned no cdn host.\");\n\n return /^https?:\\/\\//i.test(cdnHost) ? cdnHost : `https://${cdnHost}`;\n }\n\n async getInfo(\n youtubeUrl: string\n ): Promise<{ cdnBase: string; info: SaveTubeInfo }> {\n const cdnBase = await this.getCdnBase();\n\n const infoResponse = await this.axiosBase.post(\n `${cdnBase}/v2/info`,\n { url: youtubeUrl },\n {\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n }\n );\n\n const raw = infoResponse.data;\n if (!raw?.data)\n throw new Error(\"Invalid SaveTube /v2/info response (missing data).\");\n\n const decoded = decode(raw.data);\n\n if (!decoded?.key)\n throw new Error(\"Invalid SaveTube decoded info (missing key).\");\n\n return { cdnBase, info: decoded as SaveTubeInfo };\n }\n\n async getAudio(\n youtubeUrl: string,\n qualityKbps: number\n ): Promise<DownloadInfo> {\n const { cdnBase, info } = await this.getInfo(youtubeUrl);\n\n const resp = await this.axiosBase.post(\n `${cdnBase}/download`,\n { downloadType: \"audio\", quality: String(qualityKbps), key: info.key },\n {\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n }\n );\n\n const downloadUrl = resp.data?.data?.downloadUrl as string | undefined;\n if (!downloadUrl) throw new Error(\"Invalid SaveTube audio downloadUrl.\");\n\n return {\n title: info.title,\n author: info.author,\n duration: info.duration,\n quality: `${qualityKbps}kbps`,\n filename: `${info.title ?? \"audio\"} ${qualityKbps}kbps.mp3`,\n downloadUrl,\n };\n }\n\n async getVideo(youtubeUrl: string, qualityP: number): Promise<DownloadInfo> {\n const { cdnBase, info } = await this.getInfo(youtubeUrl);\n\n const resp = await this.axiosBase.post(\n `${cdnBase}/download`,\n { downloadType: \"video\", quality: qualityP, key: info.key },\n {\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n }\n );\n\n const downloadUrl = resp.data?.data?.downloadUrl as string | undefined;\n if (!downloadUrl) throw new Error(\"Invalid SaveTube video downloadUrl.\");\n\n return {\n title: info.title,\n author: info.author,\n duration: info.duration,\n quality: `${qualityP}p`,\n filename: `${info.title ?? \"video\"} ${qualityP}p.mp4`,\n downloadUrl,\n };\n }\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport axios, { type AxiosInstance } from \"axios\";\n\nimport { ensureDirSync } from \"./paths.js\";\n\nexport interface DownloadToFileOptions {\n axios?: AxiosInstance;\n timeoutMs?: number;\n fileMode?: number;\n}\n\nexport async function downloadToFile(\n url: string,\n filePath: string,\n opts: DownloadToFileOptions = {}\n): Promise<{ size: number }> {\n const timeoutMs = opts.timeoutMs ?? 120_000;\n const fileMode = opts.fileMode ?? 0o666;\n\n ensureDirSync(path.dirname(filePath));\n\n const client = opts.axios ?? axios;\n\n const response = await client.request({\n url,\n method: \"GET\",\n responseType: \"stream\",\n timeout: timeoutMs,\n });\n\n const writer = fs.createWriteStream(filePath, { mode: fileMode });\n\n await new Promise<void>((resolve, reject) => {\n let done = false;\n\n const finish = () => {\n if (done) return;\n done = true;\n resolve();\n };\n\n const fail = (err: any) => {\n if (done) return;\n done = true;\n reject(err);\n };\n\n writer.on(\"finish\", finish);\n writer.on(\"error\", fail);\n response.data.on(\"error\", fail);\n\n response.data.pipe(writer);\n });\n\n const stats = fs.statSync(filePath);\n if (stats.size === 0) {\n try {\n fs.unlinkSync(filePath);\n } catch {\n // ignore\n }\n throw new Error(\"Downloaded file is empty.\");\n }\n\n return { size: stats.size };\n}\n\nexport interface DownloadToBufferOptions {\n axios?: AxiosInstance;\n timeoutMs?: number;\n maxBytes?: number;\n}\n\nexport async function downloadToBuffer(\n url: string,\n opts: DownloadToBufferOptions = {}\n): Promise<Buffer> {\n const timeoutMs = opts.timeoutMs ?? 120_000;\n const maxBytes = opts.maxBytes ?? 200 * 1024 * 1024;\n\n const client = opts.axios ?? axios;\n\n const response = await client.request({\n url,\n method: \"GET\",\n responseType: \"arraybuffer\",\n timeout: timeoutMs,\n });\n\n const buf = Buffer.from(response.data);\n if (buf.length === 0) throw new Error(\"Downloaded buffer is empty.\");\n if (buf.length > maxBytes)\n throw new Error(`Downloaded buffer exceeds maxBytes (${maxBytes}).`);\n\n return buf;\n}\n","import yts from \"yt-search\";\n\nimport type { PlayMetadata } from \"./types.js\";\n\nexport function stripWeirdUrlWrappers(input: string): string {\n let s = (input || \"\").trim();\n const mdAll = [...s.matchAll(/\\[[^\\]]*\\]\\((https?:\\/\\/[^)\\s]+)\\)/gi)];\n if (mdAll.length > 0) return mdAll[0][1].trim();\n s = s.replace(/^<([^>]+)>$/, \"$1\").trim();\n s = s.replace(/^[\"'`](.*)[\"'`]$/, \"$1\").trim();\n\n return s;\n}\n\nexport function getYouTubeVideoId(input: string): string | null {\n const regex =\n /(?:https?:\\/\\/)?(?:www\\.)?(?:youtube\\.com\\/(?:[^\\/]+\\/.+\\/|(?:v|e(?:mbed)?)\\/|.*[?&]v=|shorts\\/)|youtu\\.be\\/)([^\"&?\\/\\s]{11})/i;\n\n const match = (input || \"\").match(regex);\n return match ? match[1] : null;\n}\n\nexport function normalizeYoutubeUrl(input: string): string | null {\n const cleaned0 = stripWeirdUrlWrappers(input);\n\n const firstUrl = cleaned0.match(/https?:\\/\\/[^\\s)]+/i)?.[0] ?? cleaned0;\n\n const id = getYouTubeVideoId(firstUrl);\n if (!id) return null;\n\n return `https://www.youtube.com/watch?v=${id}`;\n}\n\nexport async function searchBest(query: string): Promise<PlayMetadata | null> {\n const result = await yts(query);\n const v = result?.videos?.[0];\n if (!v) return null;\n\n const durationSeconds = v.duration?.seconds ?? 0;\n\n const normalizedUrl = normalizeYoutubeUrl(v.url) ?? v.url;\n\n return {\n title: v.title || \"Untitled\",\n author: v.author?.name || undefined,\n duration: v.duration?.timestamp || undefined,\n thumb: v.image || v.thumbnail || undefined,\n videoId: v.videoId,\n url: normalizedUrl,\n durationSeconds,\n };\n}\n"],"mappings":"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","names":["index_exports","__export","PlayEngine","__toCommonJS","import_node_fs","import_node_path","import_node_fs","CacheStore","opts","requestId","entry","loading","e","type","file","now","removed","f","fs","import_node_fs","import_node_path","import_node_os","ensureDirSync","dirPath","fs","resolvePaths","cacheDir","baseDir","path","os","resolvedBase","resolvedCache","import_axios","import_node_crypto","http","https","DEFAULT_UA","decode","enc","secretKeyHex","data","iv","content","key","decipher","decrypted","createAxiosBase","opts","timeout","axios","s","SaveTubeClient","cdnHost","youtubeUrl","cdnBase","raw","decoded","qualityKbps","info","downloadUrl","qualityP","import_node_fs","import_node_path","import_axios","downloadToFile","url","filePath","opts","timeoutMs","fileMode","ensureDirSync","path","response","axios","writer","fs","resolve","reject","done","finish","fail","err","stats","import_yt_search","stripWeirdUrlWrappers","input","s","mdAll","getYouTubeVideoId","regex","match","normalizeYoutubeUrl","cleaned0","firstUrl","id","searchBest","query","v","yts","durationSeconds","normalizedUrl","AUDIO_QUALITIES","VIDEO_QUALITIES","pickQuality","requested","available","sanitizeFilename","filename","PlayEngine","options","resolvePaths","CacheStore","SaveTubeClient","prefix","query","searchBest","requestId","metadata","normalized","normalizeYoutubeUrl","normalizedMeta","audioKbps","videoP","audioTask","videoTask","type","entry","cached","fs","directFile","timeoutMs","intervalMs","started","f","r","youtubeUrl","quality","info","safeTitle","filePath","path","size","downloadToFile","buffer","err","ext"]}
@@ -0,0 +1,63 @@
1
+ type MediaType = "audio" | "video";
2
+ interface PlayMetadata {
3
+ title: string;
4
+ author?: string;
5
+ duration?: string;
6
+ durationSeconds: number;
7
+ thumb?: string;
8
+ videoId: string;
9
+ url: string;
10
+ }
11
+ interface CachedFile {
12
+ path: string;
13
+ size: number;
14
+ info: {
15
+ quality: string;
16
+ };
17
+ buffer?: Buffer;
18
+ }
19
+ interface CacheEntry {
20
+ metadata: PlayMetadata;
21
+ audio: CachedFile | null;
22
+ video: CachedFile | null;
23
+ expiresAt: number;
24
+ loading: boolean;
25
+ }
26
+ interface PlayEngineOptions {
27
+ cacheDir?: string;
28
+ ttlMs?: number;
29
+ maxPreloadDurationSeconds?: number;
30
+ preferredAudioKbps?: 320 | 256 | 192 | 128 | 96 | 64;
31
+ preferredVideoP?: 1080 | 720 | 480 | 360;
32
+ preloadBuffer?: boolean;
33
+ cleanupIntervalMs?: number;
34
+ logger?: {
35
+ info?: (...args: any[]) => void;
36
+ warn?: (...args: any[]) => void;
37
+ error?: (...args: any[]) => void;
38
+ debug?: (...args: any[]) => void;
39
+ };
40
+ }
41
+
42
+ declare class PlayEngine {
43
+ private readonly opts;
44
+ private readonly paths;
45
+ private readonly cache;
46
+ private readonly saveTube;
47
+ constructor(options?: PlayEngineOptions);
48
+ generateRequestId(prefix?: string): string;
49
+ search(query: string): Promise<PlayMetadata | null>;
50
+ getFromCache(requestId: string): CacheEntry | undefined;
51
+ preload(metadata: PlayMetadata, requestId: string): Promise<void>;
52
+ getOrDownload(requestId: string, type: MediaType): Promise<{
53
+ metadata: PlayMetadata;
54
+ file: CachedFile;
55
+ direct: boolean;
56
+ }>;
57
+ waitCache(requestId: string, type: MediaType, timeoutMs?: number, intervalMs?: number): Promise<CachedFile | null>;
58
+ cleanup(requestId: string): void;
59
+ private preloadOne;
60
+ private downloadDirect;
61
+ }
62
+
63
+ export { type CachedFile, PlayEngine, type PlayEngineOptions, type PlayMetadata };
@@ -0,0 +1,63 @@
1
+ type MediaType = "audio" | "video";
2
+ interface PlayMetadata {
3
+ title: string;
4
+ author?: string;
5
+ duration?: string;
6
+ durationSeconds: number;
7
+ thumb?: string;
8
+ videoId: string;
9
+ url: string;
10
+ }
11
+ interface CachedFile {
12
+ path: string;
13
+ size: number;
14
+ info: {
15
+ quality: string;
16
+ };
17
+ buffer?: Buffer;
18
+ }
19
+ interface CacheEntry {
20
+ metadata: PlayMetadata;
21
+ audio: CachedFile | null;
22
+ video: CachedFile | null;
23
+ expiresAt: number;
24
+ loading: boolean;
25
+ }
26
+ interface PlayEngineOptions {
27
+ cacheDir?: string;
28
+ ttlMs?: number;
29
+ maxPreloadDurationSeconds?: number;
30
+ preferredAudioKbps?: 320 | 256 | 192 | 128 | 96 | 64;
31
+ preferredVideoP?: 1080 | 720 | 480 | 360;
32
+ preloadBuffer?: boolean;
33
+ cleanupIntervalMs?: number;
34
+ logger?: {
35
+ info?: (...args: any[]) => void;
36
+ warn?: (...args: any[]) => void;
37
+ error?: (...args: any[]) => void;
38
+ debug?: (...args: any[]) => void;
39
+ };
40
+ }
41
+
42
+ declare class PlayEngine {
43
+ private readonly opts;
44
+ private readonly paths;
45
+ private readonly cache;
46
+ private readonly saveTube;
47
+ constructor(options?: PlayEngineOptions);
48
+ generateRequestId(prefix?: string): string;
49
+ search(query: string): Promise<PlayMetadata | null>;
50
+ getFromCache(requestId: string): CacheEntry | undefined;
51
+ preload(metadata: PlayMetadata, requestId: string): Promise<void>;
52
+ getOrDownload(requestId: string, type: MediaType): Promise<{
53
+ metadata: PlayMetadata;
54
+ file: CachedFile;
55
+ direct: boolean;
56
+ }>;
57
+ waitCache(requestId: string, type: MediaType, timeoutMs?: number, intervalMs?: number): Promise<CachedFile | null>;
58
+ cleanup(requestId: string): void;
59
+ private preloadOne;
60
+ private downloadDirect;
61
+ }
62
+
63
+ export { type CachedFile, PlayEngine, type PlayEngineOptions, type PlayMetadata };
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import x from"fs";import B from"path";import S from"fs";var f=class{constructor(e){this.opts=e}store=new Map;cleanupTimer;get(e){return this.store.get(e)}set(e,t){this.store.set(e,t)}has(e){return this.store.has(e)}delete(e){this.cleanupEntry(e),this.store.delete(e)}markLoading(e,t){let r=this.store.get(e);r&&(r.loading=t)}setFile(e,t,r){let o=this.store.get(e);o&&(o[t]=r)}cleanupExpired(e=Date.now()){let t=0;for(let[r,o]of this.store.entries())e>o.expiresAt&&(this.delete(r),t++);return t}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(e){let t=this.store.get(e);t&&["audio","video"].forEach(r=>{let o=t[r];if(o?.path&&S.existsSync(o.path))try{S.unlinkSync(o.path)}catch{}})}};import u from"fs";import v from"path";import U from"os";function h(n){u.mkdirSync(n,{recursive:!0,mode:511});try{u.chmodSync(n,511)}catch{}u.accessSync(n,u.constants.R_OK|u.constants.W_OK)}function A(n){let e=n?.trim()?n:v.join(U.tmpdir(),"yt-play"),t=v.resolve(e),r=v.join(t,"play-cache");return h(t),h(r),{baseDir:t,cacheDir:r}}import F from"axios";import{createDecipheriv as _}from"crypto";import*as P from"http";import*as E from"https";var z="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36";function j(n){let e="C5D58EF67A7584E4A29F6C35BBC4EB12",t=Buffer.from(n,"base64"),r=t.subarray(0,16),o=t.subarray(16),i=Buffer.from(e,"hex"),a=_("aes-128-cbc",i,r),s=Buffer.concat([a.update(o),a.final()]);return JSON.parse(s.toString("utf8"))}function R(n){if(n.axios)return n.axios;let e=n.timeoutMs??6e4;return F.create({timeout:e,maxRedirects:0,validateStatus:t=>t>=200&&t<400,httpAgent:new P.Agent({keepAlive:!0}),httpsAgent:new E.Agent({keepAlive:!0}),headers:{"User-Agent":n.userAgent??z,"Accept-Language":"en-US,en;q=0.9",Referer:"https://www.youtube.com",Origin:"https://www.youtube.com"}})}var m=class{constructor(e={}){this.opts=e;this.axiosBase=R(e)}axiosBase;async getCdnBase(){let t=(await this.axiosBase.get("https://media.savetube.me/api/random-cdn",{timeout:5e3})).data?.cdn;if(!t)throw new Error("SaveTube random-cdn returned no cdn host.");return/^https?:\/\//i.test(t)?t:`https://${t}`}async getInfo(e){let t=await this.getCdnBase(),o=(await this.axiosBase.post(`${t}/v2/info`,{url:e},{headers:{"Content-Type":"application/json",Accept:"application/json"}})).data;if(!o?.data)throw new Error("Invalid SaveTube /v2/info response (missing data).");let i=j(o.data);if(!i?.key)throw new Error("Invalid SaveTube decoded info (missing key).");return{cdnBase:t,info:i}}async getAudio(e,t){let{cdnBase:r,info:o}=await this.getInfo(e),a=(await this.axiosBase.post(`${r}/download`,{downloadType:"audio",quality:String(t),key:o.key},{headers:{"Content-Type":"application/json",Accept:"application/json"}})).data?.data?.downloadUrl;if(!a)throw new Error("Invalid SaveTube audio downloadUrl.");return{title:o.title,author:o.author,duration:o.duration,quality:`${t}kbps`,filename:`${o.title??"audio"} ${t}kbps.mp3`,downloadUrl:a}}async getVideo(e,t){let{cdnBase:r,info:o}=await this.getInfo(e),a=(await this.axiosBase.post(`${r}/download`,{downloadType:"video",quality:t,key:o.key},{headers:{"Content-Type":"application/json",Accept:"application/json"}})).data?.data?.downloadUrl;if(!a)throw new Error("Invalid SaveTube video downloadUrl.");return{title:o.title,author:o.author,duration:o.duration,quality:`${t}p`,filename:`${o.title??"video"} ${t}p.mp4`,downloadUrl:a}}};import b from"fs";import V from"path";import K from"axios";async function T(n,e,t={}){let r=t.timeoutMs??12e4,o=t.fileMode??438;h(V.dirname(e));let a=await(t.axios??K).request({url:n,method:"GET",responseType:"stream",timeout:r}),s=b.createWriteStream(e,{mode:o});await new Promise((d,p)=>{let l=!1,w=()=>{l||(l=!0,d())},D=O=>{l||(l=!0,p(O))};s.on("finish",w),s.on("error",D),a.data.on("error",D),a.data.pipe(s)});let c=b.statSync(e);if(c.size===0){try{b.unlinkSync(e)}catch{}throw new Error("Downloaded file is empty.")}return{size:c.size}}import L from"yt-search";function W(n){let e=(n||"").trim(),t=[...e.matchAll(/\[[^\]]*\]\((https?:\/\/[^)\s]+)\)/gi)];return t.length>0?t[0][1].trim():(e=e.replace(/^<([^>]+)>$/,"$1").trim(),e=e.replace(/^["'`](.*)["'`]$/,"$1").trim(),e)}function Y(n){let e=/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=|shorts\/)|youtu\.be\/)([^"&?\/\s]{11})/i,t=(n||"").match(e);return t?t[1]:null}function g(n){let e=W(n),t=e.match(/https?:\/\/[^\s)]+/i)?.[0]??e,r=Y(t);return r?`https://www.youtube.com/watch?v=${r}`:null}async function I(n){let t=(await L(n))?.videos?.[0];if(!t)return null;let r=t.duration?.seconds??0,o=g(t.url)??t.url;return{title:t.title||"Untitled",author:t.author?.name||void 0,duration:t.duration?.timestamp||void 0,thumb:t.image||t.thumbnail||void 0,videoId:t.videoId,url:o,durationSeconds:r}}var $=[320,256,192,128,96,64],C=[1080,720,480,360];function y(n,e){return e.includes(n)?n:e[0]}function k(n){return(n||"").replace(/[\\\/:*?"<>|]/g,"").replace(/[^\w\s-]/gi,"").trim().replace(/\s+/g," ").substring(0,100)}var M=class{opts;paths;cache;saveTube;constructor(e={}){this.opts={ttlMs:e.ttlMs??3*6e4,maxPreloadDurationSeconds:e.maxPreloadDurationSeconds??1200,preferredAudioKbps:e.preferredAudioKbps??128,preferredVideoP:e.preferredVideoP??720,preloadBuffer:e.preloadBuffer??!0,cleanupIntervalMs:e.cleanupIntervalMs??3e4,logger:e.logger},this.paths=A(e.cacheDir),this.cache=new f({cleanupIntervalMs:this.opts.cleanupIntervalMs}),this.cache.start(),this.saveTube=new m({})}generateRequestId(e="play"){return`${e}_${Date.now()}_${Math.random().toString(36).slice(2,8)}`}async search(e){return I(e)}getFromCache(e){return this.cache.get(e)}async preload(e,t){if(e.durationSeconds>this.opts.maxPreloadDurationSeconds)throw new Error("Video too long for preload.");let r=g(e.url);if(!r)throw new Error("Invalid YouTube URL.");let o={...e,url:r};this.cache.set(t,{metadata:o,audio:null,video:null,expiresAt:Date.now()+this.opts.ttlMs,loading:!0});let i=y(this.opts.preferredAudioKbps,$),a=y(this.opts.preferredVideoP,C),s=this.preloadOne(t,"audio",r,i),c=this.preloadOne(t,"video",r,a);Promise.allSettled([s,c]).finally(()=>this.cache.markLoading(t,!1)).catch(()=>this.cache.markLoading(t,!1))}async getOrDownload(e,t){let r=this.cache.get(e);if(!r)throw new Error("Request not found (cache miss).");let o=r[t];if(o?.path&&x.existsSync(o.path)&&o.size>0)return{metadata:r.metadata,file:o,direct:!1};let i=g(r.metadata.url);if(!i)throw new Error("Invalid YouTube URL.");let a=await this.downloadDirect(t,i);return{metadata:r.metadata,file:a,direct:!0}}async waitCache(e,t,r=8e3,o=500){let i=Date.now();for(;Date.now()-i<r;){let s=this.cache.get(e)?.[t];if(s?.path&&x.existsSync(s.path)&&s.size>0)return s;await new Promise(c=>setTimeout(c,o))}return null}cleanup(e){this.cache.delete(e)}async preloadOne(e,t,r,o){try{let i=t==="audio"?await this.saveTube.getAudio(r,o):await this.saveTube.getVideo(r,o),a=k(i.title??t),c=`${t}_${e}_${a}.${t==="audio"?"mp3":"mp4"}`,d=B.join(this.paths.cacheDir,c),{size:p}=await T(i.downloadUrl,d),l;this.opts.preloadBuffer&&(l=await x.promises.readFile(d));let w={path:d,size:p,info:{quality:i.quality},buffer:l};this.cache.setFile(e,t,w),this.opts.logger?.debug?.(`preloaded ${t} ${p} bytes: ${c}`)}catch(i){this.opts.logger?.error?.(`preload ${t} failed`,i)}}async downloadDirect(e,t){let r=y(this.opts.preferredAudioKbps,$),o=y(this.opts.preferredVideoP,C),i=e==="audio"?await this.saveTube.getAudio(t,r):await this.saveTube.getVideo(t,o),a=e==="audio"?"mp3":"mp4",s=k(i.title??e),c=B.join(this.paths.cacheDir,`${e}_direct_${Date.now()}_${s}.${a}`),{size:d}=await T(i.downloadUrl,c);return{path:c,size:d,info:{quality:i.quality}}}};export{M as PlayEngine};
2
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/play-engine.ts","../src/core/cache.ts","../src/core/paths.ts","../src/core/savetube.ts","../src/core/download.ts","../src/core/youtube.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport type {\n CacheEntry,\n CachedFile,\n DownloadInfo,\n MediaType,\n PlayEngineOptions,\n PlayMetadata,\n} from \"./types.js\";\nimport { CacheStore } from \"./cache.js\";\nimport { resolvePaths } from \"./paths.js\";\nimport { SaveTubeClient } from \"./savetube.js\";\nimport { downloadToBuffer, downloadToFile } from \"./download.js\";\nimport { normalizeYoutubeUrl, searchBest } from \"./youtube.js\";\n\nconst AUDIO_QUALITIES = [320, 256, 192, 128, 96, 64] as const;\nconst VIDEO_QUALITIES = [1080, 720, 480, 360] as const;\n\nfunction pickQuality<T extends number>(\n requested: T,\n available: readonly T[]\n): T {\n return (available as readonly number[]).includes(requested)\n ? requested\n : available[0];\n}\n\nfunction sanitizeFilename(filename: string): string {\n return (filename || \"\")\n .replace(/[\\\\\\/:*?\"<>|]/g, \"\")\n .replace(/[^\\w\\s-]/gi, \"\")\n .trim()\n .replace(/\\s+/g, \" \")\n .substring(0, 100);\n}\n\nexport class PlayEngine {\n private readonly opts: Required<\n Pick<\n PlayEngineOptions,\n | \"ttlMs\"\n | \"maxPreloadDurationSeconds\"\n | \"preferredAudioKbps\"\n | \"preferredVideoP\"\n | \"preloadBuffer\"\n | \"cleanupIntervalMs\"\n >\n > &\n Pick<PlayEngineOptions, \"logger\">;\n\n private readonly paths: { baseDir: string; cacheDir: string };\n private readonly cache: CacheStore;\n private readonly saveTube: SaveTubeClient;\n\n constructor(options: PlayEngineOptions = {}) {\n this.opts = {\n ttlMs: options.ttlMs ?? 3 * 60_000,\n maxPreloadDurationSeconds: options.maxPreloadDurationSeconds ?? 20 * 60,\n preferredAudioKbps: options.preferredAudioKbps ?? 128,\n preferredVideoP: options.preferredVideoP ?? 720,\n preloadBuffer: options.preloadBuffer ?? true,\n cleanupIntervalMs: options.cleanupIntervalMs ?? 30_000,\n logger: options.logger,\n };\n\n this.paths = resolvePaths(options.cacheDir);\n this.cache = new CacheStore({\n cleanupIntervalMs: this.opts.cleanupIntervalMs,\n });\n this.cache.start();\n\n this.saveTube = new SaveTubeClient({\n // mantém axios interno\n });\n }\n\n generateRequestId(prefix = \"play\"): string {\n return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;\n }\n\n async search(query: string): Promise<PlayMetadata | null> {\n return searchBest(query);\n }\n\n getFromCache(requestId: string): CacheEntry | undefined {\n return this.cache.get(requestId);\n }\n\n async preload(metadata: PlayMetadata, requestId: string): Promise<void> {\n if (metadata.durationSeconds > this.opts.maxPreloadDurationSeconds) {\n throw new Error(\"Video too long for preload.\");\n }\n\n const normalized = normalizeYoutubeUrl(metadata.url);\n if (!normalized) throw new Error(\"Invalid YouTube URL.\");\n\n const normalizedMeta: PlayMetadata = { ...metadata, url: normalized };\n\n this.cache.set(requestId, {\n metadata: normalizedMeta,\n audio: null,\n video: null,\n expiresAt: Date.now() + this.opts.ttlMs,\n loading: true,\n });\n\n const audioKbps = pickQuality(\n this.opts.preferredAudioKbps,\n AUDIO_QUALITIES\n );\n const videoP = pickQuality(this.opts.preferredVideoP, VIDEO_QUALITIES);\n\n const audioTask = this.preloadOne(\n requestId,\n \"audio\",\n normalized,\n audioKbps\n );\n const videoTask = this.preloadOne(requestId, \"video\", normalized, videoP);\n\n Promise.allSettled([audioTask, videoTask])\n .finally(() => this.cache.markLoading(requestId, false))\n .catch(() => this.cache.markLoading(requestId, false));\n }\n\n async getOrDownload(\n requestId: string,\n type: MediaType\n ): Promise<{ metadata: PlayMetadata; file: CachedFile; direct: boolean }> {\n const entry = this.cache.get(requestId);\n if (!entry) throw new Error(\"Request not found (cache miss).\");\n\n const cached = entry[type];\n if (cached?.path && fs.existsSync(cached.path) && cached.size > 0) {\n return { metadata: entry.metadata, file: cached, direct: false };\n }\n\n const normalized = normalizeYoutubeUrl(entry.metadata.url);\n if (!normalized) throw new Error(\"Invalid YouTube URL.\");\n\n const directFile = await this.downloadDirect(type, normalized);\n return { metadata: entry.metadata, file: directFile, direct: true };\n }\n\n async waitCache(\n requestId: string,\n type: MediaType,\n timeoutMs = 8_000,\n intervalMs = 500\n ): Promise<CachedFile | null> {\n const started = Date.now();\n while (Date.now() - started < timeoutMs) {\n const entry = this.cache.get(requestId);\n const f = entry?.[type];\n if (f?.path && fs.existsSync(f.path) && f.size > 0) return f;\n await new Promise((r) => setTimeout(r, intervalMs));\n }\n return null;\n }\n\n cleanup(requestId: string): void {\n this.cache.delete(requestId);\n }\n\n private async preloadOne(\n requestId: string,\n type: MediaType,\n youtubeUrl: string,\n quality: number\n ): Promise<void> {\n try {\n const info: DownloadInfo =\n type === \"audio\"\n ? await this.saveTube.getAudio(youtubeUrl, quality)\n : await this.saveTube.getVideo(youtubeUrl, quality);\n\n const safeTitle = sanitizeFilename(info.title ?? type);\n const ext = type === \"audio\" ? \"mp3\" : \"mp4\";\n const filename = `${type}_${requestId}_${safeTitle}.${ext}`;\n const filePath = path.join(this.paths.cacheDir, filename);\n\n const { size } = await downloadToFile(info.downloadUrl, filePath);\n\n let buffer: Buffer | undefined;\n if (this.opts.preloadBuffer) {\n buffer = await fs.promises.readFile(filePath);\n }\n\n const cached: CachedFile = {\n path: filePath,\n size,\n info: { quality: info.quality },\n buffer,\n };\n\n this.cache.setFile(requestId, type, cached);\n\n this.opts.logger?.debug?.(`preloaded ${type} ${size} bytes: ${filename}`);\n } catch (err) {\n this.opts.logger?.error?.(`preload ${type} failed`, err);\n }\n }\n\n private async downloadDirect(\n type: MediaType,\n youtubeUrl: string\n ): Promise<CachedFile> {\n const audioKbps = pickQuality(\n this.opts.preferredAudioKbps,\n AUDIO_QUALITIES\n );\n const videoP = pickQuality(this.opts.preferredVideoP, VIDEO_QUALITIES);\n\n const info =\n type === \"audio\"\n ? await this.saveTube.getAudio(youtubeUrl, audioKbps)\n : await this.saveTube.getVideo(youtubeUrl, videoP);\n\n const ext = type === \"audio\" ? \"mp3\" : \"mp4\";\n const safeTitle = sanitizeFilename(info.title ?? type);\n const filePath = path.join(\n this.paths.cacheDir,\n `${type}_direct_${Date.now()}_${safeTitle}.${ext}`\n );\n\n const { size } = await downloadToFile(info.downloadUrl, filePath);\n\n return {\n path: filePath,\n size,\n info: { quality: info.quality },\n };\n }\n}\n","import fs from \"node:fs\";\n\nimport type { CacheEntry, MediaType } from \"./types.js\";\n\nexport class CacheStore {\n private readonly store = new Map<string, CacheEntry>();\n private cleanupTimer?: NodeJS.Timeout;\n\n constructor(\n private readonly opts: {\n cleanupIntervalMs: number;\n }\n ) {}\n\n get(requestId: string): CacheEntry | undefined {\n return this.store.get(requestId);\n }\n\n set(requestId: string, entry: CacheEntry): void {\n this.store.set(requestId, entry);\n }\n\n has(requestId: string): boolean {\n return this.store.has(requestId);\n }\n\n delete(requestId: string): void {\n this.cleanupEntry(requestId);\n this.store.delete(requestId);\n }\n\n markLoading(requestId: string, loading: boolean): void {\n const e = this.store.get(requestId);\n if (e) e.loading = loading;\n }\n\n setFile(\n requestId: string,\n type: MediaType,\n file: CacheEntry[MediaType]\n ): void {\n const e = this.store.get(requestId);\n if (!e) return;\n e[type] = file as any;\n }\n\n cleanupExpired(now = Date.now()): number {\n let removed = 0;\n for (const [requestId, entry] of this.store.entries()) {\n if (now > entry.expiresAt) {\n this.delete(requestId);\n removed++;\n }\n }\n return removed;\n }\n\n start(): void {\n if (this.cleanupTimer) return;\n\n this.cleanupTimer = setInterval(() => {\n this.cleanupExpired(Date.now());\n }, this.opts.cleanupIntervalMs);\n\n this.cleanupTimer.unref();\n }\n\n stop(): void {\n if (!this.cleanupTimer) return;\n clearInterval(this.cleanupTimer);\n this.cleanupTimer = undefined;\n }\n\n private cleanupEntry(requestId: string) {\n const entry = this.store.get(requestId);\n if (!entry) return;\n\n ([\"audio\", \"video\"] as const).forEach((type) => {\n const f = entry[type];\n if (f?.path && fs.existsSync(f.path)) {\n try {\n fs.unlinkSync(f.path);\n } catch {\n // ignore\n }\n }\n });\n }\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport os from \"node:os\";\n\nexport interface ResolvedPaths {\n baseDir: string;\n cacheDir: string;\n}\n\nexport function ensureDirSync(dirPath: string) {\n fs.mkdirSync(dirPath, { recursive: true, mode: 0o777 });\n\n try {\n fs.chmodSync(dirPath, 0o777);\n } catch {\n // ignore\n }\n\n fs.accessSync(dirPath, fs.constants.R_OK | fs.constants.W_OK);\n}\n\nexport function resolvePaths(cacheDir?: string): ResolvedPaths {\n const baseDir = cacheDir?.trim()\n ? cacheDir\n : path.join(os.tmpdir(), \"yt-play\");\n const resolvedBase = path.resolve(baseDir);\n\n const resolvedCache = path.join(resolvedBase, \"play-cache\");\n\n ensureDirSync(resolvedBase);\n ensureDirSync(resolvedCache);\n\n return {\n baseDir: resolvedBase,\n cacheDir: resolvedCache,\n };\n}\n","import axios, { type AxiosInstance } from \"axios\";\nimport { createDecipheriv } from \"node:crypto\";\nimport * as http from \"node:http\";\nimport * as https from \"node:https\";\n\nimport type { DownloadInfo } from \"./types.js\";\n\nexport interface SaveTubeClientOptions {\n axios?: AxiosInstance;\n timeoutMs?: number;\n userAgent?: string;\n}\n\ntype SaveTubeInfo = {\n key: string;\n title?: string;\n author?: string;\n duration?: string;\n};\n\nconst DEFAULT_UA =\n \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36\";\n\nfunction decode(enc: string): any {\n const secretKeyHex = \"C5D58EF67A7584E4A29F6C35BBC4EB12\";\n const data = Buffer.from(enc, \"base64\");\n const iv = data.subarray(0, 16);\n const content = data.subarray(16);\n\n const key = Buffer.from(secretKeyHex, \"hex\");\n const decipher = createDecipheriv(\"aes-128-cbc\", key, iv);\n const decrypted = Buffer.concat([decipher.update(content), decipher.final()]);\n return JSON.parse(decrypted.toString(\"utf8\"));\n}\n\nfunction createAxiosBase(opts: SaveTubeClientOptions): AxiosInstance {\n if (opts.axios) return opts.axios;\n\n const timeout = opts.timeoutMs ?? 60_000;\n return axios.create({\n timeout,\n maxRedirects: 0,\n validateStatus: (s) => s >= 200 && s < 400,\n httpAgent: new http.Agent({ keepAlive: true }),\n httpsAgent: new https.Agent({ keepAlive: true }),\n headers: {\n \"User-Agent\": opts.userAgent ?? DEFAULT_UA,\n \"Accept-Language\": \"en-US,en;q=0.9\",\n Referer: \"https://www.youtube.com\",\n Origin: \"https://www.youtube.com\",\n },\n });\n}\n\nexport class SaveTubeClient {\n private readonly axiosBase: AxiosInstance;\n\n constructor(private readonly opts: SaveTubeClientOptions = {}) {\n this.axiosBase = createAxiosBase(opts);\n }\n\n private async getCdnBase(): Promise<string> {\n const cdnResponse = await this.axiosBase.get(\n \"https://media.savetube.me/api/random-cdn\",\n {\n timeout: 5_000,\n }\n );\n\n const cdnHost = cdnResponse.data?.cdn as string | undefined;\n if (!cdnHost) throw new Error(\"SaveTube random-cdn returned no cdn host.\");\n\n return /^https?:\\/\\//i.test(cdnHost) ? cdnHost : `https://${cdnHost}`;\n }\n\n async getInfo(\n youtubeUrl: string\n ): Promise<{ cdnBase: string; info: SaveTubeInfo }> {\n const cdnBase = await this.getCdnBase();\n\n const infoResponse = await this.axiosBase.post(\n `${cdnBase}/v2/info`,\n { url: youtubeUrl },\n {\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n }\n );\n\n const raw = infoResponse.data;\n if (!raw?.data)\n throw new Error(\"Invalid SaveTube /v2/info response (missing data).\");\n\n const decoded = decode(raw.data);\n\n if (!decoded?.key)\n throw new Error(\"Invalid SaveTube decoded info (missing key).\");\n\n return { cdnBase, info: decoded as SaveTubeInfo };\n }\n\n async getAudio(\n youtubeUrl: string,\n qualityKbps: number\n ): Promise<DownloadInfo> {\n const { cdnBase, info } = await this.getInfo(youtubeUrl);\n\n const resp = await this.axiosBase.post(\n `${cdnBase}/download`,\n { downloadType: \"audio\", quality: String(qualityKbps), key: info.key },\n {\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n }\n );\n\n const downloadUrl = resp.data?.data?.downloadUrl as string | undefined;\n if (!downloadUrl) throw new Error(\"Invalid SaveTube audio downloadUrl.\");\n\n return {\n title: info.title,\n author: info.author,\n duration: info.duration,\n quality: `${qualityKbps}kbps`,\n filename: `${info.title ?? \"audio\"} ${qualityKbps}kbps.mp3`,\n downloadUrl,\n };\n }\n\n async getVideo(youtubeUrl: string, qualityP: number): Promise<DownloadInfo> {\n const { cdnBase, info } = await this.getInfo(youtubeUrl);\n\n const resp = await this.axiosBase.post(\n `${cdnBase}/download`,\n { downloadType: \"video\", quality: qualityP, key: info.key },\n {\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n }\n );\n\n const downloadUrl = resp.data?.data?.downloadUrl as string | undefined;\n if (!downloadUrl) throw new Error(\"Invalid SaveTube video downloadUrl.\");\n\n return {\n title: info.title,\n author: info.author,\n duration: info.duration,\n quality: `${qualityP}p`,\n filename: `${info.title ?? \"video\"} ${qualityP}p.mp4`,\n downloadUrl,\n };\n }\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport axios, { type AxiosInstance } from \"axios\";\n\nimport { ensureDirSync } from \"./paths.js\";\n\nexport interface DownloadToFileOptions {\n axios?: AxiosInstance;\n timeoutMs?: number;\n fileMode?: number;\n}\n\nexport async function downloadToFile(\n url: string,\n filePath: string,\n opts: DownloadToFileOptions = {}\n): Promise<{ size: number }> {\n const timeoutMs = opts.timeoutMs ?? 120_000;\n const fileMode = opts.fileMode ?? 0o666;\n\n ensureDirSync(path.dirname(filePath));\n\n const client = opts.axios ?? axios;\n\n const response = await client.request({\n url,\n method: \"GET\",\n responseType: \"stream\",\n timeout: timeoutMs,\n });\n\n const writer = fs.createWriteStream(filePath, { mode: fileMode });\n\n await new Promise<void>((resolve, reject) => {\n let done = false;\n\n const finish = () => {\n if (done) return;\n done = true;\n resolve();\n };\n\n const fail = (err: any) => {\n if (done) return;\n done = true;\n reject(err);\n };\n\n writer.on(\"finish\", finish);\n writer.on(\"error\", fail);\n response.data.on(\"error\", fail);\n\n response.data.pipe(writer);\n });\n\n const stats = fs.statSync(filePath);\n if (stats.size === 0) {\n try {\n fs.unlinkSync(filePath);\n } catch {\n // ignore\n }\n throw new Error(\"Downloaded file is empty.\");\n }\n\n return { size: stats.size };\n}\n\nexport interface DownloadToBufferOptions {\n axios?: AxiosInstance;\n timeoutMs?: number;\n maxBytes?: number;\n}\n\nexport async function downloadToBuffer(\n url: string,\n opts: DownloadToBufferOptions = {}\n): Promise<Buffer> {\n const timeoutMs = opts.timeoutMs ?? 120_000;\n const maxBytes = opts.maxBytes ?? 200 * 1024 * 1024;\n\n const client = opts.axios ?? axios;\n\n const response = await client.request({\n url,\n method: \"GET\",\n responseType: \"arraybuffer\",\n timeout: timeoutMs,\n });\n\n const buf = Buffer.from(response.data);\n if (buf.length === 0) throw new Error(\"Downloaded buffer is empty.\");\n if (buf.length > maxBytes)\n throw new Error(`Downloaded buffer exceeds maxBytes (${maxBytes}).`);\n\n return buf;\n}\n","import yts from \"yt-search\";\n\nimport type { PlayMetadata } from \"./types.js\";\n\nexport function stripWeirdUrlWrappers(input: string): string {\n let s = (input || \"\").trim();\n const mdAll = [...s.matchAll(/\\[[^\\]]*\\]\\((https?:\\/\\/[^)\\s]+)\\)/gi)];\n if (mdAll.length > 0) return mdAll[0][1].trim();\n s = s.replace(/^<([^>]+)>$/, \"$1\").trim();\n s = s.replace(/^[\"'`](.*)[\"'`]$/, \"$1\").trim();\n\n return s;\n}\n\nexport function getYouTubeVideoId(input: string): string | null {\n const regex =\n /(?:https?:\\/\\/)?(?:www\\.)?(?:youtube\\.com\\/(?:[^\\/]+\\/.+\\/|(?:v|e(?:mbed)?)\\/|.*[?&]v=|shorts\\/)|youtu\\.be\\/)([^\"&?\\/\\s]{11})/i;\n\n const match = (input || \"\").match(regex);\n return match ? match[1] : null;\n}\n\nexport function normalizeYoutubeUrl(input: string): string | null {\n const cleaned0 = stripWeirdUrlWrappers(input);\n\n const firstUrl = cleaned0.match(/https?:\\/\\/[^\\s)]+/i)?.[0] ?? cleaned0;\n\n const id = getYouTubeVideoId(firstUrl);\n if (!id) return null;\n\n return `https://www.youtube.com/watch?v=${id}`;\n}\n\nexport async function searchBest(query: string): Promise<PlayMetadata | null> {\n const result = await yts(query);\n const v = result?.videos?.[0];\n if (!v) return null;\n\n const durationSeconds = v.duration?.seconds ?? 0;\n\n const normalizedUrl = normalizeYoutubeUrl(v.url) ?? v.url;\n\n return {\n title: v.title || \"Untitled\",\n author: v.author?.name || undefined,\n duration: v.duration?.timestamp || undefined,\n thumb: v.image || v.thumbnail || undefined,\n videoId: v.videoId,\n url: normalizedUrl,\n durationSeconds,\n };\n}\n"],"mappings":"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","names":["fs","path","fs","CacheStore","opts","requestId","entry","loading","e","type","file","now","removed","f","fs","path","os","ensureDirSync","dirPath","resolvePaths","cacheDir","baseDir","resolvedBase","resolvedCache","axios","createDecipheriv","http","https","DEFAULT_UA","decode","enc","secretKeyHex","data","iv","content","key","decipher","decrypted","createAxiosBase","opts","timeout","s","SaveTubeClient","cdnHost","youtubeUrl","cdnBase","raw","decoded","qualityKbps","info","downloadUrl","qualityP","fs","path","axios","downloadToFile","url","filePath","opts","timeoutMs","fileMode","ensureDirSync","path","response","axios","writer","fs","resolve","reject","done","finish","fail","err","stats","yts","stripWeirdUrlWrappers","input","s","mdAll","getYouTubeVideoId","regex","match","normalizeYoutubeUrl","cleaned0","firstUrl","id","searchBest","query","v","durationSeconds","normalizedUrl","AUDIO_QUALITIES","VIDEO_QUALITIES","pickQuality","requested","available","sanitizeFilename","filename","PlayEngine","options","resolvePaths","CacheStore","SaveTubeClient","prefix","query","searchBest","requestId","metadata","normalized","normalizeYoutubeUrl","normalizedMeta","audioKbps","videoP","audioTask","videoTask","type","entry","cached","fs","directFile","timeoutMs","intervalMs","started","f","r","youtubeUrl","quality","info","safeTitle","filePath","path","size","downloadToFile","buffer","err","ext"]}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@irithell-js/yt-play",
3
+ "version": "0.1.3",
4
+ "description": "YouTube search + SaveTube download engine (audio/video) with optional caching.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "exports": {
12
+ ".": {
13
+ "import": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.mjs"
16
+ },
17
+ "require": {
18
+ "types": "./dist/index.d.cts",
19
+ "default": "./dist/index.cjs"
20
+ }
21
+ }
22
+ },
23
+ "main": "./dist/index.cjs",
24
+ "module": "./dist/index.mjs",
25
+ "types": "./dist/index.d.ts",
26
+ "scripts": {
27
+ "clean": "rm -rf dist",
28
+ "build": "tsup",
29
+ "dev": "tsup --watch",
30
+ "prepublishOnly": "npm run build",
31
+ "pack:check": "npm pack --silent && node -e \"console.log('packed')\""
32
+ },
33
+ "dependencies": {
34
+ "axios": "^1.13.2",
35
+ "yt-search": "^2.13.1"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^25.0.3",
39
+ "@types/yt-search": "^2.10.3",
40
+ "tsup": "^8.5.1",
41
+ "typescript": "^5.9.3"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ }
46
+ }