Import Netscape cookies.txt
-Domains — one-click login
-| domain | cookies | fresh until | login |
|---|---|---|---|
| {{d[0]}} | {{d[1]}} | -{{'has session cookies' if d[2]==0 else utc(d[2])}} | -LOGIN → | -
{{domains|length}} domains shown
+ 📁 BROWSE WORK DRIVE + 📥 IMPORT FILES + 💾 EXPORT JSON + 💾 EXPORT TXT + +| domain | cookies | fresh until | |
|---|---|---|---|
| {{d[0]}} | +{{d[1]}} | +{{utc(d[2]) if d[2] else 'session'}} | +LOGIN → | +
+ +
+📁 Pick a folder on WORK (D:\)
--
-{% for e in entries %}
- 📁 {{e}}/ {% endfor %} +{% if parent is not none %}{% endif %} +
- + 📁 {{e}}/ {% endfor %} {% if not entries %}
- no subfolders here {% endif %}
-
+{% for e in entries %}
Scans this folder + all subfolders. Any .txt that contains Netscape cookies (any filename) gets imported.
+Scans this folder + subfolders. Any file with cookie data gets imported — any filename, any format (LLM-assisted detection).
""" +PAGE_SIZE = 40 + @app.route("/", methods=["GET"]) def index(): q = request.args.get("q", "").strip() + try: page = max(1, int(request.args.get("page", 1))) + except ValueError: page = 1 + like = f"%{q}%" if q else "%" c = db() if q: - like = f"%{q}%" - domains = c.execute("""SELECT domain, COUNT(*), MAX(CASE WHEN expiry GLOB '[0-9]*' THEN CAST(expiry AS INTEGER) ELSE 0 END) FROM cookies - WHERE domain LIKE ? GROUP BY domain ORDER BY domain LIMIT 500""", (like,)).fetchall() + total = c.execute("SELECT COUNT(DISTINCT domain) FROM cookies WHERE domain LIKE ?", (like,)).fetchone()[0] + domains = c.execute("""SELECT domain, COUNT(*), MAX(CASE WHEN expiry GLOB '[0-9]*' THEN CAST(expiry AS INTEGER) ELSE 0 END) + FROM cookies WHERE domain LIKE ? GROUP BY domain + HAVING COUNT(*) >= 2 AND domain GLOB '*.*.*' OR (domain LIKE ? AND COUNT(*) >= 1) + ORDER BY COUNT(*) DESC, domain LIMIT ? OFFSET ?""", + (like, like, PAGE_SIZE, (page-1)*PAGE_SIZE)).fetchall() else: - domains = c.execute("""SELECT domain, COUNT(*), MAX(CASE WHEN expiry GLOB '[0-9]*' THEN CAST(expiry AS INTEGER) ELSE 0 END) FROM cookies - GROUP BY domain ORDER BY domain LIMIT 500""").fetchall() + total = c.execute("SELECT COUNT(DISTINCT domain) FROM cookies").fetchone()[0] + domains = c.execute("""SELECT domain, COUNT(*), MAX(CASE WHEN expiry GLOB '[0-9]*' THEN CAST(expiry AS INTEGER) ELSE 0 END) + FROM cookies GROUP BY domain + HAVING COUNT(*) >= 3 AND domain GLOB '*.*.*' + ORDER BY COUNT(*) DESC, domain LIMIT ? OFFSET ?""", + (PAGE_SIZE, (page-1)*PAGE_SIZE)).fetchall() stats = (c.execute("SELECT COUNT(*) FROM cookies").fetchone()[0], c.execute("SELECT COUNT(DISTINCT domain) FROM cookies").fetchone()[0], c.execute("SELECT COUNT(DISTINCT source_file) FROM cookies").fetchone()[0]) c.close() - return render_template_string(PAGE, domains=domains, stats=stats, q=q, msg=request.args.get("msg"), utc=utc) + return render_template_string(HOME_PAGE, domains=domains, stats=stats, q=q, page=page, + has_more=(page*PAGE_SIZE) < total, msg=request.args.get("msg"), utc=utc) + +@app.route("/import_page") +def import_page(): + return render_template_string(IMPORT_PAGE, msg=request.args.get("msg")) @app.route("/import", methods=["POST"]) def do_import(): msg, errs = [], [] - files = request.files.getlist("files") - for f in files: + for f in request.files.getlist("files"): try: fname = os.path.basename(f.filename or "").strip() - # sanitize: keep alnum, dash, underscore, dot fname = re.sub(r"[^A-Za-z0-9._\- ]", "_", fname) if not fname or fname == ".": - # no filename — generate one from content hash data = f.read() - if not data.strip(): - continue - fname = "unnamed_%d.txt" % int(time.time() * 1000 % 1000000000) + if not data.strip(): continue + fname = "unnamed_%d.txt" % (int(time.time()*1000) % 10**9) path = os.path.join(WATCH_DIR, fname) with open(path, "wb") as fh: fh.write(data) else: path = os.path.join(WATCH_DIR, fname) f.save(path) n, new, total = import_file(path) - msg.append(f"{fname}: {n} parsed, {new} new") + msg.append(f"{fname}: {new} new" if total is not None else f"{fname}: no cookie data found") except Exception as e: - errs.append(f"{getattr(f, 'filename', '?')}: {e}") + errs.append(f"{getattr(f,'filename','?')}: {e}") paste = request.form.get("paste", "").strip() if paste: - try: - tmp = os.path.join(WATCH_DIR, "_pasted_%d.txt" % int(time.time())) - with open(tmp, "w", encoding="utf-8") as fh: fh.write(paste) - n, new, total = import_file(tmp) - msg.append(f"pasted: {n} parsed, {new} new") - except Exception as e: - errs.append(f"paste: {e}") - if errs: - msg += ["%s" % e for e in errs] - if not msg: - msg = ["nothing imported — pick a cookies.txt file or paste cookie text first"] - return redirect(url_for("index", msg=" · ".join(msg))) + tmp = os.path.join(WATCH_DIR, "_pasted_%d.txt" % int(time.time())) + with open(tmp, "w", encoding="utf-8") as fh: fh.write(paste) + n, new, total = import_file(tmp) + msg.append(f"pasted: {new} new" if total is not None else "pasted: no cookie data detected") + msg += ["%s" % e for e in errs] + return redirect(url_for("index", msg=" · ".join(msg) or "nothing imported")) @app.route("/scan", methods=["POST"]) def scan(): - return _scan_dirs(SCAN_ROOTS) + msg = [] + for fn in os.listdir(WATCH_DIR): + if fn.lower().endswith((".txt", ".json")) and not fn.startswith("_pasted"): + n, new, total = import_file(os.path.join(WATCH_DIR, fn)) + msg.append(f"{fn}: {new} new") + return redirect(url_for("index", msg=" · ".join(msg) or "nothing new")) -@app.route("/scan_work", methods=["POST"]) -def scan_work(): - """Deep-scan the Work drive: every .txt file that LOOKS like Netscape cookie format.""" - return _smart_scan(WORK_DRIVE) - -@app.route("/browse", methods=["GET"]) +@app.route("/browse") def browse(): - """List subfolders of a path on the Work drive so the user can pick one.""" sub = request.args.get("path", "").strip() base = os.path.abspath(WORK_DRIVE) target = os.path.abspath(os.path.join(base, sub.lstrip("/\\"))) if sub else base if not target.startswith(base) or not os.path.isdir(target): - return redirect(url_for("index", msg="invalid folder")) + return redirect(url_for("index", msg="invalid folder")) entries = [] - try: - for name in sorted(os.listdir(target)): - full = os.path.join(target, name) - if os.path.isdir(full) and name not in ("$RECYCLE.BIN", "System Volume Information"): - entries.append(name) - except Exception as e: - return redirect(url_for("index", msg=f"{e}")) + for name in sorted(os.listdir(target)): + if os.path.isdir(os.path.join(target, name)) and name not in ("$RECYCLE.BIN", "System Volume Information"): + entries.append(name) rel = os.path.relpath(target, base) parent = os.path.dirname(rel) if rel != "." else None return render_template_string(BROWSE_PAGE, entries=entries, rel=rel, parent=parent) @@ -269,133 +322,115 @@ def scan_folder(): base = os.path.abspath(WORK_DRIVE) target = os.path.abspath(os.path.join(base, sub.lstrip("/\\"))) if sub else base if not target.startswith(base) or not os.path.isdir(target): - return redirect(url_for("index", msg="invalid folder")) + return redirect(url_for("index", msg="invalid folder")) return _smart_scan(target) +@app.route("/scan_work", methods=["POST"]) +def scan_work(): + if not os.path.isdir(WORK_DRIVE): + return redirect(url_for("index", msg="Work drive (D:\\) not plugged in")) + return _smart_scan(WORK_DRIVE) + def _smart_scan(root): - """Smart scan: walk root, sniff every .txt for Netscape format regardless of filename.""" found = [] for rootpath, dirs, files in os.walk(root): dirs[:] = [d for d in dirs if d not in ("$RECYCLE.BIN", "System Volume Information", "node_modules")] for fn in files: - if fn.lower().endswith(".txt"): + if fn.lower().endswith((".txt", ".json", ".log")): found.append(os.path.join(rootpath, fn)) if not found: - return redirect(url_for("index", msg="no .txt files found in that folder")) + return redirect(url_for("index", msg="no text files found in that folder")) msg, errs = [], [] - new_total, imported_files = 0, 0 + new_total, imported_files, llm_used = 0, 0, 0 for p in found: try: - looks_like = False with open(p, "r", encoding="utf-8", errors="replace") as f: - for i, line in enumerate(f): - line = line.strip() - if not line or line.startswith("#"): - continue - parts = line.split("\t") if "\t" in line else line.split() - if len(parts) >= 7 and parts[4].isdigit(): - looks_like = True - if looks_like or i > 30: - break - if not looks_like: - continue + text = f.read(65536) + if not sniff_netscape(text): + # deterministic miss -> try LLM only for small-ish files (cost control) + if os.path.getsize(p) > 200_000: + continue + verdict = llm_classify(text) + if verdict is None: + continue + llm_used += 1 + if not verdict: + continue + with open(p, "r", encoding="utf-8", errors="replace") as f: + pass # full read happens in import_file n, new, total = import_file(p) + if total is None: + continue imported_files += 1 new_total += new - try: - shown = os.path.relpath(p, root) - except ValueError: - shown = p + try: shown = os.path.relpath(p, root) + except ValueError: shown = p msg.append(f"{shown}: {new} new") except Exception as e: errs.append(f"{p}: {e}") if not imported_files and not errs: - return redirect(url_for("index", msg=f"scanned {len(found)} .txt files — none were Netscape cookie format")) + return redirect(url_for("index", msg=f"scanned {len(found)} files — no cookie data found (LLM checked {llm_used} ambiguous ones)")) msg += ["%s" % e for e in errs] - return redirect(url_for("index", msg=f"Scan of {root}: {imported_files} cookie files of {len(found)} txt, {new_total} new cookies · " + " · ".join(msg[:25]))) + return redirect(url_for("index", msg=f"{imported_files} cookie files ({new_total} new cookies, LLM-assisted on {llm_used}) · " + " · ".join(msg[:20]))) -def _scan_dirs(dirs): - msg, errs = [], [] - for d in dirs: - if not os.path.isdir(d): - continue - for fn in os.listdir(d): - if fn.lower().endswith((".txt", ".json")) and not fn.startswith("_pasted"): - try: - n, new, total = import_file(os.path.join(d, fn)) - msg.append(f"{fn}: {new} new") - except Exception as e: - errs.append(f"{fn}: {e}") - msg += ["%s" % e for e in errs] - return redirect(url_for("index", msg=" · ".join(msg) or "nothing new found")) - -def _launch_login(domain, cookies): - """Runs in background thread: selenium Chrome with cookies injected.""" - from selenium import webdriver - from selenium.webdriver.chrome.options import Options - opts = Options() - profile = os.path.join(SESSION_DIR, re.sub(r'[^a-zA-Z0-9]', '_', domain)) - os.makedirs(profile, exist_ok=True) - opts.add_argument(r"--user-data-dir=" + profile) - opts.add_argument("--no-first-run") - opts.add_argument("--no-default-browser-check") - opts.add_argument("--start-maximized") - driver = webdriver.Chrome(options=opts) # selenium-manager auto-fetches driver - try: - # land on the site's origin first so cookies can be set for it - host = domain.lstrip(".") - driver.get(f"https://{host}/favicon.ico") - except Exception: - try: driver.get(f"https://{host}/") - except Exception: pass - time.sleep(1) - for ck in cookies: - c = {"name": ck[1], "value": ck[2]} - c["domain"] = ck[0] - c["path"] = ck[3] or "/" - exp = _safe_expiry(ck[4]) - if exp > 0: - c["expiry"] = exp - c["secure"] = str(ck[5]).upper() == "TRUE" - try: - driver.add_cookie(c) - except Exception: - try: - c2 = dict(c); c2.pop("expiry", None) - driver.add_cookie(c2) - except Exception: - pass - try: - driver.get(f"https://{host}/") - except Exception: - pass - # keep the process ref alive — selenium closes browser if driver is GC'd - globals().setdefault("_drivers", []).append(driver) +# ---------------- LOGIN (session-1 bridge) ---------------- @app.route("/open") def open_login(): domain = request.args.get("domain", "").strip() if not domain: return redirect(url_for("index")) - like = f"%{domain.strip('.')}%" + stem = domain.strip(".") + like = stem + "%" c = db() rows = c.execute("""SELECT domain, name, value, path, expiry, secure FROM cookies - WHERE domain LIKE ? OR domain LIKE ?""", - (domain.strip(".") + "%", "%" + domain.strip("."))).fetchall() + WHERE domain LIKE ? OR domain LIKE ? ORDER BY (domain = ?) DESC LIMIT 4000""", + (like, "%" + stem, domain)).fetchall() c.close() if not rows: - return redirect(url_for("index", msg=f"no cookies stored for {domain}")) - threading.Thread(target=_launch_login, args=(domain, rows), daemon=True).start() - time.sleep(0.5) - return redirect(url_for("index", msg=f"launching Chrome for {domain} with {len(rows)} cookies...")) + return redirect(url_for("index", msg=f"no cookies for {domain}")) + payload = {"domain": domain, "cookies": [ + {"domain": r[0], "name": r[1], "value": r[2], "path": r[3], + "expiry": _safe_expiry(r[4]), "secure": str(r[5]).upper() == "TRUE"} for r in rows]} + try: + req = urllib.request.Request(BRIDGE, data=json.dumps(payload).encode(), + headers={"Content-Type": "application/json"}) + with urllib.request.urlopen(req, timeout=15) as r: + res = json.load(r) + if res.get("ok"): + return render_template_string(LAUNCH_PAGE, domain=domain, n=len(rows), + token=res.get("token", ""), url=f"https://{stem}/") + return redirect(url_for("index", msg=f"bridge error: {res.get('error')}")) + except Exception as e: + return redirect(url_for("index", msg=f"launcher bridge not reachable ({e})")) + +LAUNCH_PAGE = STYLE + r"""