Files
ergosteur 4a2bd7aa19 feat: guard against a dead Instagram session, show progress, fix missing pings
- check-session.py: preflight in gdl-cron.sh. No unexpired sessionid in the
  Chrome profile gdl reads -> abort before any request, ping /76 (red check).
- stories-streak.py: postflight. Every fetched stories source empty for 3
  consecutive days -> exit 77, for a session Instagram stopped honouring while
  the cookie is still present. The 2026-10-07 session death went unnoticed for
  a week because every run still exited 0.
- gdl-sync.py: [n/N] source headers, a 60s heartbeat with new-file counts, and
  per-source timing, so a healthy paced run no longer looks hung.
- gdl-sync@.service now runs run-%i.sh instead of gdl-cron.sh: HC_URL is set in
  the wrappers, so scheduled runs had never pinged Healthchecks.
- Tests for both guards (offline) and the progress helpers.
2026-10-14 21:02:58 -04:00

71 lines
2.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Preflight for gdl-cron.sh: is there a usable Instagram login in the cookie jar gdl reads?
gdl-sync.py runs gallery-dl with `--cookies-from-browser chrome:<profile>`, so what matters
is not what the interactive Chrome window LOOKS like (an open tab keeps showing its logged-in
UI long after the session died -- found 2026-10-14, stories had been empty for a week) but
whether the profile's cookie DB holds a `sessionid`. This reads the same DB, the same way.
Never prints cookie values -- only the name, expiry, and a verdict.
Exit 0 logged in (a non-expired sessionid exists)
Exit 1 logged out / unreadable -- the caller must not scrape
"""
import datetime as dt
import os
import shutil
import sqlite3
import sys
import tempfile
import time
from pathlib import Path
PROFILE = Path(os.environ.get("GDL_COOKIE_PROFILE",
str(Path.home() / ".config/google-chrome-devtools")))
WARN_DAYS = 7
def main() -> int:
src = PROFILE / "Default" / "Cookies"
if not src.exists():
print(f"no cookie DB at {src} -- Chrome profile missing?")
return 1
with tempfile.TemporaryDirectory() as tmp:
# Copy first: Chrome holds the live DB open, and a copy keeps this read-only.
for suffix in ("", "-journal", "-wal", "-shm"):
f = Path(str(src) + suffix)
if f.exists():
shutil.copy2(f, Path(tmp) / f.name)
try:
con = sqlite3.connect(str(Path(tmp) / "Cookies"))
rows = con.execute(
"SELECT expires_utc, has_expires FROM cookies "
"WHERE host_key LIKE '%instagram.com' AND name = 'sessionid'").fetchall()
except sqlite3.Error as exc:
print(f"cannot read cookie DB: {exc}")
return 1
if not rows:
print("NOT LOGGED IN: no Instagram sessionid in the Chrome profile "
f"{PROFILE}. Log in to Instagram in that Chrome window (the open tab can "
"look logged in while stale -- reload it), then re-run.")
return 1
expires, has_expires = rows[0]
if has_expires:
exp = dt.datetime.fromtimestamp(expires / 1_000_000 - 11644473600, dt.timezone.utc)
left = (exp.timestamp() - time.time()) / 86400
if left <= 0:
print(f"NOT LOGGED IN: the Instagram sessionid expired {exp:%Y-%m-%d}.")
return 1
note = f"expires {exp:%Y-%m-%d}"
if left < WARN_DAYS:
note += f" (WARNING: only {left:.1f} days left)"
print(f"session ok: sessionid present, {note}")
else:
print("session ok: sessionid present (session cookie, no expiry)")
return 0
if __name__ == "__main__":
sys.exit(main())