wetvlo 0.0.8 → 0.0.10
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/dist/index.js +11 -13
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{fileURLToPath as Je}from"url";import{run as Ke}from"cmd-ts";import*as ye from"readline";import{boolean as je,command as Be,flag as We,option as Ve,string as ze}from"cmd-ts";var se={count:3,checkInterval:600,downloadTypes:["available"]},ne={downloadDir:"./downloads",tempDir:"./downloads",downloadDelay:10,maxRetries:3,initialTimeout:5,backoffMultiplier:2,jitterPercentage:10,minDuration:0},it=["available","vip"],ae="./downloads";import{existsSync as Ie}from"fs";import{readFile as Me}from"fs/promises";import{join as Ne}from"path";import*as ce from"js-yaml";var C=class extends Error{constructor(e){super(e),this.name="WetvloError"}},T=class extends C{constructor(e){super(e),this.name="ConfigError"}},N=class extends C{constructor(e){super(e),this.name="StateError"}},D=class extends C{constructor(t,o){super(t);this.url=o;this.name="HandlerError"}},P=class extends C{constructor(t,o){super(t);this.url=o;this.name="DownloadError"}},F=class extends C{constructor(e){super(e),this.name="NotificationError"}},q=class extends C{constructor(e){super(e),this.name="CookieError"}},O=class extends C{constructor(e){super(e),this.name="SchedulerError"}};function xe(a){return typeof a!="string"?a:a.replace(/\$\{([^}]+)\}/g,(e,t)=>{let o=process.env[t];if(o===void 0)throw new Error(`Environment variable "${t}" is not set`);return o})}function H(a){if(typeof a=="string")return xe(a);if(Array.isArray(a))return a.map(e=>H(e));if(a!==null&&typeof a=="object"){let e={};for(let[t,o]of Object.entries(a))e[t]=H(o);return e}return a}import{z as w}from"zod";var Ce=w.enum(["available","vip","teaser","express","preview","locked"]),K=w.object({count:w.number().positive().optional(),checkInterval:w.number().positive().optional(),downloadTypes:w.array(Ce).optional()}),Z=w.object({downloadDir:w.string().optional(),tempDir:w.string().optional(),downloadDelay:w.number().nonnegative().optional(),maxRetries:w.number().int().nonnegative().optional(),initialTimeout:w.number().positive().optional(),backoffMultiplier:w.number().positive().optional(),jitterPercentage:w.number().int().min(0).max(100).optional(),minDuration:w.number().nonnegative().optional()}),Se=w.object({botToken:w.string(),chatId:w.string()}),De=w.object({domain:w.string(),check:K.optional(),download:Z.optional()}),ke=w.object({name:w.string(),url:w.string().url(),startTime:w.string().regex(/^\d{1,2}:\d{2}$/,{message:'Must be in HH:MM format (e.g., "20:00")'}),check:K.optional(),download:Z.optional()}),Ee=w.object({check:K.optional(),download:Z.optional()}),Te=w.enum(["chrome","firefox","safari","chromium","edge"]),$e=w.object({series:w.array(ke).min(1,"Cannot be empty"),telegram:Se.optional(),globalConfigs:Ee.optional(),stateFile:w.string(),browser:Te,cookieFile:w.string().optional(),domainConfigs:w.array(De).optional()});function le(a){$e.parse(a)}var Le="./config.yaml";async function de(a=Le){let e=Ne(process.cwd(),a);if(!Ie(e))throw new T(`Configuration file not found: "${e}". Create a config.yaml file or specify a different path.`);let t=await Me(e,"utf-8"),o;try{o=ce.load(t)}catch(r){throw new T(`Failed to parse YAML: ${r instanceof Error?r.message:String(r)}`)}return le(o),H(o)}import*as L from"fs";import*as $ from"fs/promises";import{basename as Pe,join as te,resolve as E}from"path";import{execa as fe}from"execa";function ue(a){return a.replace(/[<>:"/\\|?*]/g,"_").replace(/[\x00-\x1F]/g,"").replace(/[\s.]+$/,"")}import{execa as Re}from"execa";var k={reset:"\x1B[0m",bright:"\x1B[1m",dim:"\x1B[2m",black:"\x1B[30m",red:"\x1B[31m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m",magenta:"\x1B[35m",cyan:"\x1B[36m",white:"\x1B[37m",bgRed:"\x1B[41m",bgGreen:"\x1B[42m",bgYellow:"\x1B[43m"},ee=class{config;constructor(e={}){this.config={level:e.level??"INFO",useColors:e.useColors??!0}}getEmoji(e){switch(e){case"DEBUG":return"\u{1F50D}";case"INFO":return"\u2139\uFE0F";case"SUCCESS":return"\u2705";case"WARNING":return"\u26A0\uFE0F";case"ERROR":return"\u274C";case"HIGHLIGHT":return"\u{1F31F}";default:return"\u2022"}}formatDate(e){let t=(e.getMonth()+1).toString().padStart(2,"0"),o=e.getDate().toString().padStart(2,"0"),i=e.getHours().toString().padStart(2,"0"),r=e.getMinutes().toString().padStart(2,"0"),s=e.getSeconds().toString().padStart(2,"0");return`${t}-${o} ${i}:${r}:${s}`}format(e,t){let o=this.formatDate(new Date),i=this.getEmoji(e);return`${o} ${i} ${t}`}colorize(e,t){return this.config.useColors?`${t}${e}${k.reset}`:e}debug(e){this.shouldLog("DEBUG")&&console.log(this.format("DEBUG",this.colorize(e,k.dim)))}info(e){this.shouldLog("INFO")&&console.log(this.format("INFO",this.colorize(e,k.blue)))}success(e){this.shouldLog("SUCCESS")&&console.log(this.format("SUCCESS",this.colorize(e,k.green)))}warning(e){this.shouldLog("WARNING")&&console.log(this.format("WARNING",this.colorize(e,k.yellow)))}error(e){this.shouldLog("ERROR")&&console.error(this.format("ERROR",this.colorize(e,k.red)))}highlight(e){this.shouldLog("HIGHLIGHT")&&console.log(this.format("HIGHLIGHT",this.colorize(e,k.bright+k.magenta)))}shouldLog(e){let t=["DEBUG","INFO","SUCCESS","WARNING","ERROR","HIGHLIGHT"];return t.indexOf(e)>=t.indexOf(this.config.level)}setLevel(e){this.config.level=e}},h=new ee;async function pe(a){try{let{stdout:e}=await Re("ffprobe",["-v","error","-show_entries","format=duration","-of","default=noprint_wrappers=1:nokey=1",a]),t=parseFloat(e.trim());return Number.isNaN(t)?0:t}catch(e){return h.error(`Failed to get video duration for ${a}: ${e instanceof Error?e.message:String(e)}`),0}}var R=class{stateManager;notifier;downloadDir;tempDir;cookieFile;constructor(e,t,o,i,r){this.stateManager=e,this.notifier=t,this.downloadDir=E(o),this.cookieFile=i?E(i):void 0,this.tempDir=r?E(r):void 0}async download(e,t,o,i=0){if(this.stateManager.isDownloaded(t,o.number))return!1;this.notifier.notify("highlight",`Downloading Episode ${o.number} of ${t}`);try{let r=await this.runYtDlp(t,o),s=this.verifyDownload(r.filename);if(s===0)throw await this.cleanupFiles(r.allFiles),new Error("Downloaded file is empty or does not exist");if(i>0){let l=E(r.filename),n=await pe(l);if(n<i)throw await this.cleanupFiles(r.allFiles),new Error(`Video duration ${n}s is less than minimum ${i}s`)}if(this.tempDir&&this.tempDir!==this.downloadDir){this.notifier.notify("info",`Moving files from temp directory to ${this.downloadDir}...`),await $.mkdir(this.downloadDir,{recursive:!0});for(let l of r.allFiles)try{let n=E(l);if(!L.existsSync(n)){this.notifier.notify("warning",`File not found, skipping move: ${n}`);continue}let d=Pe(n),f=te(this.downloadDir,d);await $.rename(n,f),n===E(r.filename)&&(r.filename=f)}catch(n){this.notifier.notify("error",`Failed to move file ${l}: ${n}`)}}return this.stateManager.addDownloadedEpisode(t,o.number),await this.stateManager.save(),this.notifier.notify("success",`Downloaded Episode ${o.number}: ${r.filename} (${this.formatSize(s)})`),!0}catch(r){let s=`Failed to download Episode ${o.number}: ${r instanceof Error?r.message:String(r)}`;throw this.notifier.notify("error",s),new P(s,o.url)}}async cleanupFiles(e){for(let t of e)try{let o=E(t);L.existsSync(o)&&await $.unlink(o)}catch(o){this.notifier.notify("error",`Failed to delete file ${t}: ${o}`)}}async runYtDlp(e,t){let o=String(t.number).padStart(2,"0"),i=this.tempDir||this.downloadDir;await $.mkdir(i,{recursive:!0});let r=ue(e),l=["--no-warnings","--newline","-o",te(i,`${r} - ${o}.%(ext)s`),t.url];this.cookieFile&&l.unshift("--cookies",this.cookieFile);let n=null,d=new Set,f=[];try{let g=fe("yt-dlp",l,{all:!0});for await(let u of g.all){let c=u.toString().trim();if(!c)continue;f.push(c);let p=c.match(/\[download\] Destination:\s*(.+)/);p&&(n=p[1],n&&d.add(n));let m=c.match(/\[info\] Writing video subtitles to:\s*(.+)/);m?.[1]&&d.add(m[1]);let y=c.match(/\[merge\] Merging formats into "(.*)"/);if(y&&(n=y[1],n&&d.add(n)),c.includes("[info]")||c.includes("[ffmpeg]")||c.includes("[merge]")||c.includes("[postprocessor]")){this.notifier.notify("info",`Episode ${t.number}: ${c}`);continue}if(c.includes("[download]")){let v=c.match(/\[download\]\s+(\d+\.?\d*)%\s+of\s+~?\s*([\d.]+\w+)\s+at\s+~?\s*([\d.]+\w+\/s)\s+ETA\s+(\S+)/);if(v){let[,b,S,J,be]=v;this.notifier.progress(`[${t.number}] ${b}% of ${S} at ${J} ETA ${be}`)}else this.notifier.notify("info",`Episode ${t.number}: ${c}`)}}return await g,this.notifier.endProgress(),n||(n=te(i,`${r} - ${o}.mp4`)),n&&!d.has(n)&&d.add(n),{filename:n,allFiles:Array.from(d)}}catch(g){this.notifier.endProgress();let u=g,c=u.stderr??"",p=u.stdout??"",m=f.join(`
|
|
3
|
-
`);throw new Error(`yt-dlp failed:
|
|
4
|
-
stderr: ${c}
|
|
5
|
-
stdout: ${p}
|
|
6
|
-
captured output:
|
|
7
|
-
${m}
|
|
8
|
-
message: ${u.message}`)}}verifyDownload(e){let t=E(e);try{return L.statSync(t).size}catch{return 0}}formatSize(e){let t=["B","KB","MB","GB"],o=e,i=0;for(;o>=1024&&i<t.length-1;)o/=1024,i++;return`${o.toFixed(2)} ${t[i]}`}static async checkYtDlpInstalled(){try{return await fe("yt-dlp",["--version"]),!0}catch{return!1}}};function x(a){try{return new URL(a).hostname}catch{throw new Error(`Invalid URL: "${a}"`)}}var oe=class{handlers=new Map;register(e){this.handlers.set(e.getDomain(),e)}getHandler(e){let t=x(e);if(this.handlers.has(t))return this.handlers.get(t);for(let[o,i]of this.handlers.entries())if(t===o||t.endsWith(`.${o}`)||o.endsWith(`.${t}`))return i}getDomains(){return Array.from(this.handlers.keys())}getHandlerOrThrow(e){let t=this.getHandler(e);if(!t)throw new D(`No handler found for domain: "${x(e)}". Supported domains: ${this.getDomains().join(", ")}`,e);return t}},I=new oe;import*as ge from"cheerio";var M=class{supports(e){try{let t=x(e);return t===this.getDomain()||t.endsWith(`.${this.getDomain()}`)}catch{return!1}}async fetchHtml(e,t){let o={"User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",Accept:"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8","Accept-Language":"en-US,en;q=0.9"};t&&(o.Cookie=t);try{let i=await fetch(e,{headers:o});if(!i.ok)throw new D(`HTTP ${i.status}: ${i.statusText}`,e);return await i.text()}catch(i){throw i instanceof D?i:new D(`Failed to fetch page: ${i instanceof Error?i.message:String(i)}`,e)}}parseHtml(e){return ge.load(e)}parseEpisodeNumber(e){let t=e.match(/第(\d+)集/);if(t?.[1])return parseInt(t[1],10);let o=e.match(/ep\s?(\d+)/i);if(o?.[1])return parseInt(o[1],10);let i=e.match(/(?:episode|e)\s?(\d+)/i);if(i?.[1])return parseInt(i[1],10);let r=e.match(/\b(\d+)\b/);return r?.[1]?parseInt(r[1],10):null}parseEpisodeType(e,t){let o=t(e),i=o.attr("class")||"",r=o.text().toLowerCase();return i.includes("vip")||r.includes("vip")||r.includes("\u4F1A\u5458")?"vip":i.includes("preview")||i.includes("trailer")||r.includes("preview")||r.includes("\u9884\u544A")?"preview":i.includes("locked")||i.includes("lock")||r.includes("locked")||r.includes("\u9501\u5B9A")?"locked":"available"}};var U=class extends M{getDomain(){return"iq.com"}async extractEpisodes(e,t){let o=await this.fetchHtml(e,t),i=this.extractFromNextData(o);return i.length>0?i:this.extractFromHtml(o)}extractFromNextData(e){let t=[];try{let o=e.match(/<script id="__NEXT_DATA__"[^>]*>(.+?)<\/script>/s);if(!o||!o[1])return t;let r=JSON.parse(o[1]).props?.pageProps?.data;if(!r)return t;let s=JSON.parse(r),{albumInfo:l,videoList:n=[]}=s,{albumId:d,title:f}=l||{};for(let g of n){let{vid:u,episode:c,isTrailer:p,payStatus:m}=g;if(p)continue;let y=this.parseEpisodeNumber(c);if(!y)continue;let v=`https://www.iq.com/play/${d}-${u}?lang=en_us`,b=this.determineTypeFromPayStatus(m);t.push({number:y,url:v,type:b,title:`${f} - Episode ${c}`,extractedAt:new Date})}}catch(o){console.error("Failed to extract from __NEXT_DATA__:",o)}return t.sort((o,i)=>o.number-i.number),t}extractFromHtml(e){let t=this.parseHtml(e),o=[],i=['ul li a[href*="/play/"]','.album-episode-item a[href*="/play/"]','.episode-item a[href*="/play/"]','.intl-play-item a[href*="/play/"]','[data-episode] a[href*="/play/"]'];for(let r of i){let s=t(r);if(s.length>0&&(s.each((l,n)=>{this.processEpisodeLink(t,n,o)}),o.length>0))break}return o.sort((r,s)=>r.number-s.number),o}determineTypeFromPayStatus(e){return e===6?"vip":"available"}processEpisodeLink(e,t,o){let i=e(t),r=i.attr("href");if(!r)return;let s=r.startsWith("http")?r:`https://www.iq.com${r}`,l=i.text().trim(),n=i.attr("title")||void 0;if(l.toUpperCase().includes("BTS"))return;let d=this.parseEpisodeNumber(l);if(d||(d=this.parseEpisodeNumber(r)),!d||o.some(u=>u.number===d))return;let g=this.determineEpisodeType(e,t);o.push({number:d,url:s,type:g,title:n,extractedAt:new Date})}determineEpisodeType(e,t){let o=e(t).closest("li, div");return o.length&&(o.text()||"").toUpperCase().includes("VIP")?"vip":"available"}};var Q=class extends M{getDomain(){return"wetv.vip"}async extractEpisodes(e,t){let o=await this.fetchHtml(e,t),i=this.extractFromNextData(o);return i.length>0?i:this.extractFromHtml(o)}extractFromNextData(e){let t=[];try{let o=e.match(/<script id="__NEXT_DATA__"[^>]*>(.+?)<\/script>/s);if(!o||!o[1])return t;let r=JSON.parse(o[1]).props?.pageProps?.data;if(!r)return t;let s=JSON.parse(r),{coverInfo:l,videoList:n=[]}=s,{cid:d,title:f}=l,g=n[0]?.coverList?.[0]||d;for(let u of n){let{vid:c,episode:p,isTrailer:m}=u;if(m)continue;let y=this.parseEpisodeNumber(p);if(!y)continue;let v=encodeURIComponent(f),b=`https://wetv.vip/en/play/${g}/${c}-EP${p}%3A${v}`,S=this.determineTypeFromVideo(u);t.push({number:y,url:b,type:S,title:`${f} - Episode ${p}`,extractedAt:new Date})}}catch(o){console.error("Failed to extract from __NEXT_DATA__:",o)}return t.sort((o,i)=>o.number-i.number),t}extractFromHtml(e){let t=this.parseHtml(e),o=[],i=t('a.play-video__link[href*="/play/"][href*="EP"]');return i.length===0?t('a[href*="/play/"]').filter((s,l)=>(t(l).attr("href")||"").includes("EP")).each((s,l)=>{this.processEpisodeLink(t,l,o)}):i.each((r,s)=>{this.processEpisodeLink(t,s,o)}),o.sort((r,s)=>r.number-s.number),o}determineTypeFromVideo(e){let{labels:t,payStatus:o,defaultPayStatus:i}=e;if(t)for(let s in t){let l=t[s];if(!l)continue;let n=l.text?.toLowerCase()||"";if(n==="express")return"express";if(n==="teaser")return"teaser";if(n==="vip")return"vip"}let r=o||i;return r===6?"vip":r===12?"express":"available"}processEpisodeLink(e,t,o){let i=e(t),r=i.attr("href");if(!r)return;let s=r.startsWith("http")?r:`https://wetv.vip${r}`,l=i.attr("aria-label")||"",n=this.parseEpisodeNumber(l);if(!n||o.some(g=>g.number===n))return;let f=this.determineEpisodeType(e,t);o.push({number:n,url:s,type:f,title:i.attr("title")||void 0,extractedAt:new Date})}determineEpisodeType(e,t){let o=e(t).closest("li");if(o.length){let i=o.find("span.play-video__label").first();if(i.length){let s=i.text().trim().toLowerCase();if(s==="vip"||s.includes("vip"))return"vip";if(s==="teaser"||s.includes("teaser"))return"teaser";if(s==="express"||s.includes("express"))return"express"}let r=o.text()||"";if(r.includes("VIP")&&!r.includes("Teaser"))return"vip";if(r.includes("Teaser"))return"teaser";if(r.includes("Express"))return"express"}return"available"}};var _=class{lastProgressLength=0;notify(e,t){switch(this.lastProgressLength>0&&(process.stdout.write(`\r${" ".repeat(this.lastProgressLength)}\r`),this.lastProgressLength=0),e){case"info":h.info(t);break;case"success":h.success(t);break;case"warning":h.warning(t);break;case"error":h.error(t);break;case"highlight":h.highlight(t);break}}progress(e){this.lastProgressLength>0&&process.stdout.write(`\r${" ".repeat(this.lastProgressLength)}\r`),process.stdout.write(`\r${e}`),this.lastProgressLength=e.length}endProgress(){this.lastProgressLength>0&&(process.stdout.write(`
|
|
9
|
-
`),this.lastProgressLength=0)}};var j=class{config;apiUrl;constructor(e){this.config=e,this.apiUrl=`https://api.telegram.org/bot${e.botToken}/sendMessage`}async notify(e,t){if(e==="error")try{let o=this.getEmoji(e),i=4e3,r=t;r.length>i&&(r=r.substring(0,i)+`
|
|
10
|
-
... (truncated)`);let s=this.escapeHtml(r),l=`${o} <b>wetvlo Error</b>
|
|
2
|
+
import{fileURLToPath as dt}from"url";import{run as ut}from"cmd-ts";import*as Ee from"readline";import{boolean as rt,command as it,flag as st,option as nt,string as at}from"cmd-ts";import{existsSync as $e,readFileSync as Me}from"fs";import{writeFile as Re}from"fs/promises";import{isAbsolute as Ie,join as Ne}from"path";function ae(){return{version:"3.0.0",series:{}}}var N=class s{static locks=new Map;notifier;constructor(e){this.notifier=e}isDownloaded(e,t,o){try{let i=this.loadState(e).series[t];if(!i)return!1;let n=String(o).padStart(2,"0");return i.includes(n)}catch(r){return this.handleError(r,`Failed to check episode status for ${t}`),!1}}async addDownloadedEpisode(e,t,o){return this.withLock(e,async()=>{try{let r=this.loadState(e);r.series[t]||(r.series[t]=[]);let i=String(o).padStart(2,"0");r.series[t].includes(i)||(r.series[t].push(i),r.series[t].sort()),await this.saveState(e,r)}catch(r){throw this.handleError(r,`Failed to add episode for ${t}`),r}})}getSeriesEpisodes(e,t){try{return this.loadState(e).series[t]??[]}catch(o){return this.handleError(o,`Failed to get episodes for ${t}`),[]}}async withLock(e,t){let o=s.locks.get(e);for(;o;)await o,o=s.locks.get(e);let r=(async()=>{try{return await t()}finally{s.locks.delete(e)}})();return s.locks.set(e,r),r}loadState(e){let t=this.resolvePath(e);if(!$e(t))return ae();try{let o=Me(t,"utf-8");return JSON.parse(o)}catch(o){throw new Error(`Failed to load state from ${t}: ${o instanceof Error?o.message:String(o)}`)}}async saveState(e,t){let o=this.resolvePath(e);try{let r={};Object.keys(t.series).sort().forEach(n=>{let a=t.series[n];a&&(r[n]=[...a].sort())}),t.series=r;let i=JSON.stringify(t,null,2);await Re(o,i,"utf-8")}catch(r){throw new Error(`Failed to save state to ${o}: ${r instanceof Error?r.message:String(r)}`)}}resolvePath(e){return Ie(e)?e:Ne(process.cwd(),e)}handleError(e,t){let o=e instanceof Error?e.message:String(e),r=`${t}: ${o}`;this.notifier?this.notifier.notify("error",r):console.error(r)}};var m=class s{static configRegistry;static notifier;static stateManager;static initialize(e,t,o){s.configRegistry=e,s.notifier=t,s.stateManager=o||(t?new N(t):void 0)}static getConfig(){if(!s.configRegistry)throw new Error("AppContext not initialized. Call AppContext.initialize() first.");return s.configRegistry}static getNotifier(){if(!s.notifier)throw new Error("AppContext not initialized. Call AppContext.initialize() first.");return s.notifier}static getStateManager(){if(!s.stateManager)throw new Error("AppContext not initialized. Call AppContext.initialize() first.");return s.stateManager}static reloadConfig(e){s.configRegistry=e}static setNotifier(e){s.notifier=e}static isInitialized(){return s.configRegistry!==void 0}static reset(){s.configRegistry=void 0,s.notifier=void 0,s.stateManager=void 0}};import{existsSync as Qe}from"fs";import{readFile as je}from"fs/promises";import{join as Be}from"path";import*as de from"js-yaml";var E=class extends Error{constructor(e){super(e),this.name="WetvloError"}},R=class extends E{constructor(e){super(e),this.name="ConfigError"}};var T=class extends E{constructor(t,o){super(t);this.url=o;this.name="HandlerError"}},G=class extends E{constructor(t,o){super(t);this.url=o;this.name="DownloadError"}},Q=class extends E{constructor(e){super(e),this.name="NotificationError"}},j=class extends E{constructor(e){super(e),this.name="CookieError"}},B=class extends E{constructor(e){super(e),this.name="SchedulerError"}};function Le(s){return typeof s!="string"?s:s.replace(/\$\{([^}]+)\}/g,(e,t)=>{let o=process.env[t];if(o===void 0)throw new Error(`Environment variable "${t}" is not set`);return o})}function V(s){if(typeof s=="string")return Le(s);if(Array.isArray(s))return s.map(e=>V(e));if(s!==null&&typeof s=="object"){let e={};for(let[t,o]of Object.entries(s))e[t]=V(o);return e}return s}import{z as b}from"zod";var Ae=b.enum(["available","vip","svip","teaser","express","preview","locked"]),Pe=b.object({count:b.number().positive().optional().describe("Number of episodes to check"),checkInterval:b.number().positive().optional().describe("Interval between checks in seconds"),downloadTypes:b.array(Ae).optional().describe("Episode types to download")}),Fe=b.object({downloadDir:b.string().optional().describe("Directory to save downloaded episodes"),tempDir:b.string().optional().describe("Directory for temporary files"),downloadDelay:b.number().nonnegative().optional().describe("Delay between downloads in milliseconds"),maxRetries:b.number().int().nonnegative().optional().describe("Maximum number of retry attempts"),initialTimeout:b.number().positive().optional().describe("Initial timeout for operations in milliseconds"),backoffMultiplier:b.number().positive().optional().describe("Multiplier for exponential backoff"),jitterPercentage:b.number().int().min(0).max(100).optional().describe("Jitter percentage for retry delays"),minDuration:b.number().nonnegative().optional().describe("Minimum duration in seconds for downloads")}),oe=b.object({check:Pe.optional().describe("Check settings"),download:Fe.optional().describe("Download settings")}),qe=b.object({botToken:b.string().describe("Telegram bot token"),chatId:b.string().describe("Telegram chat ID")}),Oe=b.enum(["chrome","firefox","safari","chromium","edge"]),le=oe.extend({telegram:qe.optional().describe("Telegram notification configuration"),stateFile:b.string().describe("Path to state file"),browser:Oe.optional().describe("Browser to use for scraping"),cookieFile:b.string().optional().describe("Path to cookie file")}),Ue=le.extend({domain:b.string().describe('Domain name (e.g., "weTV")')}),He=oe.extend({name:b.string().describe("Series name"),url:b.url().describe("Series URL"),startTime:b.string().regex(/^\d{1,2}:\d{2}$/,{message:'Must be in HH:MM format (e.g., "20:00")'}).optional().describe("Start time in HH:MM format"),cron:b.string().optional().describe("Cron expression for scheduling")}),Ge=oe.extend({series:b.array(He).min(1,"Cannot be empty").describe("List of series to monitor"),domainConfigs:b.array(Ue).optional().describe("Domain-specific configurations"),globalConfig:le.optional().describe("Global configuration defaults")});function ce(s){Ge.parse(s)}var Ve="./config.yaml";async function ue(s=Ve){let e=Be(process.cwd(),s);if(!Qe(e))throw new R(`Configuration file not found: "${e}". Create a config.yaml file or specify a different path.`);let t=await je(e,"utf-8"),o;try{o=de.load(t)}catch(i){throw new R(`Failed to parse YAML: ${i instanceof Error?i.message:String(i)}`)}return ce(o),V(o)}function L(s,e){if(!e)return s;let t={...s};for(let o in e)if(Object.hasOwn(e,o)){let r=e[o],i=t[o];pe(r)&&pe(i)?t[o]=L(i,r):t[o]=r}return t}function pe(s){return typeof s=="object"&&s!==null&&!Array.isArray(s)}function k(s){try{return new URL(s).hostname}catch{throw new Error(`Invalid URL: "${s}"`)}}var We={check:{count:3,checkInterval:600,downloadTypes:["available"]},download:{downloadDir:"./downloads",tempDir:"./downloads",downloadDelay:10,maxRetries:3,initialTimeout:5,backoffMultiplier:2,jitterPercentage:10,minDuration:0},stateFile:"wetvlo-state.json",browser:"chrome"};function fe(){return We}var q=class{map=new Map;seriesByUrl=new Map;constructor(e){let t=fe(),o=L(t,e.globalConfig);this.setConfig("global",o);for(let r of e.domainConfigs||[]){let i=L(o,r);this.setConfig(`domain:${r.domain}`,i)}for(let r of e.series){let i=k(r.url),n=this.getConfig(`domain:${i}`);if(!n){let l=this.getConfig("global");n=L(l,{domain:i})}let a=L(n,r);this.setConfig(`series:${r.url}`,a),this.seriesByUrl.set(r.url,a)}}getConfig(e){return this.map.get(e)}setConfig(e,t){this.map.set(e,t)}resolve(e,t){if(t==="global"){let i=this.getConfig("global");if(!i)throw new Error("Global configuration not found");return i}if(t==="domain"){let i=k(e),n=this.getConfig(`domain:${i}`);if(!n){let a=this.getConfig("global");if(!a)throw new Error("Global configuration not found");return Object.assign(a,{domain:i})}return n}let o=this.getConfig(`series:${e}`);if(!o)throw new Error(`No configuration found for URL: ${e}`);let r=o;return this.validate(r),r}listSeries(){return Array.from(this.seriesByUrl.values())}listSeriesUrls(){return Array.from(this.seriesByUrl.keys())}listDomains(){let e=new Set;for(let t of this.seriesByUrl.keys())e.add(k(t));return Array.from(e)}validate(e){if(!e.check)throw new Error("Missing check configuration");if(!e.download)throw new Error("Missing download configuration");let{check:t,download:o}=e;if(t.count<1)throw new Error(`Invalid check count: ${t.count}`);if(t.checkInterval<0)throw new Error(`Invalid check interval: ${t.checkInterval}`);if(o.downloadDelay<0)throw new Error(`Invalid download delay: ${o.downloadDelay}`);if(o.maxRetries<0)throw new Error(`Invalid max retries: ${o.maxRetries}`);if(o.initialTimeout<0)throw new Error(`Invalid initial timeout: ${o.initialTimeout}`);if(o.backoffMultiplier<1)throw new Error(`Invalid backoff multiplier: ${o.backoffMultiplier}`);if(o.minDuration<0)throw new Error(`Invalid min duration: ${o.minDuration}`)}};import*as O from"fs";import*as P from"fs/promises";import{basename as Xe,join as Ye,resolve as $}from"path";function ge(s){return s.replace(/[<>:"/\\|?*]/g,"_").replace(/[\x00-\x1F]/g,"").replace(/[\s.]+$/,"")}import{execa as ze}from"execa";var D={reset:"\x1B[0m",bright:"\x1B[1m",dim:"\x1B[2m",black:"\x1B[30m",red:"\x1B[31m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m",magenta:"\x1B[35m",cyan:"\x1B[36m",white:"\x1B[37m",bgRed:"\x1B[41m",bgGreen:"\x1B[42m",bgYellow:"\x1B[43m"},re=class{config;constructor(e={}){this.config={level:e.level??"INFO",useColors:e.useColors??!0}}getEmoji(e){switch(e){case"DEBUG":return"\u{1F50D}";case"INFO":return"\u2139\uFE0F";case"SUCCESS":return"\u2705";case"WARNING":return"\u26A0\uFE0F";case"ERROR":return"\u274C";case"HIGHLIGHT":return"\u{1F31F}";default:return"\u2022"}}formatDate(e){let t=(e.getMonth()+1).toString().padStart(2,"0"),o=e.getDate().toString().padStart(2,"0"),r=e.getHours().toString().padStart(2,"0"),i=e.getMinutes().toString().padStart(2,"0"),n=e.getSeconds().toString().padStart(2,"0");return`${t}-${o} ${r}:${i}:${n}`}format(e,t){let o=this.formatDate(new Date),r=this.getEmoji(e);return`${o} ${r} ${t}`}colorize(e,t){return this.config.useColors?`${t}${e}${D.reset}`:e}debug(e){this.shouldLog("DEBUG")&&console.log(this.format("DEBUG",this.colorize(e,D.dim)))}info(e){this.shouldLog("INFO")&&console.log(this.format("INFO",this.colorize(e,D.blue)))}success(e){this.shouldLog("SUCCESS")&&console.log(this.format("SUCCESS",this.colorize(e,D.green)))}warning(e){this.shouldLog("WARNING")&&console.log(this.format("WARNING",this.colorize(e,D.yellow)))}error(e){this.shouldLog("ERROR")&&console.error(this.format("ERROR",this.colorize(e,D.red)))}highlight(e){this.shouldLog("HIGHLIGHT")&&console.log(this.format("HIGHLIGHT",this.colorize(e,D.bright+D.magenta)))}shouldLog(e){let t=["DEBUG","INFO","SUCCESS","WARNING","ERROR","HIGHLIGHT"];return t.indexOf(e)>=t.indexOf(this.config.level)}setLevel(e){this.config.level=e}},y=new re;async function me(s){try{let{stdout:e}=await ze("ffprobe",["-v","error","-show_entries","format=duration","-of","default=noprint_wrappers=1:nokey=1",s]),t=parseFloat(e.trim());return Number.isNaN(t)?0:t}catch(e){return y.error(`Failed to get video duration for ${s}: ${e instanceof Error?e.message:String(e)}`),0}}import*as ye from"fs/promises";import{join as _e}from"path";import{execa as he}from"execa";var W=class{};var A=class extends W{getName(){return"yt-dlp"}supports(e){return!0}async download(e,t,o,r){let i=_e(t,`${o}.%(ext)s`);await ye.mkdir(t,{recursive:!0});let n=["--no-warnings","--newline","-o",i,e.url];r?.cookieFile&&n.unshift("--cookies",r.cookieFile);let a=null,l=new Set,c=[];try{let d=he("yt-dlp",n,{all:!0});if(d.all)for await(let g of d.all){let u=g.toString().trim();if(!u)continue;c.push(u);let v=u.match(/\[download\] Destination:\s*(.+)/);v&&(a=v[1],a&&l.add(a));let f=u.match(/\[info\] Writing video subtitles to:\s*(.+)/);f?.[1]&&l.add(f[1]);let w=u.match(/\[merge\] Merging formats into "(.*)"/);if(w&&(a=w[1],a&&l.add(a)),u.startsWith("[info]")||u.startsWith("[ffmpeg]")||u.startsWith("[merge]")||u.startsWith("[ExtractAudio]")||u.startsWith("[Metadata]")||u.startsWith("[Thumbnails]")){r?.onLog?.(u);continue}if(u.startsWith("[download]")){let h=u.match(/\[download\]\s+(\d+\.?\d*)%\s+of\s+~?\s*([\d.]+\w+)\s+at\s+~?\s*([\d.]+\w+\/s)\s+ETA\s+(\S+)/);if(h){let[,p,x,C,S]=h;r?.onProgress?.(`[download] ${p}% of ${x} at ${C} ETA ${S}`)}else r?.onLog?.(u);continue}r?.onLog?.(u)}if(await d,!a)throw new Error("Could not determine downloaded filename from output");return{filename:a,allFiles:Array.from(l)}}catch(d){let g=d instanceof Error?d.message:String(d),u=c.join(`
|
|
3
|
+
`);throw new Error(`yt-dlp failed: ${g}
|
|
11
4
|
|
|
12
|
-
|
|
13
|
-
${d}`)}}catch(o){console.error("Telegram notification failed:",o)}}escapeHtml(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}getEmoji(e){switch(e){case"info":return"\u2139\uFE0F";case"success":return"\u2705";case"warning":return"\u26A0\uFE0F";case"error":return"\u274C";case"highlight":return"\u{1F514}";default:return""}}async progress(e){}async endProgress(){}};import{createHash as Fe}from"crypto";var A=class{domainConfigs;globalConfigs;constructor(e=[],t){this.domainConfigs=new Map(e.map(o=>[o.domain,o])),this.globalConfigs=t}resolve(e){let t=x(e.url),o=this.domainConfigs.get(t),i={check:this.mergeCheckSettings(e.check,o?.check),download:this.mergeDownloadSettings(e.download,o?.download)};return this.validate(i),i}mergeCheckSettings(e,t){let o=this.globalConfigs?.check,i=se;return{count:e?.count??t?.count??o?.count??i.count,checkInterval:e?.checkInterval??t?.checkInterval??o?.checkInterval??i.checkInterval,downloadTypes:e?.downloadTypes??t?.downloadTypes??o?.downloadTypes??i.downloadTypes}}mergeDownloadSettings(e,t){let o=this.globalConfigs?.download,i=ne;return{downloadDir:e?.downloadDir??t?.downloadDir??o?.downloadDir??i.downloadDir,tempDir:e?.tempDir??t?.tempDir??o?.tempDir??i.tempDir,downloadDelay:e?.downloadDelay??t?.downloadDelay??o?.downloadDelay??i.downloadDelay,maxRetries:e?.maxRetries??t?.maxRetries??o?.maxRetries??i.maxRetries,initialTimeout:e?.initialTimeout??t?.initialTimeout??o?.initialTimeout??i.initialTimeout,backoffMultiplier:e?.backoffMultiplier??t?.backoffMultiplier??o?.backoffMultiplier??i.backoffMultiplier,jitterPercentage:e?.jitterPercentage??t?.jitterPercentage??o?.jitterPercentage??i.jitterPercentage,minDuration:e?.minDuration??t?.minDuration??o?.minDuration??i.minDuration}}resolveDomain(e){let t=this.domainConfigs.get(e),o={check:this.mergeCheckSettings(void 0,t?.check),download:this.mergeDownloadSettings(void 0,t?.download)};return this.validate(o),o}setGlobalConfigs(e){this.globalConfigs=e}validate(e){if(e.check.count<1)throw new Error(`Invalid check count: ${e.check.count}`);if(e.check.checkInterval<0)throw new Error(`Invalid check interval: ${e.check.checkInterval}`);if(e.download.downloadDelay<0)throw new Error(`Invalid download delay: ${e.download.downloadDelay}`);if(e.download.maxRetries<0)throw new Error(`Invalid max retries: ${e.download.maxRetries}`);if(e.download.initialTimeout<0)throw new Error(`Invalid initial timeout: ${e.download.initialTimeout}`);if(e.download.backoffMultiplier<1)throw new Error(`Invalid backoff multiplier: ${e.download.backoffMultiplier}`);if(e.download.minDuration<0)throw new Error(`Invalid min duration: ${e.download.minDuration}`)}};var B=class{queue=[];isExecuting=!1;nextAvailableAt=new Date(0);cooldownMs;constructor(e=0){this.cooldownMs=e}add(e,t){let o=new Date(Date.now()+(t??0));this.queue.push({data:e,addedAt:o})}addFirst(e,t){let o=new Date(Date.now()+(t??0));this.queue.unshift({data:e,addedAt:o})}getNext(){return this.queue.length===0?null:this.queue.shift()?.data??null}peekNext(){return this.queue.length===0?null:this.queue[0]?.data??null}canStart(e){if(this.isExecuting||e<this.nextAvailableAt)return!1;let t=this.queue[0];return!(t&&e<t.addedAt)}markStarted(){this.isExecuting=!0}markCompleted(e){this.isExecuting=!1,this.cooldownMs=e,this.nextAvailableAt=new Date(Date.now()+e)}markFailed(e){this.isExecuting=!1,this.cooldownMs=e,this.nextAvailableAt=new Date(Date.now()+e)}hasTasks(){return this.queue.length>0}getNextAvailableTime(){let e=this.nextAvailableAt,t=this.queue[0];return t&&t.addedAt>e&&(e=t.addedAt),e}getQueueLength(){return this.queue.length}getIsExecuting(){return this.isExecuting}getCooldownMs(){return this.cooldownMs}setCooldownMs(e){this.cooldownMs=e}clear(){this.queue=[]}getStatus(){let e=new Date;return{queueLength:this.queue.length,isExecuting:this.isExecuting,nextAvailableAt:this.nextAvailableAt,cooldownMs:this.cooldownMs,canStartNow:this.canStart(e)}}};var W=class{queues=new Map;queueCooldowns=new Map;executorBusy=!1;timerId=null;roundRobinIndex=0;stopped=!1;executor;onWait;constructor(e){this.executor=e}setOnWait(e){this.onWait=e}registerQueue(e,t){if(this.queues.has(e))throw new Error(`Queue ${e} is already registered`);let o=new B(t);this.queues.set(e,o),this.queueCooldowns.set(e,t)}hasQueue(e){return this.queues.has(e)}unregisterQueue(e){this.queues.delete(e),this.queueCooldowns.delete(e)}addTask(e,t,o){let i=this.queues.get(e);if(!i)throw new Error(`Queue ${e} is not registered`);i.add(t,o),this.stopped||this.scheduleNext()}addPriorityTask(e,t,o){let i=this.queues.get(e);if(!i)throw new Error(`Queue ${e} is not registered`);i.addFirst(t,o),this.stopped||this.scheduleNext()}markTaskComplete(e,t){let o=this.queues.get(e);if(!o)throw new Error(`Queue ${e} is not registered`);let i=t??this.queueCooldowns.get(e)??0;o.markCompleted(i),this.executorBusy=!1,this.stopped||this.scheduleNext()}markTaskFailed(e,t){let o=this.queues.get(e);if(!o)throw new Error(`Queue ${e} is not registered`);let i=t??this.queueCooldowns.get(e)??0;o.markFailed(i),this.executorBusy=!1,this.stopped||this.scheduleNext()}scheduleNext(){if(this.stopped||(this.clearTimer(),this.trySchedule())||this.executorBusy)return;let t=this.getEarliestAvailableTime();if(t){let o=Date.now(),i=Math.max(0,t.time.getTime()-o);this.scheduleTimer(i,t.queueName,t.time)}}trySchedule(){if(this.executorBusy)return!1;let e=new Date,t=Array.from(this.queues.keys());if(t.length===0)return!1;for(let o=0;o<t.length;o++){let i=(this.roundRobinIndex+o)%t.length,r=t[i];if(!r)continue;let s=this.queues.get(r);if(s&&s.hasTasks()&&s.canStart(e)){let l=s.getNext();if(l)return s.markStarted(),this.executorBusy=!0,this.roundRobinIndex=(i+1)%t.length,this.executeTask(r,l).catch(n=>{console.error(`[UniversalScheduler] Task execution failed: ${n}`),this.markTaskFailed(r)}),!0}}return!1}async executeTask(e,t){await this.executor(t,e)}scheduleTimer(e,t,o){this.clearTimer(),this.onWait&&e>1e3&&this.onWait(t,e,o),this.timerId=setTimeout(()=>{this.timerId=null,this.scheduleNext()},e)}clearTimer(){this.timerId!==null&&(clearTimeout(this.timerId),this.timerId=null)}getEarliestAvailableTime(){let e=null;for(let[t,o]of this.queues.entries()){if(!o.hasTasks())continue;let i=o.getNextAvailableTime();(e===null||i<e.time)&&(e={time:i,queueName:t})}return e}stop(){this.stopped=!0,this.clearTimer()}resume(){this.stopped=!1,this.scheduleNext()}getStats(){let e=new Map;for(let[t,o]of this.queues.entries()){let i=o.getStatus();e.set(t,{queueLength:i.queueLength,isExecuting:i.isExecuting,nextAvailableAt:i.nextAvailableAt})}return e}isExecutorBusy(){return this.executorBusy}hasPendingTasks(){for(let e of this.queues.values())if(e.hasTasks())return!0;return!1}getTotalPendingTasks(){let e=0;for(let t of this.queues.values())e+=t.getQueueLength();return e}};var V=class{stateManager;downloadManager;notifier;scheduler;configResolver;running=!1;domainHandlers=new Map;constructor(e,t,o,i,r=[],s,l){this.stateManager=e,this.downloadManager=t,this.notifier=o,this.configResolver=new A(r,s);let n=l||(d=>new W(d));this.scheduler=n(async(d,f)=>{await this.executeTask(d,f)}),this.scheduler.setOnWait((d,f)=>{let g=Math.round(f/1e3),u=d.split(":"),c=u[0],p=u[1];c==="download"?this.notifier.notify("info",`[${p}] Next download in ${g}s...`):c==="check"&&this.notifier.notify("info",`[${p}] Next check in ${g}s...`)})}addSeriesCheck(e){let t=x(e.url);this.registerDownloadQueue(t);let o=this.registerSeriesCheckQueue(t,e),i={seriesUrl:e.url,seriesName:e.name,config:e,attemptNumber:1,retryCount:0};this.scheduler.addTask(o,i),this.notifier.notify("info",`[QueueManager] Added ${e.name} to check queue for domain ${t}`)}addEpisodes(e,t,o,i){if(o.length===0)return;let r=x(e);this.registerDownloadQueue(r);let s;i?s=this.configResolver.resolve(i):s=this.configResolver.resolveDomain(r);let{downloadDelay:l}=s.download;for(let n=0;n<o.length;n++){let d=o[n];if(!d)continue;let f={seriesUrl:e,seriesName:t,episode:d,config:i,retryCount:0},g=`download:${r}`,u=n*l*1e3;this.scheduler.addTask(g,f,u)}this.notifier.notify("success",`[QueueManager] Added ${o.length} episodes to download queue for ${t} (domain ${r})`)}updateConfig(e=[],t){this.configResolver=new A(e,t),this.notifier.notify("info","[QueueManager] Configuration updated")}start(){if(this.running)throw new Error("QueueManager is already running");this.running=!0,this.scheduler.resume(),this.notifier.notify("info","[QueueManager] Started queue processing")}async stop(){this.running&&(this.notifier.notify("info","[QueueManager] Stopping queue processing..."),this.scheduler.stop(),this.running=!1,this.notifier.notify("info","[QueueManager] Queue processing stopped"))}hasActiveProcessing(){return this.scheduler.isExecutorBusy()||this.scheduler.hasPendingTasks()}getQueueStats(){let e=this.scheduler.getStats(),t={},o={};for(let[i,r]of e.entries())if(i.startsWith("check:")){let l=i.split(":")[1];if(!l)continue;t[l]||(t[l]={length:0,processing:!1}),t[l].length+=r.queueLength,r.isExecuting&&(t[l].processing=!0)}else if(i.startsWith("download:")){let s=i.slice(9);o[s]={length:r.queueLength,processing:r.isExecuting}}return{checkQueues:t,downloadQueues:o}}registerDownloadQueue(e){let t=`download:${e}`;if(this.scheduler.hasQueue(t))return;let o=this.configResolver.resolveDomain(e),{downloadDelay:i}=o.download;this.scheduler.registerQueue(t,i*1e3)}registerSeriesCheckQueue(e,t){let o=Fe("md5").update(t.url).digest("hex").substring(0,12),i=`check:${e}:${o}`;if(this.scheduler.hasQueue(i))return i;let r=this.configResolver.resolve(t),{checkInterval:s}=r.check;if(this.scheduler.registerQueue(i,s*1e3),!this.domainHandlers.has(e)){let l=I.getHandlerOrThrow(`https://${e}/`);this.domainHandlers.set(e,l)}return i}async executeTask(e,t){let o=t.split(":"),i=o[0],r=o[1];if(!i||!r)throw new Error(`Invalid queue name format: ${t}`);if(i==="check")await this.executeCheck(e,r,t);else if(i==="download")await this.executeDownload(e,r,t);else throw new Error(`Unknown queue type: ${i}`)}async executeCheck(e,t,o){let{seriesUrl:i,seriesName:r,config:s,attemptNumber:l,retryCount:n=0}=e,d=this.domainHandlers.get(t);if(!d)throw new Error(`No handler found for domain ${t}`);let f=this.configResolver.resolve(s),{count:g,checkInterval:u}=f.check;try{let c=await this.performCheck(d,i,r,f,l,t);if(c.hasNewEpisodes)this.notifier.notify("success",`[${t}] Found ${c.episodes.length} new episodes for ${r} (attempt ${l}/${g})`),this.addEpisodes(i,r,c.episodes,s),this.scheduler.markTaskComplete(o,u*1e3);else if(l<g){let p=u*1e3,m=c.requeueDelay??p;this.notifier.notify("info",`[${t}] No new episodes for ${r} (attempt ${l}/${g}), requeueing in ${Math.round(m/1e3)}s`);let y={...e,attemptNumber:l+1,retryCount:0};this.scheduler.addTask(o,y,m),this.scheduler.markTaskComplete(o,u*1e3)}else this.notifier.notify("info",`[${t}] Checks exhausted for ${r} (${g} attempts with no new episodes)`),this.scheduler.markTaskComplete(o,u*1e3)}catch(c){let p=c instanceof Error?c.message:String(c),{maxRetries:m,initialTimeout:y,backoffMultiplier:v,jitterPercentage:b}=f.download;if(n<m){let S=this.calculateBackoff(n,y*1e3,v,b);this.notifier.notify("warning",`[${t}] Check failed for ${r}, retrying in ${Math.round(S/1e3)}s (attempt ${n+1}/${m})`);let J={...e,retryCount:n+1};this.scheduler.addPriorityTask(o,J,S),this.scheduler.markTaskComplete(o,u*1e3)}else this.notifier.notify("error",`[${t}] Failed to check ${r} after ${n} retry attempts: ${p}`),this.scheduler.markTaskComplete(o,u*1e3)}}async executeDownload(e,t,o){let{seriesUrl:i,seriesName:r,episode:s,config:l,retryCount:n=0}=e,d;l?d=this.configResolver.resolve(l):d=this.configResolver.resolveDomain(t);let{downloadDelay:f,minDuration:g}=d.download;try{await this.downloadManager.download(i,r,s,g),this.notifier.notify("success",`[${t}] Successfully queued download of Episode ${s.number} for ${r}`),this.scheduler.markTaskComplete(o,f*1e3)}catch(u){let c=u instanceof Error?u.message:String(u),{maxRetries:p,initialTimeout:m,backoffMultiplier:y,jitterPercentage:v}=d.download;if(n<p){let b=this.calculateBackoff(n,m*1e3,y,v);this.notifier.notify("warning",`[${t}] Download failed for Episode ${s.number}, retrying in ${Math.round(b/1e3)}s (attempt ${n+1}/${p})`);let S={...e,retryCount:n+1};this.scheduler.addPriorityTask(o,S,b),this.scheduler.markTaskComplete(o,f*1e3)}else this.notifier.notify("error",`[${t}] Failed to download Episode ${s.number} after ${n+1} attempts: ${c}`),this.scheduler.markTaskComplete(o,f*1e3)}}async performCheck(e,t,o,i,r,s){let l=i.check.count;this.notifier.notify("info",`[${s}] Checking ${t} for new episodes... (attempt ${r}/${l})`);let n=await e.extractEpisodes(t),d=new Map;n.forEach(c=>{let p=d.get(c.type)||0;d.set(c.type,p+1)});let f=Array.from(d.entries()).map(([c,p])=>`${c}: ${p}`).join(", ");this.notifier.notify("info",`[${s}] Found ${n.length} total episodes on ${t} (${f})`);let g=i.check.downloadTypes,u=n.filter(c=>{let p=g.includes(c.type),m=!this.stateManager.isDownloaded(o,c.number);return p&&m});if(n.length!==u.length){let c=n.length-u.length;this.notifier.notify("info",`[${s}] Filtering to ${g.join(" or ")}: ${u.length} episodes to download, ${c} skipped`)}return u.length>0?{hasNewEpisodes:!0,episodes:u}:{hasNewEpisodes:!1,episodes:[],shouldRequeue:!0}}calculateBackoff(e,t,o,i){let r=t*o**e,s=r*i/100,l=(Math.random()*2-1)*s,n=Math.max(0,r+l);return Math.floor(n)}setGlobalConfigs(e){this.configResolver.setGlobalConfigs(e)}};function qe(a){let e=a.match(/^(\d{1,2}):(\d{2})$/);if(!e)throw new Error(`Invalid time format: "${a}". Expected HH:MM`);let[,t,o]=e,i=parseInt(t||"0",10),r=parseInt(o||"0",10);if(i<0||i>23)throw new Error(`Invalid hours: ${i}. Must be between 0 and 23`);if(r<0||r>59)throw new Error(`Invalid minutes: ${r}. Must be between 0 and 59`);let s=new Date;return s.setHours(i,r,0,0),s}function he(a){let e=qe(a),t=new Date,o=new Date(t);return o.setHours(e.getHours(),e.getMinutes(),0,0),o<=t&&o.setDate(o.getDate()+1),o.getTime()-t.getTime()}function me(a){return new Promise(e=>setTimeout(e,a))}var z=class{configs;stateManager;downloadManager;notifier;cookies;options;queueManager;running=!1;stopped=!0;globalConfigs;domainConfigs;timeProvider;scheduleTimer=null;constructor(e,t,o,i,r,s={mode:"scheduled"},l,n,d,f){this.configs=e,this.stateManager=t,this.downloadManager=o,this.notifier=i,this.cookies=r,this.options=s,this.globalConfigs=l,this.domainConfigs=n,this.timeProvider=d||{getMsUntilTime:he,sleep:me};let g=f||((u,c,p,m,y,v)=>new V(u,c,p,m,y,v));this.queueManager=g(this.stateManager,this.downloadManager,this.notifier,this.cookies,this.domainConfigs,this.globalConfigs)}async start(){if(this.running)throw new O("Scheduler is already running");if(this.running=!0,this.stopped=!1,this.queueManager.start(),this.options.mode==="once")this.notifier.notify("info","Single-run mode: checking all series once"),await this.runOnce(),this.running=!1;else return this.notifier.notify("info","Scheduler started (queue-based architecture)"),this.scheduleNextBatch(),new Promise(e=>{let t=setInterval(()=>{this.running||(clearInterval(t),e())},100)})}scheduleNextBatch(){if(this.stopped)return;let e=this.groupConfigsByStartTime(),t=null,o=Number.MAX_SAFE_INTEGER;for(let r of e.keys()){let s=this.timeProvider.getMsUntilTime(r);s<o&&(o=s,t=r)}if(!t){this.notifier.notify("warning","No scheduled configs found.");return}let i=e.get(t);i&&(o>0&&this.notifier.notify("info",`Waiting ${Math.floor(o/1e3/60)} minutes until ${t}...`),this.scheduleTimer=setTimeout(async()=>{this.stopped||(await this.runConfigs(i),this.scheduleNextBatch())},o))}async stop(){this.notifier.notify("info","Stopping scheduler..."),this.stopped=!0,this.scheduleTimer&&(clearTimeout(this.scheduleTimer),this.scheduleTimer=null),await this.queueManager.stop(),await this.stateManager.save(),this.running=!1,this.notifier.notify("info","Scheduler stopped")}async reload(e,t,o){this.notifier.notify("info","Reloading configuration..."),this.configs=e,this.globalConfigs=t,this.domainConfigs=o,this.queueManager.updateConfig(o,t),this.running&&this.options.mode==="scheduled"&&(this.scheduleTimer&&(clearTimeout(this.scheduleTimer),this.scheduleTimer=null),this.scheduleNextBatch()),this.notifier.notify("success","Configuration reloaded")}async triggerAllChecks(){this.notifier.notify("info","Triggering immediate checks for all series...");for(let e of this.configs)this.queueManager.addSeriesCheck(e)}groupConfigsByStartTime(){let e=new Map;for(let t of this.configs){let o=e.get(t.startTime)||[];o.push(t),e.set(t.startTime,o)}return e}async runConfigs(e){for(let o of e){if(this.stopped)break;this.queueManager.addSeriesCheck(o)}let t=this.queueManager.getQueueStats();this.notifier.notify("info",`Added ${e.length} series to check queues. Queue stats: ${JSON.stringify(t)}`)}async runOnce(){for(let e of this.configs){if(this.stopped)break;this.queueManager.addSeriesCheck(e)}for(;this.queueManager.hasActiveProcessing()&&!this.stopped;)await this.timeProvider.sleep(1e3);await this.stateManager.save(),this.notifier.notify("success","Single-run complete")}isRunning(){return this.running&&!this.stopped}getQueueManager(){return this.queueManager}};import{existsSync as Oe}from"fs";import{readFile as He,writeFile as Ge}from"fs/promises";import{join as Ue}from"path";function X(){return{version:"3.0.0",series:{}}}var Y=class{state;statePath;dirty=!1;constructor(e){this.statePath=Ue(process.cwd(),e),this.state=X()}async load(){if(!Oe(this.statePath)){this.state=X(),this.dirty=!0,await this.save();return}try{let e=await He(this.statePath,"utf-8");this.state=JSON.parse(e),this.dirty=!1}catch(e){throw new N(`Failed to load state file: ${e instanceof Error?e.message:String(e)}`)}}async save(){if(this.dirty)try{let e={};Object.keys(this.state.series).sort().forEach(o=>{let i=this.state.series[o];i&&(e[o]=[...i].sort())}),this.state.series=e;let t=JSON.stringify(this.state,null,2);await Ge(this.statePath,t,"utf-8"),this.dirty=!1}catch(e){throw new N(`Failed to save state file: ${e instanceof Error?e.message:String(e)}`)}}isDownloaded(e,t){let o=this.state.series[e];if(!o)return!1;let i=String(t).padStart(2,"0");return o.includes(i)}addDownloadedEpisode(e,t){this.state.series[e]||(this.state.series[e]=[]);let o=String(t).padStart(2,"0");this.state.series[e].includes(o)||(this.state.series[e].push(o),this.dirty=!0)}getSeriesEpisodes(e){return this.state.series[e]??[]}deleteSeries(e){this.state.series[e]&&(delete this.state.series[e],this.dirty=!0)}getAllSeriesNames(){return Object.keys(this.state.series)}getDownloadedCount(){let e=0;for(let t of Object.values(this.state.series))e+=t.length;return e}clearAll(){this.state=X(),this.dirty=!0}async forceSave(){this.dirty=!0,await this.save()}};import{existsSync as Qe}from"fs";import{readFile as _e}from"fs/promises";import{homedir as Co}from"os";import{join as Do}from"path";async function we(a){if(!Qe(a))throw new q(`Cookie file not found: "${a}"`);let t=(await _e(a,"utf-8")).split(`
|
|
14
|
-
`),
|
|
5
|
+
Log output:
|
|
6
|
+
${u}`)}}static async checkInstalled(){try{return await he("yt-dlp",["--version"]),!0}catch{return!1}}};var ie=class{downloaders=[];defaultDownloader;constructor(){this.defaultDownloader=new A}register(e){this.downloaders.push(e)}getDownloader(e){for(let t of this.downloaders)if(t.supports(e))return t;return this.defaultDownloader}},we=new ie;var U=class{stateManager;downloadDir;tempDir;cookieFile;constructor(e,t,o){this.stateManager=m.getStateManager(),this.downloadDir=$(e),this.cookieFile=t?$(t):void 0,this.tempDir=o?$(o):void 0}async download(e,t,o,r=0){let i=m.getNotifier(),l=m.getConfig().resolve(e,"series").stateFile;if(this.stateManager.isDownloaded(l,t,o.number))return!1;let c=we.getDownloader(o.url);i.notify("highlight",`Downloading Episode ${o.number} of ${t} using ${c.getName()}`);try{let d=String(o.number).padStart(2,"0"),u=`${ge(t)} - ${d}`,v=this.tempDir||this.downloadDir,f=await c.download(o,v,u,{cookieFile:this.cookieFile,onProgress:h=>i.progress(h),onLog:h=>i.notify("info",h)});i.endProgress();let w=this.verifyDownload(f.filename);if(w===0)throw await this.cleanupFiles(f.allFiles),new Error("Downloaded file is empty or does not exist");if(r>0){let h=$(f.filename),p=await me(h);if(p<r)throw await this.cleanupFiles(f.allFiles),new Error(`Video duration ${p}s is less than minimum ${r}s`)}if(this.tempDir&&this.tempDir!==this.downloadDir){i.notify("info",`Moving files from temp directory to ${this.downloadDir}...`),await P.mkdir(this.downloadDir,{recursive:!0});for(let h of f.allFiles)try{let p=$(h);if(!O.existsSync(p)){i.notify("warning",`File not found, skipping move: ${p}`);continue}let x=Xe(p),C=Ye(this.downloadDir,x);await P.rename(p,C),p===$(f.filename)&&(f.filename=C)}catch(p){i.notify("error",`Failed to move file ${h}: ${p}`)}}return await this.stateManager.addDownloadedEpisode(l,t,o.number),i.notify("success",`Downloaded Episode ${o.number}: ${f.filename} (${this.formatSize(w)})`),!0}catch(d){i.endProgress();let g=`Failed to download Episode ${o.number}: ${d instanceof Error?d.message:String(d)}`;throw i.notify("error",g),new G(g,o.url)}}async cleanupFiles(e){let t=m.getNotifier();for(let o of e)try{let r=$(o);O.existsSync(r)&&await P.unlink(r)}catch(r){t.notify("error",`Failed to delete file ${o}: ${r}`)}}verifyDownload(e){let t=$(e);try{return O.statSync(t).size}catch{return 0}}formatSize(e){let t=["B","KB","MB","GB"],o=e,r=0;for(;o>=1024&&r<t.length-1;)o/=1024,r++;return`${o.toFixed(2)} ${t[r]}`}static async checkYtDlpInstalled(){return A.checkInstalled()}};var se=class{handlers=new Map;register(e){this.handlers.set(e.getDomain(),e)}getHandler(e){let t=k(e);if(this.handlers.has(t))return this.handlers.get(t);for(let[o,r]of this.handlers.entries())if(t===o||t.endsWith(`.${o}`)||o.endsWith(`.${t}`))return r}getDomains(){return Array.from(this.handlers.keys())}getHandlerOrThrow(e){let t=this.getHandler(e);if(!t)throw new T(`No handler found for domain: "${k(e)}". Supported domains: ${this.getDomains().join(", ")}`,e);return t}},I=new se;import*as ve from"cheerio";var M=class{supports(e){try{let t=k(e);return t===this.getDomain()||t.endsWith(`.${this.getDomain()}`)}catch{return!1}}async fetchHtml(e,t){let o={"User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",Accept:"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8","Accept-Language":"en-US,en;q=0.9"};t&&(o.Cookie=t);try{let r=await fetch(e,{headers:o});if(!r.ok)throw new T(`HTTP ${r.status}: ${r.statusText}`,e);return await r.text()}catch(r){throw r instanceof T?r:new T(`Failed to fetch page: ${r instanceof Error?r.message:String(r)}`,e)}}parseHtml(e){return ve.load(e)}parseEpisodeNumber(e){let t=e.match(/第(\d+)集/);if(t?.[1])return parseInt(t[1],10);let o=e.match(/ep\s?(\d+)/i);if(o?.[1])return parseInt(o[1],10);let r=e.match(/(?:episode|e)\s?(\d+)/i);if(r?.[1])return parseInt(r[1],10);let i=e.match(/\b(\d+)\b/);return i?.[1]?parseInt(i[1],10):null}parseEpisodeType(e,t){let o=t(e),r=o.attr("class")||"",i=o.text().toLowerCase();return r.includes("vip")||i.includes("vip")||i.includes("\u4F1A\u5458")?"vip":r.includes("preview")||r.includes("trailer")||i.includes("preview")||i.includes("\u9884\u544A")?"preview":r.includes("locked")||r.includes("lock")||i.includes("locked")||i.includes("\u9501\u5B9A")?"locked":"available"}};var z=class extends M{getDomain(){return"iq.com"}async extractEpisodes(e,t){let o=await this.fetchHtml(e,t),r=this.extractFromNextData(o);return r.length>0?r:this.extractFromHtml(o)}extractFromNextData(e){let t=[];try{let o=e.match(/<script id="__NEXT_DATA__"[^>]*>(.+?)<\/script>/s);if(!o||!o[1])return t;let i=JSON.parse(o[1]).props?.pageProps?.data;if(!i)return t;let n=JSON.parse(i),{albumInfo:a,videoList:l=[]}=n,{albumId:c,title:d}=a||{};for(let g of l){let{vid:u,episode:v,isTrailer:f,payStatus:w}=g;if(f)continue;let h=this.parseEpisodeNumber(v);if(!h)continue;let p=`https://www.iq.com/play/${c}-${u}?lang=en_us`,x=this.determineTypeFromPayStatus(w);t.push({number:h,url:p,type:x,title:`${d} - Episode ${v}`,extractedAt:new Date})}}catch(o){console.error("Failed to extract from __NEXT_DATA__:",o)}return t.sort((o,r)=>o.number-r.number),t}extractFromHtml(e){let t=this.parseHtml(e),o=[],r=['ul li a[href*="/play/"]','.album-episode-item a[href*="/play/"]','.episode-item a[href*="/play/"]','.intl-play-item a[href*="/play/"]','[data-episode] a[href*="/play/"]'];for(let i of r){let n=t(i);if(n.length>0&&(n.each((a,l)=>{this.processEpisodeLink(t,l,o)}),o.length>0))break}return o.sort((i,n)=>i.number-n.number),o}determineTypeFromPayStatus(e){return e===6?"vip":"available"}processEpisodeLink(e,t,o){let r=e(t),i=r.attr("href");if(!i)return;let n=i.startsWith("http")?i:`https://www.iq.com${i}`,a=r.text().trim(),l=r.attr("title")||void 0;if(a.toUpperCase().includes("BTS"))return;let c=this.parseEpisodeNumber(a);if(c||(c=this.parseEpisodeNumber(i)),!c||o.some(u=>u.number===c))return;let g=this.determineEpisodeType(e,t);o.push({number:c,url:n,type:g,title:l,extractedAt:new Date})}determineEpisodeType(e,t){let o=e(t).closest("li, div");return o.length&&(o.text()||"").toUpperCase().includes("VIP")?"vip":"available"}};var K=class extends M{getDomain(){return"mgtv.com"}async extractEpisodes(e,t){let o=this.extractVideoId(e);if(!o)throw new Error("Could not extract video ID from URL");let r=[],i=0,n=1;do{let a=`https://tinker.glb.mgtv.com/episode/list?src=intelmgtv&abroad=10&_support=10000000&version=5.5.35&video_id=${o}&page=${i}&size=50&platform=4`,l=await this.fetchHtml(a,t),c;try{c=JSON.parse(l)}catch{throw new Error("Failed to parse MGTV API response")}if(c.code!==200)throw new Error(`MGTV API error: ${c.msg}`);n=c.data.total_page;for(let d of c.data.list){let g=this.parseEpisodeNumber(d.t1);if(!g)continue;let u=`https://w.mgtv.com${d.url}`;r.push({number:g,title:d.t2||d.t4||`Episode ${g}`,url:u,type:d.isvip==="1"?"vip":"available",extractedAt:new Date})}i++}while(i<n);return this.deduplicateEpisodes(r)}extractVideoId(e){let t=e.match(/\/b\/\d+\/(\d+)\.html/);return t&&t[1]||null}deduplicateEpisodes(e){let t=new Map;for(let o of e)t.has(o.number)||t.set(o.number,o);return Array.from(t.values()).sort((o,r)=>o.number-r.number)}};var _=class extends M{getDomain(){return"wetv.vip"}async extractEpisodes(e,t){let o=await this.fetchHtml(e,t),r=this.extractFromNextData(o);return r.length>0?r:this.extractFromHtml(o)}extractFromNextData(e){let t=[];try{let o=e.match(/<script id="__NEXT_DATA__"[^>]*>(.+?)<\/script>/s);if(!o||!o[1])return t;let i=JSON.parse(o[1]).props?.pageProps?.data;if(!i)return t;let n=JSON.parse(i),{coverInfo:a,videoList:l=[]}=n,{cid:c,title:d}=a,g=l[0]?.coverList?.[0]||c;for(let u of l){let{vid:v,episode:f,isTrailer:w}=u;if(w)continue;let h=this.parseEpisodeNumber(f);if(!h)continue;let p=encodeURIComponent(d),x=`https://wetv.vip/en/play/${g}/${v}-EP${f}%3A${p}`,C=this.determineTypeFromVideo(u);t.push({number:h,url:x,type:C,title:`${d} - Episode ${f}`,extractedAt:new Date})}}catch(o){console.error("Failed to extract from __NEXT_DATA__:",o)}return t.sort((o,r)=>o.number-r.number),t}extractFromHtml(e){let t=this.parseHtml(e),o=[],r=t('a.play-video__link[href*="/play/"][href*="EP"]');return r.length===0?t('a[href*="/play/"]').filter((n,a)=>(t(a).attr("href")||"").includes("EP")).each((n,a)=>{this.processEpisodeLink(t,a,o)}):r.each((i,n)=>{this.processEpisodeLink(t,n,o)}),o.sort((i,n)=>i.number-n.number),o}determineTypeFromVideo(e){let{labels:t,payStatus:o,defaultPayStatus:r}=e;if(t)for(let n in t){let a=t[n];if(!a)continue;let l=a.text?.toLowerCase()||"";if(l==="express")return"express";if(l==="teaser")return"teaser";if(l==="vip")return"vip"}let i=o||r;return i===6?"vip":i===12?"express":"available"}processEpisodeLink(e,t,o){let r=e(t),i=r.attr("href");if(!i)return;let n=i.startsWith("http")?i:`https://wetv.vip${i}`,a=r.attr("aria-label")||"",l=this.parseEpisodeNumber(a);if(!l||o.some(g=>g.number===l))return;let d=this.determineEpisodeType(e,t);o.push({number:l,url:n,type:d,title:r.attr("title")||void 0,extractedAt:new Date})}determineEpisodeType(e,t){let o=e(t).closest("li");if(o.length){let r=o.find("span.play-video__label").first();if(r.length){let n=r.text().trim().toLowerCase();if(n==="vip"||n.includes("vip"))return"vip";if(n==="teaser"||n.includes("teaser"))return"teaser";if(n==="express"||n.includes("express"))return"express"}let i=o.text()||"";if(i.includes("VIP")&&!i.includes("Teaser"))return"vip";if(i.includes("Teaser"))return"teaser";if(i.includes("Express"))return"express"}return"available"}};var X=class{lastProgressLength=0;notify(e,t){switch(this.lastProgressLength>0&&(process.stdout.write(`\r${" ".repeat(this.lastProgressLength)}\r`),this.lastProgressLength=0),e){case"info":y.info(t);break;case"success":y.success(t);break;case"warning":y.warning(t);break;case"error":y.error(t);break;case"highlight":y.highlight(t);break}}progress(e){this.lastProgressLength>0&&process.stdout.write(`\r${" ".repeat(this.lastProgressLength)}\r`),process.stdout.write(`\r${e}`),this.lastProgressLength=e.length}endProgress(){this.lastProgressLength>0&&(process.stdout.write(`
|
|
7
|
+
`),this.lastProgressLength=0)}};var Y=class{config;apiUrl;constructor(e){this.config=e,this.apiUrl=`https://api.telegram.org/bot${e.botToken}/sendMessage`}async notify(e,t){if(e==="error")try{let o=this.getEmoji(e),r=4e3,i=t;i.length>r&&(i=`${i.substring(0,r)}
|
|
8
|
+
... (truncated)`);let n=this.escapeHtml(i),a=`${o} <b>wetvlo Error</b>
|
|
9
|
+
|
|
10
|
+
<pre>${n}</pre>`,l=await fetch(this.apiUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({chat_id:this.config.chatId,text:a,parse_mode:"HTML"})});if(!l.ok){let c=await l.text();throw new Q(`Failed to send Telegram notification: ${l.status} ${l.statusText}
|
|
11
|
+
${c}`)}}catch(o){console.error("Telegram notification failed:",o)}}escapeHtml(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}getEmoji(e){switch(e){case"info":return"\u2139\uFE0F";case"success":return"\u2705";case"warning":return"\u26A0\uFE0F";case"error":return"\u274C";case"highlight":return"\u{1F514}";default:return""}}async progress(e){}async endProgress(){}};import{createHash as Je}from"crypto";var J=class{queue=[];isExecuting=!1;nextAvailableAt=new Date(0);cooldownMs;constructor(e=0){this.cooldownMs=e}add(e,t){let o=new Date(Date.now()+(t??0));this.queue.push({data:e,addedAt:o})}addFirst(e,t){let o=new Date(Date.now()+(t??0));this.queue.unshift({data:e,addedAt:o})}getNext(){return this.queue.length===0?null:this.queue.shift()?.data??null}peekNext(){return this.queue.length===0?null:this.queue[0]?.data??null}canStart(e){if(this.isExecuting||e<this.nextAvailableAt)return!1;let t=this.queue[0];return!(t&&e<t.addedAt)}markStarted(){this.isExecuting=!0}markCompleted(e){this.isExecuting=!1,this.cooldownMs=e,this.nextAvailableAt=new Date(Date.now()+e)}markFailed(e){this.isExecuting=!1,this.cooldownMs=e,this.nextAvailableAt=new Date(Date.now()+e)}hasTasks(){return this.queue.length>0}getNextAvailableTime(){let e=this.nextAvailableAt,t=this.queue[0];return t&&t.addedAt>e&&(e=t.addedAt),e}getQueueLength(){return this.queue.length}getIsExecuting(){return this.isExecuting}getCooldownMs(){return this.cooldownMs}setCooldownMs(e){this.cooldownMs=e}clear(){this.queue=[]}getStatus(){let e=new Date;return{queueLength:this.queue.length,isExecuting:this.isExecuting,nextAvailableAt:this.nextAvailableAt,cooldownMs:this.cooldownMs,canStartNow:this.canStart(e)}}};var Z=class{queues=new Map;queueCooldowns=new Map;executorBusy=!1;timerId=null;roundRobinIndex=0;stopped=!1;executor;onWait;constructor(e){this.executor=e}setOnWait(e){this.onWait=e}registerQueue(e,t){if(this.queues.has(e))throw new Error(`Queue ${e} is already registered`);let o=new J(t);this.queues.set(e,o),this.queueCooldowns.set(e,t)}hasQueue(e){return this.queues.has(e)}unregisterQueue(e){this.queues.delete(e),this.queueCooldowns.delete(e)}addTask(e,t,o){let r=this.queues.get(e);if(!r)throw new Error(`Queue ${e} is not registered`);r.add(t,o),this.stopped||this.scheduleNext()}addPriorityTask(e,t,o){let r=this.queues.get(e);if(!r)throw new Error(`Queue ${e} is not registered`);r.addFirst(t,o),this.stopped||this.scheduleNext()}markTaskComplete(e,t){let o=this.queues.get(e);if(!o)throw new Error(`Queue ${e} is not registered`);let r=t??this.queueCooldowns.get(e)??0;o.markCompleted(r),this.executorBusy=!1,this.stopped||this.scheduleNext()}markTaskFailed(e,t){let o=this.queues.get(e);if(!o)throw new Error(`Queue ${e} is not registered`);let r=t??this.queueCooldowns.get(e)??0;o.markFailed(r),this.executorBusy=!1,this.stopped||this.scheduleNext()}scheduleNext(){if(this.stopped||(this.clearTimer(),this.trySchedule())||this.executorBusy)return;let t=this.getEarliestAvailableTime();if(t){let o=Date.now(),r=Math.max(0,t.time.getTime()-o);this.scheduleTimer(r,t.queueName,t.time)}}trySchedule(){if(this.executorBusy)return!1;let e=new Date,t=Array.from(this.queues.keys());if(t.length===0)return!1;for(let o=0;o<t.length;o++){let r=(this.roundRobinIndex+o)%t.length,i=t[r];if(!i)continue;let n=this.queues.get(i);if(n&&n.hasTasks()&&n.canStart(e)){let a=n.getNext();if(a)return n.markStarted(),this.executorBusy=!0,this.roundRobinIndex=(r+1)%t.length,this.executeTask(i,a).catch(l=>{console.error(`[UniversalScheduler] Task execution failed: ${l}`),this.markTaskFailed(i)}),!0}}return!1}async executeTask(e,t){await this.executor(t,e)}scheduleTimer(e,t,o){this.clearTimer(),this.onWait&&e>1e3&&this.onWait(t,e,o),this.timerId=setTimeout(()=>{this.timerId=null,this.scheduleNext()},e)}clearTimer(){this.timerId!==null&&(clearTimeout(this.timerId),this.timerId=null)}getEarliestAvailableTime(){let e=null;for(let[t,o]of this.queues.entries()){if(!o.hasTasks())continue;let r=o.getNextAvailableTime();(e===null||r<e.time)&&(e={time:r,queueName:t})}return e}stop(){this.stopped=!0,this.clearTimer()}resume(){this.stopped=!1,this.scheduleNext()}getStats(){let e=new Map;for(let[t,o]of this.queues.entries()){let r=o.getStatus();e.set(t,{queueLength:r.queueLength,isExecuting:r.isExecuting,nextAvailableAt:r.nextAvailableAt})}return e}isExecutorBusy(){return this.executorBusy}hasPendingTasks(){for(let e of this.queues.values())if(e.hasTasks())return!0;return!1}getTotalPendingTasks(){let e=0;for(let t of this.queues.values())e+=t.getQueueLength();return e}};var ee=class{stateManager;downloadManager;scheduler;running=!1;domainHandlers=new Map;constructor(e,t,o){this.stateManager=m.getStateManager(),this.downloadManager=e;let r=o||(i=>new Z(i));this.scheduler=r(async(i,n)=>{await this.executeTask(i,n)}),this.scheduler.setOnWait((i,n)=>{let a=m.getNotifier(),l=Math.round(n/1e3),c=i.split(":"),d=c[0],g=c[1];d==="download"?a.notify("info",`[${g}] Next download in ${l}s...`):d==="check"&&a.notify("info",`[${g}] Next check in ${l}s...`)})}addSeriesCheck(e){let t=m.getNotifier(),o=k(e.url);this.registerDownloadQueue(o);let r=this.registerSeriesCheckQueue(o,e),i={seriesUrl:e.url,seriesName:e.name,config:e,attemptNumber:1,retryCount:0};this.scheduler.addTask(r,i),t.notify("info",`[QueueManager] Added ${e.name} to check queue for domain ${o}`)}addEpisodes(e,t,o,r){let i=m.getNotifier(),n=m.getConfig();if(o.length===0)return;let a=k(e);this.registerDownloadQueue(a);let l=n.resolve(e,"series"),{downloadDelay:c}=l.download;for(let d=0;d<o.length;d++){let g=o[d];if(!g)continue;let u={seriesUrl:e,seriesName:t,episode:g,config:r,retryCount:0},v=`download:${a}`,f=d*c*1e3;this.scheduler.addTask(v,u,f)}i.notify("success",`[QueueManager] Added ${o.length} episodes to download queue for ${t} (domain ${a})`)}updateConfig(){m.getNotifier().notify("info","[QueueManager] Configuration will be reloaded from AppContext")}start(){let e=m.getNotifier();if(this.running)throw new Error("QueueManager is already running");this.running=!0,this.scheduler.resume(),e.notify("info","[QueueManager] Started queue processing")}async stop(){let e=m.getNotifier();this.running&&(e.notify("info","[QueueManager] Stopping queue processing..."),this.scheduler.stop(),this.running=!1,e.notify("info","[QueueManager] Queue processing stopped"))}hasActiveProcessing(){return this.scheduler.isExecutorBusy()||this.scheduler.hasPendingTasks()}getQueueStats(){let e=this.scheduler.getStats(),t={},o={};for(let[r,i]of e.entries())if(r.startsWith("check:")){let a=r.split(":")[1];if(!a)continue;t[a]||(t[a]={length:0,processing:!1}),t[a].length+=i.queueLength,i.isExecuting&&(t[a].processing=!0)}else if(r.startsWith("download:")){let n=r.slice(9);o[n]={length:i.queueLength,processing:i.isExecuting}}return{checkQueues:t,downloadQueues:o}}registerDownloadQueue(e){let t=m.getConfig(),o=`download:${e}`;if(this.scheduler.hasQueue(o))return;let r=`https://${e}/`,i=t.resolve(r,"domain"),{downloadDelay:n}=i.download;this.scheduler.registerQueue(o,n*1e3)}registerSeriesCheckQueue(e,t){let o=m.getConfig(),r=Je("md5").update(t.url).digest("hex").substring(0,12),i=`check:${e}:${r}`;if(this.scheduler.hasQueue(i))return i;let n=o.resolve(t.url,"series"),{checkInterval:a}=n.check;if(this.scheduler.registerQueue(i,a*1e3),!this.domainHandlers.has(e)){let l=I.getHandlerOrThrow(`https://${e}/`);this.domainHandlers.set(e,l)}return i}async executeTask(e,t){let o=t.split(":"),r=o[0],i=o[1];if(!r||!i)throw new Error(`Invalid queue name format: ${t}`);if(r==="check")await this.executeCheck(e,i,t);else if(r==="download")await this.executeDownload(e,i,t);else throw new Error(`Unknown queue type: ${r}`)}async executeCheck(e,t,o){let r=m.getNotifier(),i=m.getConfig(),{seriesUrl:n,seriesName:a,config:l,attemptNumber:c,retryCount:d=0}=e,g=this.domainHandlers.get(t);if(!g)throw new Error(`No handler found for domain ${t}`);let u=i.resolve(n,"series"),{count:v,checkInterval:f}=u.check;try{let w=await this.performCheck(g,n,a,u,c,t);if(w.hasNewEpisodes)r.notify("success",`[${t}] Found ${w.episodes.length} new episodes for ${a} (attempt ${c}/${v})`),this.addEpisodes(n,a,w.episodes,l),this.scheduler.markTaskComplete(o,f*1e3);else if(c<v){let h=f*1e3,p=w.requeueDelay??h;r.notify("info",`[${t}] No new episodes for ${a} (attempt ${c}/${v}), requeueing in ${Math.round(p/1e3)}s`);let x={...e,attemptNumber:c+1,retryCount:0};this.scheduler.addTask(o,x,p),this.scheduler.markTaskComplete(o,f*1e3)}else r.notify("info",`[${t}] Checks exhausted for ${a} (${v} attempts with no new episodes)`),this.scheduler.markTaskComplete(o,f*1e3)}catch(w){let h=w instanceof Error?w.message:String(w),{maxRetries:p,initialTimeout:x,backoffMultiplier:C,jitterPercentage:S}=u.download;if(d<p){let H=this.calculateBackoff(d,x*1e3,C,S);r.notify("warning",`[${t}] Check failed for ${a}, retrying in ${Math.round(H/1e3)}s (attempt ${d+1}/${p})`);let De={...e,retryCount:d+1};this.scheduler.addPriorityTask(o,De,H),this.scheduler.markTaskComplete(o,f*1e3)}else r.notify("error",`[${t}] Failed to check ${a} after ${d} retry attempts: ${h}`),this.scheduler.markTaskComplete(o,f*1e3)}}async executeDownload(e,t,o){let r=m.getNotifier(),i=m.getConfig(),{seriesUrl:n,seriesName:a,episode:l,retryCount:c=0}=e,d=i.resolve(n,"series"),{downloadDelay:g,minDuration:u}=d.download;try{await this.downloadManager.download(n,a,l,u),r.notify("success",`[${t}] Successfully queued download of Episode ${l.number} for ${a}`),this.scheduler.markTaskComplete(o,g*1e3)}catch(v){let f=v instanceof Error?v.message:String(v),{maxRetries:w,initialTimeout:h,backoffMultiplier:p,jitterPercentage:x}=d.download;if(c<w){let C=this.calculateBackoff(c,h*1e3,p,x);r.notify("warning",`[${t}] Download failed for Episode ${l.number}, retrying in ${Math.round(C/1e3)}s (attempt ${c+1}/${w})`);let S={...e,retryCount:c+1};this.scheduler.addPriorityTask(o,S,C),this.scheduler.markTaskComplete(o,g*1e3)}else r.notify("error",`[${t}] Failed to download Episode ${l.number} after ${c+1} attempts: ${f}`),this.scheduler.markTaskComplete(o,g*1e3)}}async performCheck(e,t,o,r,i,n){let a=m.getNotifier(),l=r.check.count;a.notify("info",`[${n}] Checking ${t} for new episodes... (attempt ${i}/${l})`);let c=await e.extractEpisodes(t),d=new Map;c.forEach(f=>{let w=d.get(f.type)||0;d.set(f.type,w+1)});let g=Array.from(d.entries()).map(([f,w])=>`${f}: ${w}`).join(", ");a.notify("info",`[${n}] Found ${c.length} total episodes on ${t} (${g})`);let{downloadTypes:u}=r.check,v=c.filter(f=>{let w=u.includes(f.type),h=r.stateFile,p=!this.stateManager.isDownloaded(h,o,f.number);return w&&p});if(c.length!==v.length){let f=c.length-v.length;a.notify("info",`[${n}] Filtering to ${u.join(" or ")}: ${v.length} episodes to download, ${f} skipped`)}return v.length>0?{hasNewEpisodes:!0,episodes:v}:{hasNewEpisodes:!1,episodes:[],shouldRequeue:!0}}calculateBackoff(e,t,o,r){let i=t*o**e,n=i*r/100,a=(Math.random()*2-1)*n,l=Math.max(0,i+a);return Math.floor(l)}};import Ze from"cron-parser";function et(s){let e=s.match(/^(\d{1,2}):(\d{2})$/);if(!e)throw new Error(`Invalid time format: "${s}". Expected HH:MM`);let[,t,o]=e,r=parseInt(t||"0",10),i=parseInt(o||"0",10);if(r<0||r>23)throw new Error(`Invalid hours: ${r}. Must be between 0 and 23`);if(i<0||i>59)throw new Error(`Invalid minutes: ${i}. Must be between 0 and 59`);let n=new Date;return n.setHours(r,i,0,0),n}function xe(s){let e=et(s),t=new Date,o=new Date(t);return o.setHours(e.getHours(),e.getMinutes(),0,0),o<=t&&o.setDate(o.getDate()+1),o.getTime()-t.getTime()}function Ce(s){try{let t=Ze.parseExpression(s).next().toDate(),o=new Date;return t.getTime()-o.getTime()}catch(e){throw new Error(`Invalid cron expression: "${s}". Error: ${e instanceof Error?e.message:String(e)}`)}}function Se(s){return new Promise(e=>setTimeout(e,s))}var te=class{configs;downloadManager;cookies;options;queueManager;running=!1;stopped=!0;timeProvider;scheduleTimer=null;constructor(e,t,o,r={mode:"scheduled"},i,n){this.configs=e,this.downloadManager=t,this.cookies=o,this.options=r,this.timeProvider=i||{getMsUntilTime:xe,getMsUntilCron:Ce,sleep:Se};let a=n||((l,c)=>new ee(l,c));this.queueManager=a(this.downloadManager,this.cookies)}async start(){if(this.running)throw new B("Scheduler is already running");this.running=!0,this.stopped=!1,this.queueManager.start();let e=m.getNotifier();if(this.options.mode==="once")e.notify("info","Single-run mode: checking all series once"),await this.runOnce(),this.running=!1;else return e.notify("info","Scheduler started (queue-based architecture)"),this.scheduleNextBatch(),new Promise(t=>{let o=setInterval(()=>{this.running||(clearInterval(o),t())},100)})}scheduleNextBatch(){if(this.stopped)return;let e=m.getNotifier(),t=this.groupConfigsBySchedule(),o=null,r=Number.MAX_SAFE_INTEGER;for(let n of t.keys()){let a;try{/^\d{1,2}:\d{2}$/.test(n)?a=this.timeProvider.getMsUntilTime(n):a=this.timeProvider.getMsUntilCron(n),a<r&&(r=a,o=n)}catch(l){e.notify("error",`Error calculating next run time for schedule "${n}": ${l instanceof Error?l.message:String(l)}`)}}if(!o){e.notify("warning","No scheduled configs found.");return}let i=t.get(o);i&&(r>0&&(this.options.onIdle?.(),e.notify("info",`Waiting ${Math.floor(r/1e3/60)} minutes until next run (${o})...`)),this.scheduleTimer=setTimeout(async()=>{this.stopped||(await this.runConfigs(i),await this.waitForQueueDrain(),this.scheduleNextBatch())},r))}async waitForQueueDrain(){for(;this.queueManager.hasActiveProcessing()&&!this.stopped;)await this.timeProvider.sleep(1e3)}async stop(){let e=m.getNotifier();e.notify("info","Stopping scheduler..."),this.stopped=!0,this.scheduleTimer&&(clearTimeout(this.scheduleTimer),this.scheduleTimer=null),await this.queueManager.stop(),this.running=!1,e.notify("info","Scheduler stopped")}async reload(e){let t=m.getNotifier();t.notify("info","Reloading configuration..."),this.configs=e,this.queueManager.updateConfig(),this.running&&this.options.mode==="scheduled"&&(this.scheduleTimer&&(clearTimeout(this.scheduleTimer),this.scheduleTimer=null),this.scheduleNextBatch()),t.notify("success","Configuration reloaded")}async triggerAllChecks(){m.getNotifier().notify("info","Triggering immediate checks for all series...");for(let t of this.configs)this.queueManager.addSeriesCheck(t)}groupConfigsBySchedule(){let e=m.getNotifier(),t=new Map;for(let o of this.configs){let r=o.cron||o.startTime;if(!r){e.notify("warning",`Series "${o.name}" has no startTime or cron configured. Skipping.`);continue}let i=t.get(r)||[];i.push(o),t.set(r,i)}return t}async runConfigs(e){let t=m.getNotifier();for(let r of e){if(this.stopped)break;this.queueManager.addSeriesCheck(r)}let o=this.queueManager.getQueueStats();t.notify("info",`Added ${e.length} series to check queues. Queue stats: ${JSON.stringify(o)}`)}async runOnce(){let e=m.getNotifier();for(let t of this.configs){if(this.stopped)break;this.queueManager.addSeriesCheck(t)}for(;this.queueManager.hasActiveProcessing()&&!this.stopped;)await this.timeProvider.sleep(1e3);e.notify("success","Single-run complete")}isRunning(){return this.running&&!this.stopped}getQueueManager(){return this.queueManager}};import{existsSync as tt}from"fs";import{readFile as ot}from"fs/promises";import{homedir as Yo}from"os";import{join as Zo}from"path";async function ke(s){if(!tt(s))throw new j(`Cookie file not found: "${s}"`);let t=(await ot(s,"utf-8")).split(`
|
|
12
|
+
`),o=[];for(let r of t){let i=r.trim();if(i.startsWith("#")||!i)continue;let n=r.split(" ");if(n.length>=7){let a=n[5],l=n[6];if(a&&l){let c=l.trim();c&&o.push(`${a}=${c}`)}}}return o.join("; ")}var lt={loadConfig:ue,checkYtDlpInstalled:U.checkYtDlpInstalled,readCookieFile:ke,createDownloadManager:(s,e,t)=>new U(s,e,t),createScheduler:(s,e,t,o)=>new te(s,e,t,o)};async function ne(s){y.info("Shutting down gracefully...");try{await s.stop(),y.success("Shutdown complete")}catch(e){y.error(`Error during shutdown: ${e instanceof Error?e.message:String(e)}`)}}async function ct(s,e,t=lt){if(y.info(`Mode: ${e==="once"?"Single-run (checks once, exits)":"Scheduled (waits for startTime)"}`),y.info("Checking yt-dlp installation..."),!await t.checkYtDlpInstalled())throw new Error(`yt-dlp is not installed. Please install it first:
|
|
15
13
|
- macOS: brew install yt-dlp
|
|
16
14
|
- Linux: pip install yt-dlp
|
|
17
|
-
- Windows: winget install yt-dlp`);
|
|
15
|
+
- Windows: winget install yt-dlp`);y.info(`Loading configuration from ${s}...`);let r=await t.loadConfig(s);y.success("Configuration loaded");let i=new q(r),n=[new X],a=i.getConfig("global");if(a.telegram)try{n.push(new Y(a.telegram)),y.info("Telegram notifications enabled for errors")}catch(p){y.warning(`Failed to set up Telegram: ${p instanceof Error?p.message:String(p)}`)}let l={notify:async(p,x)=>{await Promise.all(n.map(C=>C.notify(p,x)))},progress:p=>{for(let x of n)x.progress(p)},endProgress:()=>{for(let p of n)p.endProgress()}},c=new N(l);m.initialize(i,l,c),y.info("AppContext initialized"),I.register(new _),I.register(new z),I.register(new K),y.info(`Registered handlers: ${I.getDomains().join(", ")}`);let d;if(a.cookieFile)try{d=await t.readCookieFile(a.cookieFile),y.success("Cookies loaded from file")}catch(p){y.warning(`Failed to load cookies: ${p instanceof Error?p.message:String(p)}`)}let g=a.download.downloadDir,u=a.download.tempDir,v=a.cookieFile,f=t.createDownloadManager(g,v,u),w;e==="scheduled"&&process.stdin.isTTY&&(w=()=>{y.info("Interactive mode enabled:"),y.info(" [r] Reload configuration"),y.info(" [c] Trigger immediate checks"),y.info(" [q] Quit")}),y.info("Using queue-based scheduler");let h=t.createScheduler(r.series,f,d,{mode:e,onIdle:w});process.on("SIGINT",async()=>{await ne(h),process.exit(0)}),process.on("SIGTERM",async()=>{await ne(h),process.exit(0)}),e==="scheduled"&&process.stdin.isTTY&&(Ee.emitKeypressEvents(process.stdin),process.stdin.setRawMode(!0),process.stdin.on("keypress",async(p,x)=>{if(!x)return;let C=x.name||"";if(C==="q"||C==="\u0439"||x.ctrl&&C==="c")await ne(h),process.exit(0);else if(C==="r"||C==="\u043A")try{y.info(`Reloading configuration from ${s}...`);let S=await t.loadConfig(s),H=new q(S);m.reloadConfig(H),await h.reload(S.series)}catch(S){y.error(`Failed to reload config: ${S instanceof Error?S.message:String(S)}`)}else(C==="c"||C==="\u0441")&&await h.triggerAllChecks()})),await h.start()}var Te=it({name:"wetvlo",description:"CLI Video Downloader for Chinese streaming sites",version:"0.0.1",args:{config:nt({type:at,long:"config",short:"c",defaultValue:()=>"./config.yaml",description:"Path to configuration file (default: ./config.yaml)"}),once:st({type:rt,long:"once",short:"o",description:"Run in single-run mode (check once and exit)"})},handler:async({config:s,once:e})=>{try{await ct(s,e?"once":"scheduled")}catch(t){t instanceof R?y.error(`Configuration error: ${t.message}`):y.error(`Fatal error: ${t instanceof Error?t.message:String(t)}`),process.exit(1)}}});async function pt(s=process.argv.slice(2)){await ut(Te,s)}var ft=import.meta.main||process.argv[1]&&process.argv[1]===dt(import.meta.url);ft&&await pt();export{pt as main};
|
|
18
16
|
//# sourceMappingURL=index.js.map
|