wip: estado de trabajo pendiente antes de la vista grid (suite 230 verde)

This commit is contained in:
urieljareth
2026-08-22 19:37:23 -06:00
parent 4f5a68b572
commit 8a59b39c98
103 changed files with 70954 additions and 1825 deletions
+113 -1
View File
@@ -122,7 +122,9 @@ def backfill_from_markdown(
for md_path in md_files:
try:
text = md_path.read_text(encoding="utf-8")
except OSError as exc:
except (OSError, UnicodeDecodeError) as exc:
# UnicodeDecodeError is a ValueError, not an OSError — letting it
# escape aborted the loop and silently skipped every later file.
_log(f"backfill: skip unreadable {md_path}: {exc}")
continue
parsed = parse_markdown(text)
@@ -170,6 +172,116 @@ def backfill_from_markdown(
return n
def reconcile_markdown(
store: Store,
md_root: Path,
log: Callable[[str], None] | None = None,
*,
prune: bool = False,
) -> dict[str, int]:
"""Make the DB agree with what is actually on disk.
`backfill_from_markdown` fills in segments and metadata but never touches
`status` or `markdown_path`, so a video whose .md exists can sit at
`error`/`no_subtitles`/`pending` forever and the UI keeps showing a failure
for work that is already done. This walks the markdown tree and repairs:
- a row with a real .md but a non-done status -> marked done
`prune=True` additionally sends `done` rows whose .md has disappeared back
to pending. That direction is opt-in because it is destructive when aimed
at the wrong root: pointed at an empty or unrelated markdown tree it would
demote every finished video in the database. It is also skipped outright
when the tree contains no .md at all, which is never a real "everything was
deleted" state — it means the root is wrong.
Returns counts so the caller can report what changed. Idempotent.
"""
def _log(msg: str) -> None:
if log:
log(msg)
else:
logging.getLogger(__name__).info(msg)
md_root = Path(md_root)
data_root = md_root.parent
out = {
"scanned": 0, "repaired_done": 0, "orphan_md": 0,
"missing_md": 0, "backfilled": 0, "stale_dupe": 0,
}
if not md_root.exists():
_log(f"reconcile: markdown root not found: {md_root}")
return out
out["backfilled"] = backfill_from_markdown(store, md_root, log=log)
seen: dict[str, Path] = {}
for md_path in sorted(md_root.rglob("*.md")):
out["scanned"] += 1
try:
text = md_path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError) as exc:
_log(f"reconcile: skip unreadable {md_path}: {exc}")
continue
meta = parse_markdown(text).metadata
video_id = meta.get("video_id")
if not video_id:
continue
row = store.get_video(video_id)
if not row:
out["orphan_md"] += 1
continue
rel = md_path.relative_to(data_root).as_posix()
# A second .md for a video the DB already resolves elsewhere. Older
# re-renders built the filename from a differently-formatted date, so
# they wrote a sibling file the DB never learned about; it is dead
# weight that every later scan has to wade through.
canonical = (row.markdown_path or "").replace("\\", "/")
if video_id in seen or (row.status == "done" and canonical and canonical != rel):
out["stale_dupe"] += 1
if prune:
try:
md_path.unlink()
_log(f"reconcile: removed stale duplicate {rel}")
except OSError as exc:
_log(f"reconcile: could not remove {rel}: {exc}")
else:
_log(f"reconcile: stale duplicate (use prune to delete): {rel}")
continue
seen[video_id] = md_path
if row.status != "done" or not row.markdown_path:
store.mark_done(
video_id, rel,
meta.get("transcript_lang") or row.transcript_lang,
meta.get("transcript_src") or row.transcript_src,
bool(meta.get("has_chapters")) or bool(row.has_chapters),
)
out["repaired_done"] += 1
_log(f"reconcile: {video_id} had a .md on disk but status={row.status} -> done")
# The other direction is destructive, so it needs both an explicit opt-in
# and evidence that we are looking at a real markdown tree.
if prune and out["scanned"]:
for row in store.get_all():
if row.status != "done" or row.video_id in seen:
continue
path = data_root / row.markdown_path if row.markdown_path else None
if path is None or not path.exists():
store.mark_status(row.video_id, "pending", "markdown file missing on disk")
out["missing_md"] += 1
_log(f"reconcile: {row.video_id} marked done but .md is gone -> pending")
elif prune:
_log("reconcile: markdown tree is empty — refusing to prune (wrong root?)")
_log(
"reconcile: scanned {scanned} .md, repaired {repaired_done}, "
"re-queued {missing_md}, orphans {orphan_md}, stale duplicates {stale_dupe}".format(**out)
)
return out
def _to_int(value: str | None) -> int | None:
if value is None:
return None