135 lines
4.2 KiB
Python
135 lines
4.2 KiB
Python
"""Theme export: read StationConfig from the DB and dump a theme.json file.
|
|
|
|
This module provides functions to:
|
|
- Build a clean, consumer-friendly JSON structure from StationConfig.
|
|
- Write it atomically to disk (for scp by build machines).
|
|
- Check whether the file exists and return its metadata.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Any, Dict
|
|
|
|
from app.config import settings
|
|
from app.database import async_session
|
|
from app.models import StationConfig
|
|
from sqlalchemy import select
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def resolve_image_url(base_url: str, relative_url: str) -> str | None:
|
|
"""Convert a relative image URL to an absolute one.
|
|
|
|
If the URL is already absolute (starts with http:// or https://), return as-is.
|
|
Returns None for empty strings.
|
|
"""
|
|
if not relative_url:
|
|
return None
|
|
if relative_url.startswith(("http://", "https://")):
|
|
return relative_url
|
|
path = relative_url.lstrip("/")
|
|
base = base_url.rstrip("/")
|
|
return f"{base}/{path}"
|
|
|
|
|
|
def build_theme_dict(config: StationConfig) -> Dict[str, Any]:
|
|
"""Build the theme.json dict from a StationConfig ORM object."""
|
|
base_url = settings.WEBSITE_URL
|
|
|
|
images: Dict[str, str] = {}
|
|
for key, field in [
|
|
("logo", config.logo_url),
|
|
("hero_background", config.hero_background_url),
|
|
("hero_icon", config.hero_icon_url),
|
|
("hero_divider", config.hero_divider_url),
|
|
]:
|
|
resolved = resolve_image_url(base_url, field)
|
|
if resolved:
|
|
images[key] = resolved
|
|
|
|
return {
|
|
"version": "1.0",
|
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
"station": {
|
|
"callsign": config.callsign,
|
|
"name_primary": config.name_primary,
|
|
"name_secondary": config.name_secondary,
|
|
"tagline": config.tagline,
|
|
"frequency": config.frequency,
|
|
},
|
|
"colors": {
|
|
"primary": config.color_primary,
|
|
"primary_light": config.color_primary_light,
|
|
"primary_dark": config.color_primary_dark,
|
|
"primary_muted": config.color_primary_muted,
|
|
"accent": config.color_accent,
|
|
"accent_light": config.color_accent_light,
|
|
"accent_dark": config.color_accent_dark,
|
|
"accent_muted": config.color_accent_muted,
|
|
"background": config.color_background,
|
|
"text": config.color_text,
|
|
"success": config.color_success,
|
|
"danger": config.color_danger,
|
|
"info": config.color_info,
|
|
"white": config.color_white,
|
|
"light": config.color_light,
|
|
"medium": config.color_medium,
|
|
"black": config.color_black,
|
|
},
|
|
"images": images,
|
|
}
|
|
|
|
|
|
async def write_theme_json() -> Dict[str, Any]:
|
|
"""Read the current station config and write theme.json to disk.
|
|
|
|
Uses atomic write (write to .tmp, then os.replace) to prevent
|
|
reading a partially-written file.
|
|
|
|
Returns the theme dict that was written.
|
|
"""
|
|
async with async_session() as session:
|
|
result = await session.execute(
|
|
select(StationConfig).where(StationConfig.key == "default")
|
|
)
|
|
config = result.scalar_one_or_none()
|
|
if config is None:
|
|
raise RuntimeError("Station config not found in database")
|
|
|
|
theme = build_theme_dict(config)
|
|
|
|
# Atomic write
|
|
path = Path(settings.THEME_JSON_PATH)
|
|
tmp_path = path.with_suffix(".tmp")
|
|
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
tmp_path.write_text(json.dumps(theme, indent=2) + "\n", encoding="utf-8")
|
|
tmp_path.replace(path)
|
|
|
|
return theme
|
|
|
|
|
|
def get_theme_status() -> Dict[str, Any]:
|
|
"""Check if theme.json exists on disk and return its metadata."""
|
|
path = Path(settings.THEME_JSON_PATH)
|
|
if not path.exists():
|
|
return {"exists": False, "path": str(path)}
|
|
|
|
stat = path.stat()
|
|
try:
|
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
generated_at = data.get("generated_at")
|
|
except Exception:
|
|
generated_at = None
|
|
|
|
return {
|
|
"exists": True,
|
|
"path": str(path),
|
|
"generated_at": generated_at,
|
|
"size": stat.st_size,
|
|
}
|