feat: v2.9.6 — free-first NUX, cookie extraction, quality scoring
Setup wizard with consent-first cookie extraction (Chrome/Firefox/Safari), yt-dlp auto-install, ScrapeCreators push, quality scoring (5 core sources), status banner redesign, honest Reddit labeling, inline YouTube transcripts, Exa free web search, Reddit public fallback, and post-research quality nudge. Co-authored-by: Matt Van Horn <mvanhorn@MacBook-Pro.local> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
"""First-run setup wizard for last30days.
|
||||
|
||||
Detects first run, performs auto-setup (cookie extraction + yt-dlp check),
|
||||
and writes configuration. The actual wizard UI is SKILL.md-driven (the LLM
|
||||
presents it), but this module provides the detection and setup actions.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def is_first_run(config: Dict[str, Any]) -> bool:
|
||||
"""Return True if the setup wizard has not been completed.
|
||||
|
||||
Checks for SETUP_COMPLETE in the config dict. If it's not set
|
||||
(None or empty string), the user hasn't gone through setup yet.
|
||||
"""
|
||||
return not config.get("SETUP_COMPLETE")
|
||||
|
||||
|
||||
def run_auto_setup(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Perform the auto-setup actions.
|
||||
|
||||
- Runs cookie extraction in auto mode for all registered domains
|
||||
- Checks if yt-dlp is installed
|
||||
|
||||
Returns:
|
||||
Dict with keys:
|
||||
cookies_found: {source_name: browser_name} for each source where cookies were found
|
||||
ytdlp_installed: bool
|
||||
env_written: bool (always False here — caller writes config separately)
|
||||
"""
|
||||
from . import cookie_extract
|
||||
from .env import COOKIE_DOMAINS
|
||||
|
||||
cookies_found: Dict[str, str] = {}
|
||||
|
||||
for source_name, spec in COOKIE_DOMAINS.items():
|
||||
domain = spec["domain"]
|
||||
cookie_names = spec["cookies"]
|
||||
|
||||
try:
|
||||
result = cookie_extract.extract_cookies_with_source("auto", domain, cookie_names)
|
||||
except Exception as exc:
|
||||
logger.debug("Cookie extraction failed for %s: %s", source_name, exc)
|
||||
continue
|
||||
|
||||
if result is not None:
|
||||
_cookies, browser_name = result
|
||||
cookies_found[source_name] = browser_name
|
||||
|
||||
# Check yt-dlp availability and install via Homebrew if missing
|
||||
ytdlp_action: str
|
||||
if shutil.which("yt-dlp") is not None:
|
||||
ytdlp_installed = True
|
||||
ytdlp_action = "already_installed"
|
||||
elif shutil.which("brew") is not None:
|
||||
brew_stderr = ""
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
["brew", "install", "yt-dlp"],
|
||||
capture_output=True, text=True, timeout=120,
|
||||
)
|
||||
if proc.returncode == 0:
|
||||
ytdlp_installed = True
|
||||
ytdlp_action = "installed"
|
||||
else:
|
||||
ytdlp_installed = False
|
||||
ytdlp_action = "install_failed"
|
||||
brew_stderr = proc.stderr
|
||||
logger.warning("brew install yt-dlp failed: %s", proc.stderr)
|
||||
except Exception as exc:
|
||||
ytdlp_installed = False
|
||||
ytdlp_action = "install_failed"
|
||||
brew_stderr = str(exc)
|
||||
logger.warning("brew install yt-dlp exception: %s", exc)
|
||||
else:
|
||||
ytdlp_installed = False
|
||||
ytdlp_action = "no_homebrew"
|
||||
|
||||
results: Dict[str, Any] = {
|
||||
"cookies_found": cookies_found,
|
||||
"ytdlp_installed": ytdlp_installed,
|
||||
"ytdlp_action": ytdlp_action,
|
||||
"env_written": False,
|
||||
}
|
||||
if ytdlp_action == "install_failed":
|
||||
results["ytdlp_stderr"] = brew_stderr
|
||||
return results
|
||||
|
||||
|
||||
def write_setup_config(env_path: Path, from_browser: str = "auto") -> bool:
|
||||
"""Write SETUP_COMPLETE and FROM_BROWSER to the .env file.
|
||||
|
||||
Creates the file and parent directories if needed.
|
||||
Appends to existing file without overwriting existing keys.
|
||||
|
||||
Args:
|
||||
env_path: Path to the .env file (e.g. ~/.config/last30days/.env)
|
||||
from_browser: Browser extraction mode to write (default: "auto")
|
||||
|
||||
Returns:
|
||||
True if config was written successfully, False on error.
|
||||
"""
|
||||
try:
|
||||
env_path = Path(env_path)
|
||||
env_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Read existing content to avoid overwriting keys
|
||||
existing_keys: set = set()
|
||||
existing_content = ""
|
||||
if env_path.exists():
|
||||
existing_content = env_path.read_text(encoding="utf-8")
|
||||
for line in existing_content.splitlines():
|
||||
stripped = line.strip()
|
||||
if stripped and not stripped.startswith("#") and "=" in stripped:
|
||||
key = stripped.split("=", 1)[0].strip()
|
||||
existing_keys.add(key)
|
||||
|
||||
lines_to_add = []
|
||||
if "SETUP_COMPLETE" not in existing_keys:
|
||||
lines_to_add.append("SETUP_COMPLETE=true")
|
||||
if "FROM_BROWSER" not in existing_keys:
|
||||
lines_to_add.append(f"FROM_BROWSER={from_browser}")
|
||||
|
||||
if not lines_to_add:
|
||||
return True # Nothing to write, already configured
|
||||
|
||||
# Ensure trailing newline before appending
|
||||
with open(env_path, "a", encoding="utf-8") as f:
|
||||
if existing_content and not existing_content.endswith("\n"):
|
||||
f.write("\n")
|
||||
f.write("\n".join(lines_to_add) + "\n")
|
||||
|
||||
return True
|
||||
|
||||
except OSError as exc:
|
||||
logger.error("Failed to write setup config to %s: %s", env_path, exc)
|
||||
return False
|
||||
|
||||
|
||||
def get_setup_status_text(results: Dict[str, Any]) -> str:
|
||||
"""Return a human-readable summary of auto-setup results.
|
||||
|
||||
Args:
|
||||
results: Dict from run_auto_setup()
|
||||
|
||||
Returns:
|
||||
Multi-line status text.
|
||||
"""
|
||||
lines = []
|
||||
lines.append("Setup complete! Here's what I found:")
|
||||
lines.append("")
|
||||
|
||||
cookies_found = results.get("cookies_found", {})
|
||||
if cookies_found:
|
||||
for source, browser in cookies_found.items():
|
||||
lines.append(f" - {source.upper()} cookies found in {browser}")
|
||||
else:
|
||||
lines.append(" - No browser cookies found for X/Twitter")
|
||||
|
||||
ytdlp_action = results.get("ytdlp_action", "")
|
||||
if ytdlp_action == "installed":
|
||||
lines.append(" - Installed yt-dlp via Homebrew")
|
||||
elif ytdlp_action == "install_failed":
|
||||
lines.append(" - yt-dlp install failed \u2014 run `brew install yt-dlp` manually")
|
||||
elif ytdlp_action == "no_homebrew":
|
||||
lines.append(" - yt-dlp not found. Install Homebrew first, then: brew install yt-dlp")
|
||||
elif ytdlp_action == "already_installed":
|
||||
lines.append(" - yt-dlp already installed")
|
||||
elif results.get("ytdlp_installed", False):
|
||||
lines.append(" - yt-dlp is installed (YouTube search ready)")
|
||||
else:
|
||||
lines.append(" - yt-dlp not found (install with: brew install yt-dlp)")
|
||||
|
||||
env_written = results.get("env_written", False)
|
||||
if env_written:
|
||||
lines.append("")
|
||||
lines.append("Configuration saved. Future runs will auto-detect your browsers.")
|
||||
|
||||
return "\n".join(lines)
|
||||
Reference in New Issue
Block a user