wetvlo 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +112 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Maxim (neiromaster) Gavrilenko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# wetvlo
|
|
2
|
+
|
|
3
|
+
**wetvlo** is a powerful CLI application for automatically monitoring and downloading TV series episodes from popular Asian streaming platforms (WeTV, iQIYI).
|
|
4
|
+
|
|
5
|
+
## ๐ Features
|
|
6
|
+
|
|
7
|
+
* **Automatic Monitoring**: Checks for new episodes at scheduled times.
|
|
8
|
+
* **Smart Queue**: Sequential downloading and checking to prevent IP bans and ensure stability.
|
|
9
|
+
* **Platform Support**: Built-in support for WeTV and iQIYI.
|
|
10
|
+
* **Reliability**: Retry system with exponential backoff for network errors.
|
|
11
|
+
* **Notifications**: Telegram integration for error alerts.
|
|
12
|
+
* **Flexible Configuration**: Per-series, per-domain, or global settings.
|
|
13
|
+
* **History**: Tracks downloaded episodes to prevent duplicates.
|
|
14
|
+
|
|
15
|
+
## ๐ Requirements
|
|
16
|
+
|
|
17
|
+
* [Bun](https://bun.sh/) (Runtime)
|
|
18
|
+
* [yt-dlp](https://github.com/yt-dlp/yt-dlp) (must be installed and available in PATH)
|
|
19
|
+
|
|
20
|
+
## ๐ Installation
|
|
21
|
+
|
|
22
|
+
1. Clone the repository:
|
|
23
|
+
```bash
|
|
24
|
+
git clone https://github.com/your-username/wetvlo.git
|
|
25
|
+
cd wetvlo
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
2. Install dependencies:
|
|
29
|
+
```bash
|
|
30
|
+
bun install
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## โ๏ธ Configuration
|
|
34
|
+
|
|
35
|
+
1. Create a configuration file by copying the example:
|
|
36
|
+
```bash
|
|
37
|
+
cp config.example.yaml config.yaml
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
2. Edit `config.yaml` to suit your needs. Main sections:
|
|
41
|
+
|
|
42
|
+
* **series**: List of series to monitor (highest priority).
|
|
43
|
+
* **domainConfigs**: Settings for specific sites (e.g., delays for WeTV).
|
|
44
|
+
* **globalConfigs**: Global default settings.
|
|
45
|
+
* **telegram**: Bot settings for notifications (optional).
|
|
46
|
+
|
|
47
|
+
Example series configuration:
|
|
48
|
+
```yaml
|
|
49
|
+
series:
|
|
50
|
+
- name: "Series Name"
|
|
51
|
+
url: "https://wetv.vip/play/series-id"
|
|
52
|
+
startTime: "20:00" # Check start time (HH:MM)
|
|
53
|
+
download:
|
|
54
|
+
maxRetries: 5 # Number of retry attempts on failure
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## โถ๏ธ Usage
|
|
58
|
+
|
|
59
|
+
### Development Mode
|
|
60
|
+
Run with default configuration (`./config.yaml`):
|
|
61
|
+
```bash
|
|
62
|
+
bun start
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Run in single-pass mode (check once and exit without waiting for schedule):
|
|
66
|
+
```bash
|
|
67
|
+
bun start:once
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Build and Run (Production)
|
|
71
|
+
Build the project into a single file:
|
|
72
|
+
```bash
|
|
73
|
+
bun run build
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Run the built file:
|
|
77
|
+
```bash
|
|
78
|
+
bun dist/index.js
|
|
79
|
+
# or with a custom config
|
|
80
|
+
bun dist/index.js --config ./my-config.yaml
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## ๐งช Development
|
|
84
|
+
|
|
85
|
+
### Testing
|
|
86
|
+
Run all tests:
|
|
87
|
+
```bash
|
|
88
|
+
bun test
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Linting and Formatting
|
|
92
|
+
Check code style:
|
|
93
|
+
```bash
|
|
94
|
+
bun run lint
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Format code:
|
|
98
|
+
```bash
|
|
99
|
+
bun run format
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## ๐ Architecture
|
|
103
|
+
|
|
104
|
+
The application is built on a task queue (`QueueManager`) that manages update checks and downloads.
|
|
105
|
+
* **Scheduler**: Triggers check tasks according to schedule.
|
|
106
|
+
* **Handlers**: Modules for parsing specific site pages (in `src/handlers`).
|
|
107
|
+
* **DownloadManager**: Wrapper around `yt-dlp` for downloading videos.
|
|
108
|
+
* **StateManager**: Persists progress in `downloads_state.json`.
|
|
109
|
+
|
|
110
|
+
## ๐ License
|
|
111
|
+
|
|
112
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{run as Be}from"cmd-ts";import{boolean as Ge,command as He,flag as Qe,option as Ue,string as je}from"cmd-ts";var ie={count:3,checkInterval:600,downloadTypes:["available"]},re={downloadDir:"./downloads",tempDir:"./downloads",downloadDelay:10,maxRetries:3,initialTimeout:5,backoffMultiplier:2,jitterPercentage:10,minDuration:0},Ke=["available","vip"],se="./downloads";import{existsSync as $e}from"fs";import{join as Me}from"path";import*as ae from"js-yaml";var k=class extends Error{constructor(e){super(e),this.name="WetvloError"}},T=class extends k{constructor(e){super(e),this.name="ConfigError"}},N=class extends k{constructor(e){super(e),this.name="StateError"}},C=class extends k{constructor(t,o){super(t);this.url=o;this.name="HandlerError"}},P=class extends k{constructor(t,o){super(t);this.url=o;this.name="DownloadError"}},A=class extends k{constructor(e){super(e),this.name="NotificationError"}},F=class extends k{constructor(e){super(e),this.name="CookieError"}},q=class extends k{constructor(e){super(e),this.name="SchedulerError"}};function be(a){return typeof a!="string"?a:a.replace(/\$\{([^}]+)\}/g,(e,t)=>{let o=process.env[t];if(o===void 0)throw new Error(`Environment variable "${t}" is not set`);return o})}function O(a){if(typeof a=="string")return be(a);if(Array.isArray(a))return a.map(e=>O(e));if(a!==null&&typeof a=="object"){let e={};for(let[t,o]of Object.entries(a))e[t]=O(o);return e}return a}import{z as g}from"zod";var xe=g.enum(["available","vip","teaser","express","preview","locked"]),X=g.object({count:g.number().positive().optional(),checkInterval:g.number().positive().optional(),downloadTypes:g.array(xe).optional()}),J=g.object({downloadDir:g.string().optional(),tempDir:g.string().optional(),downloadDelay:g.number().nonnegative().optional(),maxRetries:g.number().int().nonnegative().optional(),initialTimeout:g.number().positive().optional(),backoffMultiplier:g.number().positive().optional(),jitterPercentage:g.number().int().min(0).max(100).optional(),minDuration:g.number().nonnegative().optional()}),ke=g.object({botToken:g.string(),chatId:g.string()}),Ce=g.object({domain:g.string(),check:X.optional(),download:J.optional()}),Se=g.object({name:g.string(),url:g.string().url(),startTime:g.string().regex(/^\d{1,2}:\d{2}$/,{message:'Must be in HH:MM format (e.g., "20:00")'}),check:X.optional(),download:J.optional()}),De=g.object({check:X.optional(),download:J.optional()}),Ee=g.enum(["chrome","firefox","safari","chromium","edge"]),Te=g.object({series:g.array(Se).min(1,"Cannot be empty"),telegram:ke.optional(),globalConfigs:De.optional(),stateFile:g.string(),browser:Ee,cookieFile:g.string().optional(),domainConfigs:g.array(Ce).optional()});function ne(a){Te.parse(a)}var Ie="./config.yaml";async function le(a=Ie){let e=Me(process.cwd(),a);if(!$e(e))throw new T(`Configuration file not found: "${e}". Create a config.yaml file or specify a different path.`);let o=await Bun.file(e).text(),i;try{i=ae.load(o)}catch(s){throw new T(`Failed to parse YAML: ${s instanceof Error?s.message:String(s)}`)}return ne(i),O(i)}import*as te from"fs";import*as $ from"fs/promises";import{basename as Le,join as ee,resolve as D}from"path";import{execa as ue}from"execa";function ce(a){return a.replace(/[<>:"/\\|?*]/g,"_").replace(/[\x00-\x1F]/g,"").replace(/[\s.]+$/,"")}import{execa as Re}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"},Z=class{config;constructor(e={}){this.config={level:e.level??"INFO",useColors:e.useColors??!0}}format(e,t){return`[${new Date().toISOString()}] [${e}] ${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.blue)))}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}},h=new Z;async function de(a){try{let{stdout:e}=await Re("ffprobe",["-v","error","-show_entries","format=duration","-of","default=noprint_wrappers=1:nokey=1",a]),t=parseFloat(e.trim());return Number.isNaN(t)?0:t}catch(e){return h.error(`Failed to get video duration for ${a}: ${e instanceof Error?e.message:String(e)}`),0}}var L=class{stateManager;notifier;downloadDir;tempDir;cookieFile;constructor(e,t,o,i,r){this.stateManager=e,this.notifier=t,this.downloadDir=D(o),this.cookieFile=i?D(i):void 0,this.tempDir=r?D(r):void 0}async download(e,t,o,i=0){if(this.stateManager.isDownloaded(e,o.number))return!1;this.notifier.notify("highlight",`Downloading Episode ${o.number} of ${t}`);try{let r=await this.runYtDlp(t,o),s=this.verifyDownload(r.filename);if(s===0)throw await this.cleanupFiles(r.allFiles),new Error("Downloaded file is empty or does not exist");if(i>0){let c=D(r.filename),n=await de(c);if(n<i)throw await this.cleanupFiles(r.allFiles),new Error(`Video duration ${n}s is less than minimum ${i}s`)}if(this.tempDir&&this.tempDir!==this.downloadDir){this.notifier.notify("info",`Moving files from temp directory to ${this.downloadDir}...`),await $.mkdir(this.downloadDir,{recursive:!0});for(let c of r.allFiles)try{let n=D(c);if(!te.existsSync(n)){this.notifier.notify("warning",`File not found, skipping move: ${n}`);continue}let d=Le(n),u=ee(this.downloadDir,d);await $.rename(n,u),n===D(r.filename)&&(r.filename=u)}catch(n){this.notifier.notify("error",`Failed to move file ${c}: ${n}`)}}return this.stateManager.addDownloadedEpisode(e,t,{number:o.number,url:o.url,filename:r.filename,size:s}),await this.stateManager.save(),this.notifier.notify("success",`Downloaded Episode ${o.number}: ${r.filename} (${this.formatSize(s)})`),!0}catch(r){let s=`Failed to download Episode ${o.number}: ${r instanceof Error?r.message:String(r)}`;throw this.notifier.notify("error",s),new P(s,o.url)}}async cleanupFiles(e){for(let t of e)try{let o=D(t);te.existsSync(o)&&await $.unlink(o)}catch(o){this.notifier.notify("error",`Failed to delete file ${t}: ${o}`)}}async runYtDlp(e,t){let o=String(t.number).padStart(2,"0"),i=this.tempDir||this.downloadDir;await $.mkdir(i,{recursive:!0});let r=ce(e),c=["--no-warnings","--newline","-o",ee(i,`${r} - ${o}.%(ext)s`),t.url];this.cookieFile&&c.unshift("--cookies",this.cookieFile);let n=null,d=new Set,u=[];try{let p=ue("yt-dlp",c,{all:!0});for await(let f of p.all){let l=f.toString().trim();if(!l)continue;u.push(l);let m=l.match(/\[download\] Destination:\s*(.+)/);m&&(n=m[1],n&&d.add(n));let w=l.match(/\[info\] Writing video subtitles to:\s*(.+)/);w?.[1]&&d.add(w[1]);let y=l.match(/\[merge\] Merging formats into "(.*)"/);if(y&&(n=y[1],n&&d.add(n)),l.includes("[info]")||l.includes("[ffmpeg]")||l.includes("[merge]")||l.includes("[postprocessor]")){this.notifier.notify("info",`Episode ${t.number}: ${l}`);continue}if(l.includes("[download]")){let v=l.match(/\[download\]\s+(\d+\.?\d*)%\s+of\s+~?\s*([\d.]+\w+)\s+at\s+~?\s*([\d.]+\w+\/s)\s+ETA\s+(\S+)/);if(v){let[,b,E,R,ye]=v;this.notifier.progress(`[${t.number}] ${b}% of ${E} at ${R} ETA ${ye}`)}else this.notifier.notify("info",`Episode ${t.number}: ${l}`)}}return await p,this.notifier.endProgress(),n||(n=ee(i,`${r} - ${o}.mp4`)),n&&!d.has(n)&&d.add(n),{filename:n,allFiles:Array.from(d)}}catch(p){this.notifier.endProgress();let f=p,l=f.stderr??"",m=f.stdout??"",w=u.join(`
|
|
3
|
+
`);throw new Error(`yt-dlp failed:
|
|
4
|
+
stderr: ${l}
|
|
5
|
+
stdout: ${m}
|
|
6
|
+
captured output:
|
|
7
|
+
${w}
|
|
8
|
+
message: ${f.message}`)}}verifyDownload(e){let t=D(e);try{return Bun.file(t).size}catch{return 0}}formatSize(e){let t=["B","KB","MB","GB"],o=e,i=0;for(;o>=1024&&i<t.length-1;)o/=1024,i++;return`${o.toFixed(2)} ${t[i]}`}static async checkYtDlpInstalled(){try{return await ue("yt-dlp",["--version"]),!0}catch{return!1}}};function x(a){try{return new URL(a).hostname}catch{throw new Error(`Invalid URL: "${a}"`)}}var oe=class{handlers=new Map;register(e){this.handlers.set(e.getDomain(),e)}getHandler(e){let t=x(e);if(this.handlers.has(t))return this.handlers.get(t);for(let[o,i]of this.handlers.entries())if(t===o||t.endsWith(`.${o}`)||o.endsWith(`.${t}`))return i}getDomains(){return Array.from(this.handlers.keys())}getHandlerOrThrow(e){let t=this.getHandler(e);if(!t)throw new C(`No handler found for domain: "${x(e)}". Supported domains: ${this.getDomains().join(", ")}`,e);return t}},M=new oe;import*as fe from"cheerio";var I=class{supports(e){try{let t=x(e);return t===this.getDomain()||t.endsWith(`.${this.getDomain()}`)}catch{return!1}}async fetchHtml(e,t){let o={"User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",Accept:"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8","Accept-Language":"en-US,en;q=0.9"};t&&(o.Cookie=t);try{let i=await fetch(e,{headers:o});if(!i.ok)throw new C(`HTTP ${i.status}: ${i.statusText}`,e);return await i.text()}catch(i){throw i instanceof C?i:new C(`Failed to fetch page: ${i instanceof Error?i.message:String(i)}`,e)}}parseHtml(e){return fe.load(e)}parseEpisodeNumber(e){let t=e.match(/็ฌฌ(\d+)้/);if(t?.[1])return parseInt(t[1],10);let o=e.match(/ep\s?(\d+)/i);if(o?.[1])return parseInt(o[1],10);let i=e.match(/(?:episode|e)\s?(\d+)/i);if(i?.[1])return parseInt(i[1],10);let r=e.match(/\b(\d+)\b/);return r?.[1]?parseInt(r[1],10):null}parseEpisodeType(e,t){let o=t(e),i=o.attr("class")||"",r=o.text().toLowerCase();return i.includes("vip")||r.includes("vip")||r.includes("\u4F1A\u5458")?"vip":i.includes("preview")||i.includes("trailer")||r.includes("preview")||r.includes("\u9884\u544A")?"preview":i.includes("locked")||i.includes("lock")||r.includes("locked")||r.includes("\u9501\u5B9A")?"locked":"available"}};var H=class extends I{getDomain(){return"iq.com"}async extractEpisodes(e,t){let o=await this.fetchHtml(e,t),i=this.parseHtml(o),r=[],s=[".album-episode-item",".episode-item",".intl-play-item","[data-episode]",'a[href*="/play/"]'];for(let c of s){let n=i(c);if(n.length>0&&(n.each((d,u)=>{let p=i(u),f=p.is("a")?p:p.find("a").first(),l=f.attr("href")||"";if(!l)return;let m=l.startsWith("http")?l:`https://www.iq.com${l}`,v=`${p.text()} ${l}`,b=this.parseEpisodeNumber(v);b&&(r.some(R=>R.number===b)||r.push({number:b,url:m,type:this.parseEpisodeType(u,i),title:p.attr("title")||f.attr("title")||void 0,extractedAt:new Date}))}),r.length>0))break}return r.sort((c,n)=>c.number-n.number),r}};var Q=class extends I{getDomain(){return"wetv.vip"}async extractEpisodes(e,t){let o=await this.fetchHtml(e,t),i=this.parseHtml(o),r=[],s=i('a.play-video__link[href*="/play/"][href*="EP"]');return s.length===0?i('a[href*="/play/"]').filter((n,d)=>(i(d).attr("href")||"").includes("EP")).each((n,d)=>{this.processEpisodeLink(i,d,r)}):s.each((c,n)=>{this.processEpisodeLink(i,n,r)}),r.sort((c,n)=>c.number-n.number),r}processEpisodeLink(e,t,o){let i=e(t),r=i.attr("href");if(!r)return;let s=r.startsWith("http")?r:`https://wetv.vip${r}`,c=i.attr("aria-label")||"",n=this.parseEpisodeNumber(c);if(!n||o.some(p=>p.number===n))return;let u=this.determineEpisodeType(e,t);o.push({number:n,url:s,type:u,title:i.attr("title")||void 0,extractedAt:new Date})}determineEpisodeType(e,t){let o=e(t).closest("li");if(o.length){let i=o.find("span.play-video__label").first();if(i.length){let s=i.text().trim().toLowerCase();if(s==="vip"||s.includes("vip"))return"vip";if(s==="teaser"||s.includes("teaser"))return"teaser";if(s==="express"||s.includes("express"))return"express"}let r=o.text()||"";if(r.includes("VIP")&&!r.includes("Teaser"))return"vip";if(r.includes("Teaser"))return"teaser";if(r.includes("Express"))return"express"}return"available"}};var U=class{lastProgressLength=0;notify(e,t){switch(this.lastProgressLength>0&&(process.stdout.write(`\r${" ".repeat(this.lastProgressLength)}\r`),this.lastProgressLength=0),e){case"info":h.info(t);break;case"success":h.success(t);break;case"warning":h.warning(t);break;case"error":h.error(t);break;case"highlight":h.highlight(t);break}}progress(e){this.lastProgressLength>0&&process.stdout.write(`\r${" ".repeat(this.lastProgressLength)}\r`),process.stdout.write(`\r${e}`),this.lastProgressLength=e.length}endProgress(){this.lastProgressLength>0&&(process.stdout.write(`
|
|
9
|
+
`),this.lastProgressLength=0)}};var j=class{config;apiUrl;constructor(e){this.config=e,this.apiUrl=`https://api.telegram.org/bot${e.botToken}/sendMessage`}async notify(e,t){if(e==="error")try{let i=`${this.getEmoji(e)} *wetvlo Error*
|
|
10
|
+
|
|
11
|
+
${t}`,r=await fetch(this.apiUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({chat_id:this.config.chatId,text:i,parse_mode:"Markdown"})});if(!r.ok){let s=await r.text();throw new A(`Failed to send Telegram notification: ${r.status} ${r.statusText}
|
|
12
|
+
${s}`)}}catch(o){console.error("Telegram notification failed:",o)}}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(){}};var W=class{domainConfigs;globalConfigs;constructor(e=[],t){this.domainConfigs=new Map(e.map(o=>[o.domain,o])),this.globalConfigs=t}resolve(e){let t=x(e.url),o=this.domainConfigs.get(t),i={check:this.mergeCheckSettings(e.check,o?.check),download:this.mergeDownloadSettings(e.download,o?.download)};return this.validate(i),i}mergeCheckSettings(e,t){let o=this.globalConfigs?.check,i=ie;return{count:e?.count??t?.count??o?.count??i.count,checkInterval:e?.checkInterval??t?.checkInterval??o?.checkInterval??i.checkInterval,downloadTypes:e?.downloadTypes??t?.downloadTypes??o?.downloadTypes??i.downloadTypes}}mergeDownloadSettings(e,t){let o=this.globalConfigs?.download,i=re;return{downloadDir:e?.downloadDir??t?.downloadDir??o?.downloadDir??i.downloadDir,tempDir:e?.tempDir??t?.tempDir??o?.tempDir??i.tempDir,downloadDelay:e?.downloadDelay??t?.downloadDelay??o?.downloadDelay??i.downloadDelay,maxRetries:e?.maxRetries??t?.maxRetries??o?.maxRetries??i.maxRetries,initialTimeout:e?.initialTimeout??t?.initialTimeout??o?.initialTimeout??i.initialTimeout,backoffMultiplier:e?.backoffMultiplier??t?.backoffMultiplier??o?.backoffMultiplier??i.backoffMultiplier,jitterPercentage:e?.jitterPercentage??t?.jitterPercentage??o?.jitterPercentage??i.jitterPercentage,minDuration:e?.minDuration??t?.minDuration??o?.minDuration??i.minDuration}}resolveDomain(e){let t=this.domainConfigs.get(e),o={check:this.mergeCheckSettings(void 0,t?.check),download:this.mergeDownloadSettings(void 0,t?.download)};return this.validate(o),o}setGlobalConfigs(e){this.globalConfigs=e}validate(e){if(e.check.count<1)throw new Error(`Invalid check count: ${e.check.count}`);if(e.check.checkInterval<0)throw new Error(`Invalid check interval: ${e.check.checkInterval}`);if(e.download.downloadDelay<0)throw new Error(`Invalid download delay: ${e.download.downloadDelay}`);if(e.download.maxRetries<0)throw new Error(`Invalid max retries: ${e.download.maxRetries}`);if(e.download.initialTimeout<0)throw new Error(`Invalid initial timeout: ${e.download.initialTimeout}`);if(e.download.backoffMultiplier<1)throw new Error(`Invalid backoff multiplier: ${e.download.backoffMultiplier}`);if(e.download.minDuration<0)throw new Error(`Invalid min duration: ${e.download.minDuration}`)}};var _=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 B=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 _(t);this.queues.set(e,o),this.queueCooldowns.set(e,t)}unregisterQueue(e){this.queues.delete(e),this.queueCooldowns.delete(e)}addTask(e,t,o){let i=this.queues.get(e);if(!i)throw new Error(`Queue ${e} is not registered`);i.add(t,o),this.stopped||this.scheduleNext()}addPriorityTask(e,t,o){let i=this.queues.get(e);if(!i)throw new Error(`Queue ${e} is not registered`);i.addFirst(t,o),this.stopped||this.scheduleNext()}markTaskComplete(e,t){let o=this.queues.get(e);if(!o)throw new Error(`Queue ${e} is not registered`);let i=t??this.queueCooldowns.get(e)??0;o.markCompleted(i),this.executorBusy=!1,this.stopped||this.scheduleNext()}markTaskFailed(e,t){let o=this.queues.get(e);if(!o)throw new Error(`Queue ${e} is not registered`);let i=t??this.queueCooldowns.get(e)??0;o.markFailed(i),this.executorBusy=!1,this.stopped||this.scheduleNext()}scheduleNext(){if(this.stopped||(this.clearTimer(),this.trySchedule())||this.executorBusy)return;let t=this.getEarliestAvailableTime();if(t){let o=Date.now(),i=Math.max(0,t.time.getTime()-o);this.scheduleTimer(i,t.queueName,t.time)}}trySchedule(){if(this.executorBusy)return!1;let e=new Date,t=Array.from(this.queues.keys());if(t.length===0)return!1;for(let o=0;o<t.length;o++){let i=(this.roundRobinIndex+o)%t.length,r=t[i];if(!r)continue;let s=this.queues.get(r);if(s&&s.hasTasks()&&s.canStart(e)){let c=s.getNext();if(c)return s.markStarted(),this.executorBusy=!0,this.roundRobinIndex=(i+1)%t.length,this.executeTask(r,c).catch(n=>{console.error(`[UniversalScheduler] Task execution failed: ${n}`),this.markTaskFailed(r)}),!0}}return!1}async executeTask(e,t){await this.executor(t,e)}scheduleTimer(e,t,o){this.clearTimer(),this.onWait&&e>1e3&&this.onWait(t,e,o),this.timerId=setTimeout(()=>{this.timerId=null,this.scheduleNext()},e)}clearTimer(){this.timerId!==null&&(clearTimeout(this.timerId),this.timerId=null)}getEarliestAvailableTime(){let e=null;for(let[t,o]of this.queues.entries()){if(!o.hasTasks())continue;let i=o.getNextAvailableTime();(e===null||i<e.time)&&(e={time:i,queueName:t})}return e}stop(){this.stopped=!0,this.clearTimer()}resume(){this.stopped=!1,this.scheduleNext()}getStats(){let e=new Map;for(let[t,o]of this.queues.entries()){let i=o.getStatus();e.set(t,{queueLength:i.queueLength,isExecuting:i.isExecuting,nextAvailableAt:i.nextAvailableAt})}return e}isExecutorBusy(){return this.executorBusy}hasPendingTasks(){for(let e of this.queues.values())if(e.hasTasks())return!0;return!1}getTotalPendingTasks(){let e=0;for(let t of this.queues.values())e+=t.getQueueLength();return e}};var z=class{stateManager;downloadManager;notifier;scheduler;configResolver;running=!1;domainHandlers=new Map;constructor(e,t,o,i,r=[],s,c){this.stateManager=e,this.downloadManager=t,this.notifier=o,this.configResolver=new W(r,s);let n=c||(d=>new B(d));this.scheduler=n(async(d,u)=>{await this.executeTask(d,u)}),this.scheduler.setOnWait((d,u)=>{let p=Math.round(u/1e3),f=d.split(":"),l=f[0],m=f[1];l==="download"?this.notifier.notify("info",`[${m}] Next download in ${p}s...`):l==="check"&&this.notifier.notify("info",`[${m}] Next check in ${p}s...`)})}addSeriesCheck(e){let t=x(e.url);this.registerDomainQueues(t);let o={seriesUrl:e.url,seriesName:e.name,config:e,attemptNumber:1,retryCount:0},i=`check:${t}`;this.scheduler.addTask(i,o),this.notifier.notify("info",`[QueueManager] Added ${e.name} to check queue for domain ${t}`)}addEpisodes(e,t,o,i){if(o.length===0)return;let r=x(e);this.registerDomainQueues(r);let s=this.configResolver.resolveDomain(r),{downloadDelay:c}=s.download;for(let n=0;n<o.length;n++){let d=o[n];if(!d)continue;let u={seriesUrl:e,seriesName:t,episode:d,config:i,retryCount:0},p=`download:${r}`,f=n*c*1e3;this.scheduler.addTask(p,u,f)}this.notifier.notify("success",`[QueueManager] Added ${o.length} episodes to download queue for ${t} (domain ${r})`)}start(){if(this.running)throw new Error("QueueManager is already running");this.running=!0,this.scheduler.resume(),this.notifier.notify("info","[QueueManager] Started queue processing")}async stop(){this.running&&(this.notifier.notify("info","[QueueManager] Stopping queue processing..."),this.scheduler.stop(),this.running=!1,this.notifier.notify("info","[QueueManager] Queue processing stopped"))}hasActiveProcessing(){return this.scheduler.isExecutorBusy()||this.scheduler.hasPendingTasks()}getQueueStats(){let e=this.scheduler.getStats(),t={},o={};for(let[i,r]of e.entries())if(i.startsWith("check:")){let s=i.slice(6);t[s]={length:r.queueLength,processing:r.isExecuting}}else if(i.startsWith("download:")){let s=i.slice(9);o[s]={length:r.queueLength,processing:r.isExecuting}}return{checkQueues:t,downloadQueues:o}}registerDomainQueues(e){let t=`check:${e}`,o=`download:${e}`,i=this.scheduler.getStats();if(i.has(t)&&i.has(o))return;let r=this.configResolver.resolveDomain(e),s=M.getHandlerOrThrow(`https://${e}/`);this.domainHandlers.set(e,s);let{checkInterval:c}=r.check,{downloadDelay:n}=r.download;this.scheduler.registerQueue(t,c*1e3),this.scheduler.registerQueue(o,n*1e3)}async executeTask(e,t){let o=t.split(":"),i=o[0],r=o[1];if(!i||!r)throw new Error(`Invalid queue name format: ${t}`);if(i==="check")await this.executeCheck(e,r,t);else if(i==="download")await this.executeDownload(e,r,t);else throw new Error(`Unknown queue type: ${i}`)}async executeCheck(e,t,o){let{seriesUrl:i,seriesName:r,config:s,attemptNumber:c,retryCount:n=0}=e,d=this.domainHandlers.get(t);if(!d)throw new Error(`No handler found for domain ${t}`);let u=this.configResolver.resolve(s),{count:p,checkInterval:f}=u.check;try{let l=await this.performCheck(d,i,r,u,c,t);if(l.hasNewEpisodes)this.notifier.notify("success",`[${t}] Found ${l.episodes.length} new episodes for ${r} (attempt ${c}/${p})`),this.addEpisodes(i,r,l.episodes,s),this.scheduler.markTaskComplete(o,f*1e3);else if(c<p){let m=f*1e3,w=l.requeueDelay??m;this.notifier.notify("info",`[${t}] No new episodes for ${r} (attempt ${c}/${p}), requeueing in ${Math.round(w/1e3)}s`);let y={...e,attemptNumber:c+1,retryCount:0};this.scheduler.addTask(o,y,w),this.scheduler.markTaskComplete(o,f*1e3)}else this.notifier.notify("info",`[${t}] Checks exhausted for ${r} (${p} attempts with no new episodes)`),this.scheduler.markTaskComplete(o,f*1e3)}catch(l){let m=l instanceof Error?l.message:String(l),{maxRetries:w,initialTimeout:y,backoffMultiplier:v,jitterPercentage:b}=u.download;if(n<w){let E=this.calculateBackoff(n,y*1e3,v,b);this.notifier.notify("warning",`[${t}] Check failed for ${r}, retrying in ${Math.round(E/1e3)}s (attempt ${n+1}/${w})`);let R={...e,retryCount:n+1};this.scheduler.addPriorityTask(o,R,E),this.scheduler.markTaskComplete(o,f*1e3)}else this.notifier.notify("error",`[${t}] Failed to check ${r} after ${n} retry attempts: ${m}`),this.scheduler.markTaskComplete(o,f*1e3)}}async executeDownload(e,t,o){let{seriesUrl:i,seriesName:r,episode:s,config:c,retryCount:n=0}=e,d;c?d=this.configResolver.resolve(c):d=this.configResolver.resolveDomain(t);let{downloadDelay:u,minDuration:p}=d.download;try{await this.downloadManager.download(i,r,s,p),this.notifier.notify("success",`[${t}] Successfully queued download of Episode ${s.number} for ${r}`),this.scheduler.markTaskComplete(o,u*1e3)}catch(f){let l=f instanceof Error?f.message:String(f),{maxRetries:m,initialTimeout:w,backoffMultiplier:y,jitterPercentage:v}=d.download;if(n<m){let b=this.calculateBackoff(n,w*1e3,y,v);this.notifier.notify("warning",`[${t}] Download failed for Episode ${s.number}, retrying in ${Math.round(b/1e3)}s (attempt ${n+1}/${m})`);let E={...e,retryCount:n+1};this.scheduler.addPriorityTask(o,E,b),this.scheduler.markTaskComplete(o,u*1e3)}else this.notifier.notify("error",`[${t}] Failed to download Episode ${s.number} after ${n+1} attempts: ${l}`),this.scheduler.markTaskComplete(o,u*1e3)}}async performCheck(e,t,o,i,r,s){let c=i.check.count;this.notifier.notify("info",`[${s}] Checking ${t} for new episodes... (attempt ${r}/${c})`);let n=await e.extractEpisodes(t);this.notifier.notify("info",`[${s}] Found ${n.length} total episodes on ${t}`);let d=i.check.downloadTypes,u=n.filter(p=>{let f=d.includes(p.type),l=!this.stateManager.isDownloaded(t,p.number);return f&&l});return u.length>0?{hasNewEpisodes:!0,episodes:u}:{hasNewEpisodes:!1,episodes:[],shouldRequeue:!0}}calculateBackoff(e,t,o,i){let r=t*o**e,s=r*i/100,c=(Math.random()*2-1)*s,n=Math.max(0,r+c);return Math.floor(n)}setGlobalConfigs(e){this.configResolver.setGlobalConfigs(e)}};function Pe(a){let e=a.match(/^(\d{1,2}):(\d{2})$/);if(!e)throw new Error(`Invalid time format: "${a}". Expected HH:MM`);let[,t,o]=e,i=parseInt(t||"0",10),r=parseInt(o||"0",10);if(i<0||i>23)throw new Error(`Invalid hours: ${i}. Must be between 0 and 23`);if(r<0||r>59)throw new Error(`Invalid minutes: ${r}. Must be between 0 and 59`);let s=new Date;return s.setHours(i,r,0,0),s}function pe(a){let e=Pe(a),t=new Date,o=new Date(t);return o.setHours(e.getHours(),e.getMinutes(),0,0),o<=t&&o.setDate(o.getDate()+1),o.getTime()-t.getTime()}function ge(a){return new Promise(e=>setTimeout(e,a))}var V=class{configs;stateManager;downloadManager;notifier;cookies;options;queueManager;running=!1;stopped=!0;globalConfigs;domainConfigs;timeProvider;constructor(e,t,o,i,r,s={mode:"scheduled"},c,n,d,u){this.configs=e,this.stateManager=t,this.downloadManager=o,this.notifier=i,this.cookies=r,this.options=s,this.globalConfigs=c,this.domainConfigs=n,this.timeProvider=d||{getMsUntilTime:pe,sleep:ge};let p=u||((f,l,m,w,y,v)=>new z(f,l,m,w,y,v));this.queueManager=p(this.stateManager,this.downloadManager,this.notifier,this.cookies,this.domainConfigs,this.globalConfigs)}async start(){if(this.running)throw new q("Scheduler is already running");if(this.running=!0,this.stopped=!1,this.queueManager.start(),this.options.mode==="once")this.notifier.notify("info","Single-run mode: checking all series once"),await this.runOnce();else{this.notifier.notify("info","Scheduler started (queue-based architecture)");let e=this.groupConfigsByStartTime();for(;!this.stopped;){let t=null,o=Number.MAX_SAFE_INTEGER;for(let r of e.keys()){let s=this.timeProvider.getMsUntilTime(r);s<o&&(o=s,t=r)}if(!t){this.notifier.notify("warning","No scheduled configs found. Exiting loop.");break}let i=e.get(t);if(!i||(o>0&&(this.notifier.notify("info",`Waiting ${Math.floor(o/1e3/60)} minutes until ${t}...`),await this.timeProvider.sleep(o)),this.stopped))break;for(await this.runConfigs(i);this.queueManager.hasActiveProcessing()&&!this.stopped;)await this.timeProvider.sleep(1e3)}}this.running=!1}async stop(){this.notifier.notify("info","Stopping scheduler..."),this.stopped=!0,await this.queueManager.stop(),await this.stateManager.save(),this.running=!1,this.notifier.notify("info","Scheduler stopped")}groupConfigsByStartTime(){let e=new Map;for(let t of this.configs){let o=e.get(t.startTime)||[];o.push(t),e.set(t.startTime,o)}return e}async runConfigs(e){for(let o of e){if(this.stopped)break;this.queueManager.addSeriesCheck(o)}let t=this.queueManager.getQueueStats();this.notifier.notify("info",`Added ${e.length} series to check queues. Queue stats: ${JSON.stringify(t)}`)}async runOnce(){for(let e of this.configs){if(this.stopped)break;this.queueManager.addSeriesCheck(e)}for(;this.queueManager.hasActiveProcessing()&&!this.stopped;)await this.timeProvider.sleep(1e3);await this.stateManager.save(),this.notifier.notify("success","Single-run complete")}isRunning(){return this.running&&!this.stopped}getQueueManager(){return this.queueManager}};import{existsSync as Ae}from"fs";import{join as Fe}from"path";function Y(){return{version:"2.0.0",series:{},lastUpdated:new Date().toISOString()}}var K=class{state;statePath;dirty=!1;constructor(e){this.statePath=Fe(process.cwd(),e),this.state=Y()}async load(){if(!Ae(this.statePath)){this.state=Y(),this.dirty=!0,await this.save();return}try{let t=await Bun.file(this.statePath).text();this.state=JSON.parse(t),this.dirty=!1}catch(e){throw new N(`Failed to load state file: ${e instanceof Error?e.message:String(e)}`)}}async save(){if(this.dirty)try{this.state.lastUpdated=new Date().toISOString();let e=JSON.stringify(this.state,null,2);await Bun.write(this.statePath,e),this.dirty=!1}catch(e){throw new N(`Failed to save state file: ${e instanceof Error?e.message:String(e)}`)}}isDownloaded(e,t){let o=this.state.series[e];if(!o)return!1;let i=String(t).padStart(2,"0");return!!o.episodes[i]}addDownloadedEpisode(e,t,o){this.state.series[e]||(this.state.series[e]={name:t,episodes:{}});let i=String(o.number).padStart(2,"0");this.state.series[e].episodes[i]||(this.state.series[e].episodes[i]={url:o.url,filename:o.filename,downloadedAt:new Date().toISOString(),size:o.size},this.dirty=!0)}getSeriesEpisodes(e){return this.state.series[e]?.episodes??{}}deleteSeries(e){this.state.series[e]&&(delete this.state.series[e],this.dirty=!0)}getAllSeriesUrls(){return Object.keys(this.state.series)}getSeriesName(e){return this.state.series[e]?.name??null}getDownloadedCount(){let e=0;for(let t of Object.values(this.state.series))e+=Object.keys(t.episodes).length;return e}clearAll(){this.state=Y(),this.dirty=!0}async forceSave(){this.dirty=!0,await this.save()}};import{existsSync as qe}from"fs";import{readFile as Oe}from"fs/promises";import{homedir as uo}from"os";import{join as po}from"path";async function he(a){if(!qe(a))throw new F(`Cookie file not found: "${a}"`);let t=(await Oe(a,"utf-8")).split(`
|
|
13
|
+
`),o=[];for(let i of t){let r=i.trim();if(r.startsWith("#")||!r)continue;let s=i.split(" ");if(s.length>=7){let c=s[5],n=s[6];if(c&&n){let d=n.trim();d&&o.push(`${c}=${d}`)}}}return o.join("; ")}var We={loadConfig:le,checkYtDlpInstalled:L.checkYtDlpInstalled,readCookieFile:he,createStateManager:a=>new K(a),createDownloadManager:(a,e,t,o,i)=>new L(a,e,t,o,i),createScheduler:(a,e,t,o,i,r,s,c)=>new V(a,e,t,o,i,r,s,c)};async function me(a,e){h.info("Shutting down gracefully...");try{await a.stop(),await e.save(),h.success("Shutdown complete")}catch(t){h.error(`Error during shutdown: ${t instanceof Error?t.message:String(t)}`)}}async function _e(a,e,t=We){if(h.info(`Mode: ${e==="once"?"Single-run (checks once, exits)":"Scheduled (waits for startTime)"}`),h.info("Checking yt-dlp installation..."),!await t.checkYtDlpInstalled())throw new Error(`yt-dlp is not installed. Please install it first:
|
|
14
|
+
- macOS: brew install yt-dlp
|
|
15
|
+
- Linux: pip install yt-dlp
|
|
16
|
+
- Windows: winget install yt-dlp`);h.info(`Loading configuration from ${a}...`);let i=await t.loadConfig(a);h.success("Configuration loaded");let r=t.createStateManager(i.stateFile);await r.load(),h.info(`State loaded: ${r.getDownloadedCount()} downloaded episodes`);let s=[new U];if(i.telegram)try{s.push(new j(i.telegram)),h.info("Telegram notifications enabled for errors")}catch(l){h.warning(`Failed to set up Telegram: ${l instanceof Error?l.message:String(l)}`)}let c={notify:async(l,m)=>{await Promise.all(s.map(w=>w.notify(l,m)))},progress:l=>{for(let m of s)m.progress(l)},endProgress:()=>{for(let l of s)l.endProgress()}};M.register(new Q),M.register(new H),h.info(`Registered handlers: ${M.getDomains().join(", ")}`);let n;if(i.cookieFile)try{n=await t.readCookieFile(i.cookieFile),h.success("Cookies loaded from file")}catch(l){h.warning(`Failed to load cookies: ${l instanceof Error?l.message:String(l)}`)}let d=i.globalConfigs?.download?.downloadDir??se,u=i.globalConfigs?.download?.tempDir,p=t.createDownloadManager(r,c,d,i.cookieFile,u);h.info("Using queue-based scheduler");let f=t.createScheduler(i.series,r,p,c,n,{mode:e},i.globalConfigs,i.domainConfigs);process.on("SIGINT",async()=>{await me(f,r),process.exit(0)}),process.on("SIGTERM",async()=>{await me(f,r),process.exit(0)}),await f.start()}var we=He({name:"wetvlo",description:"CLI Video Downloader for Chinese streaming sites",version:"0.0.1",args:{config:Ue({type:je,long:"config",short:"c",defaultValue:()=>"./config.yaml",description:"Path to configuration file (default: ./config.yaml)"}),once:Qe({type:Ge,long:"once",short:"o",description:"Run in single-run mode (check once and exit)"})},handler:async({config:a,once:e})=>{try{await _e(a,e?"once":"scheduled")}catch(t){t instanceof T?h.error(`Configuration error: ${t.message}`):h.error(`Fatal error: ${t instanceof Error?t.message:String(t)}`),process.exit(1)}}});async function ze(a=process.argv.slice(2)){await Be(we,a)}import.meta.main&&await ze();export{ze as main};
|
|
17
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/app.ts","../src/config/config-defaults.ts","../src/config/config-loader.ts","../src/errors/custom-errors.ts","../src/utils/env-resolver.ts","../src/config/config-schema.ts","../src/downloader/download-manager.ts","../src/utils/filename-sanitizer.ts","../src/utils/video-validator.ts","../src/utils/logger.ts","../src/utils/url-utils.ts","../src/handlers/handler-registry.ts","../src/handlers/base/base-handler.ts","../src/handlers/impl/iqiyi-handler.ts","../src/handlers/impl/wetv-handler.ts","../src/notifications/console-notifier.ts","../src/notifications/telegram-notifier.ts","../src/config/config-resolver.ts","../src/queue/typed-queue.ts","../src/queue/universal-scheduler.ts","../src/queue/queue-manager.ts","../src/utils/time-utils.ts","../src/scheduler/scheduler.ts","../src/state/state-manager.ts","../src/types/state.types.ts","../src/utils/cookie-extractor.ts"],"sourcesContent":["import { run } from 'cmd-ts';\nimport { cli } from './app.js';\n\n/**\n * Main entry point\n */\nexport async function main(args: string[] = process.argv.slice(2)): Promise<void> {\n await run(cli, args);\n}\n\n// Only run if this is the entry point\nif (import.meta.main) {\n await main();\n}\n","import { boolean, command, flag, option, string } from 'cmd-ts';\nimport { DEFAULT_DOWNLOAD_DIR } from './config/config-defaults.js';\nimport { loadConfig } from './config/config-loader.js';\nimport { DownloadManager } from './downloader/download-manager.js';\nimport { ConfigError } from './errors/custom-errors.js';\nimport { handlerRegistry } from './handlers/handler-registry.js';\nimport { IQiyiHandler } from './handlers/impl/iqiyi-handler.js';\nimport { WeTVHandler } from './handlers/impl/wetv-handler.js';\nimport { ConsoleNotifier } from './notifications/console-notifier.js';\nimport type { NotificationLevel, Notifier } from './notifications/notifier.js';\nimport { TelegramNotifier } from './notifications/telegram-notifier.js';\nimport { Scheduler } from './scheduler/scheduler.js';\nimport { StateManager } from './state/state-manager.js';\nimport type { DomainConfig, GlobalConfigs, SchedulerMode, SeriesConfig } from './types/config.types.js';\nimport { readCookieFile } from './utils/cookie-extractor.js';\nimport { logger } from './utils/logger.js';\n\nexport type AppDependencies = {\n loadConfig: typeof loadConfig;\n checkYtDlpInstalled: () => Promise<boolean>;\n readCookieFile: typeof readCookieFile;\n createStateManager: (path: string) => StateManager;\n createDownloadManager: (\n stateManager: StateManager,\n notifier: Notifier,\n downloadDir: string,\n cookieFile?: string,\n tempDir?: string,\n ) => DownloadManager;\n createScheduler: (\n configs: SeriesConfig[],\n stateManager: StateManager,\n downloadManager: DownloadManager,\n notifier: Notifier,\n cookies?: string,\n options?: { mode: SchedulerMode },\n globalConfigs?: GlobalConfigs,\n domainConfigs?: DomainConfig[],\n ) => Scheduler;\n};\n\nconst defaultDependencies: AppDependencies = {\n loadConfig,\n checkYtDlpInstalled: DownloadManager.checkYtDlpInstalled,\n readCookieFile,\n createStateManager: (path) => new StateManager(path),\n createDownloadManager: (sm, n, dir, cf, temp) => new DownloadManager(sm, n, dir, cf, temp),\n createScheduler: (c, sm, dm, n, cook, opt, gc, dc) => new Scheduler(c, sm, dm, n, cook, opt, gc, dc),\n};\n\n/**\n * Handle graceful shutdown\n */\nexport async function handleShutdown(scheduler: Scheduler, stateManager: StateManager): Promise<void> {\n logger.info('Shutting down gracefully...');\n\n try {\n await scheduler.stop();\n await stateManager.save();\n logger.success('Shutdown complete');\n } catch (error) {\n logger.error(`Error during shutdown: ${error instanceof Error ? error.message : String(error)}`);\n }\n}\n\nexport async function runApp(\n configPath: string,\n mode: SchedulerMode,\n deps: AppDependencies = defaultDependencies,\n): Promise<void> {\n logger.info(`Mode: ${mode === 'once' ? 'Single-run (checks once, exits)' : 'Scheduled (waits for startTime)'}`);\n\n // Check if yt-dlp is installed\n logger.info('Checking yt-dlp installation...');\n const ytDlpInstalled = await deps.checkYtDlpInstalled();\n\n if (!ytDlpInstalled) {\n throw new Error(\n 'yt-dlp is not installed. Please install it first:\\n' +\n ' - macOS: brew install yt-dlp\\n' +\n ' - Linux: pip install yt-dlp\\n' +\n ' - Windows: winget install yt-dlp',\n );\n }\n\n // Load configuration\n logger.info(`Loading configuration from ${configPath}...`);\n const config = await deps.loadConfig(configPath);\n logger.success('Configuration loaded');\n\n // Initialize state manager\n const stateManager = deps.createStateManager(config.stateFile);\n await stateManager.load();\n logger.info(`State loaded: ${stateManager.getDownloadedCount()} downloaded episodes`);\n\n // Set up notifiers\n const notifiers: Array<ConsoleNotifier | TelegramNotifier> = [new ConsoleNotifier()];\n\n if (config.telegram) {\n try {\n notifiers.push(new TelegramNotifier(config.telegram));\n logger.info('Telegram notifications enabled for errors');\n } catch (error) {\n logger.warning(`Failed to set up Telegram: ${error instanceof Error ? error.message : String(error)}`);\n }\n }\n\n // Create composite notifier\n const notifier: Notifier = {\n notify: async (level: NotificationLevel, message: string): Promise<void> => {\n await Promise.all(notifiers.map((n) => n.notify(level, message)));\n },\n progress: (message: string): void => {\n for (const n of notifiers) {\n n.progress(message);\n }\n },\n endProgress: (): void => {\n for (const n of notifiers) {\n n.endProgress();\n }\n },\n };\n\n // Register handlers\n handlerRegistry.register(new WeTVHandler());\n handlerRegistry.register(new IQiyiHandler());\n logger.info(`Registered handlers: ${handlerRegistry.getDomains().join(', ')}`);\n\n // Load cookies if specified\n let cookies: string | undefined;\n if (config.cookieFile) {\n try {\n cookies = await deps.readCookieFile(config.cookieFile);\n logger.success('Cookies loaded from file');\n } catch (error) {\n logger.warning(`Failed to load cookies: ${error instanceof Error ? error.message : String(error)}`);\n }\n }\n\n // Create download manager\n const downloadDir = config.globalConfigs?.download?.downloadDir ?? DEFAULT_DOWNLOAD_DIR;\n const tempDir = config.globalConfigs?.download?.tempDir;\n const downloadManager = deps.createDownloadManager(stateManager, notifier, downloadDir, config.cookieFile, tempDir);\n\n // Create and start scheduler with queue-based architecture\n logger.info('Using queue-based scheduler');\n const scheduler = deps.createScheduler(\n config.series,\n stateManager,\n downloadManager,\n notifier,\n cookies,\n { mode },\n config.globalConfigs,\n config.domainConfigs,\n );\n\n // Set up signal handlers for graceful shutdown\n process.on('SIGINT', async () => {\n await handleShutdown(scheduler, stateManager);\n process.exit(0);\n });\n process.on('SIGTERM', async () => {\n await handleShutdown(scheduler, stateManager);\n process.exit(0);\n });\n\n // Start the scheduler\n await scheduler.start();\n}\n\n// Define CLI using cmd-ts\nexport const cli = command({\n name: 'wetvlo',\n description: 'CLI Video Downloader for Chinese streaming sites',\n version: '0.0.1',\n args: {\n config: option({\n type: string,\n long: 'config',\n short: 'c',\n defaultValue: () => './config.yaml',\n description: 'Path to configuration file (default: ./config.yaml)',\n }),\n once: flag({\n type: boolean,\n long: 'once',\n short: 'o',\n description: 'Run in single-run mode (check once and exit)',\n }),\n },\n handler: async ({ config, once }: { config: string; once: boolean }) => {\n try {\n const mode: SchedulerMode = once ? 'once' : 'scheduled';\n await runApp(config, mode);\n } catch (error) {\n if (error instanceof ConfigError) {\n logger.error(`Configuration error: ${error.message}`);\n } else {\n logger.error(`Fatal error: ${error instanceof Error ? error.message : String(error)}`);\n }\n process.exit(1);\n }\n },\n});\n","/**\n * Default configuration values\n *\n * Centralized defaults for all configuration settings.\n * Used when no value is provided at series, domain, or global level.\n */\n\nimport { EpisodeType } from '../types/episode.types.js';\nimport type { CheckSettings, DownloadSettings } from './config-schema.js';\n\n/**\n * Default check settings\n */\nexport const DEFAULT_CHECK_SETTINGS: Required<CheckSettings> = {\n count: 3, // Number of times to check for new episodes\n checkInterval: 600, // Seconds between checks\n downloadTypes: ['available'], // Episode types to download\n};\n\n/**\n * Default download settings\n */\nexport const DEFAULT_DOWNLOAD_SETTINGS: Required<DownloadSettings> = {\n downloadDir: './downloads',\n tempDir: './downloads', // Default temp dir same as download dir\n downloadDelay: 10, // Seconds between downloads\n maxRetries: 3, // Maximum retry attempts on failure\n initialTimeout: 5, // Initial retry delay in seconds\n backoffMultiplier: 2, // Exponential backoff multiplier\n jitterPercentage: 10, // Random jitter (0-100%)\n minDuration: 0, // Minimum duration in seconds (0 = disabled)\n};\n\n/**\n * Default episode types as EpisodeType enum\n */\nexport const DEFAULT_DOWNLOAD_TYPES_ENUM: EpisodeType[] = [EpisodeType.AVAILABLE, EpisodeType.VIP];\n\n/**\n * Default download directory\n */\nexport const DEFAULT_DOWNLOAD_DIR = './downloads';\n","import { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport * as yaml from 'js-yaml';\nimport { ConfigError } from '../errors/custom-errors';\nimport type { Config, RawConfig } from '../types/config.types';\nimport { resolveEnvRecursive } from '../utils/env-resolver';\nimport { validateConfig } from './config-schema';\n\n/**\n * Default config file path\n */\nexport const DEFAULT_CONFIG_PATH = './config.yaml';\n\n/**\n * Load and parse configuration from YAML file\n *\n * @param configPath - Path to config file\n * @returns Parsed configuration\n * @throws ConfigError if file doesn't exist or is invalid\n */\nexport async function loadConfig(configPath: string = DEFAULT_CONFIG_PATH): Promise<Config> {\n // Resolve relative path\n const absolutePath = join(process.cwd(), configPath);\n\n if (!existsSync(absolutePath)) {\n throw new ConfigError(\n `Configuration file not found: \"${absolutePath}\". Create a config.yaml file or specify a different path.`,\n );\n }\n\n const file = Bun.file(absolutePath);\n const content = await file.text();\n\n let rawConfig: RawConfig;\n\n try {\n rawConfig = yaml.load(content) as RawConfig;\n } catch (error) {\n throw new ConfigError(`Failed to parse YAML: ${error instanceof Error ? error.message : String(error)}`);\n }\n\n // Validate configuration structure\n validateConfig(rawConfig);\n\n // Resolve environment variables\n const config = resolveEnvRecursive(rawConfig) as unknown as Config;\n\n return config;\n}\n\n/**\n * Load config with defaults for optional fields\n */\nexport async function loadConfigWithDefaults(configPath: string = DEFAULT_CONFIG_PATH): Promise<Config> {\n const config = await loadConfig(configPath);\n\n // Set defaults for optional fields\n if (!config.telegram) {\n delete config.telegram;\n }\n\n return config;\n}\n","/**\n * Base error class for wetvlo\n */\nexport class WetvloError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'WetvloError';\n }\n}\n\n/**\n * Configuration error\n */\nexport class ConfigError extends WetvloError {\n constructor(message: string) {\n super(message);\n this.name = 'ConfigError';\n }\n}\n\n/**\n * State file error\n */\nexport class StateError extends WetvloError {\n constructor(message: string) {\n super(message);\n this.name = 'StateError';\n }\n}\n\n/**\n * Handler error (episode extraction issues)\n */\nexport class HandlerError extends WetvloError {\n constructor(\n message: string,\n public readonly url: string,\n ) {\n super(message);\n this.name = 'HandlerError';\n }\n}\n\n/**\n * Download error\n */\nexport class DownloadError extends WetvloError {\n constructor(\n message: string,\n public readonly url: string,\n ) {\n super(message);\n this.name = 'DownloadError';\n }\n}\n\n/**\n * Notification error\n */\nexport class NotificationError extends WetvloError {\n constructor(message: string) {\n super(message);\n this.name = 'NotificationError';\n }\n}\n\n/**\n * Cookie extraction error\n */\nexport class CookieError extends WetvloError {\n constructor(message: string) {\n super(message);\n this.name = 'CookieError';\n }\n}\n\n/**\n * Scheduling error\n */\nexport class SchedulerError extends WetvloError {\n constructor(message: string) {\n super(message);\n this.name = 'SchedulerError';\n }\n}\n","/**\n * Resolve environment variables in strings\n * Supports ${VAR_NAME} syntax\n *\n * @param value - String that may contain ${VAR_NAME} placeholders\n * @returns String with environment variables resolved\n */\nexport function resolveEnv(value: string): string {\n if (typeof value !== 'string') {\n return value;\n }\n\n return value.replace(/\\$\\{([^}]+)\\}/g, (_match, varName) => {\n const envValue = process.env[varName];\n if (envValue === undefined) {\n throw new Error(`Environment variable \"${varName}\" is not set`);\n }\n return envValue;\n });\n}\n\n/**\n * Recursively resolve environment variables in object\n *\n * @param obj - Object that may contain strings with ${VAR_NAME}\n * @returns Object with all environment variables resolved\n */\nexport function resolveEnvRecursive<T>(obj: T): T {\n if (typeof obj === 'string') {\n return resolveEnv(obj) as T;\n }\n\n if (Array.isArray(obj)) {\n return obj.map((item) => resolveEnvRecursive(item)) as T;\n }\n\n if (obj !== null && typeof obj === 'object') {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n result[key] = resolveEnvRecursive(value);\n }\n return result as T;\n }\n\n return obj;\n}\n","/**\n * Zod schemas for configuration validation\n *\n * This file defines both the validation schemas AND the TypeScript types.\n * Types are automatically inferred from the schemas, ensuring they stay in sync.\n */\n\nimport { z } from 'zod';\n\n/**\n * Episode types\n */\nconst EpisodeTypeSchema = z.enum(['available', 'vip', 'teaser', 'express', 'preview', 'locked']);\n\n/**\n * Check settings for series/domain\n */\nexport const CheckSettingsSchema = z.object({\n count: z.number().positive().optional(),\n checkInterval: z.number().positive().optional(),\n downloadTypes: z.array(EpisodeTypeSchema).optional(),\n});\n\nexport type CheckSettings = z.infer<typeof CheckSettingsSchema>;\n\n/**\n * Download settings for series/domain\n */\nexport const DownloadSettingsSchema = z.object({\n downloadDir: z.string().optional(),\n tempDir: z.string().optional(),\n downloadDelay: z.number().nonnegative().optional(),\n maxRetries: z.number().int().nonnegative().optional(),\n initialTimeout: z.number().positive().optional(),\n backoffMultiplier: z.number().positive().optional(),\n jitterPercentage: z.number().int().min(0).max(100).optional(),\n minDuration: z.number().nonnegative().optional(),\n});\n\nexport type DownloadSettings = z.infer<typeof DownloadSettingsSchema>;\n\n/**\n * Telegram notification configuration\n */\nexport const TelegramConfigSchema = z.object({\n botToken: z.string(),\n chatId: z.string(),\n});\n\nexport type TelegramConfig = z.infer<typeof TelegramConfigSchema>;\n\n/**\n * Domain-specific configuration\n */\nexport const DomainConfigSchema = z.object({\n domain: z.string(),\n check: CheckSettingsSchema.optional(),\n download: DownloadSettingsSchema.optional(),\n});\n\nexport type DomainConfig = z.infer<typeof DomainConfigSchema>;\n\n/**\n * Series configuration\n */\nexport const SeriesConfigSchema = z.object({\n name: z.string(),\n url: z.string().url(),\n startTime: z.string().regex(/^\\d{1,2}:\\d{2}$/, {\n message: 'Must be in HH:MM format (e.g., \"20:00\")',\n }),\n check: CheckSettingsSchema.optional(),\n download: DownloadSettingsSchema.optional(),\n});\n\nexport type SeriesConfig = z.infer<typeof SeriesConfigSchema>;\n\n/**\n * Global configuration defaults\n */\nexport const GlobalConfigsSchema = z.object({\n check: CheckSettingsSchema.optional(),\n download: DownloadSettingsSchema.optional(),\n});\n\nexport type GlobalConfigs = z.infer<typeof GlobalConfigsSchema>;\n\n/**\n * Browser options\n */\nconst BrowserSchema = z.enum(['chrome', 'firefox', 'safari', 'chromium', 'edge']);\n\n/**\n * Main configuration schema\n */\nexport const ConfigSchema = z.object({\n series: z.array(SeriesConfigSchema).min(1, 'Cannot be empty'),\n telegram: TelegramConfigSchema.optional(),\n globalConfigs: GlobalConfigsSchema.optional(),\n stateFile: z.string(),\n browser: BrowserSchema,\n cookieFile: z.string().optional(),\n domainConfigs: z.array(DomainConfigSchema).optional(),\n});\n\nexport type Config = z.infer<typeof ConfigSchema>;\n\n/**\n * Raw configuration before env var resolution\n */\nexport type RawConfig = Record<string, unknown>;\n\n/**\n * Validate configuration using Zod\n *\n * @param rawConfig - Raw configuration object from YAML\n * @throws z.ZodError if validation fails\n */\nexport function validateConfig(rawConfig: RawConfig): void {\n ConfigSchema.parse(rawConfig);\n}\n\n/**\n * Validate with custom error formatting\n *\n * @param rawConfig - Raw configuration object\n * @returns Object with { success: boolean, error?: string }\n */\nexport function validateConfigSafe(rawConfig: RawConfig): { success: true } | { success: false; error: string } {\n try {\n ConfigSchema.parse(rawConfig);\n return { success: true };\n } catch (error) {\n if (error instanceof z.ZodError) {\n return { success: false, error: formatZodError(error) };\n }\n return { success: false, error: String(error) };\n }\n}\n\n/**\n * Format Zod error into a readable message\n */\nfunction formatZodError(error: z.ZodError): string {\n return error.issues\n .map((issue) => {\n const path = issue.path.length > 0 ? `\"${issue.path.join('.')}\"` : 'value';\n const code = issue.code.toUpperCase();\n return `${path} ${issue.message} [${code}]`;\n })\n .join('; ');\n}\n","import * as fs from 'node:fs';\nimport * as fsPromises from 'node:fs/promises';\nimport { basename, join, resolve } from 'node:path';\nimport { execa } from 'execa';\nimport { DownloadError } from '../errors/custom-errors';\nimport type { Notifier } from '../notifications/notifier';\nimport { NotificationLevel } from '../notifications/notifier';\nimport type { StateManager } from '../state/state-manager';\nimport type { Episode } from '../types/episode.types';\nimport { sanitizeFilename } from '../utils/filename-sanitizer';\nimport * as VideoValidator from '../utils/video-validator';\n\n/**\n * Error type returned by execa when a subprocess fails\n */\ntype ExecaError = {\n stderr?: string;\n stdout?: string;\n message?: string;\n};\n\n/**\n * Download manager for yt-dlp with progress tracking\n */\nexport class DownloadManager {\n private stateManager: StateManager;\n private notifier: Notifier;\n private downloadDir: string;\n private tempDir?: string;\n private cookieFile?: string;\n\n constructor(\n stateManager: StateManager,\n notifier: Notifier,\n downloadDir: string,\n cookieFile?: string,\n tempDir?: string,\n ) {\n this.stateManager = stateManager;\n this.notifier = notifier;\n this.downloadDir = resolve(downloadDir);\n this.cookieFile = cookieFile ? resolve(cookieFile) : undefined;\n this.tempDir = tempDir ? resolve(tempDir) : undefined;\n }\n\n /**\n * Download an episode using yt-dlp with progress tracking\n */\n async download(seriesUrl: string, seriesName: string, episode: Episode, minDuration: number = 0): Promise<boolean> {\n // Check if already downloaded\n if (this.stateManager.isDownloaded(seriesUrl, episode.number)) {\n return false;\n }\n\n this.notifier.notify(NotificationLevel.HIGHLIGHT, `Downloading Episode ${episode.number} of ${seriesName}`);\n\n try {\n const result = await this.runYtDlp(seriesName, episode);\n\n // Verify file exists and has size\n const fileSize = this.verifyDownload(result.filename);\n\n if (fileSize === 0) {\n await this.cleanupFiles(result.allFiles);\n throw new Error('Downloaded file is empty or does not exist');\n }\n\n // Verify duration if required\n if (minDuration > 0) {\n const fullPath = resolve(result.filename);\n const duration = await VideoValidator.getVideoDuration(fullPath);\n if (duration < minDuration) {\n // Delete all downloaded files\n await this.cleanupFiles(result.allFiles);\n throw new Error(`Video duration ${duration}s is less than minimum ${minDuration}s`);\n }\n }\n\n // Move files from tempDir to downloadDir if needed\n if (this.tempDir && this.tempDir !== this.downloadDir) {\n this.notifier.notify(NotificationLevel.INFO, `Moving files from temp directory to ${this.downloadDir}...`);\n\n // Ensure download directory exists\n await fsPromises.mkdir(this.downloadDir, { recursive: true });\n\n for (const file of result.allFiles) {\n try {\n // Resolve 'file' to absolute path just in case\n const absFile = resolve(file);\n\n if (!fs.existsSync(absFile)) {\n this.notifier.notify(NotificationLevel.WARNING, `File not found, skipping move: ${absFile}`);\n continue;\n }\n\n const fileName = basename(absFile);\n const newPath = join(this.downloadDir, fileName);\n await fsPromises.rename(absFile, newPath);\n\n // Update filename if it matches the main file\n if (absFile === resolve(result.filename)) {\n result.filename = newPath;\n }\n } catch (e) {\n this.notifier.notify(NotificationLevel.ERROR, `Failed to move file ${file}: ${e}`);\n }\n }\n }\n\n // Add to state\n this.stateManager.addDownloadedEpisode(seriesUrl, seriesName, {\n number: episode.number,\n url: episode.url,\n filename: result.filename,\n size: fileSize,\n });\n await this.stateManager.save();\n\n this.notifier.notify(\n NotificationLevel.SUCCESS,\n `Downloaded Episode ${episode.number}: ${result.filename} (${this.formatSize(fileSize)})`,\n );\n\n return true;\n } catch (error) {\n const message = `Failed to download Episode ${episode.number}: ${\n error instanceof Error ? error.message : String(error)\n }`;\n\n this.notifier.notify(NotificationLevel.ERROR, message);\n throw new DownloadError(message, episode.url);\n }\n }\n\n /**\n * Clean up downloaded files\n */\n private async cleanupFiles(files: string[]): Promise<void> {\n for (const file of files) {\n try {\n const fullPath = resolve(file);\n if (fs.existsSync(fullPath)) {\n await fsPromises.unlink(fullPath);\n }\n } catch (e) {\n this.notifier.notify(NotificationLevel.ERROR, `Failed to delete file ${file}: ${e}`);\n }\n }\n }\n\n /**\n * Run yt-dlp with execa and progress tracking\n */\n private async runYtDlp(seriesName: string, episode: Episode): Promise<{ filename: string; allFiles: string[] }> {\n const paddedNumber = String(episode.number).padStart(2, '0');\n const targetDir = this.tempDir || this.downloadDir;\n\n // Ensure directory exists\n await fsPromises.mkdir(targetDir, { recursive: true });\n\n const sanitizedSeriesName = sanitizeFilename(seriesName);\n const outputTemplate = join(targetDir, `${sanitizedSeriesName} - ${paddedNumber}.%(ext)s`);\n\n const args = ['--no-warnings', '--newline', '-o', outputTemplate, episode.url];\n\n if (this.cookieFile) {\n args.unshift('--cookies', this.cookieFile);\n }\n\n let filename: string | null = null;\n const allFiles: Set<string> = new Set();\n const outputBuffer: string[] = [];\n\n try {\n const subprocess = execa('yt-dlp', args, { all: true });\n\n for await (const line of subprocess.all) {\n const text = line.toString().trim();\n if (!text) continue;\n\n // Buffer all output for error debugging\n outputBuffer.push(text);\n\n // Capture filename from \"[download] Destination: ...\" line\n const destMatch = text.match(/\\[download\\] Destination:\\s*(.+)/);\n if (destMatch) {\n filename = destMatch[1];\n if (filename) allFiles.add(filename);\n }\n\n // Capture subtitles from \"[info] Writing video subtitles to: ...\"\n const subMatch = text.match(/\\[info\\] Writing video subtitles to:\\s*(.+)/);\n if (subMatch?.[1]) {\n allFiles.add(subMatch[1]);\n }\n\n // Capture merged file from \"[merge] Merging formats into \"...\"\n const mergeMatch = text.match(/\\[merge\\] Merging formats into \"(.*)\"/);\n if (mergeMatch) {\n filename = mergeMatch[1];\n if (filename) allFiles.add(filename);\n }\n\n // Status messages: [info], [ffmpeg], [merge] - check FIRST\n if (\n text.includes('[info]') ||\n text.includes('[ffmpeg]') ||\n text.includes('[merge]') ||\n text.includes('[postprocessor]')\n ) {\n this.notifier.notify(NotificationLevel.INFO, `Episode ${episode.number}: ${text}`);\n continue;\n }\n\n // Detailed progress with file size\n if (text.includes('[download]')) {\n // Match: [download] 23.8% of ~ 145.41MiB at 563.37KiB/s ETA 03:34 (frag 48/203)\n // or: [download] 0.0% of ~ 68.02MiB at 2.83KiB/s ETA Unknown (frag 0/203)\n // The format has:\n // - Optional ~ before size (indicates estimated)\n // - (frag X/Y) suffix at end\n // - Extra whitespace\n // - ETA can be \"Unknown\" or a time like \"03:34\"\n const progressMatch = text.match(\n /\\[download\\]\\s+(\\d+\\.?\\d*)%\\s+of\\s+~?\\s*([\\d.]+\\w+)\\s+at\\s+~?\\s*([\\d.]+\\w+\\/s)\\s+ETA\\s+(\\S+)/,\n );\n\n if (progressMatch) {\n const [, percentage, totalSize, speed, eta] = progressMatch;\n // Use progress() to update on same line\n this.notifier.progress(`[${episode.number}] ${percentage}% of ${totalSize} at ${speed} ETA ${eta}`);\n } else {\n // Other download status: Destination, Resuming, etc. - show normally\n this.notifier.notify(NotificationLevel.INFO, `Episode ${episode.number}: ${text}`);\n }\n }\n }\n\n await subprocess;\n\n // End progress display (add newline)\n this.notifier.endProgress();\n\n if (!filename) {\n // Fallback if we couldn't parse the filename\n filename = join(targetDir, `${sanitizedSeriesName} - ${paddedNumber}.mp4`);\n }\n\n // Ensure the main filename is included in allFiles\n if (filename && !allFiles.has(filename)) {\n allFiles.add(filename);\n }\n\n return { filename, allFiles: Array.from(allFiles) };\n } catch (error) {\n // End progress display on error\n this.notifier.endProgress();\n\n const err = error as ExecaError;\n const stderr = err.stderr ?? '';\n const stdout = err.stdout ?? '';\n const allOutput = outputBuffer.join('\\n');\n\n throw new Error(\n `yt-dlp failed:\\n` +\n `stderr: ${stderr}\\n` +\n `stdout: ${stdout}\\n` +\n `captured output:\\n${allOutput}\\n` +\n `message: ${err.message}`,\n );\n }\n }\n\n /**\n * Verify downloaded file exists and get its size\n */\n private verifyDownload(filename: string): number {\n const fullPath = resolve(filename);\n\n try {\n const file = Bun.file(fullPath);\n return file.size;\n } catch {\n return 0;\n }\n }\n\n /**\n * Format file size for display\n */\n private formatSize(bytes: number): string {\n const units = ['B', 'KB', 'MB', 'GB'];\n let size = bytes;\n let unit = 0;\n\n while (size >= 1024 && unit < units.length - 1) {\n size /= 1024;\n unit++;\n }\n\n return `${size.toFixed(2)} ${units[unit]}`;\n }\n\n /**\n * Check if yt-dlp is installed\n */\n static async checkYtDlpInstalled(): Promise<boolean> {\n try {\n await execa('yt-dlp', ['--version']);\n return true;\n } catch {\n return false;\n }\n }\n}\n","/**\n * Utility to sanitize filenames for cross-platform compatibility\n * Specifically targets Windows restrictions which are stricter than *nix\n */\nexport function sanitizeFilename(name: string): string {\n return (\n name\n // Replace Windows illegal characters: < > : \" / \\ | ? *\n .replace(/[<>:\"/\\\\|?*]/g, '_')\n // Remove control characters (0-31 in ASCII)\n // biome-ignore lint/suspicious/noControlCharactersInRegex: Needed to strip control characters\n .replace(/[\\x00-\\x1F]/g, '')\n // Remove trailing spaces and dots (Windows doesn't like them)\n .replace(/[\\s.]+$/, '')\n );\n}\n","import { execa } from 'execa';\nimport { logger } from './logger';\n\n/**\n * Utility to validate video files\n */\n\n/**\n * Get video duration in seconds using ffprobe\n *\n * @param filePath - Path to video file\n * @returns Duration in seconds, or 0 if failed\n */\nexport async function getVideoDuration(filePath: string): Promise<number> {\n try {\n // ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 input.mp4\n const { stdout } = await execa('ffprobe', [\n '-v',\n 'error',\n '-show_entries',\n 'format=duration',\n '-of',\n 'default=noprint_wrappers=1:nokey=1',\n filePath,\n ]);\n\n const duration = parseFloat(stdout.trim());\n return Number.isNaN(duration) ? 0 : duration;\n } catch (error) {\n logger.error(\n `Failed to get video duration for ${filePath}: ${error instanceof Error ? error.message : String(error)}`,\n );\n return 0;\n }\n}\n\n/**\n * Check if ffprobe is installed\n */\nexport async function checkFfprobeInstalled(): Promise<boolean> {\n try {\n await execa('ffprobe', ['-version']);\n return true;\n } catch {\n return false;\n }\n}\n","/**\n * Log level\n */\nexport enum LogLevel {\n DEBUG = 'DEBUG',\n INFO = 'INFO',\n SUCCESS = 'SUCCESS',\n WARNING = 'WARNING',\n ERROR = 'ERROR',\n HIGHLIGHT = 'HIGHLIGHT',\n}\n\n/**\n * Logger configuration\n */\nexport type LoggerConfig = {\n level: LogLevel;\n useColors: boolean;\n};\n\n/**\n * ANSI color codes\n */\nconst colors = {\n reset: '\\x1b[0m',\n bright: '\\x1b[1m',\n dim: '\\x1b[2m',\n\n // Foreground colors\n black: '\\x1b[30m',\n red: '\\x1b[31m',\n green: '\\x1b[32m',\n yellow: '\\x1b[33m',\n blue: '\\x1b[34m',\n magenta: '\\x1b[35m',\n cyan: '\\x1b[36m',\n white: '\\x1b[37m',\n\n // Background colors\n bgRed: '\\x1b[41m',\n bgGreen: '\\x1b[42m',\n bgYellow: '\\x1b[43m',\n};\n\n/**\n * Logger class with colored console output\n */\nexport class Logger {\n private config: LoggerConfig;\n\n constructor(config: Partial<LoggerConfig> = {}) {\n this.config = {\n level: config.level ?? LogLevel.INFO,\n useColors: config.useColors ?? true,\n };\n }\n\n /**\n * Format log message with timestamp and level\n */\n private format(level: LogLevel, message: string): string {\n const timestamp = new Date().toISOString();\n return `[${timestamp}] [${level}] ${message}`;\n }\n\n /**\n * Apply color to text\n */\n private colorize(text: string, color: string): string {\n if (!this.config.useColors) return text;\n return `${color}${text}${colors.reset}`;\n }\n\n /**\n * Log debug message\n */\n debug(message: string): void {\n if (this.shouldLog(LogLevel.DEBUG)) {\n console.log(this.format(LogLevel.DEBUG, this.colorize(message, colors.dim)));\n }\n }\n\n /**\n * Log info message\n */\n info(message: string): void {\n if (this.shouldLog(LogLevel.INFO)) {\n console.log(this.format(LogLevel.INFO, this.colorize(message, colors.blue)));\n }\n }\n\n /**\n * Log success message\n */\n success(message: string): void {\n if (this.shouldLog(LogLevel.SUCCESS)) {\n console.log(this.format(LogLevel.SUCCESS, this.colorize(message, colors.green)));\n }\n }\n\n /**\n * Log warning message\n */\n warning(message: string): void {\n if (this.shouldLog(LogLevel.WARNING)) {\n console.log(this.format(LogLevel.WARNING, this.colorize(message, colors.yellow)));\n }\n }\n\n /**\n * Log error message\n */\n error(message: string): void {\n if (this.shouldLog(LogLevel.ERROR)) {\n console.error(this.format(LogLevel.ERROR, this.colorize(message, colors.red)));\n }\n }\n\n /**\n * Log highlighted message\n */\n highlight(message: string): void {\n if (this.shouldLog(LogLevel.HIGHLIGHT)) {\n console.log(this.format(LogLevel.HIGHLIGHT, this.colorize(message, colors.bright + colors.magenta)));\n }\n }\n\n /**\n * Check if message should be logged based on level\n */\n private shouldLog(level: LogLevel): boolean {\n const levels = [\n LogLevel.DEBUG,\n LogLevel.INFO,\n LogLevel.SUCCESS,\n LogLevel.WARNING,\n LogLevel.ERROR,\n LogLevel.HIGHLIGHT,\n ];\n return levels.indexOf(level) >= levels.indexOf(this.config.level);\n }\n\n /**\n * Set log level\n */\n setLevel(level: LogLevel): void {\n this.config.level = level;\n }\n}\n\n// Default logger instance\nexport const logger: Logger = new Logger();\n","/**\n * Extract domain from URL\n *\n * @param url - URL to extract domain from\n * @returns Domain (e.g., \"wetv.vip\")\n */\nexport function extractDomain(url: string): string {\n try {\n const urlObj = new URL(url);\n return urlObj.hostname;\n } catch {\n throw new Error(`Invalid URL: \"${url}\"`);\n }\n}\n\n/**\n * Check if URL is valid\n *\n * @param url - URL to validate\n * @returns True if URL is valid\n */\nexport function isValidUrl(url: string): boolean {\n try {\n new URL(url);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Normalize URL by removing trailing slash and fragment\n *\n * @param url - URL to normalize\n * @returns Normalized URL\n */\nexport function normalizeUrl(url: string): string {\n try {\n const urlObj = new URL(url);\n urlObj.hash = '';\n // Remove trailing slash from pathname\n if (urlObj.pathname.endsWith('/')) {\n urlObj.pathname = urlObj.pathname.slice(0, -1);\n }\n return urlObj.toString();\n } catch {\n return url;\n }\n}\n","import { HandlerError } from '../errors/custom-errors';\nimport type { DomainHandler, HandlerRegistry } from '../types/handler.types';\nimport { extractDomain } from '../utils/url-utils';\n\n/**\n * Handler registry implementation\n */\nexport class Registry implements HandlerRegistry {\n private handlers: Map<string, DomainHandler> = new Map();\n\n /**\n * Register a handler\n */\n register(handler: DomainHandler): void {\n this.handlers.set(handler.getDomain(), handler);\n }\n\n /**\n * Get handler for URL\n */\n getHandler(url: string): DomainHandler | undefined {\n const domain = extractDomain(url);\n\n // First try exact match\n if (this.handlers.has(domain)) {\n return this.handlers.get(domain);\n }\n\n // Then try subdomain match (e.g., www.wetv.vip -> wetv.vip)\n for (const [handlerDomain, handler] of this.handlers.entries()) {\n if (domain === handlerDomain || domain.endsWith(`.${handlerDomain}`) || handlerDomain.endsWith(`.${domain}`)) {\n return handler;\n }\n }\n\n return undefined;\n }\n\n /**\n * Get all registered domains\n */\n getDomains(): string[] {\n return Array.from(this.handlers.keys());\n }\n\n /**\n * Get handler or throw error\n */\n getHandlerOrThrow(url: string): DomainHandler {\n const handler = this.getHandler(url);\n if (!handler) {\n throw new HandlerError(\n `No handler found for domain: \"${extractDomain(url)}\". ` + `Supported domains: ${this.getDomains().join(', ')}`,\n url,\n );\n }\n return handler;\n }\n}\n\n// Global registry instance\nexport const handlerRegistry: Registry = new Registry();\n","import * as cheerio from 'cheerio';\nimport type { AnyNode } from 'domhandler';\nimport { HandlerError } from '../../errors/custom-errors';\nimport type { Episode, EpisodeType } from '../../types/episode.types';\nimport type { DomainHandler } from '../../types/handler.types';\nimport { extractDomain } from '../../utils/url-utils';\n\n/**\n * Base handler class with common functionality\n */\nexport abstract class BaseHandler implements DomainHandler {\n abstract getDomain(): string;\n\n abstract extractEpisodes(url: string, cookies?: string): Promise<Episode[]>;\n\n /**\n * Check if handler supports the given URL\n */\n supports(url: string): boolean {\n try {\n const domain = extractDomain(url);\n return domain === this.getDomain() || domain.endsWith(`.${this.getDomain()}`);\n } catch {\n return false;\n }\n }\n\n /**\n * Fetch HTML from URL with optional cookies\n */\n protected async fetchHtml(url: string, cookies?: string): Promise<string> {\n const headers: Record<string, string> = {\n 'User-Agent':\n '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',\n Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',\n 'Accept-Language': 'en-US,en;q=0.9',\n };\n\n if (cookies) {\n headers.Cookie = cookies;\n }\n\n try {\n const response = await fetch(url, { headers });\n\n if (!response.ok) {\n throw new HandlerError(`HTTP ${response.status}: ${response.statusText}`, url);\n }\n\n return await response.text();\n } catch (error) {\n if (error instanceof HandlerError) {\n throw error;\n }\n throw new HandlerError(`Failed to fetch page: ${error instanceof Error ? error.message : String(error)}`, url);\n }\n }\n\n /**\n * Parse cheerio document from HTML\n */\n protected parseHtml(html: string): cheerio.CheerioAPI {\n return cheerio.load(html);\n }\n\n /**\n * Parse episode number from text\n * Handles formats like \"็ฌฌ1้\", \"EP1\", \"Episode 1\", etc.\n */\n protected parseEpisodeNumber(text: string): number | null {\n // Chinese format: ็ฌฌX้\n const chineseMatch = text.match(/็ฌฌ(\\d+)้/);\n if (chineseMatch?.[1]) {\n return parseInt(chineseMatch[1], 10);\n }\n\n // EP prefix: EP1, ep01, etc.\n const epMatch = text.match(/ep\\s?(\\d+)/i);\n if (epMatch?.[1]) {\n return parseInt(epMatch[1], 10);\n }\n\n // Episode prefix: Episode 1, E1, etc.\n const episodeMatch = text.match(/(?:episode|e)\\s?(\\d+)/i);\n if (episodeMatch?.[1]) {\n return parseInt(episodeMatch[1], 10);\n }\n\n // Standalone number\n const numberMatch = text.match(/\\b(\\d+)\\b/);\n if (numberMatch?.[1]) {\n return parseInt(numberMatch[1], 10);\n }\n\n return null;\n }\n\n /**\n * Parse episode type from class names or text\n */\n protected parseEpisodeType(element: AnyNode, $: cheerio.CheerioAPI): EpisodeType {\n const $el = $(element);\n const className = $el.attr('class') || '';\n const text = $el.text().toLowerCase();\n\n // Check for VIP indicators\n if (className.includes('vip') || text.includes('vip') || text.includes('ไผๅ')) {\n return 'vip' as EpisodeType;\n }\n\n // Check for preview indicators\n if (\n className.includes('preview') ||\n className.includes('trailer') ||\n text.includes('preview') ||\n text.includes('้ขๅ')\n ) {\n return 'preview' as EpisodeType;\n }\n\n // Check for locked indicators\n if (\n className.includes('locked') ||\n className.includes('lock') ||\n text.includes('locked') ||\n text.includes('้ๅฎ')\n ) {\n return 'locked' as EpisodeType;\n }\n\n return 'available' as EpisodeType;\n }\n}\n","import type { Episode } from '../../types/episode.types';\nimport { BaseHandler } from '../base/base-handler';\n\n/**\n * Handler for iq.com domain (iQIYI international)\n */\nexport class IQiyiHandler extends BaseHandler {\n getDomain(): string {\n return 'iq.com';\n }\n\n async extractEpisodes(url: string, cookies?: string): Promise<Episode[]> {\n const html = await this.fetchHtml(url, cookies);\n const $ = this.parseHtml(html);\n\n const episodes: Episode[] = [];\n\n // iQIYI typically has episode lists in specific selectors\n // Common patterns:\n // - .album-episode-item\n // - .episode-item\n // - .intl-play-item\n // - Links with data-episode attributes\n\n const selectors = [\n '.album-episode-item',\n '.episode-item',\n '.intl-play-item',\n '[data-episode]',\n 'a[href*=\"/play/\"]',\n ];\n\n for (const selector of selectors) {\n const elements = $(selector);\n\n if (elements.length > 0) {\n elements.each((_, element) => {\n const $el = $(element);\n const link = $el.is('a') ? $el : $el.find('a').first();\n const href = link.attr('href') || '';\n\n if (!href) return;\n\n // Build full URL if relative\n const episodeUrl = href.startsWith('http') ? href : `https://www.iq.com${href}`;\n\n // Parse episode number from text or href\n const text = $el.text();\n const hrefText = href;\n const combinedText = `${text} ${hrefText}`;\n\n const episodeNumber = this.parseEpisodeNumber(combinedText);\n\n if (episodeNumber) {\n // Check if already added\n const exists = episodes.some((ep) => ep.number === episodeNumber);\n if (!exists) {\n episodes.push({\n number: episodeNumber,\n url: episodeUrl,\n type: this.parseEpisodeType(element, $),\n title: $el.attr('title') || link.attr('title') || undefined,\n extractedAt: new Date(),\n });\n }\n }\n });\n\n // If we found episodes with this selector, break\n if (episodes.length > 0) {\n break;\n }\n }\n }\n\n // Sort by episode number\n episodes.sort((a, b) => a.number - b.number);\n\n return episodes;\n }\n}\n","import type { CheerioAPI } from 'cheerio';\nimport type { AnyNode } from 'domhandler';\nimport type { Episode } from '../../types/episode.types';\nimport { EpisodeType } from '../../types/episode.types';\nimport { BaseHandler } from '../base/base-handler';\n\n/**\n * Handler for wetv.vip domain\n */\nexport class WeTVHandler extends BaseHandler {\n getDomain(): string {\n return 'wetv.vip';\n }\n\n async extractEpisodes(url: string, cookies?: string): Promise<Episode[]> {\n const html = await this.fetchHtml(url, cookies);\n const $ = this.parseHtml(html);\n\n const episodes: Episode[] = [];\n\n // WeTV uses play-video__link class for episode links\n // Pattern: a.play-video__link[href*=\"/play/\"][href*=\"EP\"]\n const episodeLinks = $('a.play-video__link[href*=\"/play/\"][href*=\"EP\"]');\n\n if (episodeLinks.length === 0) {\n // Fallback: try generic selector\n const fallbackLinks = $('a[href*=\"/play/\"]').filter((_, el) => {\n const href = $(el).attr('href') || '';\n return href.includes('EP');\n });\n\n fallbackLinks.each((_, element) => {\n this.processEpisodeLink($, element, episodes);\n });\n } else {\n episodeLinks.each((_, element) => {\n this.processEpisodeLink($, element, episodes);\n });\n }\n\n // Sort by episode number\n episodes.sort((a, b) => a.number - b.number);\n\n return episodes;\n }\n\n /**\n * Process a single episode link element\n */\n private processEpisodeLink($: CheerioAPI, element: AnyNode, episodes: Episode[]): void {\n const $el = $(element);\n const href = $el.attr('href');\n\n if (!href) return;\n\n // Build full URL if relative\n const episodeUrl = href.startsWith('http') ? href : `https://wetv.vip${href}`;\n\n // Extract episode number from aria-label (e.g., \"Play episode 01\")\n const ariaLabel = $el.attr('aria-label') || '';\n const episodeNumber = this.parseEpisodeNumber(ariaLabel);\n\n if (!episodeNumber) return;\n\n // Check if already added\n const exists = episodes.some((ep) => ep.number === episodeNumber);\n if (exists) return;\n\n // Determine episode type based on badges\n const type = this.determineEpisodeType($, element);\n\n episodes.push({\n number: episodeNumber,\n url: episodeUrl,\n type,\n title: $el.attr('title') || undefined,\n extractedAt: new Date(),\n });\n }\n\n /**\n * Determine episode type based on badges (VIP, Teaser, Express)\n */\n private determineEpisodeType($: CheerioAPI, element: AnyNode): EpisodeType {\n // Check for badges in parent li or sibling elements\n const $li = $(element).closest('li');\n\n if ($li.length) {\n // Look for span.play-video__label\n const badge = $li.find('span.play-video__label').first();\n\n if (badge.length) {\n const badgeText = badge.text().trim().toLowerCase();\n\n // Check badge types\n if (badgeText === 'vip' || badgeText.includes('vip')) {\n return EpisodeType.VIP;\n }\n if (badgeText === 'teaser' || badgeText.includes('teaser')) {\n return EpisodeType.TEASER;\n }\n if (badgeText === 'express' || badgeText.includes('express')) {\n return EpisodeType.EXPRESS;\n }\n }\n\n // Also check text content for badges\n const liText = $li.text() || '';\n if (liText.includes('VIP') && !liText.includes('Teaser')) {\n return EpisodeType.VIP;\n }\n if (liText.includes('Teaser')) {\n return EpisodeType.TEASER;\n }\n if (liText.includes('Express')) {\n return EpisodeType.EXPRESS;\n }\n }\n\n // Default: available (free episodes)\n return EpisodeType.AVAILABLE;\n }\n}\n","import { logger } from '../utils/logger';\nimport type { Notifier } from './notifier';\nimport { NotificationLevel } from './notifier';\n\n/**\n * Console notifier for terminal output\n */\nexport class ConsoleNotifier implements Notifier {\n private lastProgressLength = 0;\n\n notify(level: NotificationLevel, message: string): void {\n // If there was an active progress line, clear it first so the log appears cleanly\n if (this.lastProgressLength > 0) {\n process.stdout.write(`\\r${' '.repeat(this.lastProgressLength)}\\r`);\n this.lastProgressLength = 0;\n }\n\n switch (level) {\n case NotificationLevel.INFO:\n logger.info(message);\n break;\n case NotificationLevel.SUCCESS:\n logger.success(message);\n break;\n case NotificationLevel.WARNING:\n logger.warning(message);\n break;\n case NotificationLevel.ERROR:\n logger.error(message);\n break;\n case NotificationLevel.HIGHLIGHT:\n logger.highlight(message);\n break;\n }\n }\n\n progress(message: string): void {\n // Clear previous progress by overwriting with spaces\n if (this.lastProgressLength > 0) {\n process.stdout.write(`\\r${' '.repeat(this.lastProgressLength)}\\r`);\n }\n\n // Write new progress message\n process.stdout.write(`\\r${message}`);\n this.lastProgressLength = message.length;\n }\n\n /**\n * Finalize progress (add newline after last progress update)\n */\n endProgress(): void {\n if (this.lastProgressLength > 0) {\n process.stdout.write('\\n');\n this.lastProgressLength = 0;\n }\n }\n}\n","import { NotificationError } from '../errors/custom-errors';\nimport type { Notifier } from './notifier';\nimport { NotificationLevel } from './notifier';\n\n/**\n * Telegram configuration\n */\nexport type TelegramConfig = {\n botToken: string;\n chatId: string;\n};\n\n/**\n * Telegram notifier for error notifications only\n */\nexport class TelegramNotifier implements Notifier {\n private config: TelegramConfig;\n private apiUrl: string;\n\n constructor(config: TelegramConfig) {\n this.config = config;\n this.apiUrl = `https://api.telegram.org/bot${config.botToken}/sendMessage`;\n }\n\n /**\n * Send notification via Telegram\n * Only sends ERROR level notifications\n */\n async notify(level: NotificationLevel, message: string): Promise<void> {\n // Only send error notifications\n if (level !== NotificationLevel.ERROR) {\n return;\n }\n\n try {\n const emoji = this.getEmoji(level);\n const formattedMessage = `${emoji} *wetvlo Error*\\n\\n${message}`;\n\n const response = await fetch(this.apiUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n chat_id: this.config.chatId,\n text: formattedMessage,\n parse_mode: 'Markdown',\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new NotificationError(\n `Failed to send Telegram notification: ${response.status} ${response.statusText}\\n${errorText}`,\n );\n }\n } catch (error) {\n // Don't throw for notification errors, just log them\n console.error('Telegram notification failed:', error);\n }\n }\n\n /**\n * Get emoji for notification level\n */\n private getEmoji(level: NotificationLevel): string {\n switch (level) {\n case NotificationLevel.INFO:\n return 'โน๏ธ';\n case NotificationLevel.SUCCESS:\n return 'โ
';\n case NotificationLevel.WARNING:\n return 'โ ๏ธ';\n case NotificationLevel.ERROR:\n return 'โ';\n case NotificationLevel.HIGHLIGHT:\n return '๐';\n default:\n return '';\n }\n }\n\n /**\n * Progress updates are not sent to Telegram (no-op)\n */\n async progress(_message: string): Promise<void> {\n // Telegram doesn't need real-time progress updates\n }\n\n /**\n * Progress finalization is no-op for Telegram\n */\n async endProgress(): Promise<void> {\n // No-op for Telegram\n }\n}\n","import { extractDomain } from '../utils/url-utils.js';\nimport { DEFAULT_CHECK_SETTINGS, DEFAULT_DOWNLOAD_SETTINGS } from './config-defaults.js';\nimport type { CheckSettings, DomainConfig, DownloadSettings, GlobalConfigs, SeriesConfig } from './config-schema.js';\nimport type { ResolvedCheckSettings, ResolvedDownloadSettings, ResolvedSeriesConfig } from './resolved-config.types.js';\n\n/**\n * Centralized configuration resolver\n *\n * Handles the merging hierarchy:\n * 1. Series Config (Highest Priority)\n * 2. Domain Config\n * 3. Global Config\n * 4. Default Config (Lowest Priority)\n */\nexport class ConfigResolver {\n private domainConfigs: Map<string, DomainConfig>;\n private globalConfigs?: GlobalConfigs;\n\n /**\n * Create a new ConfigResolver\n *\n * @param domainConfigs - List of domain-specific configurations\n * @param globalConfigs - Global configuration defaults\n */\n constructor(domainConfigs: DomainConfig[] = [], globalConfigs?: GlobalConfigs) {\n this.domainConfigs = new Map(domainConfigs.map((c) => [c.domain, c]));\n this.globalConfigs = globalConfigs;\n }\n\n /**\n * Resolve configuration for a specific series\n *\n * Merges settings from all levels of the hierarchy to produce a guaranteed\n * full configuration object with no missing values.\n *\n * @param series - The series configuration\n * @returns Fully resolved configuration\n */\n public resolve(series: SeriesConfig): ResolvedSeriesConfig {\n const domain = extractDomain(series.url);\n const domainConfig = this.domainConfigs.get(domain);\n\n const config = {\n check: this.mergeCheckSettings(series.check, domainConfig?.check),\n download: this.mergeDownloadSettings(series.download, domainConfig?.download),\n };\n\n this.validate(config);\n return config;\n }\n\n /**\n * Merge check settings according to hierarchy\n */\n private mergeCheckSettings(series?: CheckSettings, domain?: CheckSettings): ResolvedCheckSettings {\n const global = this.globalConfigs?.check;\n const defaults = DEFAULT_CHECK_SETTINGS;\n\n return {\n count: series?.count ?? domain?.count ?? global?.count ?? defaults.count,\n checkInterval: series?.checkInterval ?? domain?.checkInterval ?? global?.checkInterval ?? defaults.checkInterval,\n downloadTypes: series?.downloadTypes ?? domain?.downloadTypes ?? global?.downloadTypes ?? defaults.downloadTypes,\n };\n }\n\n /**\n * Merge download settings according to hierarchy\n */\n private mergeDownloadSettings(series?: DownloadSettings, domain?: DownloadSettings): ResolvedDownloadSettings {\n const global = this.globalConfigs?.download;\n const defaults = DEFAULT_DOWNLOAD_SETTINGS;\n\n return {\n downloadDir: series?.downloadDir ?? domain?.downloadDir ?? global?.downloadDir ?? defaults.downloadDir,\n tempDir: series?.tempDir ?? domain?.tempDir ?? global?.tempDir ?? defaults.tempDir,\n downloadDelay: series?.downloadDelay ?? domain?.downloadDelay ?? global?.downloadDelay ?? defaults.downloadDelay,\n maxRetries: series?.maxRetries ?? domain?.maxRetries ?? global?.maxRetries ?? defaults.maxRetries,\n initialTimeout:\n series?.initialTimeout ?? domain?.initialTimeout ?? global?.initialTimeout ?? defaults.initialTimeout,\n backoffMultiplier:\n series?.backoffMultiplier ??\n domain?.backoffMultiplier ??\n global?.backoffMultiplier ??\n defaults.backoffMultiplier,\n jitterPercentage:\n series?.jitterPercentage ?? domain?.jitterPercentage ?? global?.jitterPercentage ?? defaults.jitterPercentage,\n minDuration: series?.minDuration ?? domain?.minDuration ?? global?.minDuration ?? defaults.minDuration,\n };\n }\n\n /**\n * Get resolved domain configuration (without series context)\n * Useful for retries where series config might not be readily available,\n * though prefer using resolved series config when possible.\n */\n public resolveDomain(domain: string): ResolvedSeriesConfig {\n const domainConfig = this.domainConfigs.get(domain);\n\n const config = {\n check: this.mergeCheckSettings(undefined, domainConfig?.check),\n download: this.mergeDownloadSettings(undefined, domainConfig?.download),\n };\n\n this.validate(config);\n return config;\n }\n\n /**\n * Update global configurations\n *\n * @param globalConfigs - New global configuration defaults\n */\n public setGlobalConfigs(globalConfigs: GlobalConfigs): void {\n this.globalConfigs = globalConfigs;\n }\n\n /**\n * Validate resolved configuration\n */\n private validate(config: ResolvedSeriesConfig): void {\n if (config.check.count < 1) throw new Error(`Invalid check count: ${config.check.count}`);\n if (config.check.checkInterval < 0) throw new Error(`Invalid check interval: ${config.check.checkInterval}`);\n if (config.download.downloadDelay < 0) throw new Error(`Invalid download delay: ${config.download.downloadDelay}`);\n if (config.download.maxRetries < 0) throw new Error(`Invalid max retries: ${config.download.maxRetries}`);\n if (config.download.initialTimeout < 0)\n throw new Error(`Invalid initial timeout: ${config.download.initialTimeout}`);\n if (config.download.backoffMultiplier < 1)\n throw new Error(`Invalid backoff multiplier: ${config.download.backoffMultiplier}`);\n if (config.download.minDuration < 0) throw new Error(`Invalid min duration: ${config.download.minDuration}`);\n }\n}\n","/**\n * TypedQueue - Passive queue for storing tasks of a single type\n *\n * A queue that manages tasks of a single type but does NOT execute them.\n * This is a passive data store that:\n * - Stores tasks in FIFO order\n * - Tracks cooldown time (next available timestamp)\n * - Tracks if a task of this type is currently executing\n * - Does NOT auto-start or have a processor function\n *\n * Key differences from AsyncQueue:\n * - No auto-start when items added\n * - No processor function (passive data store)\n * - Tracks cooldown from completion time\n * - No internal timing/sleep calls\n */\n\nexport type TaskItem<TaskType> = {\n data: TaskType;\n addedAt: Date;\n};\n\n/**\n * TypedQueue for a single task type\n */\nexport class TypedQueue<TaskType> {\n // State\n private queue: TaskItem<TaskType>[] = [];\n private isExecuting: boolean = false;\n private nextAvailableAt: Date = new Date(0); // Past = available\n private cooldownMs: number;\n\n /**\n * Create a new TypedQueue\n *\n * @param cooldownMs - Cooldown in milliseconds between task completions\n */\n constructor(cooldownMs: number = 0) {\n this.cooldownMs = cooldownMs;\n }\n\n /**\n * Add a task to the queue\n *\n * @param task - Task to add\n * @param delay - Optional delay in milliseconds before task is available\n */\n add(task: TaskType, delay?: number): void {\n const addedAt = new Date(Date.now() + (delay ?? 0));\n this.queue.push({ data: task, addedAt });\n }\n\n /**\n * Add a task to the front of the queue (priority)\n *\n * @param task - Task to add\n * @param delay - Optional delay in milliseconds before task is available\n */\n addFirst(task: TaskType, delay?: number): void {\n const addedAt = new Date(Date.now() + (delay ?? 0));\n this.queue.unshift({ data: task, addedAt });\n }\n\n /**\n * Get the next task from the queue\n *\n * @returns Next task or null if queue is empty\n */\n getNext(): TaskType | null {\n if (this.queue.length === 0) {\n return null;\n }\n\n const item = this.queue.shift();\n return item?.data ?? null;\n }\n\n /**\n * Peek at the next task without removing it\n *\n * @returns Next task or null if queue is empty\n */\n peekNext(): TaskType | null {\n if (this.queue.length === 0) {\n return null;\n }\n return this.queue[0]?.data ?? null;\n }\n\n /**\n * Check if a task can start at the given time\n *\n * @param now - Current time\n * @returns Whether task can start\n */\n canStart(now: Date): boolean {\n if (this.isExecuting) {\n return false;\n }\n\n if (now < this.nextAvailableAt) {\n return false;\n }\n\n // Check if head task is ready (respect delay)\n const head = this.queue[0];\n if (head && now < head.addedAt) {\n return false;\n }\n\n return true;\n }\n\n /**\n * Mark a task as started\n */\n markStarted(): void {\n this.isExecuting = true;\n }\n\n /**\n * Mark a task as completed and set cooldown\n *\n * @param cooldownMs - Cooldown in milliseconds from now\n */\n markCompleted(cooldownMs: number): void {\n this.isExecuting = false;\n this.cooldownMs = cooldownMs;\n this.nextAvailableAt = new Date(Date.now() + cooldownMs);\n }\n\n /**\n * Mark a task as failed and set cooldown\n *\n * @param cooldownMs - Cooldown in milliseconds from now\n */\n markFailed(cooldownMs: number): void {\n this.isExecuting = false;\n this.cooldownMs = cooldownMs;\n this.nextAvailableAt = new Date(Date.now() + cooldownMs);\n }\n\n /**\n * Check if queue has tasks\n *\n * @returns Whether queue has tasks\n */\n hasTasks(): boolean {\n return this.queue.length > 0;\n }\n\n /**\n * Get the next time this queue can start a task\n *\n * @returns Next available time\n */\n getNextAvailableTime(): Date {\n // Start with cooldown time\n let time = this.nextAvailableAt;\n\n // Check head task delay\n const head = this.queue[0];\n if (head && head.addedAt > time) {\n time = head.addedAt;\n }\n\n return time;\n }\n\n /**\n * Get queue length\n *\n * @returns Number of tasks in queue\n */\n getQueueLength(): number {\n return this.queue.length;\n }\n\n /**\n * Check if a task is currently executing\n *\n * @returns Whether a task is executing\n */\n getIsExecuting(): boolean {\n return this.isExecuting;\n }\n\n /**\n * Get cooldown duration\n *\n * @returns Cooldown in milliseconds\n */\n getCooldownMs(): number {\n return this.cooldownMs;\n }\n\n /**\n * Set cooldown duration\n *\n * @param cooldownMs - New cooldown in milliseconds\n */\n setCooldownMs(cooldownMs: number): void {\n this.cooldownMs = cooldownMs;\n }\n\n /**\n * Clear all tasks from the queue\n */\n clear(): void {\n this.queue = [];\n }\n\n /**\n * Get status information\n *\n * @returns Status object\n */\n getStatus(): {\n queueLength: number;\n isExecuting: boolean;\n nextAvailableAt: Date;\n cooldownMs: number;\n canStartNow: boolean;\n } {\n const now = new Date();\n return {\n queueLength: this.queue.length,\n isExecuting: this.isExecuting,\n nextAvailableAt: this.nextAvailableAt,\n cooldownMs: this.cooldownMs,\n canStartNow: this.canStart(now),\n };\n }\n}\n","/**\n * UniversalScheduler - Central scheduler for all typed queues\n *\n * Coordinates all typed queues with a single executor:\n * - Only one task executing globally\n * - Single active timer (cleared on scheduling attempt)\n * - Fair round-robin queue selection\n * - Event-driven (triggers on task add, completion, timer)\n *\n * Key features:\n * - Centralized scheduling logic\n * - Proper cooldowns (end-to-start timing)\n * - Reusable for any task type\n * - Timer-based instead of polling\n */\n\nimport { TypedQueue } from './typed-queue.js';\n\n/**\n * Executor callback function type\n */\nexport type ExecutorCallback<TaskType> = (task: TaskType, queueName: string) => Promise<void>;\n\n/**\n * Universal scheduler for coordinating all typed queues\n */\nexport class UniversalScheduler<TaskType> {\n // State\n private queues: Map<string, TypedQueue<TaskType>> = new Map();\n private queueCooldowns: Map<string, number> = new Map(); // Store default cooldown per queue\n private executorBusy: boolean = false;\n private timerId: ReturnType<typeof setTimeout> | null = null;\n private roundRobinIndex: number = 0;\n private stopped: boolean = false;\n\n // Callback\n private executor: ExecutorCallback<TaskType>;\n private onWait?: (queueName: string, waitMs: number, nextTime: Date) => void;\n\n /**\n * Create a new UniversalScheduler\n *\n * @param executor - Function to execute a task\n */\n constructor(executor: ExecutorCallback<TaskType>) {\n this.executor = executor;\n }\n\n /**\n * Set callback for when the scheduler is waiting\n *\n * @param callback - Callback function\n */\n setOnWait(callback: (queueName: string, waitMs: number, nextTime: Date) => void): void {\n this.onWait = callback;\n }\n\n /**\n * Register a new queue type\n *\n * @param typeName - Unique name for this queue type\n * @param cooldownMs - Default cooldown in milliseconds\n */\n registerQueue(typeName: string, cooldownMs: number): void {\n if (this.queues.has(typeName)) {\n throw new Error(`Queue ${typeName} is already registered`);\n }\n\n const queue = new TypedQueue<TaskType>(cooldownMs);\n this.queues.set(typeName, queue);\n this.queueCooldowns.set(typeName, cooldownMs);\n }\n\n /**\n * Unregister a queue type\n *\n * @param typeName - Queue type name to unregister\n */\n unregisterQueue(typeName: string): void {\n this.queues.delete(typeName);\n this.queueCooldowns.delete(typeName);\n }\n\n /**\n * Add a task to a specific queue\n *\n * Triggers scheduling attempt.\n *\n * @param typeName - Queue type name\n * @param task - Task to add\n * @param delay - Optional delay in milliseconds before task is available\n */\n addTask(typeName: string, task: TaskType, delay?: number): void {\n const queue = this.queues.get(typeName);\n if (!queue) {\n throw new Error(`Queue ${typeName} is not registered`);\n }\n\n queue.add(task, delay);\n\n // Trigger scheduling attempt (might be executable immediately)\n if (!this.stopped) {\n this.scheduleNext();\n }\n }\n\n /**\n * Add a priority task to the front of a specific queue\n *\n * @param typeName - Queue type name\n * @param task - Task to add\n * @param delay - Optional delay in milliseconds\n */\n addPriorityTask(typeName: string, task: TaskType, delay?: number): void {\n const queue = this.queues.get(typeName);\n if (!queue) {\n throw new Error(`Queue ${typeName} is not registered`);\n }\n\n queue.addFirst(task, delay);\n\n // Trigger scheduling attempt\n if (!this.stopped) {\n this.scheduleNext();\n }\n }\n\n /**\n * Mark a task as complete\n *\n * Called by executor when task completes successfully.\n * Triggers next scheduling attempt.\n *\n * @param typeName - Queue type name\n * @param cooldownMs - Optional cooldown override (uses queue default if not provided)\n */\n markTaskComplete(typeName: string, cooldownMs?: number): void {\n const queue = this.queues.get(typeName);\n if (!queue) {\n throw new Error(`Queue ${typeName} is not registered`);\n }\n\n const actualCooldown = cooldownMs ?? this.queueCooldowns.get(typeName) ?? 0;\n queue.markCompleted(actualCooldown);\n this.executorBusy = false;\n\n // Trigger next scheduling attempt\n if (!this.stopped) {\n this.scheduleNext();\n }\n }\n\n /**\n * Mark a task as failed\n *\n * Called by executor when task fails.\n * Triggers next scheduling attempt.\n *\n * @param typeName - Queue type name\n * @param cooldownMs - Optional cooldown override (uses queue default if not provided)\n */\n markTaskFailed(typeName: string, cooldownMs?: number): void {\n const queue = this.queues.get(typeName);\n if (!queue) {\n throw new Error(`Queue ${typeName} is not registered`);\n }\n\n const actualCooldown = cooldownMs ?? this.queueCooldowns.get(typeName) ?? 0;\n queue.markFailed(actualCooldown);\n this.executorBusy = false;\n\n // Trigger next scheduling attempt\n if (!this.stopped) {\n this.scheduleNext();\n }\n }\n\n /**\n * Schedule the next task\n *\n * Attempts to schedule immediately if possible,\n * otherwise sets a timer for the earliest available time.\n */\n scheduleNext(): void {\n if (this.stopped) {\n return;\n }\n\n // Clear any existing timer\n this.clearTimer();\n\n // Try to schedule immediately\n const scheduled = this.trySchedule();\n\n if (scheduled) {\n // Task scheduled and executor is busy.\n // No need to set timer, completion will trigger next schedule.\n return;\n }\n\n // If executor is busy but nothing new was scheduled (because it was already busy),\n // we also don't need a timer.\n if (this.executorBusy) {\n return;\n }\n\n // No task running and none could be scheduled.\n // Check if we should set a timer for the next available time\n const next = this.getEarliestAvailableTime();\n if (next) {\n const now = Date.now();\n const waitMs = Math.max(0, next.time.getTime() - now);\n this.scheduleTimer(waitMs, next.queueName, next.time);\n }\n }\n\n /**\n * Try to schedule a task now\n *\n * @returns Whether a task was scheduled\n */\n private trySchedule(): boolean {\n // Can't schedule if executor is busy\n if (this.executorBusy) {\n return false;\n }\n\n const now = new Date();\n\n // Collect queue names for round-robin\n const queueNames = Array.from(this.queues.keys());\n if (queueNames.length === 0) {\n return false;\n }\n\n // Try each queue in round-robin order\n for (let i = 0; i < queueNames.length; i++) {\n const index = (this.roundRobinIndex + i) % queueNames.length;\n const queueName = queueNames[index];\n if (!queueName) continue;\n\n const queue = this.queues.get(queueName);\n if (!queue) continue;\n\n // Check if queue has tasks and can start\n if (queue.hasTasks() && queue.canStart(now)) {\n // Get next task\n const task = queue.getNext();\n if (task) {\n // Mark as started\n queue.markStarted();\n this.executorBusy = true;\n this.roundRobinIndex = (index + 1) % queueNames.length;\n\n // Execute task (fire and forget - executor will call back)\n this.executeTask(queueName, task).catch((error) => {\n // Execution failed - mark as failed and continue\n console.error(`[UniversalScheduler] Task execution failed: ${error}`);\n this.markTaskFailed(queueName);\n });\n\n return true;\n }\n }\n }\n\n return false;\n }\n\n /**\n * Execute a task\n *\n * @param queueName - Queue name\n * @param task - Task to execute\n */\n private async executeTask(queueName: string, task: TaskType): Promise<void> {\n await this.executor(task, queueName);\n }\n\n /**\n * Schedule a timer for the next attempt\n *\n * @param waitMs - Milliseconds to wait\n * @param queueName - Name of the queue we are waiting for\n * @param nextTime - Time when the task will be ready\n */\n private scheduleTimer(waitMs: number, queueName: string, nextTime: Date): void {\n this.clearTimer();\n\n // Notify waiting state if callback defined and wait is significant (>1s)\n if (this.onWait && waitMs > 1000) {\n this.onWait(queueName, waitMs, nextTime);\n }\n\n this.timerId = setTimeout(() => {\n this.timerId = null;\n this.scheduleNext();\n }, waitMs);\n }\n\n /**\n * Clear the active timer\n */\n private clearTimer(): void {\n if (this.timerId !== null) {\n clearTimeout(this.timerId);\n this.timerId = null;\n }\n }\n\n /**\n * Get the earliest available time across all queues\n *\n * @returns Earliest available time and queue name, or null if no queues with tasks\n */\n private getEarliestAvailableTime(): { time: Date; queueName: string } | null {\n let result: { time: Date; queueName: string } | null = null;\n\n for (const [name, queue] of this.queues.entries()) {\n // Only consider queues that have tasks\n if (!queue.hasTasks()) {\n continue;\n }\n\n const nextTime = queue.getNextAvailableTime();\n if (result === null || nextTime < result.time) {\n result = { time: nextTime, queueName: name };\n }\n }\n\n return result;\n }\n\n /**\n * Stop the scheduler\n *\n * Clears timers and prevents further scheduling.\n */\n stop(): void {\n this.stopped = true;\n this.clearTimer();\n }\n\n /**\n * Resume the scheduler\n */\n resume(): void {\n this.stopped = false;\n this.scheduleNext();\n }\n\n /**\n * Get statistics for all queues\n *\n * @returns Map of queue name to status\n */\n getStats(): Map<string, { queueLength: number; isExecuting: boolean; nextAvailableAt: Date }> {\n const stats = new Map();\n\n for (const [name, queue] of this.queues.entries()) {\n const status = queue.getStatus();\n stats.set(name, {\n queueLength: status.queueLength,\n isExecuting: status.isExecuting,\n nextAvailableAt: status.nextAvailableAt,\n });\n }\n\n return stats;\n }\n\n /**\n * Check if executor is busy\n *\n * @returns Whether executor is busy\n */\n isExecutorBusy(): boolean {\n return this.executorBusy;\n }\n\n /**\n * Check if there are any pending tasks\n *\n * @returns Whether there are pending tasks\n */\n hasPendingTasks(): boolean {\n for (const queue of this.queues.values()) {\n if (queue.hasTasks()) {\n return true;\n }\n }\n return false;\n }\n\n /**\n * Get total pending tasks across all queues\n *\n * @returns Total pending task count\n */\n getTotalPendingTasks(): number {\n let total = 0;\n for (const queue of this.queues.values()) {\n total += queue.getQueueLength();\n }\n return total;\n }\n}\n","/**\n * QueueManager - Orchestrates check and download queues with UniversalScheduler\n *\n * Manages the queue-based architecture with:\n * - Per-domain check and download queues\n * - Universal scheduler for single-task execution globally\n * - Event-driven scheduling (no polling loops)\n * - Graceful shutdown\n * - Proper end-to-start cooldowns\n */\n\nimport { ConfigResolver } from '../config/config-resolver.js';\nimport type { ResolvedSeriesConfig } from '../config/resolved-config.types.js';\nimport type { DownloadManager } from '../downloader/download-manager.js';\nimport { handlerRegistry } from '../handlers/handler-registry.js';\nimport type { Notifier } from '../notifications/notifier.js';\nimport { NotificationLevel } from '../notifications/notifier.js';\nimport type { StateManager } from '../state/state-manager.js';\nimport type { GlobalConfigs, SeriesConfig } from '../types/config.types.js';\nimport type { Episode, EpisodeType } from '../types/episode.types.js';\nimport { extractDomain } from '../utils/url-utils.js';\nimport type { CheckQueueItem, DomainConfig, DownloadQueueItem } from './types.js';\nimport { UniversalScheduler } from './universal-scheduler.js';\n\n/**\n * Queue Manager for orchestrating all queues with universal scheduler\n */\nexport class QueueManager {\n private stateManager: StateManager;\n private downloadManager: DownloadManager;\n private notifier: Notifier;\n\n // Universal scheduler (handles all check and download queues)\n private scheduler: UniversalScheduler<CheckQueueItem | DownloadQueueItem>;\n\n // Config resolver\n private configResolver: ConfigResolver;\n\n // Running state\n private running = false;\n\n // Domain handlers cache\n private domainHandlers = new Map<string, ReturnType<typeof handlerRegistry.getHandlerOrThrow>>();\n\n /**\n * Create a new QueueManager\n *\n * @param stateManager - State manager instance\n * @param downloadManager - Download manager instance\n * @param notifier - Notifier instance\n * @param _cookieFile - Optional cookie file path (unused, kept for API compatibility)\n * @param domainConfigs - Optional domain configurations\n * @param globalConfigs - Optional global configuration defaults\n */\n constructor(\n stateManager: StateManager,\n downloadManager: DownloadManager,\n notifier: Notifier,\n _cookieFile: string | undefined,\n domainConfigs: DomainConfig[] = [],\n globalConfigs?: GlobalConfigs,\n schedulerFactory?: (\n executor: (task: CheckQueueItem | DownloadQueueItem, queueName: string) => Promise<void>,\n ) => UniversalScheduler<CheckQueueItem | DownloadQueueItem>,\n ) {\n this.stateManager = stateManager;\n this.downloadManager = downloadManager;\n this.notifier = notifier;\n\n // Initialize config resolver\n this.configResolver = new ConfigResolver(domainConfigs, globalConfigs);\n\n // Create universal scheduler with executor callback\n const createScheduler = schedulerFactory || ((executor) => new UniversalScheduler(executor));\n this.scheduler = createScheduler(async (task, queueName) => {\n await this.executeTask(task, queueName);\n });\n\n // Set up wait notification\n this.scheduler.setOnWait((queueName, waitMs) => {\n const seconds = Math.round(waitMs / 1000);\n const parts = queueName.split(':');\n const type = parts[0];\n const domain = parts[1];\n\n if (type === 'download') {\n this.notifier.notify(NotificationLevel.INFO, `[${domain}] Next download in ${seconds}s...`);\n } else if (type === 'check') {\n this.notifier.notify(NotificationLevel.INFO, `[${domain}] Next check in ${seconds}s...`);\n }\n });\n }\n\n /**\n * Add a series to the check queue\n *\n * @param config - Series configuration\n */\n addSeriesCheck(config: SeriesConfig): void {\n const domain = extractDomain(config.url);\n\n // Register queues for this domain if not already registered\n this.registerDomainQueues(domain);\n\n // Add series to check queue with config\n const item: CheckQueueItem = {\n seriesUrl: config.url,\n seriesName: config.name,\n config: config,\n attemptNumber: 1,\n retryCount: 0,\n };\n\n const queueName = `check:${domain}`;\n this.scheduler.addTask(queueName, item);\n\n this.notifier.notify(\n NotificationLevel.INFO,\n `[QueueManager] Added ${config.name} to check queue for domain ${domain}`,\n );\n }\n\n /**\n * Add episodes to the download queue\n *\n * @param seriesUrl - Series URL\n * @param seriesName - Series name\n * @param episodes - Episodes to download\n * @param config - Series configuration (optional)\n */\n addEpisodes(seriesUrl: string, seriesName: string, episodes: Episode[], config?: SeriesConfig): void {\n if (episodes.length === 0) {\n return;\n }\n\n const domain = extractDomain(seriesUrl);\n\n // Register queues for this domain if not already registered\n this.registerDomainQueues(domain);\n\n // Get download delay from resolved config\n const resolvedConfig = this.configResolver.resolveDomain(domain);\n const { downloadDelay } = resolvedConfig.download;\n\n // Add episodes to download queue with staggered delays\n for (let i = 0; i < episodes.length; i++) {\n const episode = episodes[i];\n if (!episode) continue;\n\n const item: DownloadQueueItem = {\n seriesUrl,\n seriesName,\n episode,\n config,\n retryCount: 0,\n };\n\n const queueName = `download:${domain}`;\n // Stagger episodes by downloadDelay\n const delayMs = i * downloadDelay * 1000;\n this.scheduler.addTask(queueName, item, delayMs);\n }\n\n this.notifier.notify(\n NotificationLevel.SUCCESS,\n `[QueueManager] Added ${episodes.length} episodes to download queue for ${seriesName} (domain ${domain})`,\n );\n }\n\n /**\n * Start all queues\n */\n start(): void {\n if (this.running) {\n throw new Error('QueueManager is already running');\n }\n\n this.running = true;\n this.scheduler.resume();\n\n this.notifier.notify(NotificationLevel.INFO, '[QueueManager] Started queue processing');\n }\n\n /**\n * Stop all queues gracefully\n *\n * Waits for current task to complete.\n */\n async stop(): Promise<void> {\n if (!this.running) {\n return;\n }\n\n this.notifier.notify(NotificationLevel.INFO, '[QueueManager] Stopping queue processing...');\n\n this.scheduler.stop();\n this.running = false;\n\n this.notifier.notify(NotificationLevel.INFO, '[QueueManager] Queue processing stopped');\n }\n\n /**\n * Check if there is active processing or pending tasks\n *\n * @returns Whether scheduler is actively processing or has pending tasks\n */\n hasActiveProcessing(): boolean {\n return this.scheduler.isExecutorBusy() || this.scheduler.hasPendingTasks();\n }\n\n /**\n * Get queue statistics\n *\n * @returns Object with queue statistics\n */\n getQueueStats(): {\n checkQueues: Record<string, { length: number; processing: boolean }>;\n downloadQueues: Record<string, { length: number; processing: boolean }>;\n } {\n const stats = this.scheduler.getStats();\n const checkQueues: Record<string, { length: number; processing: boolean }> = {};\n const downloadQueues: Record<string, { length: number; processing: boolean }> = {};\n\n for (const [queueName, queueStats] of stats.entries()) {\n if (queueName.startsWith('check:')) {\n const domain = queueName.slice(6); // Remove \"check:\" prefix\n checkQueues[domain] = {\n length: queueStats.queueLength,\n processing: queueStats.isExecuting,\n };\n } else if (queueName.startsWith('download:')) {\n const domain = queueName.slice(9); // Remove \"download:\" prefix\n downloadQueues[domain] = {\n length: queueStats.queueLength,\n processing: queueStats.isExecuting,\n };\n }\n }\n\n return { checkQueues, downloadQueues };\n }\n\n /**\n * Register queues for a domain if not already registered\n *\n * @param domain - Domain name\n */\n private registerDomainQueues(domain: string): void {\n const checkQueueName = `check:${domain}`;\n const downloadQueueName = `download:${domain}`;\n\n // Check if queues are already registered\n const existingStats = this.scheduler.getStats();\n if (existingStats.has(checkQueueName) && existingStats.has(downloadQueueName)) {\n return;\n }\n\n // Resolve configuration\n const resolvedConfig = this.configResolver.resolveDomain(domain);\n\n // Get handler for this domain\n const handler = handlerRegistry.getHandlerOrThrow(`https://${domain}/`);\n this.domainHandlers.set(domain, handler);\n\n // Get cooldowns\n const { checkInterval } = resolvedConfig.check;\n const { downloadDelay } = resolvedConfig.download;\n\n // Register queues with scheduler\n this.scheduler.registerQueue(checkQueueName, checkInterval * 1000); // Convert to ms\n this.scheduler.registerQueue(downloadQueueName, downloadDelay * 1000); // Convert to ms\n }\n\n /**\n * Execute a task from the scheduler\n *\n * This is the executor callback that handles both check and download tasks.\n *\n * @param task - Task to execute\n * @param queueName - Queue name (format: \"check:domain\" or \"download:domain\")\n */\n private async executeTask(task: CheckQueueItem | DownloadQueueItem, queueName: string): Promise<void> {\n const parts = queueName.split(':');\n const type = parts[0];\n const domain = parts[1];\n\n if (!type || !domain) {\n throw new Error(`Invalid queue name format: ${queueName}`);\n }\n\n if (type === 'check') {\n await this.executeCheck(task as CheckQueueItem, domain, queueName);\n } else if (type === 'download') {\n await this.executeDownload(task as DownloadQueueItem, domain, queueName);\n } else {\n throw new Error(`Unknown queue type: ${type}`);\n }\n }\n\n /**\n * Execute a check task\n *\n * @param item - Check queue item\n * @param domain - Domain name\n * @param queueName - Queue name for scheduler callbacks\n */\n private async executeCheck(item: CheckQueueItem, domain: string, queueName: string): Promise<void> {\n const { seriesUrl, seriesName, config, attemptNumber, retryCount = 0 } = item;\n\n // Get handler for this domain\n const handler = this.domainHandlers.get(domain);\n if (!handler) {\n throw new Error(`No handler found for domain ${domain}`);\n }\n\n // Get settings\n const resolvedConfig = this.configResolver.resolve(config);\n const { count: checksCount, checkInterval } = resolvedConfig.check;\n\n try {\n // Perform the check\n const result = await this.performCheck(handler, seriesUrl, seriesName, resolvedConfig, attemptNumber, domain);\n\n if (result.hasNewEpisodes) {\n // Episodes found - send to download queue, do NOT requeue\n this.notifier.notify(\n NotificationLevel.SUCCESS,\n `[${domain}] Found ${result.episodes.length} new episodes for ${seriesName} (attempt ${attemptNumber}/${checksCount})`,\n );\n\n // Add episodes to download queue\n this.addEpisodes(seriesUrl, seriesName, result.episodes, config);\n\n // Session complete - do not requeue\n this.scheduler.markTaskComplete(queueName, checkInterval * 1000);\n } else {\n // No episodes found - check if we should requeue\n if (attemptNumber < checksCount) {\n // Requeue with interval delay\n const intervalMs = checkInterval * 1000;\n const requeueDelay = result.requeueDelay ?? intervalMs;\n\n this.notifier.notify(\n NotificationLevel.INFO,\n `[${domain}] No new episodes for ${seriesName} (attempt ${attemptNumber}/${checksCount}), requeueing in ${Math.round(requeueDelay / 1000)}s`,\n );\n\n // Requeue with incremented attempt number\n const requeuedItem: CheckQueueItem = {\n ...item,\n attemptNumber: attemptNumber + 1,\n retryCount: 0,\n };\n\n this.scheduler.addTask(queueName, requeuedItem, requeueDelay);\n this.scheduler.markTaskComplete(queueName, checkInterval * 1000);\n } else {\n // Checks exhausted - do not requeue\n this.notifier.notify(\n NotificationLevel.INFO,\n `[${domain}] Checks exhausted for ${seriesName} (${checksCount} attempts with no new episodes)`,\n );\n this.scheduler.markTaskComplete(queueName, checkInterval * 1000);\n }\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n\n // Get download settings for retry config\n const { maxRetries, initialTimeout, backoffMultiplier, jitterPercentage } = resolvedConfig.download;\n\n if (retryCount < maxRetries) {\n // Retry with exponential backoff (convert seconds to ms)\n const retryDelay = this.calculateBackoff(\n retryCount,\n initialTimeout * 1000,\n backoffMultiplier,\n jitterPercentage,\n );\n\n this.notifier.notify(\n NotificationLevel.WARNING,\n `[${domain}] Check failed for ${seriesName}, retrying in ${Math.round(retryDelay / 1000)}s (attempt ${retryCount + 1}/${maxRetries})`,\n );\n\n // Requeue with incremented retry count (same attempt number)\n const requeuedItem: CheckQueueItem = {\n ...item,\n retryCount: retryCount + 1,\n };\n\n this.scheduler.addPriorityTask(queueName, requeuedItem, retryDelay);\n this.scheduler.markTaskComplete(queueName, checkInterval * 1000);\n } else {\n // Max retries exceeded - log error and give up\n this.notifier.notify(\n NotificationLevel.ERROR,\n `[${domain}] Failed to check ${seriesName} after ${retryCount} retry attempts: ${errorMessage}`,\n );\n this.scheduler.markTaskComplete(queueName, checkInterval * 1000);\n }\n }\n }\n\n /**\n * Execute a download task\n *\n * @param item - Download queue item\n * @param domain - Domain name\n * @param queueName - Queue name for scheduler callbacks\n */\n private async executeDownload(item: DownloadQueueItem, domain: string, queueName: string): Promise<void> {\n const { seriesUrl, seriesName, episode, config, retryCount = 0 } = item;\n\n // Resolve config\n let resolvedConfig: ResolvedSeriesConfig;\n if (config) {\n resolvedConfig = this.configResolver.resolve(config);\n } else {\n resolvedConfig = this.configResolver.resolveDomain(domain);\n }\n\n const { downloadDelay, minDuration } = resolvedConfig.download;\n\n try {\n // Attempt download\n await this.downloadManager.download(seriesUrl, seriesName, episode, minDuration);\n\n // Success - log and continue\n this.notifier.notify(\n NotificationLevel.SUCCESS,\n `[${domain}] Successfully queued download of Episode ${episode.number} for ${seriesName}`,\n );\n\n this.scheduler.markTaskComplete(queueName, downloadDelay * 1000);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n\n // Check if we should retry\n const { maxRetries, initialTimeout, backoffMultiplier, jitterPercentage } = resolvedConfig.download;\n\n if (retryCount < maxRetries) {\n // Retry with backoff\n const retryDelay = this.calculateBackoff(\n retryCount,\n initialTimeout * 1000, // convert seconds to ms\n backoffMultiplier,\n jitterPercentage,\n );\n\n this.notifier.notify(\n NotificationLevel.WARNING,\n `[${domain}] Download failed for Episode ${episode.number}, retrying in ${Math.round(retryDelay / 1000)}s (attempt ${retryCount + 1}/${maxRetries})`,\n );\n\n // Requeue with incremented retry count\n const requeuedItem: DownloadQueueItem = {\n ...item,\n retryCount: retryCount + 1,\n };\n\n this.scheduler.addPriorityTask(queueName, requeuedItem, retryDelay);\n this.scheduler.markTaskComplete(queueName, downloadDelay * 1000);\n } else {\n // Max retries exceeded - log error and give up\n this.notifier.notify(\n NotificationLevel.ERROR,\n `[${domain}] Failed to download Episode ${episode.number} after ${retryCount + 1} attempts: ${errorMessage}`,\n );\n this.scheduler.markTaskComplete(queueName, downloadDelay * 1000);\n }\n }\n }\n\n /**\n * Perform the actual check for new episodes\n *\n * @param handler - Domain handler\n * @param seriesUrl - Series URL\n * @param _seriesName - Series name\n * @param config - Series configuration\n * @param attemptNumber - Current attempt number\n * @param domain - Domain name\n * @returns Check result\n */\n private async performCheck(\n handler: ReturnType<typeof handlerRegistry.getHandlerOrThrow>,\n seriesUrl: string,\n _seriesName: string,\n config: ResolvedSeriesConfig,\n attemptNumber: number,\n domain: string,\n ): Promise<{ hasNewEpisodes: boolean; episodes: Episode[]; requeueDelay?: number }> {\n const checksCount = config.check.count;\n\n this.notifier.notify(\n NotificationLevel.INFO,\n `[${domain}] Checking ${seriesUrl} for new episodes... (attempt ${attemptNumber}/${checksCount})`,\n );\n\n // Extract episodes from the series page\n const episodes = await handler.extractEpisodes(seriesUrl);\n\n this.notifier.notify(NotificationLevel.INFO, `[${domain}] Found ${episodes.length} total episodes on ${seriesUrl}`);\n\n // Get download types from config\n const downloadTypes = config.check.downloadTypes;\n\n // Filter for episodes matching download types and not yet downloaded\n const newEpisodes = episodes.filter((ep) => {\n const shouldDownload = downloadTypes.includes(ep.type as EpisodeType);\n const notDownloaded = !this.stateManager.isDownloaded(seriesUrl, ep.number);\n return shouldDownload && notDownloaded;\n });\n\n if (newEpisodes.length > 0) {\n return {\n hasNewEpisodes: true,\n episodes: newEpisodes,\n };\n }\n\n // No new episodes\n return {\n hasNewEpisodes: false,\n episodes: [],\n shouldRequeue: true,\n } as { hasNewEpisodes: false; episodes: Episode[]; requeueDelay?: number };\n }\n\n /**\n * Calculate exponential backoff with jitter\n *\n * @param retryCount - Current retry count\n * @param initialTimeout - Initial timeout in ms\n * @param backoffMultiplier - Multiplier for exponential backoff\n * @param jitterPercentage - Percentage of jitter (0-100)\n * @returns Delay in milliseconds\n */\n private calculateBackoff(\n retryCount: number,\n initialTimeout: number,\n backoffMultiplier: number,\n jitterPercentage: number,\n ): number {\n // Calculate base delay with exponential backoff\n const baseDelay = initialTimeout * backoffMultiplier ** retryCount;\n\n // Calculate jitter amount\n const jitterAmount = (baseDelay * jitterPercentage) / 100;\n\n // Generate random jitter within ยฑjitterAmount\n const jitter = (Math.random() * 2 - 1) * jitterAmount;\n\n // Calculate final delay (ensure non-negative)\n const finalDelay = Math.max(0, baseDelay + jitter);\n\n return Math.floor(finalDelay);\n }\n\n /**\n * Set global configs (for dependency injection)\n */\n setGlobalConfigs(globalConfigs: GlobalConfigs): void {\n this.configResolver.setGlobalConfigs(globalConfigs);\n }\n}\n","/**\n * Parse time string in HH:MM format\n *\n * @param timeStr - Time string in HH:MM format (e.g., \"20:00\")\n * @returns Date object with today's date and the specified time\n */\nexport function parseTime(timeStr: string): Date {\n const match = timeStr.match(/^(\\d{1,2}):(\\d{2})$/);\n if (!match) {\n throw new Error(`Invalid time format: \"${timeStr}\". Expected HH:MM`);\n }\n\n const [, hoursStr, minutesStr] = match;\n const hours = parseInt(hoursStr || '0', 10);\n const minutes = parseInt(minutesStr || '0', 10);\n\n if (hours < 0 || hours > 23) {\n throw new Error(`Invalid hours: ${hours}. Must be between 0 and 23`);\n }\n\n if (minutes < 0 || minutes > 59) {\n throw new Error(`Invalid minutes: ${minutes}. Must be between 0 and 59`);\n }\n\n const date = new Date();\n date.setHours(hours, minutes, 0, 0);\n return date;\n}\n\n/**\n * Get milliseconds until the next occurrence of a time\n *\n * @param timeStr - Time string in HH:MM format\n * @returns Milliseconds until the next occurrence\n */\nexport function getMsUntilTime(timeStr: string): number {\n const targetTime = parseTime(timeStr);\n const now = new Date();\n\n const targetDate = new Date(now);\n targetDate.setHours(targetTime.getHours(), targetTime.getMinutes(), 0, 0);\n\n // If the time has already passed today, schedule for tomorrow\n if (targetDate <= now) {\n targetDate.setDate(targetDate.getDate() + 1);\n }\n\n return targetDate.getTime() - now.getTime();\n}\n\n/**\n * Format duration in human-readable format\n *\n * @param ms - Duration in milliseconds\n * @returns Formatted duration string\n */\nexport function formatDuration(ms: number): string {\n const seconds = Math.floor(ms / 1000);\n const minutes = Math.floor(seconds / 60);\n const hours = Math.floor(minutes / 60);\n const days = Math.floor(hours / 24);\n\n if (days > 0) {\n return `${days}d ${hours % 24}h`;\n }\n if (hours > 0) {\n return `${hours}h ${minutes % 60}m`;\n }\n if (minutes > 0) {\n return `${minutes}m ${seconds % 60}s`;\n }\n return `${seconds}s`;\n}\n\n/**\n * Sleep for specified milliseconds\n *\n * @param ms - Milliseconds to sleep\n * @returns Promise that resolves after the sleep\n */\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * Scheduler - Queue-based architecture for managing series checks\n *\n * Features:\n * - Per-domain sequential processing (concatMap semantics)\n * - Domain-based parallelism\n * - Retry with exponential backoff\n * - \"No episodes\" requeue with interval\n * - Graceful shutdown\n */\n\nimport type { DownloadManager } from '../downloader/download-manager.js';\nimport { SchedulerError } from '../errors/custom-errors.js';\nimport type { Notifier } from '../notifications/notifier.js';\nimport { NotificationLevel } from '../notifications/notifier.js';\nimport { QueueManager } from '../queue/queue-manager.js';\nimport type { StateManager } from '../state/state-manager.js';\nimport type { DomainConfig, GlobalConfigs, SchedulerOptions, SeriesConfig } from '../types/config.types.js';\nimport { getMsUntilTime, sleep } from '../utils/time-utils.js';\n\n/**\n * Time provider type for dependency injection\n */\nexport type TimeProvider = {\n getMsUntilTime: typeof getMsUntilTime;\n sleep: typeof sleep;\n};\n\n/**\n * QueueManager factory type for dependency injection\n */\nexport type QueueManagerFactory = (\n stateManager: StateManager,\n downloadManager: DownloadManager,\n notifier: Notifier,\n cookies: string | undefined,\n domainConfigs: DomainConfig[] | undefined,\n globalConfigs: GlobalConfigs | undefined,\n) => QueueManager;\n\n/**\n * Scheduler for managing periodic checks with queue-based architecture\n */\nexport class Scheduler {\n private configs: SeriesConfig[];\n private stateManager: StateManager;\n private downloadManager: DownloadManager;\n private notifier: Notifier;\n private cookies?: string;\n private options: SchedulerOptions;\n private queueManager: QueueManager;\n private running: boolean = false;\n private stopped: boolean = true;\n private globalConfigs?: GlobalConfigs;\n private domainConfigs?: DomainConfig[];\n private timeProvider: TimeProvider;\n\n constructor(\n configs: SeriesConfig[],\n stateManager: StateManager,\n downloadManager: DownloadManager,\n notifier: Notifier,\n cookies?: string,\n options: SchedulerOptions = { mode: 'scheduled' },\n globalConfigs?: GlobalConfigs,\n domainConfigs?: DomainConfig[],\n timeProvider?: TimeProvider,\n queueManagerFactory?: QueueManagerFactory,\n ) {\n this.configs = configs;\n this.stateManager = stateManager;\n this.downloadManager = downloadManager;\n this.notifier = notifier;\n this.cookies = cookies;\n this.options = options;\n this.globalConfigs = globalConfigs;\n this.domainConfigs = domainConfigs;\n this.timeProvider = timeProvider || { getMsUntilTime, sleep };\n\n // Create queue manager\n const createQueueManager =\n queueManagerFactory ||\n ((sm, dm, notif, cook, dConf, gConf) => new QueueManager(sm, dm, notif, cook, dConf, gConf));\n\n this.queueManager = createQueueManager(\n this.stateManager,\n this.downloadManager,\n this.notifier,\n this.cookies,\n this.domainConfigs,\n this.globalConfigs,\n );\n }\n\n /**\n * Start the scheduler\n */\n async start(): Promise<void> {\n if (this.running) {\n throw new SchedulerError('Scheduler is already running');\n }\n\n this.running = true;\n this.stopped = false;\n\n // Start queue manager\n this.queueManager.start();\n\n if (this.options.mode === 'once') {\n this.notifier.notify(NotificationLevel.INFO, 'Single-run mode: checking all series once');\n await this.runOnce();\n } else {\n this.notifier.notify(NotificationLevel.INFO, 'Scheduler started (queue-based architecture)');\n // Group configs by start time\n const groupedConfigs = this.groupConfigsByStartTime();\n\n while (!this.stopped) {\n // Find the next start time\n let nextTime: string | null = null;\n let minMsUntil = Number.MAX_SAFE_INTEGER;\n\n for (const startTime of groupedConfigs.keys()) {\n const msUntil = this.timeProvider.getMsUntilTime(startTime);\n if (msUntil < minMsUntil) {\n minMsUntil = msUntil;\n nextTime = startTime;\n }\n }\n\n if (!nextTime) {\n this.notifier.notify(NotificationLevel.WARNING, 'No scheduled configs found. Exiting loop.');\n break;\n }\n\n const configs = groupedConfigs.get(nextTime);\n if (!configs) break;\n\n // Wait until start time\n if (minMsUntil > 0) {\n this.notifier.notify(\n NotificationLevel.INFO,\n `Waiting ${Math.floor(minMsUntil / 1000 / 60)} minutes until ${nextTime}...`,\n );\n // biome-ignore lint/performance/noAwaitInLoops: Sequential waiting is intentional\n await this.timeProvider.sleep(minMsUntil);\n }\n\n if (this.stopped) break;\n\n // Add all configs to queue manager\n await this.runConfigs(configs);\n\n // Wait for queues to drain (optional - can remove if not needed)\n while (this.queueManager.hasActiveProcessing()) {\n if (this.stopped) break;\n // biome-ignore lint/performance/noAwaitInLoops: Sequential polling is intentional\n await this.timeProvider.sleep(1000);\n }\n }\n }\n\n this.running = false;\n }\n\n /**\n * Stop the scheduler\n */\n async stop(): Promise<void> {\n this.notifier.notify(NotificationLevel.INFO, 'Stopping scheduler...');\n\n this.stopped = true;\n\n // Stop queue manager (drains all queues)\n await this.queueManager.stop();\n\n // Save state\n await this.stateManager.save();\n\n this.running = false;\n\n this.notifier.notify(NotificationLevel.INFO, 'Scheduler stopped');\n }\n\n /**\n * Group configs by start time\n */\n private groupConfigsByStartTime(): Map<string, SeriesConfig[]> {\n const grouped = new Map<string, SeriesConfig[]>();\n\n for (const config of this.configs) {\n const existing = grouped.get(config.startTime) || [];\n existing.push(config);\n grouped.set(config.startTime, existing);\n }\n\n return grouped;\n }\n\n /**\n * Add all configs to queue manager\n */\n private async runConfigs(configs: SeriesConfig[]): Promise<void> {\n // Add all series to the queue manager\n for (const config of configs) {\n if (this.stopped) break;\n\n this.queueManager.addSeriesCheck(config);\n }\n\n // Log queue stats\n const stats = this.queueManager.getQueueStats();\n this.notifier.notify(\n NotificationLevel.INFO,\n `Added ${configs.length} series to check queues. Queue stats: ${JSON.stringify(stats)}`,\n );\n }\n\n /**\n * Run all configs in single-run mode\n */\n private async runOnce(): Promise<void> {\n for (const config of this.configs) {\n if (this.stopped) break;\n\n this.queueManager.addSeriesCheck(config);\n }\n\n // Wait for all queues to drain\n while (this.queueManager.hasActiveProcessing()) {\n if (this.stopped) break;\n // biome-ignore lint/performance/noAwaitInLoops: Sequential polling is intentional\n await this.timeProvider.sleep(1000);\n }\n\n // Save state after all checks\n await this.stateManager.save();\n this.notifier.notify(NotificationLevel.SUCCESS, 'Single-run complete');\n }\n\n /**\n * Check if scheduler is running\n */\n isRunning(): boolean {\n return this.running && !this.stopped;\n }\n\n /**\n * Get queue manager (for testing/debugging)\n */\n getQueueManager(): QueueManager {\n return this.queueManager;\n }\n}\n","import { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { StateError } from '../errors/custom-errors';\nimport type { SeriesEpisode, State } from '../types/state.types';\nimport { createEmptyState } from '../types/state.types';\n\n/**\n * State manager class for tracking downloaded episodes (v2.0.0 - series-grouped)\n */\nexport class StateManager {\n private state: State;\n private statePath: string;\n private dirty: boolean = false;\n\n constructor(statePath: string) {\n this.statePath = join(process.cwd(), statePath);\n this.state = createEmptyState();\n }\n\n /**\n * Load state from file\n */\n async load(): Promise<void> {\n if (!existsSync(this.statePath)) {\n this.state = createEmptyState();\n this.dirty = true;\n await this.save();\n return;\n }\n\n try {\n const file = Bun.file(this.statePath);\n const content = await file.text();\n this.state = JSON.parse(content) as State;\n this.dirty = false;\n } catch (error) {\n throw new StateError(`Failed to load state file: ${error instanceof Error ? error.message : String(error)}`);\n }\n }\n\n /**\n * Save state to file\n */\n async save(): Promise<void> {\n if (!this.dirty) return;\n\n try {\n this.state.lastUpdated = new Date().toISOString();\n const content = JSON.stringify(this.state, null, 2);\n await Bun.write(this.statePath, content);\n this.dirty = false;\n } catch (error) {\n throw new StateError(`Failed to save state file: ${error instanceof Error ? error.message : String(error)}`);\n }\n }\n\n /**\n * Check if an episode has been downloaded\n */\n isDownloaded(seriesUrl: string, episodeNumber: number): boolean {\n const series = this.state.series[seriesUrl];\n if (!series) return false;\n\n const paddedNumber = String(episodeNumber).padStart(2, '0');\n return !!series.episodes[paddedNumber];\n }\n\n /**\n * Add a downloaded episode to state\n */\n addDownloadedEpisode(\n seriesUrl: string,\n seriesName: string,\n episode: {\n number: number;\n url: string;\n filename: string;\n size: number;\n },\n ): void {\n // Get or create series entry\n if (!this.state.series[seriesUrl]) {\n this.state.series[seriesUrl] = {\n name: seriesName,\n episodes: {},\n };\n }\n\n const paddedNumber = String(episode.number).padStart(2, '0');\n\n // Skip if already exists\n if (this.state.series[seriesUrl].episodes[paddedNumber]) {\n return;\n }\n\n this.state.series[seriesUrl].episodes[paddedNumber] = {\n url: episode.url,\n filename: episode.filename,\n downloadedAt: new Date().toISOString(),\n size: episode.size,\n };\n\n this.dirty = true;\n }\n\n /**\n * Get all episodes for a series\n */\n getSeriesEpisodes(seriesUrl: string): Record<string, SeriesEpisode> {\n const series = this.state.series[seriesUrl];\n return series?.episodes ?? {};\n }\n\n /**\n * Delete a series from state (for finished shows)\n */\n deleteSeries(seriesUrl: string): void {\n if (this.state.series[seriesUrl]) {\n delete this.state.series[seriesUrl];\n this.dirty = true;\n }\n }\n\n /**\n * Get all series URLs\n */\n getAllSeriesUrls(): string[] {\n return Object.keys(this.state.series);\n }\n\n /**\n * Get series name\n */\n getSeriesName(seriesUrl: string): string | null {\n return this.state.series[seriesUrl]?.name ?? null;\n }\n\n /**\n * Get total downloaded episodes count\n */\n getDownloadedCount(): number {\n let count = 0;\n for (const series of Object.values(this.state.series)) {\n count += Object.keys(series.episodes).length;\n }\n return count;\n }\n\n /**\n * Clear all downloaded episodes (for testing/debugging)\n */\n clearAll(): void {\n this.state = createEmptyState();\n this.dirty = true;\n }\n\n /**\n * Force save regardless of dirty flag\n */\n async forceSave(): Promise<void> {\n this.dirty = true;\n await this.save();\n }\n}\n","/**\n * Episode data in series-grouped state\n */\nexport type SeriesEpisode = {\n /** Episode URL */\n url: string;\n /** Downloaded filename */\n filename: string;\n /** Download timestamp */\n downloadedAt: string;\n /** File size in bytes (for verification) */\n size: number;\n};\n\n/**\n * Series data in state\n */\nexport type SeriesData = {\n /** Series name */\n name: string;\n /** Episodes keyed by padded number (e.g., \"01\", \"02\") */\n episodes: Record<string, SeriesEpisode>;\n};\n\n/**\n * State file structure (v2.0.0)\n */\nexport type State = {\n /** State format version */\n version: string;\n /** Series keyed by URL */\n series: Record<string, SeriesData>;\n /** Last update timestamp */\n lastUpdated: string;\n};\n\n/**\n * Create a new empty state (v2.0.0)\n */\nexport function createEmptyState(): State {\n return {\n version: '2.0.0',\n series: {},\n lastUpdated: new Date().toISOString(),\n };\n}\n","import { existsSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { CookieError } from '../errors/custom-errors';\n\n/**\n * Get browser cookie database path\n */\nfunction getBrowserPath(browser: string): string {\n const home = homedir();\n const platform = process.platform;\n\n const paths: Record<string, Record<string, string>> = {\n darwin: {\n chrome: join(home, 'Library/Application Support/Google/Chrome/Default/Cookies'),\n chromium: join(home, 'Library/Application Support/Chromium/Default/Cookies'),\n edge: join(home, 'Library/Application Support/Microsoft Edge/Default/Cookies'),\n firefox: join(home, 'Library/Application Support/Firefox/Profiles'),\n safari: join(home, 'Library/Cookies/Cookies.binarycookies'),\n },\n linux: {\n chrome: join(home, '.config/google-chrome/Default/Cookies'),\n chromium: join(home, '.config/chromium/Default/Cookies'),\n edge: join(home, '.config/microsoft-edge/Default/Cookies'),\n firefox: join(home, '.mozilla/firefox'),\n safari: '', // Safari not on Linux\n },\n win32: {\n chrome: join(home, 'AppData/Local/Google/Chrome/User Data/Default/Cookies'),\n chromium: join(home, 'AppData/Local/Chromium/User Data/Default/Cookies'),\n edge: join(home, 'AppData/Local/Microsoft/Edge/User Data/Default/Cookies'),\n firefox: join(home, 'AppData/Roaming/Mozilla/Firefox/Profiles'),\n safari: '', // Safari not on Windows\n },\n };\n\n const browserPaths = paths[platform];\n if (!browserPaths) {\n throw new CookieError(`Unsupported platform: ${platform}`);\n }\n\n const path = browserPaths[browser];\n if (!path) {\n throw new CookieError(`Browser \"${browser}\" not supported on ${platform}`);\n }\n\n return path;\n}\n\n/**\n * Extract cookies from browser for a specific domain\n * This is a simplified version - in production, you'd use a proper SQLite parser\n * or a library like `tough-cookie-file-store`\n *\n * @param domain - Domain to extract cookies for (e.g., \"wetv.vip\")\n * @param browser - Browser to extract from\n * @returns Cookie string in Netscape format\n */\nexport async function extractCookies(domain: string, browser: string = 'chrome'): Promise<string> {\n const cookiePath = getBrowserPath(browser);\n\n if (!existsSync(cookiePath)) {\n throw new CookieError(\n `Cookie database not found at \"${cookiePath}\". ` +\n `Make sure ${browser} is installed and you've logged in to the site.`,\n );\n }\n\n // For now, we'll use a simpler approach: tell the user to export cookies manually\n // In production, you'd use a proper SQLite parser here\n throw new CookieError(\n `Automatic cookie extraction is not yet implemented for ${browser}. ` +\n `Please export cookies manually:\\n` +\n `1. Install a browser extension like \"Get cookies.txt LOCALLY\"\\n` +\n `2. Go to ${domain} and log in\\n` +\n `3. Export cookies to a file\\n` +\n `4. Set 'cookieFile' in config.yaml to the exported file path`,\n );\n}\n\n/**\n * Read cookies from a Netscape-format cookie file\n *\n * @param cookieFile - Path to cookie file\n * @returns Cookie string for HTTP requests\n */\nexport async function readCookieFile(cookieFile: string): Promise<string> {\n if (!existsSync(cookieFile)) {\n throw new CookieError(`Cookie file not found: \"${cookieFile}\"`);\n }\n\n const content = await readFile(cookieFile, 'utf-8');\n\n // Parse Netscape cookie format and convert to Cookie header format\n const lines = content.split('\\n');\n const cookies: string[] = [];\n\n for (const line of lines) {\n // Skip comments and empty lines\n const trimmedLine = line.trim();\n if (trimmedLine.startsWith('#') || !trimmedLine) continue;\n\n const fields = line.split('\\t');\n if (fields.length >= 7) {\n const name = fields[5];\n const value = fields[6];\n\n if (name && value) {\n const cleanValue = value.trim();\n if (cleanValue) {\n cookies.push(`${name}=${cleanValue}`);\n }\n }\n }\n }\n\n return cookies.join('; ');\n}\n"],"mappings":";AAAA,OAAS,OAAAA,OAAW,SCApB,OAAS,WAAAC,GAAS,WAAAC,GAAS,QAAAC,GAAM,UAAAC,GAAQ,UAAAC,OAAc,SCahD,IAAMC,GAAkD,CAC7D,MAAO,EACP,cAAe,IACf,cAAe,CAAC,WAAW,CAC7B,EAKaC,GAAwD,CACnE,YAAa,cACb,QAAS,cACT,cAAe,GACf,WAAY,EACZ,eAAgB,EAChB,kBAAmB,EACnB,iBAAkB,GAClB,YAAa,CACf,EAKaC,GAA6C,kBAAuC,EAKpFC,GAAuB,cCzCpC,OAAS,cAAAC,OAAkB,KAC3B,OAAS,QAAAC,OAAY,OACrB,UAAYC,OAAU,UCCf,IAAMC,EAAN,cAA0B,KAAM,CACrC,YAAYC,EAAiB,CAC3B,MAAMA,CAAO,EACb,KAAK,KAAO,aACd,CACF,EAKaC,EAAN,cAA0BF,CAAY,CAC3C,YAAYC,EAAiB,CAC3B,MAAMA,CAAO,EACb,KAAK,KAAO,aACd,CACF,EAKaE,EAAN,cAAyBH,CAAY,CAC1C,YAAYC,EAAiB,CAC3B,MAAMA,CAAO,EACb,KAAK,KAAO,YACd,CACF,EAKaG,EAAN,cAA2BJ,CAAY,CAC5C,YACEC,EACgBI,EAChB,CACA,MAAMJ,CAAO,EAFG,SAAAI,EAGhB,KAAK,KAAO,cACd,CACF,EAKaC,EAAN,cAA4BN,CAAY,CAC7C,YACEC,EACgBI,EAChB,CACA,MAAMJ,CAAO,EAFG,SAAAI,EAGhB,KAAK,KAAO,eACd,CACF,EAKaE,EAAN,cAAgCP,CAAY,CACjD,YAAYC,EAAiB,CAC3B,MAAMA,CAAO,EACb,KAAK,KAAO,mBACd,CACF,EAKaO,EAAN,cAA0BR,CAAY,CAC3C,YAAYC,EAAiB,CAC3B,MAAMA,CAAO,EACb,KAAK,KAAO,aACd,CACF,EAKaQ,EAAN,cAA6BT,CAAY,CAC9C,YAAYC,EAAiB,CAC3B,MAAMA,CAAO,EACb,KAAK,KAAO,gBACd,CACF,EC7EO,SAASS,GAAWC,EAAuB,CAChD,OAAI,OAAOA,GAAU,SACZA,EAGFA,EAAM,QAAQ,iBAAkB,CAACC,EAAQC,IAAY,CAC1D,IAAMC,EAAW,QAAQ,IAAID,CAAO,EACpC,GAAIC,IAAa,OACf,MAAM,IAAI,MAAM,yBAAyBD,CAAO,cAAc,EAEhE,OAAOC,CACT,CAAC,CACH,CAQO,SAASC,EAAuBC,EAAW,CAChD,GAAI,OAAOA,GAAQ,SACjB,OAAON,GAAWM,CAAG,EAGvB,GAAI,MAAM,QAAQA,CAAG,EACnB,OAAOA,EAAI,IAAKC,GAASF,EAAoBE,CAAI,CAAC,EAGpD,GAAID,IAAQ,MAAQ,OAAOA,GAAQ,SAAU,CAC3C,IAAME,EAAkC,CAAC,EACzC,OAAW,CAACC,EAAKR,CAAK,IAAK,OAAO,QAAQK,CAAG,EAC3CE,EAAOC,CAAG,EAAIJ,EAAoBJ,CAAK,EAEzC,OAAOO,CACT,CAEA,OAAOF,CACT,CCtCA,OAAS,KAAAI,MAAS,MAKlB,IAAMC,GAAoBD,EAAE,KAAK,CAAC,YAAa,MAAO,SAAU,UAAW,UAAW,QAAQ,CAAC,EAKlFE,EAAsBF,EAAE,OAAO,CAC1C,MAAOA,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EACtC,cAAeA,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAC9C,cAAeA,EAAE,MAAMC,EAAiB,EAAE,SAAS,CACrD,CAAC,EAOYE,EAAyBH,EAAE,OAAO,CAC7C,YAAaA,EAAE,OAAO,EAAE,SAAS,EACjC,QAASA,EAAE,OAAO,EAAE,SAAS,EAC7B,cAAeA,EAAE,OAAO,EAAE,YAAY,EAAE,SAAS,EACjD,WAAYA,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,EACpD,eAAgBA,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAC/C,kBAAmBA,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAClD,iBAAkBA,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS,EAC5D,YAAaA,EAAE,OAAO,EAAE,YAAY,EAAE,SAAS,CACjD,CAAC,EAOYI,GAAuBJ,EAAE,OAAO,CAC3C,SAAUA,EAAE,OAAO,EACnB,OAAQA,EAAE,OAAO,CACnB,CAAC,EAOYK,GAAqBL,EAAE,OAAO,CACzC,OAAQA,EAAE,OAAO,EACjB,MAAOE,EAAoB,SAAS,EACpC,SAAUC,EAAuB,SAAS,CAC5C,CAAC,EAOYG,GAAqBN,EAAE,OAAO,CACzC,KAAMA,EAAE,OAAO,EACf,IAAKA,EAAE,OAAO,EAAE,IAAI,EACpB,UAAWA,EAAE,OAAO,EAAE,MAAM,kBAAmB,CAC7C,QAAS,yCACX,CAAC,EACD,MAAOE,EAAoB,SAAS,EACpC,SAAUC,EAAuB,SAAS,CAC5C,CAAC,EAOYI,GAAsBP,EAAE,OAAO,CAC1C,MAAOE,EAAoB,SAAS,EACpC,SAAUC,EAAuB,SAAS,CAC5C,CAAC,EAOKK,GAAgBR,EAAE,KAAK,CAAC,SAAU,UAAW,SAAU,WAAY,MAAM,CAAC,EAKnES,GAAeT,EAAE,OAAO,CACnC,OAAQA,EAAE,MAAMM,EAAkB,EAAE,IAAI,EAAG,iBAAiB,EAC5D,SAAUF,GAAqB,SAAS,EACxC,cAAeG,GAAoB,SAAS,EAC5C,UAAWP,EAAE,OAAO,EACpB,QAASQ,GACT,WAAYR,EAAE,OAAO,EAAE,SAAS,EAChC,cAAeA,EAAE,MAAMK,EAAkB,EAAE,SAAS,CACtD,CAAC,EAeM,SAASK,GAAeC,EAA4B,CACzDF,GAAa,MAAME,CAAS,CAC9B,CH7GO,IAAMC,GAAsB,gBASnC,eAAsBC,GAAWC,EAAqBF,GAAsC,CAE1F,IAAMG,EAAeC,GAAK,QAAQ,IAAI,EAAGF,CAAU,EAEnD,GAAI,CAACG,GAAWF,CAAY,EAC1B,MAAM,IAAIG,EACR,kCAAkCH,CAAY,2DAChD,EAIF,IAAMI,EAAU,MADH,IAAI,KAAKJ,CAAY,EACP,KAAK,EAE5BK,EAEJ,GAAI,CACFA,EAAiB,QAAKD,CAAO,CAC/B,OAASE,EAAO,CACd,MAAM,IAAIH,EAAY,yBAAyBG,aAAiB,MAAQA,EAAM,QAAU,OAAOA,CAAK,CAAC,EAAE,CACzG,CAGA,OAAAC,GAAeF,CAAS,EAGTG,EAAoBH,CAAS,CAG9C,CIhDA,UAAYI,OAAQ,KACpB,UAAYC,MAAgB,cAC5B,OAAS,YAAAC,GAAU,QAAAC,GAAM,WAAAC,MAAe,OACxC,OAAS,SAAAC,OAAa,QCCf,SAASC,GAAiBC,EAAsB,CACrD,OACEA,EAEG,QAAQ,gBAAiB,GAAG,EAG5B,QAAQ,eAAgB,EAAE,EAE1B,QAAQ,UAAW,EAAE,CAE5B,CCfA,OAAS,SAAAC,OAAa,QCuBtB,IAAMC,EAAS,CACb,MAAO,UACP,OAAQ,UACR,IAAK,UAGL,MAAO,WACP,IAAK,WACL,MAAO,WACP,OAAQ,WACR,KAAM,WACN,QAAS,WACT,KAAM,WACN,MAAO,WAGP,MAAO,WACP,QAAS,WACT,SAAU,UACZ,EAKaC,EAAN,KAAa,CACV,OAER,YAAYC,EAAgC,CAAC,EAAG,CAC9C,KAAK,OAAS,CACZ,MAAOA,EAAO,OAAS,OACvB,UAAWA,EAAO,WAAa,EACjC,CACF,CAKQ,OAAOC,EAAiBC,EAAyB,CAEvD,MAAO,IADW,IAAI,KAAK,EAAE,YAAY,CACrB,MAAMD,CAAK,KAAKC,CAAO,EAC7C,CAKQ,SAASC,EAAcC,EAAuB,CACpD,OAAK,KAAK,OAAO,UACV,GAAGA,CAAK,GAAGD,CAAI,GAAGL,EAAO,KAAK,GADFK,CAErC,CAKA,MAAMD,EAAuB,CACvB,KAAK,UAAU,OAAc,GAC/B,QAAQ,IAAI,KAAK,OAAO,QAAgB,KAAK,SAASA,EAASJ,EAAO,GAAG,CAAC,CAAC,CAE/E,CAKA,KAAKI,EAAuB,CACtB,KAAK,UAAU,MAAa,GAC9B,QAAQ,IAAI,KAAK,OAAO,OAAe,KAAK,SAASA,EAASJ,EAAO,IAAI,CAAC,CAAC,CAE/E,CAKA,QAAQI,EAAuB,CACzB,KAAK,UAAU,SAAgB,GACjC,QAAQ,IAAI,KAAK,OAAO,UAAkB,KAAK,SAASA,EAASJ,EAAO,KAAK,CAAC,CAAC,CAEnF,CAKA,QAAQI,EAAuB,CACzB,KAAK,UAAU,SAAgB,GACjC,QAAQ,IAAI,KAAK,OAAO,UAAkB,KAAK,SAASA,EAASJ,EAAO,MAAM,CAAC,CAAC,CAEpF,CAKA,MAAMI,EAAuB,CACvB,KAAK,UAAU,OAAc,GAC/B,QAAQ,MAAM,KAAK,OAAO,QAAgB,KAAK,SAASA,EAASJ,EAAO,GAAG,CAAC,CAAC,CAEjF,CAKA,UAAUI,EAAuB,CAC3B,KAAK,UAAU,WAAkB,GACnC,QAAQ,IAAI,KAAK,OAAO,YAAoB,KAAK,SAASA,EAASJ,EAAO,OAASA,EAAO,OAAO,CAAC,CAAC,CAEvG,CAKQ,UAAUG,EAA0B,CAC1C,IAAMI,EAAS,CACb,QACA,OACA,UACA,UACA,QACA,WACF,EACA,OAAOA,EAAO,QAAQJ,CAAK,GAAKI,EAAO,QAAQ,KAAK,OAAO,KAAK,CAClE,CAKA,SAASJ,EAAuB,CAC9B,KAAK,OAAO,MAAQA,CACtB,CACF,EAGaK,EAAiB,IAAIP,ED1IlC,eAAsBQ,GAAiBC,EAAmC,CACxE,GAAI,CAEF,GAAM,CAAE,OAAAC,CAAO,EAAI,MAAMC,GAAM,UAAW,CACxC,KACA,QACA,gBACA,kBACA,MACA,qCACAF,CACF,CAAC,EAEKG,EAAW,WAAWF,EAAO,KAAK,CAAC,EACzC,OAAO,OAAO,MAAME,CAAQ,EAAI,EAAIA,CACtC,OAASC,EAAO,CACd,OAAAC,EAAO,MACL,oCAAoCL,CAAQ,KAAKI,aAAiB,MAAQA,EAAM,QAAU,OAAOA,CAAK,CAAC,EACzG,EACO,CACT,CACF,CFVO,IAAME,EAAN,KAAsB,CACnB,aACA,SACA,YACA,QACA,WAER,YACEC,EACAC,EACAC,EACAC,EACAC,EACA,CACA,KAAK,aAAeJ,EACpB,KAAK,SAAWC,EAChB,KAAK,YAAcI,EAAQH,CAAW,EACtC,KAAK,WAAaC,EAAaE,EAAQF,CAAU,EAAI,OACrD,KAAK,QAAUC,EAAUC,EAAQD,CAAO,EAAI,MAC9C,CAKA,MAAM,SAASE,EAAmBC,EAAoBC,EAAkBC,EAAsB,EAAqB,CAEjH,GAAI,KAAK,aAAa,aAAaH,EAAWE,EAAQ,MAAM,EAC1D,MAAO,GAGT,KAAK,SAAS,mBAAoC,uBAAuBA,EAAQ,MAAM,OAAOD,CAAU,EAAE,EAE1G,GAAI,CACF,IAAMG,EAAS,MAAM,KAAK,SAASH,EAAYC,CAAO,EAGhDG,EAAW,KAAK,eAAeD,EAAO,QAAQ,EAEpD,GAAIC,IAAa,EACf,YAAM,KAAK,aAAaD,EAAO,QAAQ,EACjC,IAAI,MAAM,4CAA4C,EAI9D,GAAID,EAAc,EAAG,CACnB,IAAMG,EAAWP,EAAQK,EAAO,QAAQ,EAClCG,EAAW,MAAqBC,GAAiBF,CAAQ,EAC/D,GAAIC,EAAWJ,EAEb,YAAM,KAAK,aAAaC,EAAO,QAAQ,EACjC,IAAI,MAAM,kBAAkBG,CAAQ,0BAA0BJ,CAAW,GAAG,CAEtF,CAGA,GAAI,KAAK,SAAW,KAAK,UAAY,KAAK,YAAa,CACrD,KAAK,SAAS,cAA+B,uCAAuC,KAAK,WAAW,KAAK,EAGzG,MAAiB,QAAM,KAAK,YAAa,CAAE,UAAW,EAAK,CAAC,EAE5D,QAAWM,KAAQL,EAAO,SACxB,GAAI,CAEF,IAAMM,EAAUX,EAAQU,CAAI,EAE5B,GAAI,CAAI,cAAWC,CAAO,EAAG,CAC3B,KAAK,SAAS,iBAAkC,kCAAkCA,CAAO,EAAE,EAC3F,QACF,CAEA,IAAMC,EAAWC,GAASF,CAAO,EAC3BG,EAAUC,GAAK,KAAK,YAAaH,CAAQ,EAC/C,MAAiB,SAAOD,EAASG,CAAO,EAGpCH,IAAYX,EAAQK,EAAO,QAAQ,IACrCA,EAAO,SAAWS,EAEtB,OAASE,EAAG,CACV,KAAK,SAAS,eAAgC,uBAAuBN,CAAI,KAAKM,CAAC,EAAE,CACnF,CAEJ,CAGA,YAAK,aAAa,qBAAqBf,EAAWC,EAAY,CAC5D,OAAQC,EAAQ,OAChB,IAAKA,EAAQ,IACb,SAAUE,EAAO,SACjB,KAAMC,CACR,CAAC,EACD,MAAM,KAAK,aAAa,KAAK,EAE7B,KAAK,SAAS,iBAEZ,sBAAsBH,EAAQ,MAAM,KAAKE,EAAO,QAAQ,KAAK,KAAK,WAAWC,CAAQ,CAAC,GACxF,EAEO,EACT,OAASW,EAAO,CACd,IAAMC,EAAU,8BAA8Bf,EAAQ,MAAM,KAC1Dc,aAAiB,MAAQA,EAAM,QAAU,OAAOA,CAAK,CACvD,GAEA,WAAK,SAAS,eAAgCC,CAAO,EAC/C,IAAIC,EAAcD,EAASf,EAAQ,GAAG,CAC9C,CACF,CAKA,MAAc,aAAaiB,EAAgC,CACzD,QAAWV,KAAQU,EACjB,GAAI,CACF,IAAMb,EAAWP,EAAQU,CAAI,EACtB,cAAWH,CAAQ,GACxB,MAAiB,SAAOA,CAAQ,CAEpC,OAASS,EAAG,CACV,KAAK,SAAS,eAAgC,yBAAyBN,CAAI,KAAKM,CAAC,EAAE,CACrF,CAEJ,CAKA,MAAc,SAASd,EAAoBC,EAAqE,CAC9G,IAAMkB,EAAe,OAAOlB,EAAQ,MAAM,EAAE,SAAS,EAAG,GAAG,EACrDmB,EAAY,KAAK,SAAW,KAAK,YAGvC,MAAiB,QAAMA,EAAW,CAAE,UAAW,EAAK,CAAC,EAErD,IAAMC,EAAsBC,GAAiBtB,CAAU,EAGjDuB,EAAO,CAAC,gBAAiB,YAAa,KAFrBV,GAAKO,EAAW,GAAGC,CAAmB,MAAMF,CAAY,UAAU,EAEvBlB,EAAQ,GAAG,EAEzE,KAAK,YACPsB,EAAK,QAAQ,YAAa,KAAK,UAAU,EAG3C,IAAIC,EAA0B,KACxBC,EAAwB,IAAI,IAC5BC,EAAyB,CAAC,EAEhC,GAAI,CACF,IAAMC,EAAaC,GAAM,SAAUL,EAAM,CAAE,IAAK,EAAK,CAAC,EAEtD,cAAiBM,KAAQF,EAAW,IAAK,CACvC,IAAMG,EAAOD,EAAK,SAAS,EAAE,KAAK,EAClC,GAAI,CAACC,EAAM,SAGXJ,EAAa,KAAKI,CAAI,EAGtB,IAAMC,EAAYD,EAAK,MAAM,kCAAkC,EAC3DC,IACFP,EAAWO,EAAU,CAAC,EAClBP,GAAUC,EAAS,IAAID,CAAQ,GAIrC,IAAMQ,EAAWF,EAAK,MAAM,6CAA6C,EACrEE,IAAW,CAAC,GACdP,EAAS,IAAIO,EAAS,CAAC,CAAC,EAI1B,IAAMC,EAAaH,EAAK,MAAM,uCAAuC,EAOrE,GANIG,IACFT,EAAWS,EAAW,CAAC,EACnBT,GAAUC,EAAS,IAAID,CAAQ,GAKnCM,EAAK,SAAS,QAAQ,GACtBA,EAAK,SAAS,UAAU,GACxBA,EAAK,SAAS,SAAS,GACvBA,EAAK,SAAS,iBAAiB,EAC/B,CACA,KAAK,SAAS,cAA+B,WAAW7B,EAAQ,MAAM,KAAK6B,CAAI,EAAE,EACjF,QACF,CAGA,GAAIA,EAAK,SAAS,YAAY,EAAG,CAQ/B,IAAMI,EAAgBJ,EAAK,MACzB,8FACF,EAEA,GAAII,EAAe,CACjB,GAAM,CAAC,CAAEC,EAAYC,EAAWC,EAAOC,EAAG,EAAIJ,EAE9C,KAAK,SAAS,SAAS,IAAIjC,EAAQ,MAAM,KAAKkC,CAAU,QAAQC,CAAS,OAAOC,CAAK,QAAQC,EAAG,EAAE,CACpG,MAEE,KAAK,SAAS,cAA+B,WAAWrC,EAAQ,MAAM,KAAK6B,CAAI,EAAE,CAErF,CACF,CAEA,aAAMH,EAGN,KAAK,SAAS,YAAY,EAErBH,IAEHA,EAAWX,GAAKO,EAAW,GAAGC,CAAmB,MAAMF,CAAY,MAAM,GAIvEK,GAAY,CAACC,EAAS,IAAID,CAAQ,GACpCC,EAAS,IAAID,CAAQ,EAGhB,CAAE,SAAAA,EAAU,SAAU,MAAM,KAAKC,CAAQ,CAAE,CACpD,OAASV,EAAO,CAEd,KAAK,SAAS,YAAY,EAE1B,IAAMwB,EAAMxB,EACNyB,EAASD,EAAI,QAAU,GACvBE,EAASF,EAAI,QAAU,GACvBG,EAAYhB,EAAa,KAAK;AAAA,CAAI,EAExC,MAAM,IAAI,MACR;AAAA,UACac,CAAM;AAAA,UACNC,CAAM;AAAA;AAAA,EACIC,CAAS;AAAA,WAClBH,EAAI,OAAO,EAC3B,CACF,CACF,CAKQ,eAAef,EAA0B,CAC/C,IAAMnB,EAAWP,EAAQ0B,CAAQ,EAEjC,GAAI,CAEF,OADa,IAAI,KAAKnB,CAAQ,EAClB,IACd,MAAQ,CACN,MAAO,EACT,CACF,CAKQ,WAAWsC,EAAuB,CACxC,IAAMC,EAAQ,CAAC,IAAK,KAAM,KAAM,IAAI,EAChCC,EAAOF,EACPG,EAAO,EAEX,KAAOD,GAAQ,MAAQC,EAAOF,EAAM,OAAS,GAC3CC,GAAQ,KACRC,IAGF,MAAO,GAAGD,EAAK,QAAQ,CAAC,CAAC,IAAID,EAAME,CAAI,CAAC,EAC1C,CAKA,aAAa,qBAAwC,CACnD,GAAI,CACF,aAAMlB,GAAM,SAAU,CAAC,WAAW,CAAC,EAC5B,EACT,MAAQ,CACN,MAAO,EACT,CACF,CACF,EIpTO,SAASmB,EAAcC,EAAqB,CACjD,GAAI,CAEF,OADe,IAAI,IAAIA,CAAG,EACZ,QAChB,MAAQ,CACN,MAAM,IAAI,MAAM,iBAAiBA,CAAG,GAAG,CACzC,CACF,CCNO,IAAMC,GAAN,KAA0C,CACvC,SAAuC,IAAI,IAKnD,SAASC,EAA8B,CACrC,KAAK,SAAS,IAAIA,EAAQ,UAAU,EAAGA,CAAO,CAChD,CAKA,WAAWC,EAAwC,CACjD,IAAMC,EAASC,EAAcF,CAAG,EAGhC,GAAI,KAAK,SAAS,IAAIC,CAAM,EAC1B,OAAO,KAAK,SAAS,IAAIA,CAAM,EAIjC,OAAW,CAACE,EAAeJ,CAAO,IAAK,KAAK,SAAS,QAAQ,EAC3D,GAAIE,IAAWE,GAAiBF,EAAO,SAAS,IAAIE,CAAa,EAAE,GAAKA,EAAc,SAAS,IAAIF,CAAM,EAAE,EACzG,OAAOF,CAKb,CAKA,YAAuB,CACrB,OAAO,MAAM,KAAK,KAAK,SAAS,KAAK,CAAC,CACxC,CAKA,kBAAkBC,EAA4B,CAC5C,IAAMD,EAAU,KAAK,WAAWC,CAAG,EACnC,GAAI,CAACD,EACH,MAAM,IAAIK,EACR,iCAAiCF,EAAcF,CAAG,CAAC,yBAA8B,KAAK,WAAW,EAAE,KAAK,IAAI,CAAC,GAC7GA,CACF,EAEF,OAAOD,CACT,CACF,EAGaM,EAA4B,IAAIP,GC7D7C,UAAYQ,OAAa,UAUlB,IAAeC,EAAf,KAAoD,CAQzD,SAASC,EAAsB,CAC7B,GAAI,CACF,IAAMC,EAASC,EAAcF,CAAG,EAChC,OAAOC,IAAW,KAAK,UAAU,GAAKA,EAAO,SAAS,IAAI,KAAK,UAAU,CAAC,EAAE,CAC9E,MAAQ,CACN,MAAO,EACT,CACF,CAKA,MAAgB,UAAUD,EAAaG,EAAmC,CACxE,IAAMC,EAAkC,CACtC,aACE,wHACF,OAAQ,kEACR,kBAAmB,gBACrB,EAEID,IACFC,EAAQ,OAASD,GAGnB,GAAI,CACF,IAAME,EAAW,MAAM,MAAML,EAAK,CAAE,QAAAI,CAAQ,CAAC,EAE7C,GAAI,CAACC,EAAS,GACZ,MAAM,IAAIC,EAAa,QAAQD,EAAS,MAAM,KAAKA,EAAS,UAAU,GAAIL,CAAG,EAG/E,OAAO,MAAMK,EAAS,KAAK,CAC7B,OAASE,EAAO,CACd,MAAIA,aAAiBD,EACbC,EAEF,IAAID,EAAa,yBAAyBC,aAAiB,MAAQA,EAAM,QAAU,OAAOA,CAAK,CAAC,GAAIP,CAAG,CAC/G,CACF,CAKU,UAAUQ,EAAkC,CACpD,OAAe,QAAKA,CAAI,CAC1B,CAMU,mBAAmBC,EAA6B,CAExD,IAAMC,EAAeD,EAAK,MAAM,SAAS,EACzC,GAAIC,IAAe,CAAC,EAClB,OAAO,SAASA,EAAa,CAAC,EAAG,EAAE,EAIrC,IAAMC,EAAUF,EAAK,MAAM,aAAa,EACxC,GAAIE,IAAU,CAAC,EACb,OAAO,SAASA,EAAQ,CAAC,EAAG,EAAE,EAIhC,IAAMC,EAAeH,EAAK,MAAM,wBAAwB,EACxD,GAAIG,IAAe,CAAC,EAClB,OAAO,SAASA,EAAa,CAAC,EAAG,EAAE,EAIrC,IAAMC,EAAcJ,EAAK,MAAM,WAAW,EAC1C,OAAII,IAAc,CAAC,EACV,SAASA,EAAY,CAAC,EAAG,EAAE,EAG7B,IACT,CAKU,iBAAiBC,EAAkBC,EAAoC,CAC/E,IAAMC,EAAMD,EAAED,CAAO,EACfG,EAAYD,EAAI,KAAK,OAAO,GAAK,GACjCP,EAAOO,EAAI,KAAK,EAAE,YAAY,EAGpC,OAAIC,EAAU,SAAS,KAAK,GAAKR,EAAK,SAAS,KAAK,GAAKA,EAAK,SAAS,cAAI,EAClE,MAKPQ,EAAU,SAAS,SAAS,GAC5BA,EAAU,SAAS,SAAS,GAC5BR,EAAK,SAAS,SAAS,GACvBA,EAAK,SAAS,cAAI,EAEX,UAKPQ,EAAU,SAAS,QAAQ,GAC3BA,EAAU,SAAS,MAAM,GACzBR,EAAK,SAAS,QAAQ,GACtBA,EAAK,SAAS,cAAI,EAEX,SAGF,WACT,CACF,EC9HO,IAAMS,EAAN,cAA2BC,CAAY,CAC5C,WAAoB,CAClB,MAAO,QACT,CAEA,MAAM,gBAAgBC,EAAaC,EAAsC,CACvE,IAAMC,EAAO,MAAM,KAAK,UAAUF,EAAKC,CAAO,EACxCE,EAAI,KAAK,UAAUD,CAAI,EAEvBE,EAAsB,CAAC,EASvBC,EAAY,CAChB,sBACA,gBACA,kBACA,iBACA,mBACF,EAEA,QAAWC,KAAYD,EAAW,CAChC,IAAME,EAAWJ,EAAEG,CAAQ,EAE3B,GAAIC,EAAS,OAAS,IACpBA,EAAS,KAAK,CAACC,EAAGC,IAAY,CAC5B,IAAMC,EAAMP,EAAEM,CAAO,EACfE,EAAOD,EAAI,GAAG,GAAG,EAAIA,EAAMA,EAAI,KAAK,GAAG,EAAE,MAAM,EAC/CE,EAAOD,EAAK,KAAK,MAAM,GAAK,GAElC,GAAI,CAACC,EAAM,OAGX,IAAMC,EAAaD,EAAK,WAAW,MAAM,EAAIA,EAAO,qBAAqBA,CAAI,GAKvEE,EAAe,GAFRJ,EAAI,KAAK,CAEM,IADXE,CACuB,GAElCG,EAAgB,KAAK,mBAAmBD,CAAY,EAEtDC,IAEaX,EAAS,KAAMY,GAAOA,EAAG,SAAWD,CAAa,GAE9DX,EAAS,KAAK,CACZ,OAAQW,EACR,IAAKF,EACL,KAAM,KAAK,iBAAiBJ,EAASN,CAAC,EACtC,MAAOO,EAAI,KAAK,OAAO,GAAKC,EAAK,KAAK,OAAO,GAAK,OAClD,YAAa,IAAI,IACnB,CAAC,EAGP,CAAC,EAGGP,EAAS,OAAS,GACpB,KAGN,CAGA,OAAAA,EAAS,KAAK,CAACa,EAAGC,IAAMD,EAAE,OAASC,EAAE,MAAM,EAEpCd,CACT,CACF,ECvEO,IAAMe,EAAN,cAA0BC,CAAY,CAC3C,WAAoB,CAClB,MAAO,UACT,CAEA,MAAM,gBAAgBC,EAAaC,EAAsC,CACvE,IAAMC,EAAO,MAAM,KAAK,UAAUF,EAAKC,CAAO,EACxCE,EAAI,KAAK,UAAUD,CAAI,EAEvBE,EAAsB,CAAC,EAIvBC,EAAeF,EAAE,gDAAgD,EAEvE,OAAIE,EAAa,SAAW,EAEJF,EAAE,mBAAmB,EAAE,OAAO,CAACG,EAAGC,KACzCJ,EAAEI,CAAE,EAAE,KAAK,MAAM,GAAK,IACvB,SAAS,IAAI,CAC1B,EAEa,KAAK,CAACD,EAAGE,IAAY,CACjC,KAAK,mBAAmBL,EAAGK,EAASJ,CAAQ,CAC9C,CAAC,EAEDC,EAAa,KAAK,CAACC,EAAGE,IAAY,CAChC,KAAK,mBAAmBL,EAAGK,EAASJ,CAAQ,CAC9C,CAAC,EAIHA,EAAS,KAAK,CAACK,EAAGC,IAAMD,EAAE,OAASC,EAAE,MAAM,EAEpCN,CACT,CAKQ,mBAAmBD,EAAeK,EAAkBJ,EAA2B,CACrF,IAAMO,EAAMR,EAAEK,CAAO,EACfI,EAAOD,EAAI,KAAK,MAAM,EAE5B,GAAI,CAACC,EAAM,OAGX,IAAMC,EAAaD,EAAK,WAAW,MAAM,EAAIA,EAAO,mBAAmBA,CAAI,GAGrEE,EAAYH,EAAI,KAAK,YAAY,GAAK,GACtCI,EAAgB,KAAK,mBAAmBD,CAAS,EAMvD,GAJI,CAACC,GAGUX,EAAS,KAAMY,GAAOA,EAAG,SAAWD,CAAa,EACpD,OAGZ,IAAME,EAAO,KAAK,qBAAqBd,EAAGK,CAAO,EAEjDJ,EAAS,KAAK,CACZ,OAAQW,EACR,IAAKF,EACL,KAAAI,EACA,MAAON,EAAI,KAAK,OAAO,GAAK,OAC5B,YAAa,IAAI,IACnB,CAAC,CACH,CAKQ,qBAAqBR,EAAeK,EAA+B,CAEzE,IAAMU,EAAMf,EAAEK,CAAO,EAAE,QAAQ,IAAI,EAEnC,GAAIU,EAAI,OAAQ,CAEd,IAAMC,EAAQD,EAAI,KAAK,wBAAwB,EAAE,MAAM,EAEvD,GAAIC,EAAM,OAAQ,CAChB,IAAMC,EAAYD,EAAM,KAAK,EAAE,KAAK,EAAE,YAAY,EAGlD,GAAIC,IAAc,OAASA,EAAU,SAAS,KAAK,EACjD,YAEF,GAAIA,IAAc,UAAYA,EAAU,SAAS,QAAQ,EACvD,eAEF,GAAIA,IAAc,WAAaA,EAAU,SAAS,SAAS,EACzD,eAEJ,CAGA,IAAMC,EAASH,EAAI,KAAK,GAAK,GAC7B,GAAIG,EAAO,SAAS,KAAK,GAAK,CAACA,EAAO,SAAS,QAAQ,EACrD,YAEF,GAAIA,EAAO,SAAS,QAAQ,EAC1B,eAEF,GAAIA,EAAO,SAAS,SAAS,EAC3B,eAEJ,CAGA,iBACF,CACF,ECnHO,IAAMC,EAAN,KAA0C,CACvC,mBAAqB,EAE7B,OAAOC,EAA0BC,EAAuB,CAOtD,OALI,KAAK,mBAAqB,IAC5B,QAAQ,OAAO,MAAM,KAAK,IAAI,OAAO,KAAK,kBAAkB,CAAC,IAAI,EACjE,KAAK,mBAAqB,GAGpBD,EAAO,CACb,WACEE,EAAO,KAAKD,CAAO,EACnB,MACF,cACEC,EAAO,QAAQD,CAAO,EACtB,MACF,cACEC,EAAO,QAAQD,CAAO,EACtB,MACF,YACEC,EAAO,MAAMD,CAAO,EACpB,MACF,gBACEC,EAAO,UAAUD,CAAO,EACxB,KACJ,CACF,CAEA,SAASA,EAAuB,CAE1B,KAAK,mBAAqB,GAC5B,QAAQ,OAAO,MAAM,KAAK,IAAI,OAAO,KAAK,kBAAkB,CAAC,IAAI,EAInE,QAAQ,OAAO,MAAM,KAAKA,CAAO,EAAE,EACnC,KAAK,mBAAqBA,EAAQ,MACpC,CAKA,aAAoB,CACd,KAAK,mBAAqB,IAC5B,QAAQ,OAAO,MAAM;AAAA,CAAI,EACzB,KAAK,mBAAqB,EAE9B,CACF,ECzCO,IAAME,EAAN,KAA2C,CACxC,OACA,OAER,YAAYC,EAAwB,CAClC,KAAK,OAASA,EACd,KAAK,OAAS,+BAA+BA,EAAO,QAAQ,cAC9D,CAMA,MAAM,OAAOC,EAA0BC,EAAgC,CAErE,GAAID,IAAU,QAId,GAAI,CAEF,IAAME,EAAmB,GADX,KAAK,SAASF,CAAK,CACA;AAAA;AAAA,EAAsBC,CAAO,GAExDE,EAAW,MAAM,MAAM,KAAK,OAAQ,CACxC,OAAQ,OACR,QAAS,CACP,eAAgB,kBAClB,EACA,KAAM,KAAK,UAAU,CACnB,QAAS,KAAK,OAAO,OACrB,KAAMD,EACN,WAAY,UACd,CAAC,CACH,CAAC,EAED,GAAI,CAACC,EAAS,GAAI,CAChB,IAAMC,EAAY,MAAMD,EAAS,KAAK,EACtC,MAAM,IAAIE,EACR,yCAAyCF,EAAS,MAAM,IAAIA,EAAS,UAAU;AAAA,EAAKC,CAAS,EAC/F,CACF,CACF,OAASE,EAAO,CAEd,QAAQ,MAAM,gCAAiCA,CAAK,CACtD,CACF,CAKQ,SAASN,EAAkC,CACjD,OAAQA,EAAO,CACb,WACE,MAAO,eACT,cACE,MAAO,SACT,cACE,MAAO,eACT,YACE,MAAO,SACT,gBACE,MAAO,YACT,QACE,MAAO,EACX,CACF,CAKA,MAAM,SAASO,EAAiC,CAEhD,CAKA,MAAM,aAA6B,CAEnC,CACF,ECjFO,IAAMC,EAAN,KAAqB,CAClB,cACA,cAQR,YAAYC,EAAgC,CAAC,EAAGC,EAA+B,CAC7E,KAAK,cAAgB,IAAI,IAAID,EAAc,IAAKE,GAAM,CAACA,EAAE,OAAQA,CAAC,CAAC,CAAC,EACpE,KAAK,cAAgBD,CACvB,CAWO,QAAQE,EAA4C,CACzD,IAAMC,EAASC,EAAcF,EAAO,GAAG,EACjCG,EAAe,KAAK,cAAc,IAAIF,CAAM,EAE5CG,EAAS,CACb,MAAO,KAAK,mBAAmBJ,EAAO,MAAOG,GAAc,KAAK,EAChE,SAAU,KAAK,sBAAsBH,EAAO,SAAUG,GAAc,QAAQ,CAC9E,EAEA,YAAK,SAASC,CAAM,EACbA,CACT,CAKQ,mBAAmBJ,EAAwBC,EAA+C,CAChG,IAAMI,EAAS,KAAK,eAAe,MAC7BC,EAAWC,GAEjB,MAAO,CACL,MAAOP,GAAQ,OAASC,GAAQ,OAASI,GAAQ,OAASC,EAAS,MACnE,cAAeN,GAAQ,eAAiBC,GAAQ,eAAiBI,GAAQ,eAAiBC,EAAS,cACnG,cAAeN,GAAQ,eAAiBC,GAAQ,eAAiBI,GAAQ,eAAiBC,EAAS,aACrG,CACF,CAKQ,sBAAsBN,EAA2BC,EAAqD,CAC5G,IAAMI,EAAS,KAAK,eAAe,SAC7BC,EAAWE,GAEjB,MAAO,CACL,YAAaR,GAAQ,aAAeC,GAAQ,aAAeI,GAAQ,aAAeC,EAAS,YAC3F,QAASN,GAAQ,SAAWC,GAAQ,SAAWI,GAAQ,SAAWC,EAAS,QAC3E,cAAeN,GAAQ,eAAiBC,GAAQ,eAAiBI,GAAQ,eAAiBC,EAAS,cACnG,WAAYN,GAAQ,YAAcC,GAAQ,YAAcI,GAAQ,YAAcC,EAAS,WACvF,eACEN,GAAQ,gBAAkBC,GAAQ,gBAAkBI,GAAQ,gBAAkBC,EAAS,eACzF,kBACEN,GAAQ,mBACRC,GAAQ,mBACRI,GAAQ,mBACRC,EAAS,kBACX,iBACEN,GAAQ,kBAAoBC,GAAQ,kBAAoBI,GAAQ,kBAAoBC,EAAS,iBAC/F,YAAaN,GAAQ,aAAeC,GAAQ,aAAeI,GAAQ,aAAeC,EAAS,WAC7F,CACF,CAOO,cAAcL,EAAsC,CACzD,IAAME,EAAe,KAAK,cAAc,IAAIF,CAAM,EAE5CG,EAAS,CACb,MAAO,KAAK,mBAAmB,OAAWD,GAAc,KAAK,EAC7D,SAAU,KAAK,sBAAsB,OAAWA,GAAc,QAAQ,CACxE,EAEA,YAAK,SAASC,CAAM,EACbA,CACT,CAOO,iBAAiBN,EAAoC,CAC1D,KAAK,cAAgBA,CACvB,CAKQ,SAASM,EAAoC,CACnD,GAAIA,EAAO,MAAM,MAAQ,EAAG,MAAM,IAAI,MAAM,wBAAwBA,EAAO,MAAM,KAAK,EAAE,EACxF,GAAIA,EAAO,MAAM,cAAgB,EAAG,MAAM,IAAI,MAAM,2BAA2BA,EAAO,MAAM,aAAa,EAAE,EAC3G,GAAIA,EAAO,SAAS,cAAgB,EAAG,MAAM,IAAI,MAAM,2BAA2BA,EAAO,SAAS,aAAa,EAAE,EACjH,GAAIA,EAAO,SAAS,WAAa,EAAG,MAAM,IAAI,MAAM,wBAAwBA,EAAO,SAAS,UAAU,EAAE,EACxG,GAAIA,EAAO,SAAS,eAAiB,EACnC,MAAM,IAAI,MAAM,4BAA4BA,EAAO,SAAS,cAAc,EAAE,EAC9E,GAAIA,EAAO,SAAS,kBAAoB,EACtC,MAAM,IAAI,MAAM,+BAA+BA,EAAO,SAAS,iBAAiB,EAAE,EACpF,GAAIA,EAAO,SAAS,YAAc,EAAG,MAAM,IAAI,MAAM,yBAAyBA,EAAO,SAAS,WAAW,EAAE,CAC7G,CACF,ECzGO,IAAMK,EAAN,KAA2B,CAExB,MAA8B,CAAC,EAC/B,YAAuB,GACvB,gBAAwB,IAAI,KAAK,CAAC,EAClC,WAOR,YAAYC,EAAqB,EAAG,CAClC,KAAK,WAAaA,CACpB,CAQA,IAAIC,EAAgBC,EAAsB,CACxC,IAAMC,EAAU,IAAI,KAAK,KAAK,IAAI,GAAKD,GAAS,EAAE,EAClD,KAAK,MAAM,KAAK,CAAE,KAAMD,EAAM,QAAAE,CAAQ,CAAC,CACzC,CAQA,SAASF,EAAgBC,EAAsB,CAC7C,IAAMC,EAAU,IAAI,KAAK,KAAK,IAAI,GAAKD,GAAS,EAAE,EAClD,KAAK,MAAM,QAAQ,CAAE,KAAMD,EAAM,QAAAE,CAAQ,CAAC,CAC5C,CAOA,SAA2B,CACzB,OAAI,KAAK,MAAM,SAAW,EACjB,KAGI,KAAK,MAAM,MAAM,GACjB,MAAQ,IACvB,CAOA,UAA4B,CAC1B,OAAI,KAAK,MAAM,SAAW,EACjB,KAEF,KAAK,MAAM,CAAC,GAAG,MAAQ,IAChC,CAQA,SAASC,EAAoB,CAK3B,GAJI,KAAK,aAILA,EAAM,KAAK,gBACb,MAAO,GAIT,IAAMC,EAAO,KAAK,MAAM,CAAC,EACzB,MAAI,EAAAA,GAAQD,EAAMC,EAAK,QAKzB,CAKA,aAAoB,CAClB,KAAK,YAAc,EACrB,CAOA,cAAcL,EAA0B,CACtC,KAAK,YAAc,GACnB,KAAK,WAAaA,EAClB,KAAK,gBAAkB,IAAI,KAAK,KAAK,IAAI,EAAIA,CAAU,CACzD,CAOA,WAAWA,EAA0B,CACnC,KAAK,YAAc,GACnB,KAAK,WAAaA,EAClB,KAAK,gBAAkB,IAAI,KAAK,KAAK,IAAI,EAAIA,CAAU,CACzD,CAOA,UAAoB,CAClB,OAAO,KAAK,MAAM,OAAS,CAC7B,CAOA,sBAA6B,CAE3B,IAAIM,EAAO,KAAK,gBAGVD,EAAO,KAAK,MAAM,CAAC,EACzB,OAAIA,GAAQA,EAAK,QAAUC,IACzBA,EAAOD,EAAK,SAGPC,CACT,CAOA,gBAAyB,CACvB,OAAO,KAAK,MAAM,MACpB,CAOA,gBAA0B,CACxB,OAAO,KAAK,WACd,CAOA,eAAwB,CACtB,OAAO,KAAK,UACd,CAOA,cAAcN,EAA0B,CACtC,KAAK,WAAaA,CACpB,CAKA,OAAc,CACZ,KAAK,MAAQ,CAAC,CAChB,CAOA,WAME,CACA,IAAMI,EAAM,IAAI,KAChB,MAAO,CACL,YAAa,KAAK,MAAM,OACxB,YAAa,KAAK,YAClB,gBAAiB,KAAK,gBACtB,WAAY,KAAK,WACjB,YAAa,KAAK,SAASA,CAAG,CAChC,CACF,CACF,EC/MO,IAAMG,EAAN,KAAmC,CAEhC,OAA4C,IAAI,IAChD,eAAsC,IAAI,IAC1C,aAAwB,GACxB,QAAgD,KAChD,gBAA0B,EAC1B,QAAmB,GAGnB,SACA,OAOR,YAAYC,EAAsC,CAChD,KAAK,SAAWA,CAClB,CAOA,UAAUC,EAA6E,CACrF,KAAK,OAASA,CAChB,CAQA,cAAcC,EAAkBC,EAA0B,CACxD,GAAI,KAAK,OAAO,IAAID,CAAQ,EAC1B,MAAM,IAAI,MAAM,SAASA,CAAQ,wBAAwB,EAG3D,IAAME,EAAQ,IAAIC,EAAqBF,CAAU,EACjD,KAAK,OAAO,IAAID,EAAUE,CAAK,EAC/B,KAAK,eAAe,IAAIF,EAAUC,CAAU,CAC9C,CAOA,gBAAgBD,EAAwB,CACtC,KAAK,OAAO,OAAOA,CAAQ,EAC3B,KAAK,eAAe,OAAOA,CAAQ,CACrC,CAWA,QAAQA,EAAkBI,EAAgBC,EAAsB,CAC9D,IAAMH,EAAQ,KAAK,OAAO,IAAIF,CAAQ,EACtC,GAAI,CAACE,EACH,MAAM,IAAI,MAAM,SAASF,CAAQ,oBAAoB,EAGvDE,EAAM,IAAIE,EAAMC,CAAK,EAGhB,KAAK,SACR,KAAK,aAAa,CAEtB,CASA,gBAAgBL,EAAkBI,EAAgBC,EAAsB,CACtE,IAAMH,EAAQ,KAAK,OAAO,IAAIF,CAAQ,EACtC,GAAI,CAACE,EACH,MAAM,IAAI,MAAM,SAASF,CAAQ,oBAAoB,EAGvDE,EAAM,SAASE,EAAMC,CAAK,EAGrB,KAAK,SACR,KAAK,aAAa,CAEtB,CAWA,iBAAiBL,EAAkBC,EAA2B,CAC5D,IAAMC,EAAQ,KAAK,OAAO,IAAIF,CAAQ,EACtC,GAAI,CAACE,EACH,MAAM,IAAI,MAAM,SAASF,CAAQ,oBAAoB,EAGvD,IAAMM,EAAiBL,GAAc,KAAK,eAAe,IAAID,CAAQ,GAAK,EAC1EE,EAAM,cAAcI,CAAc,EAClC,KAAK,aAAe,GAGf,KAAK,SACR,KAAK,aAAa,CAEtB,CAWA,eAAeN,EAAkBC,EAA2B,CAC1D,IAAMC,EAAQ,KAAK,OAAO,IAAIF,CAAQ,EACtC,GAAI,CAACE,EACH,MAAM,IAAI,MAAM,SAASF,CAAQ,oBAAoB,EAGvD,IAAMM,EAAiBL,GAAc,KAAK,eAAe,IAAID,CAAQ,GAAK,EAC1EE,EAAM,WAAWI,CAAc,EAC/B,KAAK,aAAe,GAGf,KAAK,SACR,KAAK,aAAa,CAEtB,CAQA,cAAqB,CAmBnB,GAlBI,KAAK,UAKT,KAAK,WAAW,EAGE,KAAK,YAAY,IAU/B,KAAK,aACP,OAKF,IAAMC,EAAO,KAAK,yBAAyB,EAC3C,GAAIA,EAAM,CACR,IAAMC,EAAM,KAAK,IAAI,EACfC,EAAS,KAAK,IAAI,EAAGF,EAAK,KAAK,QAAQ,EAAIC,CAAG,EACpD,KAAK,cAAcC,EAAQF,EAAK,UAAWA,EAAK,IAAI,CACtD,CACF,CAOQ,aAAuB,CAE7B,GAAI,KAAK,aACP,MAAO,GAGT,IAAMC,EAAM,IAAI,KAGVE,EAAa,MAAM,KAAK,KAAK,OAAO,KAAK,CAAC,EAChD,GAAIA,EAAW,SAAW,EACxB,MAAO,GAIT,QAASC,EAAI,EAAGA,EAAID,EAAW,OAAQC,IAAK,CAC1C,IAAMC,GAAS,KAAK,gBAAkBD,GAAKD,EAAW,OAChDG,EAAYH,EAAWE,CAAK,EAClC,GAAI,CAACC,EAAW,SAEhB,IAAMX,EAAQ,KAAK,OAAO,IAAIW,CAAS,EACvC,GAAKX,GAGDA,EAAM,SAAS,GAAKA,EAAM,SAASM,CAAG,EAAG,CAE3C,IAAMJ,EAAOF,EAAM,QAAQ,EAC3B,GAAIE,EAEF,OAAAF,EAAM,YAAY,EAClB,KAAK,aAAe,GACpB,KAAK,iBAAmBU,EAAQ,GAAKF,EAAW,OAGhD,KAAK,YAAYG,EAAWT,CAAI,EAAE,MAAOU,GAAU,CAEjD,QAAQ,MAAM,+CAA+CA,CAAK,EAAE,EACpE,KAAK,eAAeD,CAAS,CAC/B,CAAC,EAEM,EAEX,CACF,CAEA,MAAO,EACT,CAQA,MAAc,YAAYA,EAAmBT,EAA+B,CAC1E,MAAM,KAAK,SAASA,EAAMS,CAAS,CACrC,CASQ,cAAcJ,EAAgBI,EAAmBE,EAAsB,CAC7E,KAAK,WAAW,EAGZ,KAAK,QAAUN,EAAS,KAC1B,KAAK,OAAOI,EAAWJ,EAAQM,CAAQ,EAGzC,KAAK,QAAU,WAAW,IAAM,CAC9B,KAAK,QAAU,KACf,KAAK,aAAa,CACpB,EAAGN,CAAM,CACX,CAKQ,YAAmB,CACrB,KAAK,UAAY,OACnB,aAAa,KAAK,OAAO,EACzB,KAAK,QAAU,KAEnB,CAOQ,0BAAqE,CAC3E,IAAIO,EAAmD,KAEvD,OAAW,CAACC,EAAMf,CAAK,IAAK,KAAK,OAAO,QAAQ,EAAG,CAEjD,GAAI,CAACA,EAAM,SAAS,EAClB,SAGF,IAAMa,EAAWb,EAAM,qBAAqB,GACxCc,IAAW,MAAQD,EAAWC,EAAO,QACvCA,EAAS,CAAE,KAAMD,EAAU,UAAWE,CAAK,EAE/C,CAEA,OAAOD,CACT,CAOA,MAAa,CACX,KAAK,QAAU,GACf,KAAK,WAAW,CAClB,CAKA,QAAe,CACb,KAAK,QAAU,GACf,KAAK,aAAa,CACpB,CAOA,UAA8F,CAC5F,IAAME,EAAQ,IAAI,IAElB,OAAW,CAACD,EAAMf,CAAK,IAAK,KAAK,OAAO,QAAQ,EAAG,CACjD,IAAMiB,EAASjB,EAAM,UAAU,EAC/BgB,EAAM,IAAID,EAAM,CACd,YAAaE,EAAO,YACpB,YAAaA,EAAO,YACpB,gBAAiBA,EAAO,eAC1B,CAAC,CACH,CAEA,OAAOD,CACT,CAOA,gBAA0B,CACxB,OAAO,KAAK,YACd,CAOA,iBAA2B,CACzB,QAAWhB,KAAS,KAAK,OAAO,OAAO,EACrC,GAAIA,EAAM,SAAS,EACjB,MAAO,GAGX,MAAO,EACT,CAOA,sBAA+B,CAC7B,IAAIkB,EAAQ,EACZ,QAAWlB,KAAS,KAAK,OAAO,OAAO,EACrCkB,GAASlB,EAAM,eAAe,EAEhC,OAAOkB,CACT,CACF,EC3XO,IAAMC,EAAN,KAAmB,CAChB,aACA,gBACA,SAGA,UAGA,eAGA,QAAU,GAGV,eAAiB,IAAI,IAY7B,YACEC,EACAC,EACAC,EACAC,EACAC,EAAgC,CAAC,EACjCC,EACAC,EAGA,CACA,KAAK,aAAeN,EACpB,KAAK,gBAAkBC,EACvB,KAAK,SAAWC,EAGhB,KAAK,eAAiB,IAAIK,EAAeH,EAAeC,CAAa,EAGrE,IAAMG,EAAkBF,IAAsBG,GAAa,IAAIC,EAAmBD,CAAQ,GAC1F,KAAK,UAAYD,EAAgB,MAAOG,EAAMC,IAAc,CAC1D,MAAM,KAAK,YAAYD,EAAMC,CAAS,CACxC,CAAC,EAGD,KAAK,UAAU,UAAU,CAACA,EAAWC,IAAW,CAC9C,IAAMC,EAAU,KAAK,MAAMD,EAAS,GAAI,EAClCE,EAAQH,EAAU,MAAM,GAAG,EAC3BI,EAAOD,EAAM,CAAC,EACdE,EAASF,EAAM,CAAC,EAElBC,IAAS,WACX,KAAK,SAAS,cAA+B,IAAIC,CAAM,sBAAsBH,CAAO,MAAM,EACjFE,IAAS,SAClB,KAAK,SAAS,cAA+B,IAAIC,CAAM,mBAAmBH,CAAO,MAAM,CAE3F,CAAC,CACH,CAOA,eAAeI,EAA4B,CACzC,IAAMD,EAASE,EAAcD,EAAO,GAAG,EAGvC,KAAK,qBAAqBD,CAAM,EAGhC,IAAMG,EAAuB,CAC3B,UAAWF,EAAO,IAClB,WAAYA,EAAO,KACnB,OAAQA,EACR,cAAe,EACf,WAAY,CACd,EAEMN,EAAY,SAASK,CAAM,GACjC,KAAK,UAAU,QAAQL,EAAWQ,CAAI,EAEtC,KAAK,SAAS,cAEZ,wBAAwBF,EAAO,IAAI,8BAA8BD,CAAM,EACzE,CACF,CAUA,YAAYI,EAAmBC,EAAoBC,EAAqBL,EAA6B,CACnG,GAAIK,EAAS,SAAW,EACtB,OAGF,IAAMN,EAASE,EAAcE,CAAS,EAGtC,KAAK,qBAAqBJ,CAAM,EAGhC,IAAMO,EAAiB,KAAK,eAAe,cAAcP,CAAM,EACzD,CAAE,cAAAQ,CAAc,EAAID,EAAe,SAGzC,QAASE,EAAI,EAAGA,EAAIH,EAAS,OAAQG,IAAK,CACxC,IAAMC,EAAUJ,EAASG,CAAC,EAC1B,GAAI,CAACC,EAAS,SAEd,IAAMP,EAA0B,CAC9B,UAAAC,EACA,WAAAC,EACA,QAAAK,EACA,OAAAT,EACA,WAAY,CACd,EAEMN,EAAY,YAAYK,CAAM,GAE9BW,EAAUF,EAAID,EAAgB,IACpC,KAAK,UAAU,QAAQb,EAAWQ,EAAMQ,CAAO,CACjD,CAEA,KAAK,SAAS,iBAEZ,wBAAwBL,EAAS,MAAM,mCAAmCD,CAAU,YAAYL,CAAM,GACxG,CACF,CAKA,OAAc,CACZ,GAAI,KAAK,QACP,MAAM,IAAI,MAAM,iCAAiC,EAGnD,KAAK,QAAU,GACf,KAAK,UAAU,OAAO,EAEtB,KAAK,SAAS,cAA+B,yCAAyC,CACxF,CAOA,MAAM,MAAsB,CACrB,KAAK,UAIV,KAAK,SAAS,cAA+B,6CAA6C,EAE1F,KAAK,UAAU,KAAK,EACpB,KAAK,QAAU,GAEf,KAAK,SAAS,cAA+B,yCAAyC,EACxF,CAOA,qBAA+B,CAC7B,OAAO,KAAK,UAAU,eAAe,GAAK,KAAK,UAAU,gBAAgB,CAC3E,CAOA,eAGE,CACA,IAAMY,EAAQ,KAAK,UAAU,SAAS,EAChCC,EAAuE,CAAC,EACxEC,EAA0E,CAAC,EAEjF,OAAW,CAACnB,EAAWoB,CAAU,IAAKH,EAAM,QAAQ,EAClD,GAAIjB,EAAU,WAAW,QAAQ,EAAG,CAClC,IAAMK,EAASL,EAAU,MAAM,CAAC,EAChCkB,EAAYb,CAAM,EAAI,CACpB,OAAQe,EAAW,YACnB,WAAYA,EAAW,WACzB,CACF,SAAWpB,EAAU,WAAW,WAAW,EAAG,CAC5C,IAAMK,EAASL,EAAU,MAAM,CAAC,EAChCmB,EAAed,CAAM,EAAI,CACvB,OAAQe,EAAW,YACnB,WAAYA,EAAW,WACzB,CACF,CAGF,MAAO,CAAE,YAAAF,EAAa,eAAAC,CAAe,CACvC,CAOQ,qBAAqBd,EAAsB,CACjD,IAAMgB,EAAiB,SAAShB,CAAM,GAChCiB,EAAoB,YAAYjB,CAAM,GAGtCkB,EAAgB,KAAK,UAAU,SAAS,EAC9C,GAAIA,EAAc,IAAIF,CAAc,GAAKE,EAAc,IAAID,CAAiB,EAC1E,OAIF,IAAMV,EAAiB,KAAK,eAAe,cAAcP,CAAM,EAGzDmB,EAAUC,EAAgB,kBAAkB,WAAWpB,CAAM,GAAG,EACtE,KAAK,eAAe,IAAIA,EAAQmB,CAAO,EAGvC,GAAM,CAAE,cAAAE,CAAc,EAAId,EAAe,MACnC,CAAE,cAAAC,CAAc,EAAID,EAAe,SAGzC,KAAK,UAAU,cAAcS,EAAgBK,EAAgB,GAAI,EACjE,KAAK,UAAU,cAAcJ,EAAmBT,EAAgB,GAAI,CACtE,CAUA,MAAc,YAAYd,EAA0CC,EAAkC,CACpG,IAAMG,EAAQH,EAAU,MAAM,GAAG,EAC3BI,EAAOD,EAAM,CAAC,EACdE,EAASF,EAAM,CAAC,EAEtB,GAAI,CAACC,GAAQ,CAACC,EACZ,MAAM,IAAI,MAAM,8BAA8BL,CAAS,EAAE,EAG3D,GAAII,IAAS,QACX,MAAM,KAAK,aAAaL,EAAwBM,EAAQL,CAAS,UACxDI,IAAS,WAClB,MAAM,KAAK,gBAAgBL,EAA2BM,EAAQL,CAAS,MAEvE,OAAM,IAAI,MAAM,uBAAuBI,CAAI,EAAE,CAEjD,CASA,MAAc,aAAaI,EAAsBH,EAAgBL,EAAkC,CACjG,GAAM,CAAE,UAAAS,EAAW,WAAAC,EAAY,OAAAJ,EAAQ,cAAAqB,EAAe,WAAAC,EAAa,CAAE,EAAIpB,EAGnEgB,EAAU,KAAK,eAAe,IAAInB,CAAM,EAC9C,GAAI,CAACmB,EACH,MAAM,IAAI,MAAM,+BAA+BnB,CAAM,EAAE,EAIzD,IAAMO,EAAiB,KAAK,eAAe,QAAQN,CAAM,EACnD,CAAE,MAAOuB,EAAa,cAAAH,CAAc,EAAId,EAAe,MAE7D,GAAI,CAEF,IAAMkB,EAAS,MAAM,KAAK,aAAaN,EAASf,EAAWC,EAAYE,EAAgBe,EAAetB,CAAM,EAE5G,GAAIyB,EAAO,eAET,KAAK,SAAS,iBAEZ,IAAIzB,CAAM,WAAWyB,EAAO,SAAS,MAAM,qBAAqBpB,CAAU,aAAaiB,CAAa,IAAIE,CAAW,GACrH,EAGA,KAAK,YAAYpB,EAAWC,EAAYoB,EAAO,SAAUxB,CAAM,EAG/D,KAAK,UAAU,iBAAiBN,EAAW0B,EAAgB,GAAI,UAG3DC,EAAgBE,EAAa,CAE/B,IAAME,EAAaL,EAAgB,IAC7BM,EAAeF,EAAO,cAAgBC,EAE5C,KAAK,SAAS,cAEZ,IAAI1B,CAAM,yBAAyBK,CAAU,aAAaiB,CAAa,IAAIE,CAAW,oBAAoB,KAAK,MAAMG,EAAe,GAAI,CAAC,GAC3I,EAGA,IAAMC,EAA+B,CACnC,GAAGzB,EACH,cAAemB,EAAgB,EAC/B,WAAY,CACd,EAEA,KAAK,UAAU,QAAQ3B,EAAWiC,EAAcD,CAAY,EAC5D,KAAK,UAAU,iBAAiBhC,EAAW0B,EAAgB,GAAI,CACjE,MAEE,KAAK,SAAS,cAEZ,IAAIrB,CAAM,0BAA0BK,CAAU,KAAKmB,CAAW,iCAChE,EACA,KAAK,UAAU,iBAAiB7B,EAAW0B,EAAgB,GAAI,CAGrE,OAASQ,EAAO,CACd,IAAMC,EAAeD,aAAiB,MAAQA,EAAM,QAAU,OAAOA,CAAK,EAGpE,CAAE,WAAAE,EAAY,eAAAC,EAAgB,kBAAAC,EAAmB,iBAAAC,CAAiB,EAAI3B,EAAe,SAE3F,GAAIgB,EAAaQ,EAAY,CAE3B,IAAMI,EAAa,KAAK,iBACtBZ,EACAS,EAAiB,IACjBC,EACAC,CACF,EAEA,KAAK,SAAS,iBAEZ,IAAIlC,CAAM,sBAAsBK,CAAU,iBAAiB,KAAK,MAAM8B,EAAa,GAAI,CAAC,cAAcZ,EAAa,CAAC,IAAIQ,CAAU,GACpI,EAGA,IAAMH,EAA+B,CACnC,GAAGzB,EACH,WAAYoB,EAAa,CAC3B,EAEA,KAAK,UAAU,gBAAgB5B,EAAWiC,EAAcO,CAAU,EAClE,KAAK,UAAU,iBAAiBxC,EAAW0B,EAAgB,GAAI,CACjE,MAEE,KAAK,SAAS,eAEZ,IAAIrB,CAAM,qBAAqBK,CAAU,UAAUkB,CAAU,oBAAoBO,CAAY,EAC/F,EACA,KAAK,UAAU,iBAAiBnC,EAAW0B,EAAgB,GAAI,CAEnE,CACF,CASA,MAAc,gBAAgBlB,EAAyBH,EAAgBL,EAAkC,CACvG,GAAM,CAAE,UAAAS,EAAW,WAAAC,EAAY,QAAAK,EAAS,OAAAT,EAAQ,WAAAsB,EAAa,CAAE,EAAIpB,EAG/DI,EACAN,EACFM,EAAiB,KAAK,eAAe,QAAQN,CAAM,EAEnDM,EAAiB,KAAK,eAAe,cAAcP,CAAM,EAG3D,GAAM,CAAE,cAAAQ,EAAe,YAAA4B,CAAY,EAAI7B,EAAe,SAEtD,GAAI,CAEF,MAAM,KAAK,gBAAgB,SAASH,EAAWC,EAAYK,EAAS0B,CAAW,EAG/E,KAAK,SAAS,iBAEZ,IAAIpC,CAAM,6CAA6CU,EAAQ,MAAM,QAAQL,CAAU,EACzF,EAEA,KAAK,UAAU,iBAAiBV,EAAWa,EAAgB,GAAI,CACjE,OAASqB,EAAO,CACd,IAAMC,EAAeD,aAAiB,MAAQA,EAAM,QAAU,OAAOA,CAAK,EAGpE,CAAE,WAAAE,EAAY,eAAAC,EAAgB,kBAAAC,EAAmB,iBAAAC,CAAiB,EAAI3B,EAAe,SAE3F,GAAIgB,EAAaQ,EAAY,CAE3B,IAAMI,EAAa,KAAK,iBACtBZ,EACAS,EAAiB,IACjBC,EACAC,CACF,EAEA,KAAK,SAAS,iBAEZ,IAAIlC,CAAM,iCAAiCU,EAAQ,MAAM,iBAAiB,KAAK,MAAMyB,EAAa,GAAI,CAAC,cAAcZ,EAAa,CAAC,IAAIQ,CAAU,GACnJ,EAGA,IAAMH,EAAkC,CACtC,GAAGzB,EACH,WAAYoB,EAAa,CAC3B,EAEA,KAAK,UAAU,gBAAgB5B,EAAWiC,EAAcO,CAAU,EAClE,KAAK,UAAU,iBAAiBxC,EAAWa,EAAgB,GAAI,CACjE,MAEE,KAAK,SAAS,eAEZ,IAAIR,CAAM,gCAAgCU,EAAQ,MAAM,UAAUa,EAAa,CAAC,cAAcO,CAAY,EAC5G,EACA,KAAK,UAAU,iBAAiBnC,EAAWa,EAAgB,GAAI,CAEnE,CACF,CAaA,MAAc,aACZW,EACAf,EACAiC,EACApC,EACAqB,EACAtB,EACkF,CAClF,IAAMwB,EAAcvB,EAAO,MAAM,MAEjC,KAAK,SAAS,cAEZ,IAAID,CAAM,cAAcI,CAAS,iCAAiCkB,CAAa,IAAIE,CAAW,GAChG,EAGA,IAAMlB,EAAW,MAAMa,EAAQ,gBAAgBf,CAAS,EAExD,KAAK,SAAS,cAA+B,IAAIJ,CAAM,WAAWM,EAAS,MAAM,sBAAsBF,CAAS,EAAE,EAGlH,IAAMkC,EAAgBrC,EAAO,MAAM,cAG7BsC,EAAcjC,EAAS,OAAQkC,GAAO,CAC1C,IAAMC,EAAiBH,EAAc,SAASE,EAAG,IAAmB,EAC9DE,EAAgB,CAAC,KAAK,aAAa,aAAatC,EAAWoC,EAAG,MAAM,EAC1E,OAAOC,GAAkBC,CAC3B,CAAC,EAED,OAAIH,EAAY,OAAS,EAChB,CACL,eAAgB,GAChB,SAAUA,CACZ,EAIK,CACL,eAAgB,GAChB,SAAU,CAAC,EACX,cAAe,EACjB,CACF,CAWQ,iBACNhB,EACAS,EACAC,EACAC,EACQ,CAER,IAAMS,EAAYX,EAAiBC,GAAqBV,EAGlDqB,EAAgBD,EAAYT,EAAoB,IAGhDW,GAAU,KAAK,OAAO,EAAI,EAAI,GAAKD,EAGnCE,EAAa,KAAK,IAAI,EAAGH,EAAYE,CAAM,EAEjD,OAAO,KAAK,MAAMC,CAAU,CAC9B,CAKA,iBAAiB1D,EAAoC,CACnD,KAAK,eAAe,iBAAiBA,CAAa,CACpD,CACF,EChjBO,SAAS2D,GAAUC,EAAuB,CAC/C,IAAMC,EAAQD,EAAQ,MAAM,qBAAqB,EACjD,GAAI,CAACC,EACH,MAAM,IAAI,MAAM,yBAAyBD,CAAO,mBAAmB,EAGrE,GAAM,CAAC,CAAEE,EAAUC,CAAU,EAAIF,EAC3BG,EAAQ,SAASF,GAAY,IAAK,EAAE,EACpCG,EAAU,SAASF,GAAc,IAAK,EAAE,EAE9C,GAAIC,EAAQ,GAAKA,EAAQ,GACvB,MAAM,IAAI,MAAM,kBAAkBA,CAAK,4BAA4B,EAGrE,GAAIC,EAAU,GAAKA,EAAU,GAC3B,MAAM,IAAI,MAAM,oBAAoBA,CAAO,4BAA4B,EAGzE,IAAMC,EAAO,IAAI,KACjB,OAAAA,EAAK,SAASF,EAAOC,EAAS,EAAG,CAAC,EAC3BC,CACT,CAQO,SAASC,GAAeP,EAAyB,CACtD,IAAMQ,EAAaT,GAAUC,CAAO,EAC9BS,EAAM,IAAI,KAEVC,EAAa,IAAI,KAAKD,CAAG,EAC/B,OAAAC,EAAW,SAASF,EAAW,SAAS,EAAGA,EAAW,WAAW,EAAG,EAAG,CAAC,EAGpEE,GAAcD,GAChBC,EAAW,QAAQA,EAAW,QAAQ,EAAI,CAAC,EAGtCA,EAAW,QAAQ,EAAID,EAAI,QAAQ,CAC5C,CAgCO,SAASE,GAAMC,EAA2B,CAC/C,OAAO,IAAI,QAASC,GAAY,WAAWA,EAASD,CAAE,CAAC,CACzD,CCvCO,IAAME,EAAN,KAAgB,CACb,QACA,aACA,gBACA,SACA,QACA,QACA,aACA,QAAmB,GACnB,QAAmB,GACnB,cACA,cACA,aAER,YACEC,EACAC,EACAC,EACAC,EACAC,EACAC,EAA4B,CAAE,KAAM,WAAY,EAChDC,EACAC,EACAC,EACAC,EACA,CACA,KAAK,QAAUT,EACf,KAAK,aAAeC,EACpB,KAAK,gBAAkBC,EACvB,KAAK,SAAWC,EAChB,KAAK,QAAUC,EACf,KAAK,QAAUC,EACf,KAAK,cAAgBC,EACrB,KAAK,cAAgBC,EACrB,KAAK,aAAeC,GAAgB,CAAE,eAAAE,GAAgB,MAAAC,EAAM,EAG5D,IAAMC,EACJH,IACC,CAACI,EAAIC,EAAIC,EAAOC,EAAMC,EAAOC,IAAU,IAAIC,EAAaN,EAAIC,EAAIC,EAAOC,EAAMC,EAAOC,CAAK,GAE5F,KAAK,aAAeN,EAClB,KAAK,aACL,KAAK,gBACL,KAAK,SACL,KAAK,QACL,KAAK,cACL,KAAK,aACP,CACF,CAKA,MAAM,OAAuB,CAC3B,GAAI,KAAK,QACP,MAAM,IAAIQ,EAAe,8BAA8B,EASzD,GANA,KAAK,QAAU,GACf,KAAK,QAAU,GAGf,KAAK,aAAa,MAAM,EAEpB,KAAK,QAAQ,OAAS,OACxB,KAAK,SAAS,cAA+B,2CAA2C,EACxF,MAAM,KAAK,QAAQ,MACd,CACL,KAAK,SAAS,cAA+B,8CAA8C,EAE3F,IAAMC,EAAiB,KAAK,wBAAwB,EAEpD,KAAO,CAAC,KAAK,SAAS,CAEpB,IAAIC,EAA0B,KAC1BC,EAAa,OAAO,iBAExB,QAAWC,KAAaH,EAAe,KAAK,EAAG,CAC7C,IAAMI,EAAU,KAAK,aAAa,eAAeD,CAAS,EACtDC,EAAUF,IACZA,EAAaE,EACbH,EAAWE,EAEf,CAEA,GAAI,CAACF,EAAU,CACb,KAAK,SAAS,iBAAkC,2CAA2C,EAC3F,KACF,CAEA,IAAMtB,EAAUqB,EAAe,IAAIC,CAAQ,EAa3C,GAZI,CAACtB,IAGDuB,EAAa,IACf,KAAK,SAAS,cAEZ,WAAW,KAAK,MAAMA,EAAa,IAAO,EAAE,CAAC,kBAAkBD,CAAQ,KACzE,EAEA,MAAM,KAAK,aAAa,MAAMC,CAAU,GAGtC,KAAK,SAAS,MAMlB,IAHA,MAAM,KAAK,WAAWvB,CAAO,EAGtB,KAAK,aAAa,oBAAoB,GACvC,MAAK,SAET,MAAM,KAAK,aAAa,MAAM,GAAI,CAEtC,CACF,CAEA,KAAK,QAAU,EACjB,CAKA,MAAM,MAAsB,CAC1B,KAAK,SAAS,cAA+B,uBAAuB,EAEpE,KAAK,QAAU,GAGf,MAAM,KAAK,aAAa,KAAK,EAG7B,MAAM,KAAK,aAAa,KAAK,EAE7B,KAAK,QAAU,GAEf,KAAK,SAAS,cAA+B,mBAAmB,CAClE,CAKQ,yBAAuD,CAC7D,IAAM0B,EAAU,IAAI,IAEpB,QAAWC,KAAU,KAAK,QAAS,CACjC,IAAMC,EAAWF,EAAQ,IAAIC,EAAO,SAAS,GAAK,CAAC,EACnDC,EAAS,KAAKD,CAAM,EACpBD,EAAQ,IAAIC,EAAO,UAAWC,CAAQ,CACxC,CAEA,OAAOF,CACT,CAKA,MAAc,WAAW1B,EAAwC,CAE/D,QAAW2B,KAAU3B,EAAS,CAC5B,GAAI,KAAK,QAAS,MAElB,KAAK,aAAa,eAAe2B,CAAM,CACzC,CAGA,IAAME,EAAQ,KAAK,aAAa,cAAc,EAC9C,KAAK,SAAS,cAEZ,SAAS7B,EAAQ,MAAM,yCAAyC,KAAK,UAAU6B,CAAK,CAAC,EACvF,CACF,CAKA,MAAc,SAAyB,CACrC,QAAWF,KAAU,KAAK,QAAS,CACjC,GAAI,KAAK,QAAS,MAElB,KAAK,aAAa,eAAeA,CAAM,CACzC,CAGA,KAAO,KAAK,aAAa,oBAAoB,GACvC,MAAK,SAET,MAAM,KAAK,aAAa,MAAM,GAAI,EAIpC,MAAM,KAAK,aAAa,KAAK,EAC7B,KAAK,SAAS,iBAAkC,qBAAqB,CACvE,CAKA,WAAqB,CACnB,OAAO,KAAK,SAAW,CAAC,KAAK,OAC/B,CAKA,iBAAgC,CAC9B,OAAO,KAAK,YACd,CACF,EC5PA,OAAS,cAAAG,OAAkB,KAC3B,OAAS,QAAAC,OAAY,OCsCd,SAASC,GAA0B,CACxC,MAAO,CACL,QAAS,QACT,OAAQ,CAAC,EACT,YAAa,IAAI,KAAK,EAAE,YAAY,CACtC,CACF,CDpCO,IAAMC,EAAN,KAAmB,CAChB,MACA,UACA,MAAiB,GAEzB,YAAYC,EAAmB,CAC7B,KAAK,UAAYC,GAAK,QAAQ,IAAI,EAAGD,CAAS,EAC9C,KAAK,MAAQE,EAAiB,CAChC,CAKA,MAAM,MAAsB,CAC1B,GAAI,CAACC,GAAW,KAAK,SAAS,EAAG,CAC/B,KAAK,MAAQD,EAAiB,EAC9B,KAAK,MAAQ,GACb,MAAM,KAAK,KAAK,EAChB,MACF,CAEA,GAAI,CAEF,IAAME,EAAU,MADH,IAAI,KAAK,KAAK,SAAS,EACT,KAAK,EAChC,KAAK,MAAQ,KAAK,MAAMA,CAAO,EAC/B,KAAK,MAAQ,EACf,OAASC,EAAO,CACd,MAAM,IAAIC,EAAW,8BAA8BD,aAAiB,MAAQA,EAAM,QAAU,OAAOA,CAAK,CAAC,EAAE,CAC7G,CACF,CAKA,MAAM,MAAsB,CAC1B,GAAK,KAAK,MAEV,GAAI,CACF,KAAK,MAAM,YAAc,IAAI,KAAK,EAAE,YAAY,EAChD,IAAMD,EAAU,KAAK,UAAU,KAAK,MAAO,KAAM,CAAC,EAClD,MAAM,IAAI,MAAM,KAAK,UAAWA,CAAO,EACvC,KAAK,MAAQ,EACf,OAASC,EAAO,CACd,MAAM,IAAIC,EAAW,8BAA8BD,aAAiB,MAAQA,EAAM,QAAU,OAAOA,CAAK,CAAC,EAAE,CAC7G,CACF,CAKA,aAAaE,EAAmBC,EAAgC,CAC9D,IAAMC,EAAS,KAAK,MAAM,OAAOF,CAAS,EAC1C,GAAI,CAACE,EAAQ,MAAO,GAEpB,IAAMC,EAAe,OAAOF,CAAa,EAAE,SAAS,EAAG,GAAG,EAC1D,MAAO,CAAC,CAACC,EAAO,SAASC,CAAY,CACvC,CAKA,qBACEH,EACAI,EACAC,EAMM,CAED,KAAK,MAAM,OAAOL,CAAS,IAC9B,KAAK,MAAM,OAAOA,CAAS,EAAI,CAC7B,KAAMI,EACN,SAAU,CAAC,CACb,GAGF,IAAMD,EAAe,OAAOE,EAAQ,MAAM,EAAE,SAAS,EAAG,GAAG,EAGvD,KAAK,MAAM,OAAOL,CAAS,EAAE,SAASG,CAAY,IAItD,KAAK,MAAM,OAAOH,CAAS,EAAE,SAASG,CAAY,EAAI,CACpD,IAAKE,EAAQ,IACb,SAAUA,EAAQ,SAClB,aAAc,IAAI,KAAK,EAAE,YAAY,EACrC,KAAMA,EAAQ,IAChB,EAEA,KAAK,MAAQ,GACf,CAKA,kBAAkBL,EAAkD,CAElE,OADe,KAAK,MAAM,OAAOA,CAAS,GAC3B,UAAY,CAAC,CAC9B,CAKA,aAAaA,EAAyB,CAChC,KAAK,MAAM,OAAOA,CAAS,IAC7B,OAAO,KAAK,MAAM,OAAOA,CAAS,EAClC,KAAK,MAAQ,GAEjB,CAKA,kBAA6B,CAC3B,OAAO,OAAO,KAAK,KAAK,MAAM,MAAM,CACtC,CAKA,cAAcA,EAAkC,CAC9C,OAAO,KAAK,MAAM,OAAOA,CAAS,GAAG,MAAQ,IAC/C,CAKA,oBAA6B,CAC3B,IAAIM,EAAQ,EACZ,QAAWJ,KAAU,OAAO,OAAO,KAAK,MAAM,MAAM,EAClDI,GAAS,OAAO,KAAKJ,EAAO,QAAQ,EAAE,OAExC,OAAOI,CACT,CAKA,UAAiB,CACf,KAAK,MAAQX,EAAiB,EAC9B,KAAK,MAAQ,EACf,CAKA,MAAM,WAA2B,CAC/B,KAAK,MAAQ,GACb,MAAM,KAAK,KAAK,CAClB,CACF,EEnKA,OAAS,cAAAY,OAAkB,KAC3B,OAAS,YAAAC,OAAgB,cACzB,OAAS,WAAAC,OAAe,KACxB,OAAS,QAAAC,OAAY,OAoFrB,eAAsBC,GAAeC,EAAqC,CACxE,GAAI,CAACC,GAAWD,CAAU,EACxB,MAAM,IAAIE,EAAY,2BAA2BF,CAAU,GAAG,EAMhE,IAAMG,GAHU,MAAMC,GAASJ,EAAY,OAAO,GAG5B,MAAM;AAAA,CAAI,EAC1BK,EAAoB,CAAC,EAE3B,QAAWC,KAAQH,EAAO,CAExB,IAAMI,EAAcD,EAAK,KAAK,EAC9B,GAAIC,EAAY,WAAW,GAAG,GAAK,CAACA,EAAa,SAEjD,IAAMC,EAASF,EAAK,MAAM,GAAI,EAC9B,GAAIE,EAAO,QAAU,EAAG,CACtB,IAAMC,EAAOD,EAAO,CAAC,EACfE,EAAQF,EAAO,CAAC,EAEtB,GAAIC,GAAQC,EAAO,CACjB,IAAMC,EAAaD,EAAM,KAAK,EAC1BC,GACFN,EAAQ,KAAK,GAAGI,CAAI,IAAIE,CAAU,EAAE,CAExC,CACF,CACF,CAEA,OAAON,EAAQ,KAAK,IAAI,CAC1B,CzB7EA,IAAMO,GAAuC,CAC3C,WAAAC,GACA,oBAAqBC,EAAgB,oBACrC,eAAAC,GACA,mBAAqBC,GAAS,IAAIC,EAAaD,CAAI,EACnD,sBAAuB,CAACE,EAAIC,EAAGC,EAAKC,EAAIC,IAAS,IAAIR,EAAgBI,EAAIC,EAAGC,EAAKC,EAAIC,CAAI,EACzF,gBAAiB,CAACC,EAAGL,EAAIM,EAAIL,EAAGM,EAAMC,EAAKC,EAAIC,IAAO,IAAIC,EAAUN,EAAGL,EAAIM,EAAIL,EAAGM,EAAMC,EAAKC,EAAIC,CAAE,CACrG,EAKA,eAAsBE,GAAeC,EAAsBC,EAA2C,CACpGC,EAAO,KAAK,6BAA6B,EAEzC,GAAI,CACF,MAAMF,EAAU,KAAK,EACrB,MAAMC,EAAa,KAAK,EACxBC,EAAO,QAAQ,mBAAmB,CACpC,OAASC,EAAO,CACdD,EAAO,MAAM,0BAA0BC,aAAiB,MAAQA,EAAM,QAAU,OAAOA,CAAK,CAAC,EAAE,CACjG,CACF,CAEA,eAAsBC,GACpBC,EACAC,EACAC,EAAwB1B,GACT,CAOf,GANAqB,EAAO,KAAK,SAASI,IAAS,OAAS,kCAAoC,iCAAiC,EAAE,EAG9GJ,EAAO,KAAK,iCAAiC,EAGzC,CAFmB,MAAMK,EAAK,oBAAoB,EAGpD,MAAM,IAAI,MACR;AAAA;AAAA;AAAA,mCAIF,EAIFL,EAAO,KAAK,8BAA8BG,CAAU,KAAK,EACzD,IAAMG,EAAS,MAAMD,EAAK,WAAWF,CAAU,EAC/CH,EAAO,QAAQ,sBAAsB,EAGrC,IAAMD,EAAeM,EAAK,mBAAmBC,EAAO,SAAS,EAC7D,MAAMP,EAAa,KAAK,EACxBC,EAAO,KAAK,iBAAiBD,EAAa,mBAAmB,CAAC,sBAAsB,EAGpF,IAAMQ,EAAuD,CAAC,IAAIC,CAAiB,EAEnF,GAAIF,EAAO,SACT,GAAI,CACFC,EAAU,KAAK,IAAIE,EAAiBH,EAAO,QAAQ,CAAC,EACpDN,EAAO,KAAK,2CAA2C,CACzD,OAASC,EAAO,CACdD,EAAO,QAAQ,8BAA8BC,aAAiB,MAAQA,EAAM,QAAU,OAAOA,CAAK,CAAC,EAAE,CACvG,CAIF,IAAMS,EAAqB,CACzB,OAAQ,MAAOC,EAA0BC,IAAmC,CAC1E,MAAM,QAAQ,IAAIL,EAAU,IAAKrB,GAAMA,EAAE,OAAOyB,EAAOC,CAAO,CAAC,CAAC,CAClE,EACA,SAAWA,GAA0B,CACnC,QAAW1B,KAAKqB,EACdrB,EAAE,SAAS0B,CAAO,CAEtB,EACA,YAAa,IAAY,CACvB,QAAW1B,KAAKqB,EACdrB,EAAE,YAAY,CAElB,CACF,EAGA2B,EAAgB,SAAS,IAAIC,CAAa,EAC1CD,EAAgB,SAAS,IAAIE,CAAc,EAC3Cf,EAAO,KAAK,wBAAwBa,EAAgB,WAAW,EAAE,KAAK,IAAI,CAAC,EAAE,EAG7E,IAAIG,EACJ,GAAIV,EAAO,WACT,GAAI,CACFU,EAAU,MAAMX,EAAK,eAAeC,EAAO,UAAU,EACrDN,EAAO,QAAQ,0BAA0B,CAC3C,OAASC,EAAO,CACdD,EAAO,QAAQ,2BAA2BC,aAAiB,MAAQA,EAAM,QAAU,OAAOA,CAAK,CAAC,EAAE,CACpG,CAIF,IAAMgB,EAAcX,EAAO,eAAe,UAAU,aAAeY,GAC7DC,EAAUb,EAAO,eAAe,UAAU,QAC1Cc,EAAkBf,EAAK,sBAAsBN,EAAcW,EAAUO,EAAaX,EAAO,WAAYa,CAAO,EAGlHnB,EAAO,KAAK,6BAA6B,EACzC,IAAMF,EAAYO,EAAK,gBACrBC,EAAO,OACPP,EACAqB,EACAV,EACAM,EACA,CAAE,KAAAZ,CAAK,EACPE,EAAO,cACPA,EAAO,aACT,EAGA,QAAQ,GAAG,SAAU,SAAY,CAC/B,MAAMT,GAAeC,EAAWC,CAAY,EAC5C,QAAQ,KAAK,CAAC,CAChB,CAAC,EACD,QAAQ,GAAG,UAAW,SAAY,CAChC,MAAMF,GAAeC,EAAWC,CAAY,EAC5C,QAAQ,KAAK,CAAC,CAChB,CAAC,EAGD,MAAMD,EAAU,MAAM,CACxB,CAGO,IAAMuB,GAAMC,GAAQ,CACzB,KAAM,SACN,YAAa,mDACb,QAAS,QACT,KAAM,CACJ,OAAQC,GAAO,CACb,KAAMC,GACN,KAAM,SACN,MAAO,IACP,aAAc,IAAM,gBACpB,YAAa,qDACf,CAAC,EACD,KAAMC,GAAK,CACT,KAAMC,GACN,KAAM,OACN,MAAO,IACP,YAAa,8CACf,CAAC,CACH,EACA,QAAS,MAAO,CAAE,OAAApB,EAAQ,KAAAqB,CAAK,IAAyC,CACtE,GAAI,CAEF,MAAMzB,GAAOI,EADeqB,EAAO,OAAS,WACnB,CAC3B,OAAS1B,EAAO,CACVA,aAAiB2B,EACnB5B,EAAO,MAAM,wBAAwBC,EAAM,OAAO,EAAE,EAEpDD,EAAO,MAAM,gBAAgBC,aAAiB,MAAQA,EAAM,QAAU,OAAOA,CAAK,CAAC,EAAE,EAEvF,QAAQ,KAAK,CAAC,CAChB,CACF,CACF,CAAC,EDvMD,eAAsB4B,GAAKC,EAAiB,QAAQ,KAAK,MAAM,CAAC,EAAkB,CAChF,MAAMC,GAAIC,GAAKF,CAAI,CACrB,CAGI,YAAY,MACd,MAAMD,GAAK","names":["run","boolean","command","flag","option","string","DEFAULT_CHECK_SETTINGS","DEFAULT_DOWNLOAD_SETTINGS","DEFAULT_DOWNLOAD_TYPES_ENUM","DEFAULT_DOWNLOAD_DIR","existsSync","join","yaml","WetvloError","message","ConfigError","StateError","HandlerError","url","DownloadError","NotificationError","CookieError","SchedulerError","resolveEnv","value","_match","varName","envValue","resolveEnvRecursive","obj","item","result","key","z","EpisodeTypeSchema","CheckSettingsSchema","DownloadSettingsSchema","TelegramConfigSchema","DomainConfigSchema","SeriesConfigSchema","GlobalConfigsSchema","BrowserSchema","ConfigSchema","validateConfig","rawConfig","DEFAULT_CONFIG_PATH","loadConfig","configPath","absolutePath","join","existsSync","ConfigError","content","rawConfig","error","validateConfig","resolveEnvRecursive","fs","fsPromises","basename","join","resolve","execa","sanitizeFilename","name","execa","colors","Logger","config","level","message","text","color","levels","logger","getVideoDuration","filePath","stdout","execa","duration","error","logger","DownloadManager","stateManager","notifier","downloadDir","cookieFile","tempDir","resolve","seriesUrl","seriesName","episode","minDuration","result","fileSize","fullPath","duration","getVideoDuration","file","absFile","fileName","basename","newPath","join","e","error","message","DownloadError","files","paddedNumber","targetDir","sanitizedSeriesName","sanitizeFilename","args","filename","allFiles","outputBuffer","subprocess","execa","line","text","destMatch","subMatch","mergeMatch","progressMatch","percentage","totalSize","speed","eta","err","stderr","stdout","allOutput","bytes","units","size","unit","extractDomain","url","Registry","handler","url","domain","extractDomain","handlerDomain","HandlerError","handlerRegistry","cheerio","BaseHandler","url","domain","extractDomain","cookies","headers","response","HandlerError","error","html","text","chineseMatch","epMatch","episodeMatch","numberMatch","element","$","$el","className","IQiyiHandler","BaseHandler","url","cookies","html","$","episodes","selectors","selector","elements","_","element","$el","link","href","episodeUrl","combinedText","episodeNumber","ep","a","b","WeTVHandler","BaseHandler","url","cookies","html","$","episodes","episodeLinks","_","el","element","a","b","$el","href","episodeUrl","ariaLabel","episodeNumber","ep","type","$li","badge","badgeText","liText","ConsoleNotifier","level","message","logger","TelegramNotifier","config","level","message","formattedMessage","response","errorText","NotificationError","error","_message","ConfigResolver","domainConfigs","globalConfigs","c","series","domain","extractDomain","domainConfig","config","global","defaults","DEFAULT_CHECK_SETTINGS","DEFAULT_DOWNLOAD_SETTINGS","TypedQueue","cooldownMs","task","delay","addedAt","now","head","time","UniversalScheduler","executor","callback","typeName","cooldownMs","queue","TypedQueue","task","delay","actualCooldown","next","now","waitMs","queueNames","i","index","queueName","error","nextTime","result","name","stats","status","total","QueueManager","stateManager","downloadManager","notifier","_cookieFile","domainConfigs","globalConfigs","schedulerFactory","ConfigResolver","createScheduler","executor","UniversalScheduler","task","queueName","waitMs","seconds","parts","type","domain","config","extractDomain","item","seriesUrl","seriesName","episodes","resolvedConfig","downloadDelay","i","episode","delayMs","stats","checkQueues","downloadQueues","queueStats","checkQueueName","downloadQueueName","existingStats","handler","handlerRegistry","checkInterval","attemptNumber","retryCount","checksCount","result","intervalMs","requeueDelay","requeuedItem","error","errorMessage","maxRetries","initialTimeout","backoffMultiplier","jitterPercentage","retryDelay","minDuration","_seriesName","downloadTypes","newEpisodes","ep","shouldDownload","notDownloaded","baseDelay","jitterAmount","jitter","finalDelay","parseTime","timeStr","match","hoursStr","minutesStr","hours","minutes","date","getMsUntilTime","targetTime","now","targetDate","sleep","ms","resolve","Scheduler","configs","stateManager","downloadManager","notifier","cookies","options","globalConfigs","domainConfigs","timeProvider","queueManagerFactory","getMsUntilTime","sleep","createQueueManager","sm","dm","notif","cook","dConf","gConf","QueueManager","SchedulerError","groupedConfigs","nextTime","minMsUntil","startTime","msUntil","grouped","config","existing","stats","existsSync","join","createEmptyState","StateManager","statePath","join","createEmptyState","existsSync","content","error","StateError","seriesUrl","episodeNumber","series","paddedNumber","seriesName","episode","count","existsSync","readFile","homedir","join","readCookieFile","cookieFile","existsSync","CookieError","lines","readFile","cookies","line","trimmedLine","fields","name","value","cleanValue","defaultDependencies","loadConfig","DownloadManager","readCookieFile","path","StateManager","sm","n","dir","cf","temp","c","dm","cook","opt","gc","dc","Scheduler","handleShutdown","scheduler","stateManager","logger","error","runApp","configPath","mode","deps","config","notifiers","ConsoleNotifier","TelegramNotifier","notifier","level","message","handlerRegistry","WeTVHandler","IQiyiHandler","cookies","downloadDir","DEFAULT_DOWNLOAD_DIR","tempDir","downloadManager","cli","command","option","string","flag","boolean","once","ConfigError","main","args","run","cli"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wetvlo",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "CLI application for monitoring and downloading TV series episodes from Chinese video sites (wetv.vip, iq.com)",
|
|
5
|
+
"module": "dist/index.js",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"bin": "./dist/index.js",
|
|
10
|
+
"exports": "./dist/index.js",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"start": "bun run src/index.ts",
|
|
13
|
+
"start:once": "bun run src/index.ts --once",
|
|
14
|
+
"build": "tsup",
|
|
15
|
+
"lint": "biome check . && dprint check",
|
|
16
|
+
"format": "biome check --write --unsafe . && dprint fmt",
|
|
17
|
+
"typecheck": "tsc --noEmit",
|
|
18
|
+
"test": "bun test"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@biomejs/biome": "^2.3.11",
|
|
22
|
+
"@types/bun": "^1.3.6",
|
|
23
|
+
"@types/js-yaml": "^4.0.9",
|
|
24
|
+
"dprint": "^0.51.1",
|
|
25
|
+
"lefthook": "^2.0.15",
|
|
26
|
+
"tsup": "^8.5.1",
|
|
27
|
+
"typescript": "^5.9.3"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"keywords": [
|
|
35
|
+
"video",
|
|
36
|
+
"downloader",
|
|
37
|
+
"yt-dlp",
|
|
38
|
+
"wetv",
|
|
39
|
+
"iqiyi",
|
|
40
|
+
"chinese",
|
|
41
|
+
"tv-series",
|
|
42
|
+
"cli"
|
|
43
|
+
],
|
|
44
|
+
"author": "Maxim (neiromaster) Gavrilenko",
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/neiromaster/wetvlo.git"
|
|
49
|
+
},
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/neiromaster/wetvlo/issues"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://github.com/neiromaster/wetvlo#readme",
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"cheerio": "^1.1.2",
|
|
56
|
+
"cmd-ts": "^0.14.3",
|
|
57
|
+
"execa": "^9.6.1",
|
|
58
|
+
"js-yaml": "^4.1.1",
|
|
59
|
+
"zod": "^4.3.6"
|
|
60
|
+
},
|
|
61
|
+
"publishConfig": {
|
|
62
|
+
"access": "public",
|
|
63
|
+
"provenance": true
|
|
64
|
+
}
|
|
65
|
+
}
|