wip: estado de trabajo pendiente antes de la vista grid (suite 230 verde)
This commit is contained in:
+113
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user