diff --git a/moon_download.py b/moon_download.py index 0269877..4584c6c 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 fe7a78d..8b1b371 100644 --- a/moon_engine.py +++ b/moon_engine.py @@ -40,6 +40,7 @@ _close_sess, _sanitize_filename, download_file, + count_usable_proxies, ) # ── THEME ────────────────────────────────────────────────────────────────────── @@ -138,6 +139,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_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) @@ -515,8 +521,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 +627,17 @@ def snapshot(self, cursor: int = 0) -> dict: kills = self._kills; dls = self._dls snap = list(self._bytes_acc) + if not running: + self._get_proxy_status() + else: + proxy_count = self._proxies + 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] if len(recent) > 1: @@ -671,6 +687,10 @@ def snapshot(self, cursor: int = 0) -> dict: "log": lines, "cursor": new_cursor, "proxies": self._proxies, + "proxy_info": { + "status": self._proxy_status, + "count": self._proxies + }, "tmp": self.scan_tmp() if not running else None, } @@ -680,6 +700,35 @@ def clear_files(self) -> dict: self._tracked.pop(url, None) return {"ok": True} + def _get_proxy_status(self): + # Only do disk i/o every 2 seconds + now = time.monotonic() + 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._proxies = 0 + self._proxy_status = "none_configured" + return + # 2. File exists: was it modified? + mtime = os.path.getmtime(self.proxy_path) + if mtime > self._proxy_mtime: + self._proxy_mtime = mtime + try: + 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 + 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 if __name__ == "__main__": diff --git a/web/app.js b/web/app.js index 3ead171..07f00dc 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,11 +700,24 @@ function initTabs() { } /* ── chips ────────────────────────────────────────────────────────────── */ -function setProxies(n) { - ui.lastProxies = n; +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 chip = $("#proxyChip"); - chip.textContent = n ? T("proxy_n", n) : T("no_proxy"); - chip.className = n ? "chip mint" : "chip"; + + 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"; + } } function setTmp(n) { @@ -995,7 +1008,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); }