import os import json from typing import List, Optional from pathlib import Path from .models import RippedThread, HistorySummary class ThreadStorage: def __init__(self, data_dir: str = "data"): self.data_dir = Path(data_dir) self.threads_dir = self.data_dir / "threads" self.threads_dir.mkdir(parents=True, exist_ok=True) def save(self, thread: RippedThread) -> str: """Saves a ripped thread to disk in JSON, MD, and HTML formats.""" file_path = self.threads_dir / f"{thread.id}.json" with open(file_path, "w", encoding="utf-8") as f: f.write(thread.model_dump_json(indent=2)) # Also save standalone .md and .html files for direct browsing md_path = self.threads_dir / f"{thread.id}.md" with open(md_path, "w", encoding="utf-8") as f: f.write(thread.stitched_markdown) html_path = self.threads_dir / f"{thread.id}.html" with open(html_path, "w", encoding="utf-8") as f: f.write(thread.stitched_html) txt_path = self.threads_dir / f"{thread.id}.txt" with open(txt_path, "w", encoding="utf-8") as f: f.write(thread.stitched_text) return thread.id def get(self, thread_id: str) -> Optional[RippedThread]: """Loads a saved ripped thread by ID.""" file_path = self.threads_dir / f"{thread_id}.json" if not file_path.exists(): return None with open(file_path, "r", encoding="utf-8") as f: data = json.load(f) return RippedThread(**data) def list_history(self) -> List[HistorySummary]: """Lists summaries of all previously ripped threads, newest first.""" history = [] for file in self.threads_dir.glob("*.json"): try: with open(file, "r", encoding="utf-8") as f: data = json.load(f) history.append( HistorySummary( id=data["id"], title=data.get("title", "Untitled Thread"), author_handle=data.get("author_handle", "unknown"), author_name=data.get("author_name", "Unknown"), segment_count=data.get("segment_count", len(data.get("segments", []))), total_words=data.get("total_words", 0), saved_at=str(data.get("saved_at", ""))[:19], original_url=data.get("original_url", "") ) ) except Exception: continue # Sort by saved_at descending history.sort(key=lambda x: x.saved_at, reverse=True) return history def delete(self, thread_id: str) -> bool: """Deletes a saved thread and all associated format files.""" deleted = False for ext in [".json", ".md", ".html", ".txt"]: f = self.threads_dir / f"{thread_id}{ext}" if f.exists(): f.unlink() deleted = True return deleted