feat(songs): optional album, categories list, sibling placeholder resolution
- `album` no longer required; category format changed to Category:Album:<name> - New optional `categories` list for additional arbitrary categories - `wiki.siblings` bill of materials: path+tag required for resolution, label optional - Post-Pandoc [[TAG]] substitution with four-tier fallback (link+label, link, plain label, plain tag) - Sibling map built from all published posts in the same run; unresolved siblings degrade gracefully - SCHEMA.md fully updated; README.md deferred items moved to implemented Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,10 +17,9 @@ import mwclient
|
||||
def validate(fm: dict, path: Path) -> None:
|
||||
errors = []
|
||||
|
||||
for field in ("title", "album"):
|
||||
val = fm.get(field)
|
||||
if not val or not str(val).strip():
|
||||
errors.append(f"missing or empty '{field}'")
|
||||
val = fm.get("title")
|
||||
if not val or not str(val).strip():
|
||||
errors.append("missing or empty 'title'")
|
||||
|
||||
if not isinstance(fm.get("wiki", {}).get("publish"), bool):
|
||||
errors.append("'wiki.publish' must be a boolean (true/false), not a string")
|
||||
@@ -31,6 +30,19 @@ def validate(fm: dict, path: Path) -> None:
|
||||
except ValueError:
|
||||
errors.append("'release_date' must be YYYY-MM-DD")
|
||||
|
||||
if "categories" in fm and not isinstance(fm["categories"], list):
|
||||
errors.append("'categories' must be a list")
|
||||
|
||||
for i, sib in enumerate(fm.get("wiki", {}).get("siblings") or []):
|
||||
if not isinstance(sib, dict):
|
||||
errors.append(f"wiki.siblings[{i}] must be a mapping")
|
||||
continue
|
||||
sib_path = sib.get("path")
|
||||
sib_tag = sib.get("tag")
|
||||
if sib_path and sib_tag:
|
||||
if not (path.parent / str(sib_path)).resolve().exists():
|
||||
errors.append(f"wiki.siblings[{i}]: path '{sib_path}' not found")
|
||||
|
||||
if errors:
|
||||
raise ValueError(f"{path}: " + "; ".join(errors))
|
||||
|
||||
@@ -59,11 +71,47 @@ def markdown_to_wikitext(body: str) -> str:
|
||||
return result.stdout
|
||||
|
||||
|
||||
def build_sibling_map(posts: dict) -> dict[str, str]:
|
||||
"""Map abs_path_str → wiki_title for all published posts."""
|
||||
return {str(path): str(post.metadata["title"]) for path, post in posts.items()}
|
||||
|
||||
|
||||
def apply_sibling_substitutions(wikitext: str, this_path: Path, fm_wiki: dict, sibling_map: dict[str, str]) -> str:
|
||||
"""Replace [[TAG]] placeholders using the declared sibling bill of materials."""
|
||||
for sib in (fm_wiki.get("siblings") or []):
|
||||
if not isinstance(sib, dict):
|
||||
continue
|
||||
tag = sib.get("tag")
|
||||
path = sib.get("path")
|
||||
if not tag or not path:
|
||||
continue
|
||||
label = (sib.get("label") or "").strip()
|
||||
abs_sib = str((this_path.parent / str(path)).resolve())
|
||||
title = sibling_map.get(abs_sib)
|
||||
if title and label:
|
||||
replacement = f"[[{title}|{label}]]"
|
||||
elif title:
|
||||
replacement = f"[[{title}]]"
|
||||
elif label:
|
||||
replacement = label
|
||||
else:
|
||||
replacement = tag
|
||||
wikitext = wikitext.replace(f"[[{tag}]]", replacement)
|
||||
return wikitext
|
||||
|
||||
|
||||
def build_wikitext(fm: dict, body_wikitext: str, source_url: str, source_ref: str, lrc_filename: str | None = None) -> str:
|
||||
banner = f"{{{{Auto-generated|source={source_url}|commit={source_ref}}}}}"
|
||||
category = f"[[Category:{fm['album']}]]"
|
||||
lrc_line = f"[[Media:{lrc_filename}|Synced lyrics (.lrc)]]\n" if lrc_filename else ""
|
||||
return f"{banner}\n\n{body_wikitext}{lrc_line}{category}\n"
|
||||
|
||||
cat_parts = []
|
||||
if fm.get("album"):
|
||||
cat_parts.append(f"[[Category:Album:{fm['album']}]]")
|
||||
for cat in (fm.get("categories") or []):
|
||||
cat_parts.append(f"[[Category:{cat}]]")
|
||||
categories = "\n".join(cat_parts) + "\n" if cat_parts else ""
|
||||
|
||||
return f"{banner}\n\n{body_wikitext}{lrc_line}{categories}"
|
||||
|
||||
|
||||
def connect_wiki() -> mwclient.Site:
|
||||
@@ -110,13 +158,14 @@ def upload_lrc(lrc_path: Path, title: str, site: mwclient.Site) -> bool:
|
||||
AUTO_BANNER_PREFIX = "{{Auto-generated"
|
||||
|
||||
|
||||
def publish_one(abs_path: Path, source_dir: Path, post, site: mwclient.Site, source_ref: str, gitea_repo_url: str) -> str:
|
||||
def publish_one(abs_path: Path, source_dir: Path, post, site: mwclient.Site, source_ref: str, gitea_repo_url: str, sibling_map: dict[str, str]) -> str:
|
||||
"""Returns 'published', 'noop', or 'skipped-manual'."""
|
||||
rel = abs_path.relative_to(source_dir)
|
||||
fm = post.metadata
|
||||
title = str(fm["title"])
|
||||
|
||||
body_wikitext = markdown_to_wikitext(strip_first_h1(post.content))
|
||||
body_wikitext = apply_sibling_substitutions(body_wikitext, abs_path, fm.get("wiki", {}), sibling_map)
|
||||
source_url = f"{gitea_repo_url}/src/commit/{source_ref}/{rel}"
|
||||
|
||||
lrc_filename = None
|
||||
@@ -204,6 +253,7 @@ def main() -> None:
|
||||
|
||||
print(f"Validating {len(files)} file(s)...")
|
||||
posts = load_and_validate(files, source_dir)
|
||||
sibling_map = build_sibling_map(posts)
|
||||
print("Validation passed.")
|
||||
|
||||
site = connect_wiki()
|
||||
@@ -211,7 +261,7 @@ def main() -> None:
|
||||
counts = {"published": 0, "noop": 0, "skipped-manual": 0}
|
||||
for abs_path, post in posts.items():
|
||||
rel = abs_path.relative_to(source_dir)
|
||||
outcome = publish_one(abs_path, source_dir, post, site, source_ref, gitea_repo_url)
|
||||
outcome = publish_one(abs_path, source_dir, post, site, source_ref, gitea_repo_url, sibling_map)
|
||||
counts[outcome] += 1
|
||||
if outcome == "skipped-manual":
|
||||
print(f" [skipped-manual] {rel} ← page exists without auto-gen banner; delete or add banner to hand over to bot", file=sys.stderr)
|
||||
|
||||
Reference in New Issue
Block a user