feat(ses): add SES light novel pipeline; extract shared lib/wiki.py

New pipeline at pipelines/ses/ publishes mikkeli/ses-light-novel to
the SES: MediaWiki namespace. All pages get Category:SES and
Category:SES:<type> (type from frontmatter, fallback to parent dir name).

Shared functions (connect_wiki, markdown_to_wikitext, strip_first_h1,
AUTO_BANNER_PREFIX) extracted from songs/publish.py into lib/wiki.py;
songs refactored to import from there.

Also adds publish-ses.yml workflow stub and updates Dockerfile and
root README.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 16:23:26 +09:00
parent 6144d1f399
commit dfe1f9c631
10 changed files with 491 additions and 41 deletions
+3 -41
View File
@@ -5,7 +5,6 @@ import argparse
import hashlib
import io
import os
import subprocess
import sys
from datetime import datetime
from pathlib import Path
@@ -13,6 +12,9 @@ from pathlib import Path
import frontmatter
import mwclient
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
from lib.wiki import AUTO_BANNER_PREFIX, connect_wiki, markdown_to_wikitext, strip_first_h1
def validate(fm: dict, path: Path) -> None:
errors = []
@@ -47,30 +49,6 @@ def validate(fm: dict, path: Path) -> None:
raise ValueError(f"{path}: " + "; ".join(errors))
def strip_first_h1(text: str) -> str:
"""Remove the first '# Heading' line and any immediately following blank line."""
lines = text.split("\n")
for i, line in enumerate(lines):
if line.strip().startswith("# "):
del lines[i]
if i < len(lines) and lines[i].strip() == "":
del lines[i]
break
return "\n".join(lines)
def markdown_to_wikitext(body: str) -> str:
result = subprocess.run(
["pandoc", "-f", "markdown", "-t", "mediawiki"],
input=body,
capture_output=True,
text=True,
)
if result.returncode != 0:
raise RuntimeError(f"pandoc failed: {result.stderr.strip()}")
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()}
@@ -115,19 +93,6 @@ def build_wikitext(fm: dict, body_wikitext: str, source_url: str, source_ref: st
return f"{banner}\n\n{body_wikitext}{lrc_line}{categories}"
def connect_wiki() -> mwclient.Site:
from urllib.parse import urlparse
api_url = os.environ["WIKI_API_URL"]
parsed = urlparse(api_url)
host = parsed.netloc
# Strip api.php to get the wiki root path (e.g. "/" or "/w/")
path = parsed.path[: parsed.path.rfind("/") + 1] or "/"
site = mwclient.Site(host, path=path, scheme=parsed.scheme)
site.login(os.environ["WIKI_BOT_USER"], os.environ["WIKI_BOT_PASSWORD"])
return site
def upload_lrc(lrc_path: Path, title: str, site: mwclient.Site) -> bool:
"""Upload LRC file to wiki if content has changed. Returns True if uploaded."""
if not lrc_path.exists():
@@ -156,9 +121,6 @@ def upload_lrc(lrc_path: Path, title: str, site: mwclient.Site) -> bool:
return True
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, sibling_map: dict[str, str]) -> str:
"""Returns 'published', 'noop', or 'skipped-manual'."""
rel = abs_path.relative_to(source_dir)