From 0b07920529423669487f1c55426ad12dc09b0199 Mon Sep 17 00:00:00 2001 From: 8nt0n Date: Sun, 2 Aug 2026 23:51:12 +0200 Subject: [PATCH 1/7] fix: update proxy status handling (#115) --- moon_engine.py | 46 +++++++++++++++++++++++++++++++++++++++++++--- web/app.js | 13 +++++++++++-- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/moon_engine.py b/moon_engine.py index fe7a78d..9bb9ee5 100644 --- a/moon_engine.py +++ b/moon_engine.py @@ -88,6 +88,8 @@ print(f"datanodes: up to {DN_LANES} persistent browser window(s) " "(set MOON_DN_LANES to change)") + + class Engine: def __init__(self): @@ -138,6 +140,11 @@ def __init__(self): self._loop = None self._gate = None + self.proxy_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "proxies.txt") + self._proxy_mtime = 0.0 + self._proxy_count = 0 + self._proxy_status = "none_configured" + def _inc(self, attr, delta=1): with self._lock: setattr(self, attr, getattr(self, attr) + delta) @@ -515,8 +522,7 @@ def start(self, cfg: dict) -> dict: api_key=eff["dn_apikey"], captcha_wait=eff["dn_captcha"]) - proxy_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "proxies.txt") - self._proxies, skipped = _PROXY_POOL.load(proxy_path, is_default=True) + self._proxies, skipped = _PROXY_POOL.load(self.proxy_path, is_default=True) n, d, r = eff["workers"], eff["dl_streams"], eff["retries"] self.log(f"▶ {len(urls)} links · {n} extractors · {d} streams · {r} retries · {VERSION}", "info") @@ -622,6 +628,12 @@ def snapshot(self, cursor: int = 0) -> dict: kills = self._kills; dls = self._dls snap = list(self._bytes_acc) + if not running: + proxy_status, proxy_count = self._get_proxy_status() + else: + proxy_count = self._proxies + proxy_status = "loaded" if proxy_count > 0 else "none_configured" + now = time.monotonic() recent = [(t, b) for t, b in snap if t > now - 3.0] if len(recent) > 1: @@ -670,7 +682,11 @@ def snapshot(self, cursor: int = 0) -> dict: "files": self._files_payload(), "log": lines, "cursor": new_cursor, - "proxies": self._proxies, + 'proxies': self._get_proxy_status(), + "proxy_info": { + "status": proxy_status, + "count": proxy_count + }, "tmp": self.scan_tmp() if not running else None, } @@ -680,6 +696,30 @@ def clear_files(self) -> dict: self._tracked.pop(url, None) return {"ok": True} + def _get_proxy_status(self): + # 1. Handle missing file + if not os.path.exists(self.proxy_path): + self._proxy_mtime = 0.0 + self._proxy_count = 0 + self._proxy_status = "none_configured" + return {"status": self._proxy_status, "count": self._proxy_count} + + # 2. File exists: was it modified? + mtime = os.path.getmtime(self.proxy_path) + if mtime > self._proxy_mtime: # File changed -> Update timestamp and proxies + self._proxy_mtime = mtime + proxies, skipped = _PROXY_POOL.load(self.proxy_path, is_default=True) + # Handle whether load() returned an integer count or a list + self._proxy_count = proxies if isinstance(proxies, int) else len(proxies) + + # 3. Distinguish 0 valid proxies vs N valid proxies + if self._proxy_count == 0: + self._proxy_status = "empty_file" + else: + self._proxy_status = "loaded" + + return {"status": self._proxy_status, "count": self._proxy_count} + # ── entry point ───────────────────────────────────────────────────────────── # There is no GUI in here. Start the app with: python moon_bridge.py if __name__ == "__main__": diff --git a/web/app.js b/web/app.js index 3ead171..1218026 100644 --- a/web/app.js +++ b/web/app.js @@ -703,8 +703,17 @@ function initTabs() { function setProxies(n) { ui.lastProxies = n; const chip = $("#proxyChip"); - chip.textContent = n ? T("proxy_n", n) : T("no_proxy"); - chip.className = n ? "chip mint" : "chip"; + + const status = n?.status ?? (Array.isArray(n) ? n[0] : null); + const count = n?.count ?? (Array.isArray(n) ? n[1] : 0); + + if (status === "none_configured") { // File doesn't exist + chip.textContent = T("no_proxy"); + } else if (status === "empty_file") { // File exists, but 0 valid lines + chip.textContent = "0 valid proxies found"; // am just gonna use a hardcoded string for now, someone else add translation keys later? + } else if (status === "loaded") { // File exists and has N usable proxies + chip.textContent = T("proxy_n", count); + } } function setTmp(n) { From c40c102c99c0d0ea6eff6f1db1f37c0188e83f1c Mon Sep 17 00:00:00 2001 From: 8nt0n Date: Mon, 3 Aug 2026 00:03:00 +0200 Subject: [PATCH 2/7] style: remove trailing whitespace --- moon_engine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moon_engine.py b/moon_engine.py index 9bb9ee5..e455fb3 100644 --- a/moon_engine.py +++ b/moon_engine.py @@ -684,7 +684,7 @@ def snapshot(self, cursor: int = 0) -> dict: "cursor": new_cursor, 'proxies': self._get_proxy_status(), "proxy_info": { - "status": proxy_status, + "status": proxy_status, "count": proxy_count }, "tmp": self.scan_tmp() if not running else None, From a06b623abb081b895a8e623b8270cdf8c0ccf072 Mon Sep 17 00:00:00 2001 From: 8nt0n Date: Mon, 3 Aug 2026 00:06:31 +0200 Subject: [PATCH 3/7] style: more syntax stuff (thx ruff) --- moon_engine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moon_engine.py b/moon_engine.py index e455fb3..2f4b0f5 100644 --- a/moon_engine.py +++ b/moon_engine.py @@ -711,7 +711,7 @@ def _get_proxy_status(self): proxies, skipped = _PROXY_POOL.load(self.proxy_path, is_default=True) # Handle whether load() returned an integer count or a list self._proxy_count = proxies if isinstance(proxies, int) else len(proxies) - + # 3. Distinguish 0 valid proxies vs N valid proxies if self._proxy_count == 0: self._proxy_status = "empty_file" From 975ba9ff5a3d38f721e68c41203a2c420d355bed Mon Sep 17 00:00:00 2001 From: 8nt0n Date: Mon, 3 Aug 2026 00:10:30 +0200 Subject: [PATCH 4/7] style: aaand more syntax stuff --- moon_engine.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/moon_engine.py b/moon_engine.py index 2f4b0f5..fb18fba 100644 --- a/moon_engine.py +++ b/moon_engine.py @@ -703,7 +703,6 @@ def _get_proxy_status(self): self._proxy_count = 0 self._proxy_status = "none_configured" return {"status": self._proxy_status, "count": self._proxy_count} - # 2. File exists: was it modified? mtime = os.path.getmtime(self.proxy_path) if mtime > self._proxy_mtime: # File changed -> Update timestamp and proxies @@ -711,13 +710,11 @@ def _get_proxy_status(self): proxies, skipped = _PROXY_POOL.load(self.proxy_path, is_default=True) # Handle whether load() returned an integer count or a list self._proxy_count = proxies if isinstance(proxies, int) else len(proxies) - # 3. Distinguish 0 valid proxies vs N valid proxies if self._proxy_count == 0: self._proxy_status = "empty_file" else: self._proxy_status = "loaded" - return {"status": self._proxy_status, "count": self._proxy_count} # ── entry point ───────────────────────────────────────────────────────────── From 8c0fce149b2c9398e0b3e5a16f38ece1dd0a85df Mon Sep 17 00:00:00 2001 From: 8nt0n Date: Mon, 3 Aug 2026 15:28:32 +0200 Subject: [PATCH 5/7] fix: address PR review feedback for proxy API and GUI translations --- moon_engine.py | 52 +++++++++++++++++++++++++++++++------------------- web/app.js | 47 ++++++++++++++++++++++++++++++--------------- 2 files changed, 64 insertions(+), 35 deletions(-) diff --git a/moon_engine.py b/moon_engine.py index fb18fba..0b32ef7 100644 --- a/moon_engine.py +++ b/moon_engine.py @@ -88,8 +88,6 @@ print(f"datanodes: up to {DN_LANES} persistent browser window(s) " "(set MOON_DN_LANES to change)") - - class Engine: def __init__(self): @@ -142,8 +140,8 @@ def __init__(self): self.proxy_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "proxies.txt") self._proxy_mtime = 0.0 - self._proxy_count = 0 self._proxy_status = "none_configured" + self._last_proxy_check = 0.0 def _inc(self, attr, delta=1): with self._lock: setattr(self, attr, getattr(self, attr) + delta) @@ -629,10 +627,15 @@ def snapshot(self, cursor: int = 0) -> dict: snap = list(self._bytes_acc) if not running: - proxy_status, proxy_count = self._get_proxy_status() + self._get_proxy_status() else: proxy_count = self._proxies - proxy_status = "loaded" if proxy_count > 0 else "none_configured" + if proxy_count > 0: + self._proxy_status = "loaded" + elif not os.path.exists(self.proxy_path): + self._proxy_status = "none_configured" + else: + self._proxy_status = "empty_file" now = time.monotonic() recent = [(t, b) for t, b in snap if t > now - 3.0] @@ -682,10 +685,10 @@ def snapshot(self, cursor: int = 0) -> dict: "files": self._files_payload(), "log": lines, "cursor": new_cursor, - 'proxies': self._get_proxy_status(), + "proxies": self._proxies, "proxy_info": { - "status": proxy_status, - "count": proxy_count + "status": self._proxy_status, + "count": self._proxies }, "tmp": self.scan_tmp() if not running else None, } @@ -697,25 +700,34 @@ def clear_files(self) -> dict: return {"ok": True} def _get_proxy_status(self): + # Only do disk i/o every 2 seconds + now = time.time() + if now - self._last_proxy_check < 2.0: + return + self._last_proxy_check = now + # 1. Handle missing file if not os.path.exists(self.proxy_path): self._proxy_mtime = 0.0 - self._proxy_count = 0 + self._proxies = 0 self._proxy_status = "none_configured" - return {"status": self._proxy_status, "count": self._proxy_count} + return # 2. File exists: was it modified? mtime = os.path.getmtime(self.proxy_path) - if mtime > self._proxy_mtime: # File changed -> Update timestamp and proxies + if mtime > self._proxy_mtime: self._proxy_mtime = mtime - proxies, skipped = _PROXY_POOL.load(self.proxy_path, is_default=True) - # Handle whether load() returned an integer count or a list - self._proxy_count = proxies if isinstance(proxies, int) else len(proxies) - # 3. Distinguish 0 valid proxies vs N valid proxies - if self._proxy_count == 0: - self._proxy_status = "empty_file" - else: - self._proxy_status = "loaded" - return {"status": self._proxy_status, "count": self._proxy_count} + try: + with open(self.proxy_path, "r", encoding="utf-8") as f: + # Update self._proxies directly! + self._proxies = sum(1 for line in f if line.strip() and not line.lstrip().startswith("#")) + except OSError: + self._proxies = 0 + # 3. Distinguish 0 valid proxies vs N valid proxies + if self._proxies == 0: + self._proxy_status = "empty_file" + else: + self._proxy_status = "loaded" + return # ── entry point ───────────────────────────────────────────────────────────── # There is no GUI in here. Start the app with: python moon_bridge.py diff --git a/web/app.js b/web/app.js index 1218026..e220cd4 100644 --- a/web/app.js +++ b/web/app.js @@ -52,7 +52,7 @@ const I18N = { st_queue: "queued", st_extract: "extracting", st_download: "downloading", st_ok: "saved", st_fail: "failed", st_kill: "restarting", chip_auto: "auto", chip_chrome: "chrome", chip_api: "api key", - no_proxy: "no proxy", + no_proxy: "no proxy", empty_proxies: "0 valid proxies found", filter_ph: "filter", f_all: "All", f_active: "Active", f_ok: "Saved", f_fail: "Failed", filter_none: "nothing matches", filter_none_sub: "clear the filter to see the rest", drop_title: "drop links or a .txt", drop_sub: "they are appended to the list", @@ -105,7 +105,7 @@ const I18N = { st_queue: "in coda", st_extract: "estrazione", st_download: "download", st_ok: "salvato", st_fail: "errore", st_kill: "riavvio", chip_auto: "auto", chip_chrome: "chrome", chip_api: "api key", - no_proxy: "no proxy", + no_proxy: "no proxy", empty_proxies: "0 proxy validi trovati", filter_ph: "filtra", f_all: "Tutti", f_active: "Attivi", f_ok: "Salvati", f_fail: "Errori", filter_none: "nessuna corrispondenza", filter_none_sub: "azzera il filtro per rivedere il resto", drop_title: "trascina link o un .txt", drop_sub: "vengono aggiunti in fondo alla lista", @@ -160,7 +160,7 @@ function applyLang(lang) { ui.prevState.clear(); renderFiles(ui.lastFiles); } - if (ui.lastProxies != null) setProxies(ui.lastProxies); + if (ui.lastProxies != null) setProxies(); if (ui.lastTmp != null) setTmp(ui.lastTmp); } @@ -700,19 +700,24 @@ function initTabs() { } /* ── chips ────────────────────────────────────────────────────────────── */ -function setProxies(n) { - ui.lastProxies = n; - const chip = $("#proxyChip"); +function setProxies(info) { + // If called empty (language swap etc.), use last info + if (!info) info = ui.lastProxyInfo; + if (!info) return; // if we still have no info (eg. first millisecond of startup) stop here. + ui.lastProxyInfo = info; + - const status = n?.status ?? (Array.isArray(n) ? n[0] : null); - const count = n?.count ?? (Array.isArray(n) ? n[1] : 0); + const chip = $("#proxyChip"); - if (status === "none_configured") { // File doesn't exist - chip.textContent = T("no_proxy"); - } else if (status === "empty_file") { // File exists, but 0 valid lines - chip.textContent = "0 valid proxies found"; // am just gonna use a hardcoded string for now, someone else add translation keys later? - } else if (status === "loaded") { // File exists and has N usable proxies - chip.textContent = T("proxy_n", count); + if (info.status === "none_configured") { + chip.textContent = T("no_proxy"); + chip.className = "chip"; + } else if (info.status === "empty_file") { + chip.textContent = T("empty_proxies"); + chip.className = "chip warn"; + } else if (info.status === "loaded") { + chip.textContent = T("proxy_n", info.count); + chip.className = "chip mint"; } } @@ -1004,7 +1009,19 @@ async function poll() { if (snap.metrics) renderMetrics(snap.metrics); if (snap.files) renderFiles(snap.files); if (snap.log && snap.log.length) { appendLog(snap.log); ui.cursor = snap.cursor; } - if (snap.proxies != null && snap.proxies !== ui.lastProxies) setProxies(snap.proxies); + if ( + snap.proxy_info && + ( + snap.proxy_info.status !== ui.lastProxyStatus || + snap.proxies !== ui.lastProxies || + LANG !== ui.lastLang + ) + ) { + ui.lastProxyStatus = snap.proxy_info.status; + ui.lastProxies = snap.proxies; + ui.lastLang = LANG; + setProxies(snap.proxy_info); + } if (snap.tmp != null && snap.tmp !== ui.lastTmp) setTmp(snap.tmp); if (snap.error) toast(snap.error, true); } From 2dafdf82e38a2047b6ce2667166d20fa1f5a02fe Mon Sep 17 00:00:00 2001 From: 8nt0n Date: Wed, 5 Aug 2026 13:33:45 +0200 Subject: [PATCH 6/7] fix: address PR review feedback for duplicate proxy parsing, monotonic time and style guidelines --- moon_download.py | 62 +++++++++++++++++++++++++++++++----------------- moon_engine.py | 8 +++---- web/app.js | 13 +++++----- 3 files changed, 50 insertions(+), 33 deletions(-) diff --git a/moon_download.py b/moon_download.py index 0269877..7a3bcfd 100644 --- a/moon_download.py +++ b/moon_download.py @@ -99,6 +99,41 @@ async def _close_sess(): _moon_extract._sess = _sess _moon_extract.USER_AGENTS = USER_AGENTS +def parse_proxy_line(line: str) -> dict | None: + try: + if line.startswith(("http://", "https://", "socks")): + return {"url": line, "auth": None} + parts = line.split(":") + if len(parts) == 4: + if re.match(r"^\d+\.\d+\.\d+\.\d+$", parts[0]): + ip, port, user, passwd = parts + else: + user, passwd, ip, port = parts + return { + "url": f"http://{ip}:{port}", + "auth": aiohttp.BasicAuth(user, passwd), + } + elif len(parts) == 2: + ip, port = parts + return {"url": f"http://{ip}:{port}", "auth": None} + except Exception: + pass + return None + +def count_usable_proxies(path: str) -> tuple[int, int]: + if not os.path.exists(path): + return 0, 0 + usable, skipped = 0, 0 + with open(path, encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + if parse_proxy_line(line) is not None: + usable += 1 + else: + skipped += 1 + return usable, skipped class ProxyPool: def __init__(self): @@ -119,29 +154,12 @@ def load(self, path: str, is_default: bool = False) -> tuple[int, int]: line = line.strip() if not line or line.startswith("#"): continue - try: - if line.startswith(("http://", "https://", "socks")): - loaded.append({"url": line, "auth": None}) - else: - parts = line.split(":") - if len(parts) == 4: - if re.match(r"^\d+\.\d+\.\d+\.\d+$", parts[0]): - ip, port, user, passwd = parts - else: - user, passwd, ip, port = parts - loaded.append({ - "url": f"http://{ip}:{port}", - "auth": aiohttp.BasicAuth(user, passwd), - }) - elif len(parts) == 2: - ip, port = parts - loaded.append({"url": f"http://{ip}:{port}", "auth": None}) - else: - skipped += 1 - except Exception: - # Skip line if proxy parsing or auth formatting fails + parsed = parse_proxy_line(line) + if parsed is not None: + loaded.append(parsed) + else: skipped += 1 - continue + self.proxies = loaded if not loaded: print(f"WARNING: proxy file {path} yielded 0 proxies") diff --git a/moon_engine.py b/moon_engine.py index 0b32ef7..8b1b371 100644 --- a/moon_engine.py +++ b/moon_engine.py @@ -40,6 +40,7 @@ _close_sess, _sanitize_filename, download_file, + count_usable_proxies, ) # ── THEME ────────────────────────────────────────────────────────────────────── @@ -701,7 +702,7 @@ def clear_files(self) -> dict: def _get_proxy_status(self): # Only do disk i/o every 2 seconds - now = time.time() + now = time.monotonic() if now - self._last_proxy_check < 2.0: return self._last_proxy_check = now @@ -717,9 +718,8 @@ def _get_proxy_status(self): if mtime > self._proxy_mtime: self._proxy_mtime = mtime try: - with open(self.proxy_path, "r", encoding="utf-8") as f: - # Update self._proxies directly! - self._proxies = sum(1 for line in f if line.strip() and not line.lstrip().startswith("#")) + usable, skipped = count_usable_proxies(self.proxy_path) + self._proxies = usable except OSError: self._proxies = 0 # 3. Distinguish 0 valid proxies vs N valid proxies diff --git a/web/app.js b/web/app.js index e220cd4..07f00dc 100644 --- a/web/app.js +++ b/web/app.js @@ -703,19 +703,18 @@ function initTabs() { function setProxies(info) { // If called empty (language swap etc.), use last info if (!info) info = ui.lastProxyInfo; - if (!info) return; // if we still have no info (eg. first millisecond of startup) stop here. + if (!info) return; // if we still have no info (eg. first millisecond of startup) stop here. ui.lastProxyInfo = info; - const chip = $("#proxyChip"); - if (info.status === "none_configured") { - chip.textContent = T("no_proxy"); - chip.className = "chip"; - } else if (info.status === "empty_file") { + if (info.status === "none_configured") { + chip.textContent = T("no_proxy"); + chip.className = "chip"; + } else if (info.status === "empty_file") { chip.textContent = T("empty_proxies"); chip.className = "chip warn"; - } else if (info.status === "loaded") { + } else if (info.status === "loaded") { chip.textContent = T("proxy_n", info.count); chip.className = "chip mint"; } From da6e3221904d9c4eabe8f881dbb5520eb7dec3cb Mon Sep 17 00:00:00 2001 From: 8nt0n Date: Wed, 5 Aug 2026 13:39:10 +0200 Subject: [PATCH 7/7] fix: removed whitespace from blank line --- moon_download.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moon_download.py b/moon_download.py index 7a3bcfd..4584c6c 100644 --- a/moon_download.py +++ b/moon_download.py @@ -159,7 +159,7 @@ def load(self, path: str, is_default: bool = False) -> tuple[int, int]: loaded.append(parsed) else: skipped += 1 - + self.proxies = loaded if not loaded: print(f"WARNING: proxy file {path} yielded 0 proxies")