"""The Subsonic response envelope — one serializer, two wire formats. Every Subsonic endpoint answers with a ```` wrapper carrying ``status`` / ``version`` / ``type`` / ``serverVersion``, in XML (default) or JSON (``f=json``). All handlers return through :func:`subsonic_response`; errors go through the rest-aware exception handler (see ``app.api.errors``). Payload data model (shared by both formats): * a scalar value → an XML attribute / a JSON field * a nested dict → a single child element / nested object * a list of dicts → repeated child elements / a JSON array * the key ``"value"`` → element text content (used by e.g. lyrics) ``None`` values are dropped. Subsonic always replies with **HTTP 200**, even for errors — the status lives inside the envelope — so clients parse the body. """ import json from collections.abc import Mapping from typing import Any from xml.etree import ElementTree as ET from fastapi import Response SUBSONIC_API_VERSION = "1.16.1" SERVER_TYPE = "mcma" SERVER_VERSION = "0.1.0" _XML_NS = "http://subsonic.org/restapi" _XML_MEDIA_TYPE = "application/xml; charset=utf-8" _JSON_MEDIA_TYPE = "application/json; charset=utf-8" def _is_json(fmt: str | None) -> bool: return fmt in ("json", "jsonp") def _scalar(value: object) -> str: if isinstance(value, bool): return "true" if value else "false" return str(value) def _build_xml(parent: ET.Element, data: Mapping[str, Any]) -> None: for key, value in data.items(): if value is None: continue if key == "value": parent.text = _scalar(value) elif isinstance(value, Mapping): _build_xml(ET.SubElement(parent, key), value) elif isinstance(value, list): for item in value: _build_xml(ET.SubElement(parent, key), item) else: parent.set(key, _scalar(value)) def _strip_none(value: Any) -> Any: """Recursively drop ``None`` values so JSON output matches XML (no empty attrs).""" if isinstance(value, Mapping): return {k: _strip_none(v) for k, v in value.items() if v is not None} if isinstance(value, list): return [_strip_none(v) for v in value] return value def _render(body: Mapping[str, Any], fmt: str | None) -> Response: envelope: dict[str, Any] = { "status": body["status"], "version": SUBSONIC_API_VERSION, "type": SERVER_TYPE, "serverVersion": SERVER_VERSION, "openSubsonic": True, **{k: v for k, v in body.items() if k != "status"}, } if _is_json(fmt): payload = json.dumps({"subsonic-response": _strip_none(envelope)}) return Response(content=payload, media_type=_JSON_MEDIA_TYPE) root = ET.Element("subsonic-response", {"xmlns": _XML_NS}) _build_xml(root, envelope) xml = b'\n' + ET.tostring(root, encoding="utf-8") return Response(content=xml, media_type=_XML_MEDIA_TYPE) def subsonic_response( payload: Mapping[str, Any] | None = None, *, fmt: str | None = None ) -> Response: """A successful ``status="ok"`` envelope wrapping ``payload``.""" body: dict[str, Any] = {"status": "ok"} if payload: body.update(payload) return _render(body, fmt) def subsonic_error(code: int, message: str, *, fmt: str | None = None) -> Response: """A ``status="failed"`` envelope carrying a Subsonic ````.""" body = {"status": "failed", "error": {"code": code, "message": message}} return _render(body, fmt)