fluxa-core/scripts/fetch-mdblist-docs.py
KhooLy 50ad208652 feat(core): add MDBList API support
Request-plan builders and response normalization for MDBList's media/
ratings, catalog, lists, watchlist, sync, scrobble, checkin, discussion,
and user endpoints (mdblist_plan.rs, routed via ffi/mdblist_routes.rs),
plus device-code/PKCE OAuth entries in oauth_plan.rs. Docs fetched from
the live OpenAPI spec via scripts/fetch-mdblist-docs.py into
docs/external/mdblist/ (gitignored, kept locally like the other
docs/external/ providers).
2026-07-29 02:14:52 +03:00

159 lines
5.6 KiB
Python

#!/usr/bin/env python3
"""Fetch MDBList's OpenAPI spec and render it as a grep-able markdown reference.
Usage: python3 scripts/fetch-mdblist-docs.py
Writes docs/external/mdblist/{openapi.yaml,mdblist-api-docs.md}.
"""
import urllib.request
import yaml
from pathlib import Path
SPEC_URL = "https://api.mdblist.com/schema/"
OUT_DIR = Path(__file__).resolve().parent.parent / "docs" / "external" / "mdblist"
def fetch_spec() -> dict:
req = urllib.request.Request(SPEC_URL, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req) as resp:
raw = resp.read()
OUT_DIR.mkdir(parents=True, exist_ok=True)
(OUT_DIR / "openapi.yaml").write_bytes(raw)
return yaml.safe_load(raw)
def resolve(spec: dict, node):
if isinstance(node, dict) and "$ref" in node:
ref = node["$ref"]
assert ref.startswith("#/")
target = spec
for part in ref.lstrip("#/").split("/"):
if not isinstance(target, dict) or part not in target:
return {"type": ref.rsplit("/", 1)[-1]}
target = target[part]
return target
return node
def schema_summary(spec: dict, schema, depth=0, seen=None) -> str:
if schema is None:
return "-"
seen = seen or set()
schema = resolve(spec, schema)
if isinstance(schema, dict) and "$ref" in schema:
return schema_summary(spec, schema, depth, seen)
stype = schema.get("type")
if stype == "object" or "properties" in schema:
props = schema.get("properties", {})
if not props:
return "object"
if depth > 2:
return "object { " + ", ".join(sorted(props)) + " }"
lines = []
required = set(schema.get("required", []))
for name, pschema in props.items():
pschema_r = resolve(spec, pschema)
mark = "" if name in required else "?"
lines.append(
f"{' ' * (depth + 1)}- `{name}{mark}`: {schema_summary(spec, pschema_r, depth + 1, seen)}"
)
return "object {\n" + "\n".join(lines) + f"\n{' ' * depth}}}"
if stype == "array":
items = schema.get("items")
return f"array<{schema_summary(spec, items, depth, seen)}>"
if "enum" in schema:
return f"{stype or 'enum'}({', '.join(str(v) for v in schema['enum'])})"
return stype or "any"
def render_operation(spec: dict, method: str, path: str, op: dict) -> str:
lines = [f"### `{method.upper()} {path}`"]
op_id = op.get("operationId")
if op_id:
lines.append(f"operationId: `{op_id}`")
security = op.get("security", spec.get("security"))
if security:
schemes = [name for entry in security for name in entry] or ["none"]
lines.append(f"auth: {', '.join(sorted(set(schemes))) or 'none'}")
else:
lines.append("auth: none")
summary = op.get("summary") or op.get("description")
if summary:
lines.append("")
lines.append(summary.strip())
params = op.get("parameters", [])
if params:
lines.append("")
lines.append("Parameters:")
for p in params:
p = resolve(spec, p)
pschema = resolve(spec, p.get("schema", {}))
req = "required" if p.get("required") else "optional"
desc = (p.get("description") or "").strip().splitlines()[0] if p.get("description") else ""
lines.append(
f"- `{p['name']}` ({p.get('in')}, {req}, {pschema.get('type', 'any')}){': ' + desc if desc else ''}"
)
body = op.get("requestBody")
if body:
content = body.get("content", {})
for ctype, cval in content.items():
lines.append("")
lines.append(f"Request body ({ctype}):")
lines.append("```")
lines.append(schema_summary(spec, cval.get("schema")))
lines.append("```")
responses = op.get("responses", {})
ok = responses.get("200") or responses.get("201") or responses.get("204")
if ok:
content = ok.get("content", {})
for ctype, cval in content.items():
lines.append("")
lines.append(f"Response ({ctype}):")
lines.append("```")
lines.append(schema_summary(spec, cval.get("schema")))
lines.append("```")
lines.append("")
return "\n".join(lines)
def render(spec: dict) -> str:
tags = {}
for path, methods in spec["paths"].items():
for method, op in methods.items():
if method not in ("get", "post", "put", "patch", "delete"):
continue
for tag in op.get("tags", ["untagged"]):
tags.setdefault(tag, []).append((method, path, op))
out = [
f"# {spec['info']['title']}",
"",
f"> Version {spec['info']['version']} - generated from {SPEC_URL} by `scripts/fetch-mdblist-docs.py`.",
"",
spec["info"].get("description", "").strip(),
"",
"Auth: `apiKey` = `?apikey=YOUR_KEY` query param. `bearerAuth` = `Authorization: Bearer <token>` (user OAuth).",
"",
"## Endpoint index",
"",
]
for tag in sorted(tags):
out.append(f"- **{tag}**: " + ", ".join(f"`{m.upper()} {p}`" for m, p, _ in tags[tag]))
out.append("")
for tag in sorted(tags):
out.append(f"## {tag}")
out.append("")
for method, path, op in sorted(tags[tag], key=lambda t: (t[1], t[0])):
out.append(render_operation(spec, method, path, op))
return "\n".join(out)
def main():
spec = fetch_spec()
md = render(spec)
(OUT_DIR / "mdblist-api-docs.md").write_text(md)
print(f"wrote {OUT_DIR}")
if __name__ == "__main__":
main()