Files
dickshow-index/search.py
T

137 lines
4.4 KiB
Python

"""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())