"""Search the transcript index. Usage: python search.py "isom trademark" python search.py "pull up" --tag eric_july python search.py "judge ramos" --ep 103 --limit 5 python search.py "rage" --from 2017-01-01 --to 2019-12-31 --report out.txt Output lines include EPISODE + TIMESTAMP so they can be deep-linked to audio later: [Ep 373 | 2023-08-29] 01:12:45-01:13:02 "...transcript snippet..." """ import argparse import datetime as dt import json import re import sqlite3 import sys from config import DB_PATH from index import open_db def ts(seconds: float) -> str: s = int(seconds) return f"{s // 3600:02d}:{(s % 3600) // 60:02d}:{s % 60:02d}" def parse_date(s: str): return dt.date.fromisoformat(s) def pubdate_year(pub: str): m = re.search(r"(\d{4})", pub or "") return int(m.group(1)) if m else None def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("query", help="FTS5 query (supports \"quoted phrases\", AND/OR)") ap.add_argument("--tag", action="append", default=[], help="filter by hashtag (repeatable)") ap.add_argument("--ep", type=int, action="append", default=[], help="restrict to episode number(s)") ap.add_argument("--from", dest="date_from", default=None) ap.add_argument("--to", dest="date_to", default=None) ap.add_argument("--limit", type=int, default=25) ap.add_argument("--context", type=int, default=0, help="include N neighbouring segments") ap.add_argument("--report", default=None, help="also write matches to this file") args = ap.parse_args() con = open_db(DB_PATH) q = args.query.strip() sql = """ SELECT s.rowid, e.ep_num, e.guid, e.title, e.pub_date, e.audio_url, e.page_link, s.seg_idx, s.start_sec, s.end_sec, s.text, s.tags FROM segments s JOIN episodes e ON e.id = s.episode_id WHERE segments MATCH ? """ params = [q] if args.tag: for t in args.tag: sql += " AND s.tags LIKE ?" params.append(f"%#{t}%") if args.ep: sql += " AND e.ep_num IN (%s)" % ",".join("?" * len(args.ep)) params += args.ep if args.date_from: y = parse_date(args.date_from).year # crude pub-date year filter; feed dates are RFC2822 sql += " AND CAST(substr(e.pub_date, length(e.pub_date)-3, 4) AS INTEGER) >= ?" params.append(y) if args.date_to: y = parse_date(args.date_to).year sql += " AND CAST(substr(e.pub_date, length(e.pub_date)-3, 4) AS INTEGER) <= ?" params.append(y) sql += " ORDER BY e.pub_date LIMIT ?" params.append(args.limit) rows = con.execute(sql, params).fetchall() if not rows: print("no matches") return lines = [] def parse_tags(raw: str) -> list[str]: raw = raw or "" if not raw: return [] if raw.startswith("["): try: return json.loads(raw) except ValueError: return [] return [t for t in raw.split() if t] for r in rows: (_rid, ep_num, guid, title, pub, audio_url, page_link, seg_idx, start, end, text, tags) = r label = f"Ep {ep_num}" if ep_num else title[:30] taglist = parse_tags(tags) head = (f"[{label} | {pub}] {ts(start)}-{ts(end)} " f"{' '.join(taglist)}") body = f" \"{text}\"" extra = "" if args.context: ctx = con.execute( """SELECT start_sec, end_sec, text FROM segments WHERE episode_id=(SELECT id FROM episodes WHERE guid=?) AND seg_idx BETWEEN ? AND ? ORDER BY seg_idx""", (guid, max(0, seg_idx - args.context), seg_idx + args.context)).fetchall() body = "\n".join( f" [{ts(a)}-{ts(b)}] {t}" for a, b, t in ctx) link = page_link or audio_url extra = f" -> {link}\n mp3: {audio_url}" lines.append(head + "\n" + body + "\n" + extra) out = "\n\n".join(lines) print(out[:20000]) print(f"\n({len(rows)} result(s), limit {args.limit})") if args.report: with open(args.report, "a", encoding="utf-8") as f: f.write(f"\n===== QUERY: {q} tags={args.tag} eps={args.ep} =====\n") f.write(out + "\n") print(f"appended to {args.report}") if __name__ == "__main__": sys.exit(main())