Skip to content
62 changes: 40 additions & 22 deletions moon_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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")
Expand Down
53 changes: 51 additions & 2 deletions moon_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
_close_sess,
_sanitize_filename,
download_file,
count_usable_proxies,
)

# ── THEME ──────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
}

Expand All @@ -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__":
Expand Down
41 changes: 33 additions & 8 deletions web/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}
Expand Down