wetvlo 0.0.13 → 0.0.14

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 CHANGED
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env node
2
- import{fileURLToPath as vt}from"url";import{run as bt}from"cmd-ts";import*as Ie from"readline";import{boolean as pt,command as ft,flag as gt,option as mt,string as ht}from"cmd-ts";import{existsSync as Le,readFileSync as Pe}from"fs";import{writeFile as Fe}from"fs/promises";import{isAbsolute as Oe,join as qe}from"path";function ce(){return{version:"3.0.0",series:{}}}var M=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(!Le(t))return ce();try{let o=Pe(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 Fe(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 Oe(e)?e:qe(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 v=class s{static configRegistry;static notifier;static stateManager;static initialize(e,t,o){s.configRegistry=e,s.notifier=t,s.stateManager=o||(t?new M(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 Ke}from"fs";import{readFile as Ye}from"fs/promises";import{join as Xe}from"path";import*as pe from"js-yaml";var T=class extends Error{constructor(e){super(e),this.name="WetvloError"}},R=class extends T{constructor(e){super(e),this.name="ConfigError"}};var I=class extends T{constructor(t,o){super(t);this.url=o;this.name="HandlerError"}},G=class extends T{constructor(t,o){super(t);this.url=o;this.name="DownloadError"}},Q=class extends T{constructor(e){super(e),this.name="NotificationError"}},j=class extends T{constructor(e){super(e),this.name="CookieError"}},W=class extends T{constructor(e){super(e),this.name="SchedulerError"}};function Ue(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 Ue(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 He=b.enum(["available","vip","svip","teaser","express","preview","locked"]),_e=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(He).optional().describe("Episode types to download")}),Ge=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")}),Qe=b.object({botToken:b.string().describe("Telegram bot token"),chatId:b.string().describe("Telegram chat ID")}),je=b.enum(["chrome","firefox","safari","chromium","edge"]),ie=b.object({check:_e.optional().describe("Check settings"),download:Ge.optional().describe("Download settings"),telegram:Qe.optional().describe("Telegram notification configuration"),stateFile:b.string().optional().describe("Path to state file"),browser:je.optional().describe("Browser to use for scraping"),cookieFile:b.string().optional().describe("Path to cookie file"),subLangs:b.array(b.string()).optional().describe("List of subtitle languages to download")}),We=ie,Ve=ie.extend({domain:b.string().describe('Domain name (e.g., "weTV")')}),Be=ie.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")}),ze=b.object({series:b.array(Be).min(1,"Cannot be empty").describe("List of series to monitor"),domainConfigs:b.array(Ve).optional().describe("Domain-specific configurations"),globalConfig:We.optional().describe("Global configuration defaults")});function ue(s){ze.parse(s);let e=[];if(s.globalConfig){let t=s.globalConfig;t.downloadDir&&!t.download?.downloadDir&&e.push(`'downloadDir' found directly under 'globalConfig'. It should be placed under 'globalConfig.download'. Current value: "${t.downloadDir}"`),t.tempDir&&!t.download?.tempDir&&e.push(`'tempDir' found directly under 'globalConfig'. It should be placed under 'globalConfig.download'. Current value: "${t.tempDir}"`),t.count&&!t.check?.count&&e.push("'count' found directly under 'globalConfig'. It should be placed under 'globalConfig.check'."),t.checkInterval&&!t.check?.checkInterval&&e.push("'checkInterval' found directly under 'globalConfig'. It should be placed under 'globalConfig.check'.")}s.globalConfigs&&e.push("'globalConfigs' found. Did you mean 'globalConfig'?"),s.domainConfigs&&Array.isArray(s.domainConfigs)&&s.domainConfigs.forEach((t,o)=>{let r=t;r.downloadDir&&!r.download?.downloadDir&&e.push(`'downloadDir' found directly under 'domainConfigs[${o}]. It should be placed under 'domainConfigs[${o}].download'.`)}),e.length>0&&(console.warn(`
2
+ import{fileURLToPath as bt}from"url";import{run as xt}from"cmd-ts";import*as Ae from"readline";import{boolean as Re,command as ht,flag as Me,option as yt,string as wt}from"cmd-ts";import{existsSync as Ue,readFileSync as He}from"fs";import{writeFile as _e}from"fs/promises";import{isAbsolute as Ge,join as Qe}from"path";function pe(){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(!Ue(t))return pe();try{let o=He(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 _e(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 Ge(e)?e:Qe(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 b=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 et}from"fs";import{readFile as tt}from"fs/promises";import{join as ot}from"path";import*as fe from"js-yaml";var $=class extends Error{constructor(e){super(e),this.name="WetvloError"}},M=class extends ${constructor(e){super(e),this.name="ConfigError"}};var I=class extends ${constructor(t,o){super(t);this.url=o;this.name="HandlerError"}},W=class extends ${constructor(t,o){super(t);this.url=o;this.name="DownloadError"}},B=class extends ${constructor(e){super(e),this.name="NotificationError"}},V=class extends ${constructor(e){super(e),this.name="CookieError"}},j=class extends ${constructor(e){super(e),this.name="SchedulerError"}};function We(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 z(s){if(typeof s=="string")return We(s);if(Array.isArray(s))return s.map(e=>z(e));if(s!==null&&typeof s=="object"){let e={};for(let[t,o]of Object.entries(s))e[t]=z(o);return e}return s}import{z as v}from"zod";var Be=v.enum(["available","vip","svip","teaser","express","preview","locked"]),Ve=v.object({count:v.number().positive().optional().describe("Number of episodes to check"),checkInterval:v.number().positive().optional().describe("Interval between checks in seconds"),downloadTypes:v.array(Be).optional().describe("Episode types to download")}),je=v.object({downloadDir:v.string().optional().describe("Directory to save downloaded episodes"),tempDir:v.string().optional().describe("Directory for temporary files"),downloadDelay:v.number().nonnegative().optional().describe("Delay between downloads in milliseconds"),maxRetries:v.number().int().nonnegative().optional().describe("Maximum number of retry attempts"),initialTimeout:v.number().positive().optional().describe("Initial timeout for operations in milliseconds"),backoffMultiplier:v.number().positive().optional().describe("Multiplier for exponential backoff"),jitterPercentage:v.number().int().min(0).max(100).optional().describe("Jitter percentage for retry delays"),minDuration:v.number().nonnegative().optional().describe("Minimum duration in seconds for downloads")}),ze=v.object({botToken:v.string().describe("Telegram bot token"),chatId:v.string().describe("Telegram chat ID")}),Ke=v.enum(["chrome","firefox","safari","chromium","edge"]),se=v.object({check:Ve.optional().describe("Check settings"),download:je.optional().describe("Download settings"),telegram:ze.optional().describe("Telegram notification configuration"),stateFile:v.string().optional().describe("Path to state file"),browser:Ke.optional().describe("Browser to use for scraping"),cookieFile:v.string().optional().describe("Path to cookie file"),subLangs:v.array(v.string()).optional().describe("List of subtitle languages to download")}),Ye=se,Xe=se.extend({domain:v.string().describe('Domain name (e.g., "weTV")')}),Je=se.extend({name:v.string().describe("Series name"),url:v.url().describe("Series URL"),startTime:v.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:v.string().optional().describe("Cron expression for scheduling")}),Ze=v.object({series:v.array(Je).min(1,"Cannot be empty").describe("List of series to monitor"),domainConfigs:v.array(Xe).optional().describe("Domain-specific configurations"),globalConfig:Ye.optional().describe("Global configuration defaults")});function ge(s){Ze.parse(s);let e=[];if(s.globalConfig){let t=s.globalConfig;t.downloadDir&&!t.download?.downloadDir&&e.push(`'downloadDir' found directly under 'globalConfig'. It should be placed under 'globalConfig.download'. Current value: "${t.downloadDir}"`),t.tempDir&&!t.download?.tempDir&&e.push(`'tempDir' found directly under 'globalConfig'. It should be placed under 'globalConfig.download'. Current value: "${t.tempDir}"`),t.count&&!t.check?.count&&e.push("'count' found directly under 'globalConfig'. It should be placed under 'globalConfig.check'."),t.checkInterval&&!t.check?.checkInterval&&e.push("'checkInterval' found directly under 'globalConfig'. It should be placed under 'globalConfig.check'.")}s.globalConfigs&&e.push("'globalConfigs' found. Did you mean 'globalConfig'?"),s.domainConfigs&&Array.isArray(s.domainConfigs)&&s.domainConfigs.forEach((t,o)=>{t.downloadDir&&!t.download?.downloadDir&&e.push(`'downloadDir' found directly under 'domainConfigs[${o}]. It should be placed under 'domainConfigs[${o}].download'.`)}),e.length>0&&(console.warn(`
3
3
  \u26A0\uFE0F Configuration Warnings:`),console.warn("The following configuration issues were detected:"),e.forEach((t,o)=>{console.warn(`${o+1}. ${t}`)}),console.warn(`Please fix these issues in your config.yaml file.
4
- `))}var Je="./config.yaml";async function fe(s=Je){let e=Xe(process.cwd(),s);if(!Ke(e))throw new R(`Configuration file not found: "${e}". Create a config.yaml file or specify a different path.`);let t=await Ye(e,"utf-8"),o;try{o=pe.load(t)}catch(i){throw new R(`Failed to parse YAML: ${i instanceof Error?i.message:String(i)}`)}return ue(o),V(o)}function A(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];ge(r)&&ge(i)?t[o]=A(i,r):t[o]=r}return t}function ge(s){return typeof s=="object"&&s!==null&&!Array.isArray(s)}function E(s){try{return new URL(s).hostname}catch{throw new Error(`Invalid URL: "${s}"`)}}var Ze={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 me(){return Ze}var q=class{map=new Map;seriesByUrl=new Map;constructor(e){let t=me(),o=A(t,e.globalConfig);this.setConfig("global",o);for(let r of e.domainConfigs||[]){let i=A(o,r);this.setConfig(`domain:${r.domain}`,i)}for(let r of e.series){let i=E(r.url),n=this.getConfig(`domain:${i}`);if(!n){let l=this.getConfig("global");n=A(l,{domain:i})}let a=A(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=E(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(E(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 F from"fs";import*as k from"fs/promises";import{basename as rt,join as Ce,resolve as P}from"path";function he(s){return s.replace(/[<>:"/\\|?*]/g,"_").replace(/[\x00-\x1F]/g,"").replace(/[\s.]+$/,"")}import{execa as et}from"execa";var S={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"},ne=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}${S.reset}`:e}debug(e){this.shouldLog("DEBUG")&&console.log(this.format("DEBUG",this.colorize(e,S.dim)))}info(e){this.shouldLog("INFO")&&console.log(this.format("INFO",this.colorize(e,S.dim+S.white)))}success(e){this.shouldLog("SUCCESS")&&console.log(this.format("SUCCESS",this.colorize(e,S.green)))}warning(e){this.shouldLog("WARNING")&&console.log(this.format("WARNING",this.colorize(e,S.yellow)))}error(e){this.shouldLog("ERROR")&&console.error(this.format("ERROR",this.colorize(e,S.red)))}highlight(e){this.shouldLog("HIGHLIGHT")&&console.log(this.format("HIGHLIGHT",this.colorize(e,S.bright+S.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}},w=new ne;async function ye(s){try{let{stdout:e}=await et("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 w.error(`Failed to get video duration for ${s}: ${e instanceof Error?e.message:String(e)}`),0}}function we(s){return{downloadDir:s.download.downloadDir,tempDir:s.download.tempDir,cookieFile:s.cookieFile,minDuration:s.download.minDuration,subLangs:s.subLangs}}var B=class{};import*as be from"fs/promises";import{join as ot}from"path";import{execa as ve}from"execa";var U=class{async download(e,t,o,r={}){let{args:i=[],cookieFile:n,subLangs:a,onProgress:l,onLog:d}=r,c=ot(o,`${t}.%(ext)s`);await be.mkdir(o,{recursive:!0});let p=["--no-warnings","--newline","-o",c];n&&p.unshift("--cookies",n),a&&a.length>0&&p.push("--write-subs","--sub-lang",a.join(",")),p.push(...i),i.some(h=>h===e)||p.push(e);let u=null,m=new Set,f=[];try{let h=ve("yt-dlp",p,{all:!0});if(h.all)for await(let y of h.all){let g=y.toString().trim();if(!g)continue;f.push(g);let x=g.match(/\[download\] Destination:\s*(.+)/);x&&(u=x[1],u&&m.add(u));let C=g.match(/\[info\] Writing video subtitles to:\s*(.+)/);C?.[1]&&m.add(C[1]);let N=g.match(/\[merge\] Merging formats into "(.*)"/);if(N&&(u=N[1],u&&m.add(u)),g.startsWith("[info]")||g.startsWith("[ffmpeg]")||g.startsWith("[merge]")||g.startsWith("[ExtractAudio]")||g.startsWith("[Metadata]")||g.startsWith("[Thumbnails]")){d?.(g);continue}if(g.startsWith("[download]")){let _=g.match(/\[download\]\s+(\d+\.?\d*)%\s+of\s+~?\s*([\d.]+\w+)\s+at\s+~?\s*([\d.]+\w+\/s)\s+ETA\s+(\S+)/);if(_){let[,Ne,Re,Me,Ae]=_;l?.(`[download] ${Ne}% of ${Re} at ${Me} ETA ${Ae}`)}else d?.(g);continue}d?.(g)}if(await h,!u)throw new Error("Could not determine downloaded filename from output");return{filename:u,allFiles:Array.from(m)}}catch(h){let y=h instanceof Error?h.message:String(h),g=f.join(`
5
- `);throw new Error(`yt-dlp failed: ${y}
4
+ `))}var rt="./config.yaml";async function me(s=rt){let e=ot(process.cwd(),s);if(!et(e))throw new M(`Configuration file not found: "${e}". Create a config.yaml file or specify a different path.`);let t=await tt(e,"utf-8"),o;try{o=fe.load(t)}catch(i){throw new M(`Failed to parse YAML: ${i instanceof Error?i.message:String(i)}`)}return ge(o),z(o)}function P(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];he(r)&&he(i)?t[o]=P(i,r):t[o]=r}return t}function he(s){return typeof s=="object"&&s!==null&&!Array.isArray(s)}function D(s){try{return new URL(s).hostname}catch{throw new Error(`Invalid URL: "${s}"`)}}var it={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 ye(){return it}var H=class{map=new Map;seriesByUrl=new Map;constructor(e){let t=ye(),o=P(t,e.globalConfig);this.setConfig("global",o);for(let r of e.domainConfigs||[]){let i=P(o,r);this.setConfig(`domain:${r.domain}`,i)}for(let r of e.series){let i=D(r.url),n=this.getConfig(`domain:${i}`);if(!n){let l=this.getConfig("global");n=P(l,{domain:i})}let a=P(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=D(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(D(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 q from"fs";import*as T from"fs/promises";import{basename as A,join as Ee,resolve as O}from"path";function we(s){return s.replace(/[<>:"/\\|?*]/g,"_").replace(/[\x00-\x1F]/g,"").replace(/[\s.]+$/,"")}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"},ae=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}${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.dim+k.white)))}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}},c=new ae;import{execa as nt}from"execa";async function ve(s){try{let{stdout:e}=await nt("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 c.error(`Failed to get video duration for ${s}: ${e instanceof Error?e.message:String(e)}`),0}}function be(s){return{downloadDir:s.download.downloadDir,tempDir:s.download.tempDir,cookieFile:s.cookieFile,minDuration:s.download.minDuration,subLangs:s.subLangs}}var K=class{};import*as Ce from"fs/promises";import{join as at}from"path";import{execa as xe}from"execa";var _=class{async download(e,t,o,r={}){let{args:i=[],cookieFile:n,subLangs:a,onProgress:l,onLog:u}=r,d=at(o,`${t}.%(ext)s`);await Ce.mkdir(o,{recursive:!0});let g=["--no-warnings","--newline","-o",d];n&&g.unshift("--cookies",n),a&&a.length>0&&g.push("--write-subs","--sub-lang",a.join(",")),g.push(...i),i.some(y=>y===e)||g.push(e);let p=null,h=new Set,m=[];try{let y=xe("yt-dlp",g,{all:!0});if(y.all)for await(let w of y.all){let f=w.toString().trim();if(!f)continue;m.push(f);let x=f.match(/\[download\] Destination:\s*(.+)/);x&&(p=x[1],p&&h.add(p));let C=f.match(/\[info\] Writing video subtitles to:\s*(.+)/);C?.[1]&&h.add(C[1]);let E=f.match(/\[merge\] Merging formats into "(.*)"/);if(E&&(p=E[1],p&&h.add(p)),![/:\s+Downloading webpage$/,/:\s+Downloading m3u8 information$/,/:\s+Downloading m3u8 manifest$/,/Downloading \d+ format\(s\)/,/FixupM3u8/].some(L=>L.test(f))){if(f.startsWith("[info]")||f.startsWith("[ffmpeg]")||f.startsWith("[merge]")||f.startsWith("[ExtractAudio]")||f.startsWith("[Metadata]")||f.startsWith("[Thumbnails]")){u?.(f);continue}if(f.startsWith("[download]")){let L=f.match(/\[download\]\s+(\d+\.?\d*)%\s+of\s+~?\s*([\d.]+\w+)\s+at\s+~?\s*([\d.]+\w+\/s)\s+ETA\s+(\S+)/);if(L){let[,Pe,Fe,Oe,qe]=L;l?.(`[download] ${Pe}% of ${Fe} at ${Oe} ETA ${qe}`)}else u?.(f);continue}u?.(f)}}if(await y,!p)throw new Error("Could not determine downloaded filename from output");return{filename:p,allFiles:Array.from(h)}}catch(y){let w=y instanceof Error?y.message:String(y),f=m.join(`
5
+ `);throw new Error(`yt-dlp failed: ${w}
6
6
 
7
7
  Log output:
8
- ${g}`)}}static async checkInstalled(){try{return await ve("yt-dlp",["--version"]),!0}catch{return!1}}};var L=class extends B{wrapper=new U;getName(){return"yt-dlp"}supports(e){return!0}async download(e,t,o,r){return this.wrapper.download(e.url,o,t,{args:[],cookieFile:r?.cookieFile,subLangs:r?.subLangs,onProgress:r?.onProgress,onLog:r?.onLog})}static async checkInstalled(){return U.checkInstalled()}};var se=class{downloaders=[];defaultDownloader;constructor(){this.defaultDownloader=new L}register(e){this.downloaders.push(e)}getDownloader(e){for(let t of this.downloaders)if(t.supports(e))return t;return this.defaultDownloader}},xe=new se;function it(s){return s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}var H=class{stateManager;constructor(){this.stateManager=v.getStateManager()}async download(e,t){let o=v.getNotifier(),i=v.getConfig().resolve(e,"series"),n=i.stateFile,a=i.name,l=we(i);if(this.stateManager.isDownloaded(n,a,t.number))return!1;let d=xe.getDownloader(t.url);o.notify("highlight",`Downloading Episode ${t.number} of ${a} using ${d.getName()}`);let c=String(t.number).padStart(2,"0"),u=`${he(a)} - ${c}`,m=l.tempDir||l.downloadDir;try{await this.cleanupEpisodeArtifacts(m,u);let f=await d.download(t,m,u,{cookieFile:l.cookieFile,subLangs:l.subLangs,onProgress:y=>o.progress(y),onLog:y=>o.notify("info",y)});o.endProgress();let h=this.verifyDownload(f.filename);if(h===0)throw await this.cleanupFiles(f.allFiles),new Error("Downloaded file is empty or does not exist");if(l.minDuration>0){let y=P(f.filename),g=await ye(y);if(g<l.minDuration)throw await this.cleanupFiles(f.allFiles),new Error(`Video duration ${g}s is less than minimum ${l.minDuration}s`)}if(l.tempDir&&l.tempDir!==l.downloadDir){o.notify("info",`Moving files from temp directory to ${l.downloadDir}...`),await k.mkdir(l.downloadDir,{recursive:!0});for(let y of f.allFiles)try{let g=P(y);if(!F.existsSync(g)){o.notify("warning",`File not found, skipping move: ${g}`);continue}let x=rt(g),C=Ce(l.downloadDir,x);await k.rename(g,C),g===P(f.filename)&&(f.filename=C)}catch(g){o.notify("error",`Failed to move file ${y}: ${g}`)}}return await this.stateManager.addDownloadedEpisode(n,a,t.number),o.notify("success",`Downloaded Episode ${t.number}: ${f.filename} (${this.formatSize(h)})`),!0}catch(f){o.endProgress(),await this.cleanupEpisodeArtifacts(m,u);let h=`Failed to download Episode ${t.number}: ${f instanceof Error?f.message:String(f)}`;throw o.notify("error",h),new G(h,t.url)}}async cleanupFiles(e){let t=v.getNotifier();for(let o of e)try{let r=P(o);F.existsSync(r)&&await k.unlink(r)}catch(r){t.notify("error",`Failed to delete file ${o}: ${r}`)}}async cleanupEpisodeArtifacts(e,t){let o=v.getNotifier();try{let r=P(e);if(!F.existsSync(r))return;let i=await k.readdir(r),n=new RegExp(`^${it(t)}\\..*$`),a=0;for(let l of i)if(n.test(l)){let d=Ce(r,l);try{await k.unlink(d),a++,o.notify("info",`Cleaned up artifact: ${l}`)}catch(c){o.notify("warning",`Failed to delete artifact ${l}: ${c}`)}}a>0&&o.notify("info",`Cleaned up ${a} artifact(s) for ${t}`)}catch(r){o.notify("warning",`Failed to cleanup artifacts in ${e}: ${r}`)}}verifyDownload(e){let t=P(e);try{return F.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 L.checkInstalled()}};var ae=class{handlers=new Map;register(e){this.handlers.set(e.getDomain(),e)}getHandler(e){let t=E(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 I(`No handler found for domain: "${E(e)}". Supported domains: ${this.getDomains().join(", ")}`,e);return t}},$=new ae;import*as Ee from"cheerio";var D=class{supports(e){try{let t=E(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 I(`HTTP ${r.status}: ${r.statusText}`,e);return await r.text()}catch(r){throw r instanceof I?r:new I(`Failed to fetch page: ${r instanceof Error?r.message:String(r)}`,e)}}parseHtml(e){return Ee.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 D{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:d,title:c}=a||{};for(let p of l){let{vid:u,episode:m,isTrailer:f,payStatus:h}=p;if(f)continue;let y=this.parseEpisodeNumber(m);if(!y)continue;let g=`https://www.iq.com/play/${d}-${u}?lang=en_us`,x=this.determineTypeFromPayStatus(h);t.push({number:y,url:g,type:x,title:`${c} - Episode ${m}`,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 d=this.parseEpisodeNumber(a);if(d||(d=this.parseEpisodeNumber(i)),!d||o.some(u=>u.number===d))return;let p=this.determineEpisodeType(e,t);o.push({number:d,url:n,type:p,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 D{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),d;try{d=JSON.parse(l)}catch{throw new Error("Failed to parse MGTV API response")}if(d.code!==200)throw new Error(`MGTV API error: ${d.msg}`);n=d.data.total_page;for(let c of d.data.list){let p=this.parseEpisodeNumber(c.t1);if(!p)continue;let u=`https://w.mgtv.com${c.url}`;r.push({number:p,title:c.t2||c.t4||`Episode ${p}`,url:u,type:c.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 Y=class extends D{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:d,title:c}=a,p=l[0]?.coverList?.[0]||d;for(let u of l){let{vid:m,episode:f,isTrailer:h}=u;if(h)continue;let y=this.parseEpisodeNumber(f);if(!y)continue;let g=encodeURIComponent(c),x=`https://wetv.vip/en/play/${p}/${m}-EP${f}%3A${g}`,C=this.determineTypeFromVideo(u);t.push({number:y,url:x,type:C,title:`${c} - 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(p=>p.number===l))return;let c=this.determineEpisodeType(e,t);o.push({number:l,url:n,type:c,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"}};import{chromium as nt}from"playwright";var st=["font","stylesheet","image","media","manifest","websocket"],X=class extends D{getDomain(){return"youku.tv"}async extractEpisodes(e,t){if(!this.extractVideoId(e))throw new Error("Could not extract video ID from URL");return await this.extractWithPlaywright(e,t)}extractVideoId(e){let t=e.match(/id_([^=]+)==\.html/);return t?.[1]?t[1]:null}async extractWithPlaywright(e,t){let o=await nt.launch({headless:!0});try{let r=await o.newContext({userAgent:"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"});t&&await r.addCookies([{name:"_m_h5_tk",value:t,domain:".youku.tv",path:"/"}]);let i=await r.newPage(),n=[];i.on("response",async c=>{let p=c.url();if(p.includes("mtop.youku.columbus")&&p.includes("itemStartStage"))try{if((c.headers()["content-type"]||"").includes("application/json")){let m=await c.text();w.debug(`Intercepted mtop API response (${m.length} chars)`),n.push(JSON.parse(m))}}catch(u){w.debug(`Error parsing API response: ${u}`)}}),await i.route("**/*",c=>{let p=c.request().resourceType();st.includes(p)?c.abort():c.continue()}),await i.goto(e,{waitUntil:"domcontentloaded",timeout:3e4}),await i.waitForFunction(()=>{let c=window.__INITIAL_DATA__;if(!c?.data?.data?.nodes)return!1;let p=c.data.data.nodes;return p[0]?.nodes?.[1]?.data?(p[0].nodes[1].nodes||[]).length>0:!1},{timeout:15e3});let a=await i.evaluate(()=>{let u=window.__INITIAL_DATA__?.data?.data?.nodes?.[0]?.nodes?.[1],m=u?.nodes||[],{lastStage:f=0}=u?.data||{};return{loaded:m.length,total:f}});if(w.info(`Episodes: ${a.loaded} loaded, ${a.total} total`),a.total>a.loaded){w.info(`Waiting for API to load remaining ${a.total-a.loaded} episodes...`);let c=Date.now(),p=15e3;for(;Date.now()-c<p;){await i.waitForTimeout(500);let u=await i.evaluate(()=>{let h=window.__INITIAL_DATA__?.data?.data?.nodes?.[0]?.nodes?.[1],y=h?.nodes||[],{lastStage:g=0}=h?.data||{};return{loaded:y.length,total:g}});if(u.loaded>=u.total||n.length>=3){w.success(`Captured ${n.length} API response(s), loaded ${u.loaded}/${u.total} episodes`);break}}n.length===0&&w.warning("No API responses captured within timeout")}let l=await i.evaluate(()=>window.__INITIAL_DATA__);if(!l)throw new Error("Could not find __INITIAL_DATA__ in page");await r.close();let d=this.parseInitialData(l);if(n.length>0){w.info(`Processing ${n.length} API response(s)...`);for(let c of n){let p=this.parseApiResponse(c);w.info(` Merging ${p.length} additional episodes`);let u=new Set(d.map(m=>m.number));for(let m of p)u.has(m.number)||(d.push(m),u.add(m.number))}d.sort((c,p)=>c.number-p.number)}return w.success(`Total episodes extracted: ${d.length}`),d}finally{await o.close()}}parseApiResponse(e){let t=[];try{if(!e.data)return t;let o=Object.keys(e.data);for(let r of o){let i=e.data[r];if(i?.data?.nodes){let n=i.data.nodes||[];w.debug(`API response: found ${n.length} additional episodes`);for(let a of n){if(!a.data)continue;let{stage:l,title:d,paid:c}=a.data,p=a.data?.action?.value;if(!l||l<1)continue;let u=`https://www.youku.tv/v/v_show/id_${p}.html`,m=c===1?"vip":"available";t.push({number:l,title:d||`Episode ${l}`,url:u,type:m,extractedAt:new Date})}break}}}catch(o){w.error(`Failed to parse API response: ${o}`)}return t}parseInitialData(e){let t=[];try{let o=e.data?.data?.nodes;if(!o||o.length===0)throw new Error("No nodes found in INITIAL_DATA");let r=o[0]?.nodes?.[1];if(!r)throw new Error("Episode component not found");if(!r.data)throw new Error("Episode component has no data");let{lastStage:i=0}=r.data,n=r.nodes||[];w.debug(`__INITIAL_DATA__: found ${n.length} episodes (total: ${i})`);for(let a of n){if(!a.data)continue;let{stage:l,title:d,paid:c}=a.data,p=a.data?.action?.value;if(!l||l<1)continue;let u=`https://www.youku.tv/v/v_show/id_${p}.html`,m=c===1?"vip":"available";t.push({number:l,title:d||`Episode ${l}`,url:u,type:m,extractedAt:new Date})}}catch(o){throw w.error(`Failed to parse episodes from __INITIAL_DATA__: ${o}`),o}return t.sort((o,r)=>o.number-r.number),t}};var J=class{lastProgressLength=0;notify(e,t){switch(this.lastProgressLength>0&&(process.stdout.write(`\r${" ".repeat(this.lastProgressLength)}\r`),this.lastProgressLength=0),e){case"info":w.info(t);break;case"success":w.success(t);break;case"warning":w.warning(t);break;case"error":w.error(t);break;case"highlight":w.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 Z=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
+ ${f}`)}}static async checkInstalled(){try{return await xe("yt-dlp",["--version"]),!0}catch{return!1}}};var F=class extends K{wrapper=new _;getName(){return"yt-dlp"}supports(e){return!0}async download(e,t,o,r){return this.wrapper.download(e.url,o,t,{args:[],cookieFile:r?.cookieFile,subLangs:r?.subLangs,onProgress:r?.onProgress,onLog:r?.onLog})}static async checkInstalled(){return _.checkInstalled()}};var le=class{downloaders=[];defaultDownloader;constructor(){this.defaultDownloader=new F}register(e){this.downloaders.push(e)}getDownloader(e){for(let t of this.downloaders)if(t.supports(e))return t;return this.defaultDownloader}},De=new le;function lt(s){return s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}var G=class{stateManager;constructor(){this.stateManager=b.getStateManager()}async download(e,t){let o=b.getNotifier(),i=b.getConfig().resolve(e,"series"),n=i.stateFile,a=i.name,l=be(i);if(this.stateManager.isDownloaded(n,a,t.number))return!1;let u=De.getDownloader(t.url),d=String(t.number).padStart(2,"0");o.notify("highlight",`${a} - ${d}: downloading`);let p=`${we(a)} - ${d}`,h=l.tempDir||l.downloadDir;try{await this.cleanupEpisodeArtifacts(h,p);let m=await u.download(t,h,p,{cookieFile:l.cookieFile,subLangs:l.subLangs,onProgress:w=>o.progress(w),onLog:w=>o.notify("info",w)});o.endProgress();let y=this.verifyDownload(m.filename);if(y===0)throw await this.cleanupFiles(m.allFiles),new Error("Downloaded file is empty or does not exist");if(l.minDuration>0){let w=O(m.filename),f=await ve(w);if(f<l.minDuration)throw await this.cleanupFiles(m.allFiles),new Error(`Video duration ${f}s is less than minimum ${l.minDuration}s`)}if(l.tempDir&&l.tempDir!==l.downloadDir){o.notify("info",`Moving to ${l.downloadDir}...`),await T.mkdir(l.downloadDir,{recursive:!0});for(let w of m.allFiles)try{let f=O(w);if(!q.existsSync(f)){o.notify("warning",`Skip: ${A(f)}`);continue}let x=A(f),C=Ee(l.downloadDir,x);await T.rename(f,C),f===O(m.filename)&&(m.filename=C)}catch{o.notify("error",`Move failed: ${A(w)}`)}}return await this.stateManager.addDownloadedEpisode(n,a,t.number),o.notify("success",`${a} - ${d}: ${A(m.filename)} (${this.formatSize(y)})`),!0}catch(m){o.endProgress(),await this.cleanupEpisodeArtifacts(h,p);let y=`${a} - ${d}: ${m instanceof Error?m.message:String(m)}`;throw o.notify("error",y),new W(y,t.url)}}async cleanupFiles(e){for(let t of e)try{let o=O(t);q.existsSync(o)&&await T.unlink(o)}catch(o){c.debug(`Delete failed: ${A(t)} - ${o}`)}}async cleanupEpisodeArtifacts(e,t){let o=b.getNotifier();try{let r=O(e);if(!q.existsSync(r))return;let i=await T.readdir(r),n=new RegExp(`^${lt(t)}\\..*$`),a=0;for(let l of i)if(n.test(l)){let u=Ee(r,l);try{await T.unlink(u),a++,c.debug(`Cleaned artifact: ${A(l)}`)}catch(d){c.debug(`Delete artifact failed: ${A(l)} - ${d}`)}}a>0&&o.notify("info",`Cleaned ${a} artifacts`)}catch(r){o.notify("warning",`Cleanup failed: ${r}`)}}verifyDownload(e){let t=O(e);try{return q.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 F.checkInstalled()}};var de=class{handlers=new Map;register(e){this.handlers.set(e.getDomain(),e)}getHandler(e){let t=D(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 I(`No handler found for domain: "${D(e)}". Supported domains: ${this.getDomains().join(", ")}`,e);return t}},R=new de;import*as Se from"cheerio";var S=class{supports(e){try{let t=D(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 I(`HTTP ${r.status}: ${r.statusText}`,e);return await r.text()}catch(r){throw r instanceof I?r:new I(`Failed to fetch page: ${r instanceof Error?r.message:String(r)}`,e)}}parseHtml(e){return Se.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 Y=class extends S{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:u,title:d}=a||{};for(let g of l){let{vid:p,episode:h,isTrailer:m,payStatus:y}=g;if(m)continue;let w=this.parseEpisodeNumber(h);if(!w)continue;let f=`https://www.iq.com/play/${u}-${p}?lang=en_us`,x=this.determineTypeFromPayStatus(y);t.push({number:w,url:f,type:x,title:`${d} - Episode ${h}`,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 u=this.parseEpisodeNumber(a);if(u||(u=this.parseEpisodeNumber(i)),!u||o.some(p=>p.number===u))return;let g=this.determineEpisodeType(e,t);o.push({number:u,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 X=class extends S{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),u;try{u=JSON.parse(l)}catch{throw new Error("Failed to parse MGTV API response")}if(u.code!==200)throw new Error(`MGTV API error: ${u.msg}`);n=u.data.total_page;for(let d of u.data.list){let g=this.parseEpisodeNumber(d.t1);if(!g)continue;let p=`https://w.mgtv.com${d.url}`;r.push({number:g,title:d.t2||d.t4||`Episode ${g}`,url:p,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 J=class extends S{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:u,title:d}=a,g=l[0]?.coverList?.[0]||u;for(let p of l){let{vid:h,episode:m,isTrailer:y}=p;if(y)continue;let w=this.parseEpisodeNumber(m);if(!w)continue;let f=encodeURIComponent(d),x=`https://wetv.vip/en/play/${g}/${h}-EP${m}%3A${f}`,C=this.determineTypeFromVideo(p);t.push({number:w,url:x,type:C,title:`${d} - Episode ${m}`,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"}};import{chromium as dt}from"playwright";var ct=["font","stylesheet","image","media","manifest","websocket"],Z=class extends S{getDomain(){return"youku.tv"}async extractEpisodes(e,t){if(!this.extractVideoId(e))throw new Error("Could not extract video ID from URL");return await this.extractWithPlaywright(e,t)}extractVideoId(e){let t=e.match(/id_([^=]+)==\.html/);return t?.[1]?t[1]:null}async extractWithPlaywright(e,t){let o=await dt.launch({headless:!0});try{let r=await o.newContext({userAgent:"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"});t&&await r.addCookies([{name:"_m_h5_tk",value:t,domain:".youku.tv",path:"/"}]);let i=await r.newPage(),n=[];i.on("response",async d=>{let g=d.url();if(g.includes("mtop.youku.columbus")&&g.includes("itemStartStage"))try{if((d.headers()["content-type"]||"").includes("application/json")){let h=await d.text();c.debug(`Intercepted mtop API response (${h.length} chars)`),n.push(JSON.parse(h))}}catch(p){c.debug(`Error parsing API response: ${p}`)}}),await i.route("**/*",d=>{let g=d.request().resourceType();ct.includes(g)?d.abort():d.continue()}),await i.goto(e,{waitUntil:"domcontentloaded",timeout:3e4}),await i.waitForFunction(()=>{let d=window.__INITIAL_DATA__;if(!d?.data?.data?.nodes)return!1;let g=d.data.data.nodes;return g[0]?.nodes?.[1]?.data?(g[0].nodes[1].nodes||[]).length>0:!1},{timeout:15e3});let a=await i.evaluate(()=>{let p=window.__INITIAL_DATA__?.data?.data?.nodes?.[0]?.nodes?.[1],h=p?.nodes||[],{lastStage:m=0}=p?.data||{};return{loaded:h.length,total:m}});if(c.info(`Episodes loaded: ${a.loaded}/${a.total}`),a.total>a.loaded){c.info(`Waiting for ${a.total-a.loaded} more episodes...`);let d=Date.now(),g=15e3;for(;Date.now()-d<g;){await i.waitForTimeout(500);let p=await i.evaluate(()=>{let y=window.__INITIAL_DATA__?.data?.data?.nodes?.[0]?.nodes?.[1],w=y?.nodes||[],{lastStage:f=0}=y?.data||{};return{loaded:w.length,total:f}});if(p.loaded>=p.total||n.length>=3){c.success(`Captured ${n.length} API, ${p.loaded}/${p.total} loaded`);break}}n.length===0&&c.warning("API timeout")}let l=await i.evaluate(()=>window.__INITIAL_DATA__);if(!l)throw new Error("Could not find __INITIAL_DATA__ in page");await r.close();let u=this.parseInitialData(l);if(n.length>0){c.info(`Processing ${n.length} API responses...`);for(let d of n){let g=this.parseApiResponse(d);c.info(`+${g.length} episodes`);let p=new Set(u.map(h=>h.number));for(let h of g)p.has(h.number)||(u.push(h),p.add(h.number))}u.sort((d,g)=>d.number-g.number)}return c.success(`Extracted: ${u.length} episodes`),u}finally{await o.close()}}parseApiResponse(e){let t=[];try{if(!e.data)return t;let o=Object.keys(e.data);for(let r of o){let i=e.data[r];if(i?.data?.nodes){let n=i.data.nodes||[];c.debug(`API response: found ${n.length} additional episodes`);for(let a of n){if(!a.data)continue;let{stage:l,title:u,paid:d}=a.data,g=a.data?.action?.value;if(!l||l<1)continue;let p=`https://www.youku.tv/v/v_show/id_${g}.html`,h=d===1?"vip":"available";t.push({number:l,title:u||`Episode ${l}`,url:p,type:h,extractedAt:new Date})}break}}}catch(o){c.error(`Failed to parse API response: ${o}`)}return t}parseInitialData(e){let t=[];try{let o=e.data?.data?.nodes;if(!o||o.length===0)throw new Error("No nodes found in INITIAL_DATA");let r=o[0]?.nodes?.[1];if(!r)throw new Error("Episode component not found");if(!r.data)throw new Error("Episode component has no data");let{lastStage:i=0}=r.data,n=r.nodes||[];c.debug(`__INITIAL_DATA__: found ${n.length} episodes (total: ${i})`);for(let a of n){if(!a.data)continue;let{stage:l,title:u,paid:d}=a.data,g=a.data?.action?.value;if(!l||l<1)continue;let p=`https://www.youku.tv/v/v_show/id_${g}.html`,h=d===1?"vip":"available";t.push({number:l,title:u||`Episode ${l}`,url:p,type:h,extractedAt:new Date})}}catch(o){throw c.error(`Failed to parse episodes from __INITIAL_DATA__: ${o}`),o}return t.sort((o,r)=>o.number-r.number),t}};var ee=class{lastProgressLength=0;notify(e,t){switch(this.lastProgressLength>0&&(process.stdout.write(`\r${" ".repeat(this.lastProgressLength)}\r`),this.lastProgressLength=0),e){case"info":c.info(t);break;case"success":c.success(t);break;case"warning":c.warning(t);break;case"error":c.error(t);break;case"highlight":c.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 te=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)}
10
10
  ... (truncated)`);let n=this.escapeHtml(i),a=`${o} <b>wetvlo Error</b>
11
11
 
12
- <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 d=await l.text();throw new Q(`Failed to send Telegram notification: ${l.status} ${l.statusText}
13
- ${d}`)}}catch(o){console.error("Telegram notification failed:",o)}}escapeHtml(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}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 at}from"crypto";var ee=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 te=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 ee(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 oe=class{stateManager;downloadManager;scheduler;running=!1;domainHandlers=new Map;constructor(e,t){this.stateManager=v.getStateManager(),this.downloadManager=e;let o=t||(r=>new te(r));this.scheduler=o(async(r,i)=>{await this.executeTask(r,i)}),this.scheduler.setOnWait((r,i)=>{let n=v.getNotifier(),a=Math.round(i/1e3),l=r.split(":"),d=l[0],c=l[1];d==="download"?n.notify("info",`[${c}] Next download in ${a}s...`):d==="check"&&n.notify("info",`[${c}] Next check in ${a}s...`)})}addSeriesCheck(e){let t=v.getNotifier(),o=v.getConfig(),r=E(e),n=o.resolve(e,"series").name;this.registerDownloadQueue(r);let a=this.registerSeriesCheckQueue(r,e),l={seriesUrl:e,attemptNumber:1,retryCount:0};this.scheduler.addTask(a,l),t.notify("info",`[QueueManager] Added ${n} to check queue for domain ${r}`)}addEpisodes(e,t){let o=v.getNotifier(),r=v.getConfig();if(t.length===0)return;let i=r.resolve(e,"series"),n=i.name,a=E(e);this.registerDownloadQueue(a);let{downloadDelay:l}=i.download;for(let d=0;d<t.length;d++){let c=t[d];if(!c)continue;let p={seriesUrl:e,episode:c,retryCount:0},u=`download:${a}`,m=d*l*1e3;this.scheduler.addTask(u,p,m)}o.notify("success",`[QueueManager] Added ${t.length} episodes to download queue for ${n} (domain ${a})`)}updateConfig(){v.getNotifier().notify("info","[QueueManager] Configuration will be reloaded from AppContext")}start(){let e=v.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=v.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=v.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=v.getConfig(),r=at("md5").update(t).digest("hex").substring(0,12),i=`check:${e}:${r}`;if(this.scheduler.hasQueue(i))return i;let n=o.resolve(t,"series"),{checkInterval:a}=n.check;if(this.scheduler.registerQueue(i,a*1e3),!this.domainHandlers.has(e)){let l=$.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=v.getNotifier(),i=v.getConfig(),{seriesUrl:n,attemptNumber:a,retryCount:l=0}=e,d=this.domainHandlers.get(t);if(!d)throw new Error(`No handler found for domain ${t}`);let c=i.resolve(n,"series"),p=c.name,{count:u,checkInterval:m}=c.check;try{let f=await this.performCheck(d,n,c,a,t);if(f.hasNewEpisodes)r.notify("success",`[${t}] Found ${f.episodes.length} new episodes for ${p} (attempt ${a}/${u})`),this.addEpisodes(n,f.episodes),this.scheduler.markTaskComplete(o,m*1e3);else if(a<u){let h=m*1e3,y=f.requeueDelay??h;r.notify("info",`[${t}] No new episodes for ${p} (attempt ${a}/${u}), requeueing in ${Math.round(y/1e3)}s`);let g={seriesUrl:n,attemptNumber:a+1,retryCount:0};this.scheduler.addTask(o,g,y),this.scheduler.markTaskComplete(o,m*1e3)}else r.notify("info",`[${t}] Checks exhausted for ${p} (${u} attempts with no new episodes)`),this.scheduler.markTaskComplete(o,m*1e3)}catch(f){let h=f instanceof Error?f.message:String(f),{maxRetries:y,initialTimeout:g,backoffMultiplier:x,jitterPercentage:C}=c.download;if(l<y){let N=this.calculateBackoff(l,g*1e3,x,C);r.notify("warning",`[${t}] Check failed for ${p}, retrying in ${Math.round(N/1e3)}s (attempt ${l+1}/${y})`);let _={seriesUrl:n,attemptNumber:a,retryCount:l+1};this.scheduler.addPriorityTask(o,_,N),this.scheduler.markTaskComplete(o,m*1e3)}else r.notify("error",`[${t}] Failed to check ${p} after ${l} retry attempts: ${h}`),this.scheduler.markTaskComplete(o,m*1e3)}}async executeDownload(e,t,o){let r=v.getNotifier(),i=v.getConfig(),{seriesUrl:n,episode:a,retryCount:l=0}=e,d=i.resolve(n,"series"),c=d.name,{downloadDelay:p}=d.download;try{await this.downloadManager.download(n,a),r.notify("success",`[${t}] Successfully queued download of Episode ${a.number} for ${c}`),this.scheduler.markTaskComplete(o,p*1e3)}catch(u){let m=u instanceof Error?u.message:String(u),{maxRetries:f,initialTimeout:h,backoffMultiplier:y,jitterPercentage:g}=d.download;if(l<f){let x=this.calculateBackoff(l,h*1e3,y,g);r.notify("warning",`[${t}] Download failed for Episode ${a.number}, retrying in ${Math.round(x/1e3)}s (attempt ${l+1}/${f})`);let C={seriesUrl:n,episode:a,retryCount:l+1};this.scheduler.addPriorityTask(o,C,x),this.scheduler.markTaskComplete(o,p*1e3)}else r.notify("error",`[${t}] Failed to download Episode ${a.number} after ${l+1} attempts: ${m}`),this.scheduler.markTaskComplete(o,p*1e3)}}async performCheck(e,t,o,r,i){let n=v.getNotifier(),a=o.name,l=o.check.count;n.notify("info",`[${i}] Checking ${t} for new episodes... (attempt ${r}/${l})`);let d=await e.extractEpisodes(t),c=new Map;d.forEach(f=>{let h=c.get(f.type)||0;c.set(f.type,h+1)});let p=Array.from(c.entries()).map(([f,h])=>`${f}: ${h}`).join(", ");n.notify("info",`[${i}] Found ${d.length} total episodes on ${t} (${p})`);let{downloadTypes:u}=o.check,m=d.filter(f=>{let h=u.includes(f.type),y=o.stateFile,g=!this.stateManager.isDownloaded(y,a,f.number);return h&&g});if(d.length!==m.length){let f=d.length-m.length;n.notify("info",`[${i}] Filtering to ${u.join(" or ")}: ${m.length} episodes to download, ${f} skipped`)}return m.length>0?{hasNewEpisodes:!0,episodes:m}:{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 lt from"cron-parser";function dt(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 De(s){let e=dt(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 Se(s){try{let t=lt.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 ke(s){return new Promise(e=>setTimeout(e,s))}var re=class{configs;downloadManager;options;queueManager;running=!1;stopped=!0;timeProvider;scheduleTimer=null;constructor(e,t,o={mode:"scheduled"},r,i){this.configs=e,this.downloadManager=t,this.options=o,this.timeProvider=r||{getMsUntilTime:De,getMsUntilCron:Se,sleep:ke};let n=i||(a=>new oe(a));this.queueManager=n(this.downloadManager)}async start(){if(this.running)throw new W("Scheduler is already running");this.running=!0,this.stopped=!1,this.queueManager.start();let e=v.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=v.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=v.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){v.getNotifier().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())}updateDownloadManager(e){this.downloadManager=e}async triggerAllChecks(){v.getNotifier().notify("info","Triggering immediate checks for all series...");for(let t of this.configs)this.queueManager.addSeriesCheck(t.url)}groupConfigsBySchedule(){let e=v.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=v.getNotifier();for(let r of e){if(this.stopped)break;this.queueManager.addSeriesCheck(r.url)}let o=this.queueManager.getQueueStats();t.notify("info",`Added ${e.length} series to check queues. Queue stats: ${JSON.stringify(o)}`)}async runOnce(){let e=v.getNotifier();for(let t of this.configs){if(this.stopped)break;this.queueManager.addSeriesCheck(t.url)}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 ct}from"fs";import{readFile as ut}from"fs/promises";import{homedir as mr}from"os";import{join as yr}from"path";async function Te(s){if(!ct(s))throw new j(`Cookie file not found: "${s}"`);let t=(await ut(s,"utf-8")).split(`
14
- `),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 d=l.trim();d&&o.push(`${a}=${d}`)}}}return o.join("; ")}var yt={loadConfig:fe,checkYtDlpInstalled:H.checkYtDlpInstalled,readCookieFile:Te,createDownloadManager:()=>new H,createScheduler:(s,e,t)=>new re(s,e,t)};async function de(s){w.info("Shutting down gracefully...");try{await s.stop(),w.success("Shutdown complete")}catch(e){w.error(`Error during shutdown: ${e instanceof Error?e.message:String(e)}`)}}async function wt(s,e,t=yt){if(w.info(`Mode: ${e==="once"?"Single-run (checks once, exits)":"Scheduled (waits for startTime)"}`),w.info("Checking yt-dlp installation..."),!await t.checkYtDlpInstalled())throw new Error(`yt-dlp is not installed. Please install it first:
12
+ <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 u=await l.text();throw new B(`Failed to send Telegram notification: ${l.status} ${l.statusText}
13
+ ${u}`)}}catch(o){console.error("Telegram notification failed:",o)}}escapeHtml(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}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 ut}from"crypto";var oe=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 re=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 oe(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 ie=class{stateManager;downloadManager;scheduler;running=!1;domainHandlers=new Map;constructor(e,t){this.stateManager=b.getStateManager(),this.downloadManager=e;let o=t||(r=>new re(r));this.scheduler=o(async(r,i)=>{await this.executeTask(r,i)}),this.scheduler.setOnWait((r,i)=>{let n=b.getNotifier(),a=Math.round(i/1e3),l=r.split(":"),u=l[0],d=l[1];u==="download"?n.notify("info",`[${d}] Next download: ${a}s`):u==="check"&&n.notify("info",`[${d}] Next check: ${a}s`)})}addSeriesCheck(e){let t=b.getConfig(),o=D(e),i=t.resolve(e,"series").name;this.registerDownloadQueue(o);let n=this.registerSeriesCheckQueue(o,e),a={seriesUrl:e,attemptNumber:1,retryCount:0};this.scheduler.addTask(n,a),c.debug(`[${o}] Added ${i} to check queue`)}addEpisodes(e,t){let o=b.getNotifier(),r=b.getConfig();if(t.length===0)return;let i=r.resolve(e,"series"),n=i.name,a=D(e);this.registerDownloadQueue(a);let{downloadDelay:l}=i.download;for(let u=0;u<t.length;u++){let d=t[u];if(!d)continue;let g={seriesUrl:e,episode:d,retryCount:0},p=`download:${a}`,h=u*l*1e3;this.scheduler.addTask(p,g,h)}o.notify("success",`[${a}] ${n}: queued ${t.length} episodes`)}updateConfig(){c.debug("[QueueManager] Configuration will be reloaded")}start(){if(this.running)throw new Error("QueueManager is already running");this.running=!0,this.scheduler.resume(),c.debug("[QueueManager] Started queue processing")}async stop(){this.running&&(c.debug("[QueueManager] Stopping queue processing..."),this.scheduler.stop(),this.running=!1,c.debug("[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=b.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=b.getConfig(),r=ut("md5").update(t).digest("hex").substring(0,12),i=`check:${e}:${r}`;if(this.scheduler.hasQueue(i))return i;let n=o.resolve(t,"series"),{checkInterval:a}=n.check;if(this.scheduler.registerQueue(i,a*1e3),!this.domainHandlers.has(e)){let l=R.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=b.getNotifier(),i=b.getConfig(),{seriesUrl:n,attemptNumber:a,retryCount:l=0}=e,u=this.domainHandlers.get(t);if(!u)throw new Error(`No handler found for domain ${t}`);let d=i.resolve(n,"series"),g=d.name,{count:p,checkInterval:h}=d.check;try{let m=await this.performCheck(u,n,d,a,t);if(m.hasNewEpisodes)r.notify("success",`[${t}] ${g}: ${m.episodes.length} new episodes (attempt ${a}/${p})`),this.addEpisodes(n,m.episodes),this.scheduler.markTaskComplete(o,h*1e3);else if(a<p){let y=h*1e3,w=m.requeueDelay??y;r.notify("info",`[${t}] ${g}: no new episodes, retry in ${Math.round(w/1e3)}s (attempt ${a}/${p})`);let f={seriesUrl:n,attemptNumber:a+1,retryCount:0};this.scheduler.addTask(o,f,w),this.scheduler.markTaskComplete(o,h*1e3)}else r.notify("info",`[${t}] ${g}: exhausted after ${p} attempts`),this.scheduler.markTaskComplete(o,h*1e3)}catch(m){let y=m instanceof Error?m.message:String(m),{maxRetries:w,initialTimeout:f,backoffMultiplier:x,jitterPercentage:C}=d.download;if(l<w){let E=this.calculateBackoff(l,f*1e3,x,C);r.notify("warning",`[${t}] ${g}: check retry ${l+1}/${w} in ${Math.round(E/1e3)}s`);let Q={seriesUrl:n,attemptNumber:a,retryCount:l+1};this.scheduler.addPriorityTask(o,Q,E),this.scheduler.markTaskComplete(o,h*1e3)}else r.notify("error",`[${t}] ${g}: check failed - ${y}`),this.scheduler.markTaskComplete(o,h*1e3)}}async executeDownload(e,t,o){let r=b.getNotifier(),i=b.getConfig(),{seriesUrl:n,episode:a,retryCount:l=0}=e,u=i.resolve(n,"series"),d=u.name,{downloadDelay:g}=u.download;try{await this.downloadManager.download(n,a),this.scheduler.markTaskComplete(o,g*1e3)}catch(p){let h=p instanceof Error?p.message:String(p),{maxRetries:m,initialTimeout:y,backoffMultiplier:w,jitterPercentage:f}=u.download;if(l<m){let x=this.calculateBackoff(l,y*1e3,w,f);r.notify("warning",`[${t}] ${d} - ${String(a.number).padStart(2,"0")}: retry ${l+1}/${m} in ${Math.round(x/1e3)}s`);let C={seriesUrl:n,episode:a,retryCount:l+1};this.scheduler.addPriorityTask(o,C,x),this.scheduler.markTaskComplete(o,g*1e3)}else l===0?(r.notify("error",`[${t}] ${d} - ${String(a.number).padStart(2,"0")}: ${h}`),r.notify("info",`[${t}] Episode URL: ${a.url}`)):r.notify("error",`[${t}] ${d} - ${String(a.number).padStart(2,"0")}: ${h}`),this.scheduler.markTaskComplete(o,g*1e3)}}async performCheck(e,t,o,r,i){let n=b.getNotifier(),a=o.name,l=o.check.count;n.notify("info",`[${i}] ${a}: checking (attempt ${r}/${l})`),c.debug(`[${i}] Checking URL: ${t}`);let u=await e.extractEpisodes(t),d=new Map;u.forEach(m=>{let y=d.get(m.type)||0;d.set(m.type,y+1)});let g=Array.from(d.entries()).map(([m,y])=>`${m}: ${y}`).join(", ");n.notify("info",`[${i}] ${a}: ${u.length} episodes (${g})`);let{downloadTypes:p}=o.check,h=u.filter(m=>{let y=p.includes(m.type),w=o.stateFile,f=!this.stateManager.isDownloaded(w,a,m.number);return y&&f});if(u.length!==h.length){let m=u.length-h.length;n.notify("info",`[${i}] Filtering: ${h.length} to download, ${m} skipped`)}return h.length>0?{hasNewEpisodes:!0,episodes:h}:{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 pt from"cron-parser";function gt(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 ke(s){let e=gt(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 Te(s){try{let t=pt.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 $e(s){return new Promise(e=>setTimeout(e,s))}var ne=class{configs;downloadManager;options;queueManager;running=!1;stopped=!0;timeProvider;scheduleTimer=null;constructor(e,t,o={mode:"scheduled"},r,i){this.configs=e,this.downloadManager=t,this.options=o,this.timeProvider=r||{getMsUntilTime:ke,getMsUntilCron:Te,sleep:$e};let n=i||(a=>new ie(a));this.queueManager=n(this.downloadManager)}async start(){if(this.running)throw new j("Scheduler is already running");this.running=!0,this.stopped=!1,this.queueManager.start();let e=b.getNotifier();if(this.options.mode==="once")c.debug("Single-run mode: checking all series once"),await this.runOnce(),this.running=!1;else return e.notify("info","Scheduler started"),this.scheduleNextBatch(),new Promise(t=>{let o=setInterval(()=>{this.running||(clearInterval(o),t())},100)})}scheduleNextBatch(){if(this.stopped)return;let e=b.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",`Next run: ${o} in ${Math.floor(r/1e3/60)}m`)),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(){c.debug("Stopping scheduler..."),this.stopped=!0,this.scheduleTimer&&(clearTimeout(this.scheduleTimer),this.scheduleTimer=null),await this.queueManager.stop(),this.running=!1,c.debug("Scheduler stopped")}async reload(e){c.debug("Reloading configuration..."),this.configs=e,this.queueManager.updateConfig(),this.running&&this.options.mode==="scheduled"&&(this.scheduleTimer&&(clearTimeout(this.scheduleTimer),this.scheduleTimer=null),this.scheduleNextBatch())}updateDownloadManager(e){this.downloadManager=e}async triggerAllChecks(){c.debug("Triggering immediate checks for all series...");for(let e of this.configs)this.queueManager.addSeriesCheck(e.url)}groupConfigsBySchedule(){let e=b.getNotifier(),t=new Map;for(let o of this.configs){let r=o.cron||o.startTime;if(!r){e.notify("warning",`Skip ${o.name}: no schedule`);continue}let i=t.get(r)||[];i.push(o),t.set(r,i)}return t}async runConfigs(e){let t=b.getNotifier();for(let r of e){if(this.stopped)break;this.queueManager.addSeriesCheck(r.url)}let o=this.queueManager.getQueueStats();c.debug(`Queue stats: ${JSON.stringify(o)}`),t.notify("info",`Added ${e.length} series to queue`)}async runOnce(){for(let e of this.configs){if(this.stopped)break;this.queueManager.addSeriesCheck(e.url)}for(;this.queueManager.hasActiveProcessing()&&!this.stopped;)await this.timeProvider.sleep(1e3);c.debug("Single-run complete")}isRunning(){return this.running&&!this.stopped}getQueueManager(){return this.queueManager}};import{existsSync as ft}from"fs";import{readFile as mt}from"fs/promises";import{homedir as vr}from"os";import{join as xr}from"path";async function Ie(s){if(!ft(s))throw new V(`Cookie file not found: "${s}"`);let t=(await mt(s,"utf-8")).split(`
14
+ `),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 u=l.trim();u&&o.push(`${a}=${u}`)}}}return o.join("; ")}var Le={loadConfig:me,checkYtDlpInstalled:G.checkYtDlpInstalled,readCookieFile:Ie,createDownloadManager:()=>new G,createScheduler:(s,e,t)=>new ne(s,e,t)};async function ue(s){c.debug("Shutting down gracefully...");try{await s.stop(),c.debug("Shutdown complete")}catch(e){c.error(`Error during shutdown: ${e instanceof Error?e.message:String(e)}`)}}async function vt(s,e,t=Le,o=!1){let r=o?"DEBUG":"INFO";if(c.setLevel(r),c.info(`Mode: ${e}`),c.debug("Checking yt-dlp installation..."),!await t.checkYtDlpInstalled())throw new Error(`yt-dlp is not installed. Please install it first:
15
15
  - macOS: brew install yt-dlp
16
16
  - Linux: pip install yt-dlp
17
- - Windows: winget install yt-dlp`);w.info(`Loading configuration from ${s}...`);let r=await t.loadConfig(s);w.success("Configuration loaded");let i=new q(r),n=i.getConfig("global"),a=m=>{let f=[new J],h=m.getConfig("global");if(h.telegram)try{f.push(new Z(h.telegram)),w.info("Telegram notifications enabled for errors")}catch(y){w.warning(`Failed to set up Telegram: ${y instanceof Error?y.message:String(y)}`)}return{notify:async(y,g)=>{await Promise.all(f.map(x=>x.notify(y,g)))},progress:y=>{for(let g of f)g.progress(y)},endProgress:()=>{for(let y of f)y.endProgress()}}},l=a(i),d=new M(l);v.initialize(i,l,d),w.info("AppContext initialized"),$.register(new Y),$.register(new z),$.register(new K),$.register(new X),w.info(`Registered handlers: ${$.getDomains().join(", ")}`);let c=t.createDownloadManager(),p;e==="scheduled"&&process.stdin.isTTY&&(p=()=>{w.info("Interactive mode enabled:"),w.info(" [r] Reload configuration"),w.info(" [c] Trigger immediate checks"),w.info(" [q] Quit")}),w.info("Using queue-based scheduler");let u=t.createScheduler(r.series,c,{mode:e,onIdle:p});process.on("SIGINT",async()=>{await de(u),process.exit(0)}),process.on("SIGTERM",async()=>{await de(u),process.exit(0)}),e==="scheduled"&&process.stdin.isTTY&&(Ie.emitKeypressEvents(process.stdin),process.stdin.setRawMode(!0),process.stdin.on("keypress",async(m,f)=>{if(!f)return;let h=f.name||"",y=m||"";if(h==="q"||h==="\u0439"||y==="\u0439"||f.ctrl&&h==="c")await de(u),process.exit(0);else if(h==="r"||h==="\u043A"||y==="\u043A")try{w.info(`Reloading configuration from ${s}...`);let g=await t.loadConfig(s),x=new q(g),C=x.getConfig("global"),N=a(x);v.setNotifier(N),n=C,v.reloadConfig(x),await u.reload(g.series),w.success("Configuration reloaded successfully")}catch(g){w.error(`Failed to reload config: ${g instanceof Error?g.message:String(g)}`)}else(h==="c"||h==="\u0441"||y==="\u0441")&&await u.triggerAllChecks()})),await u.start()}var $e=ft({name:"wetvlo",description:"CLI Video Downloader for Chinese streaming sites",version:"0.0.1",args:{config:mt({type:ht,long:"config",short:"c",defaultValue:()=>"./config.yaml",description:"Path to configuration file (default: ./config.yaml)"}),once:gt({type:pt,long:"once",short:"o",description:"Run in single-run mode (check once and exit)"})},handler:async({config:s,once:e})=>{try{await wt(s,e?"once":"scheduled")}catch(t){t instanceof R?w.error(`Configuration error: ${t.message}`):w.error(`Fatal error: ${t instanceof Error?t.message:String(t)}`),process.exit(1)}}});async function xt(s=process.argv.slice(2)){await bt($e,s)}var Ct=import.meta.main||process.argv[1]&&process.argv[1]===vt(import.meta.url);Ct&&await xt();export{xt as main};
17
+ - Windows: winget install yt-dlp`);c.debug(`Loading configuration from ${s}...`);let n=await t.loadConfig(s);c.debug("Configuration loaded");let a=new H(n),l=a.getConfig("global"),u=y=>{let w=[new ee],f=y.getConfig("global");if(f.telegram)try{w.push(new te(f.telegram)),c.debug("Telegram notifications enabled for errors")}catch(x){c.warning(`Failed to set up Telegram: ${x instanceof Error?x.message:String(x)}`)}return{notify:async(x,C)=>{await Promise.all(w.map(E=>E.notify(x,C)))},progress:x=>{for(let C of w)C.progress(x)},endProgress:()=>{for(let x of w)x.endProgress()}}},d=u(a),g=new N(d);b.initialize(a,d,g),c.debug("AppContext initialized"),R.register(new J),R.register(new Y),R.register(new X),R.register(new Z),c.debug(`Registered handlers: ${R.getDomains().join(", ")}`);let p=t.createDownloadManager(),h;e==="scheduled"&&process.stdin.isTTY&&(h=()=>{c.info("Interactive mode enabled:"),c.info(" [r] Reload configuration"),c.info(" [c] Trigger immediate checks"),c.info(" [q] Quit")}),c.debug("Using queue-based scheduler");let m=t.createScheduler(n.series,p,{mode:e,onIdle:h});process.on("SIGINT",async()=>{await ue(m),process.exit(0)}),process.on("SIGTERM",async()=>{await ue(m),process.exit(0)}),e==="scheduled"&&process.stdin.isTTY&&(Ae.emitKeypressEvents(process.stdin),process.stdin.setRawMode(!0),process.stdin.on("keypress",async(y,w)=>{if(!w)return;let f=w.name||"",x=y||"";if(f==="q"||f==="\u0439"||x==="\u0439"||w.ctrl&&f==="c")await ue(m),process.exit(0);else if(f==="r"||f==="\u043A"||x==="\u043A")try{c.debug(`Reloading configuration from ${s}...`);let C=await t.loadConfig(s),E=new H(C),Q=E.getConfig("global"),L=u(E);b.setNotifier(L),l=Q,b.reloadConfig(E),await m.reload(C.series),c.success("Configuration reloaded successfully")}catch(C){c.error(`Failed to reload config: ${C instanceof Error?C.message:String(C)}`)}else(f==="c"||f==="\u0441"||x==="\u0441")&&await m.triggerAllChecks()})),await m.start()}var Ne=ht({name:"wetvlo",description:"CLI Video Downloader for Chinese streaming sites",version:"0.0.1",args:{config:yt({type:wt,long:"config",short:"c",defaultValue:()=>"./config.yaml",description:"Path to configuration file (default: ./config.yaml)"}),once:Me({type:Re,long:"once",short:"o",description:"Run in single-run mode (check once and exit)"}),debug:Me({type:Re,long:"debug",short:"d",description:"Enable debug logging"})},handler:async({config:s,once:e,debug:t})=>{try{await vt(s,e?"once":"scheduled",Le,t)}catch(o){o instanceof M?c.error(`Configuration error: ${o.message}`):c.error(`Fatal error: ${o instanceof Error?o.message:String(o)}`),process.exit(1)}}});async function Ct(s=process.argv.slice(2)){await xt(Ne,s)}var Dt=import.meta.main||process.argv[1]&&process.argv[1]===bt(import.meta.url);Dt&&await Ct();export{Ct as main};
18
18
  //# sourceMappingURL=index.js.map