"""Serve org-scoped lesson MP3s for device DOWNLOAD / UPDATE_MANIFEST.""" from __future__ import annotations import re from aiohttp import web from core.api.base_handler import BaseHandler from core.app_runtime.lesson_audio import ( DEFAULT_ORG_ID, ensure_org_dirs, lesson_audio_public_root, list_lesson_clips, org_audio_dir, resolve_disk_file, ) _LEVEL_RE = re.compile(r"^l[1-6]$", re.I) _STORY_RE = re.compile(r"^\d{3}$") TAG = __name__ class LessonAudioHandler(BaseHandler): def __init__(self, config: dict): super().__init__(config) ensure_org_dirs(config) async def handle_index(self, request: web.Request) -> web.StreamResponse: org_id = request.match_info.get("org_id") or DEFAULT_ORG_ID kind = str(request.match_info.get("kind") or "").strip().lower() level = str(request.match_info.get("level") or "").strip().lower() story = str(request.match_info.get("story") or "").strip() lang = str(request.match_info.get("lang") or "").strip().lower() if ( kind not in ("repeater", "shadowing") or lang not in ("cn", "en") or not _LEVEL_RE.fullmatch(level) or not _STORY_RE.fullmatch(story) ): return web.json_response({"present": False, "files": []}, status=400) files = list_lesson_clips(self.config, org_id, kind, f"{level}.{story}", lang) return web.json_response({"present": files is not None, "files": files or []}) async def handle_get(self, request: web.Request) -> web.StreamResponse: org_id = request.match_info.get("org_id") or DEFAULT_ORG_ID rel = request.match_info.get("path") or "" disk = resolve_disk_file(self.config, org_id, rel) if not disk: return web.Response(status=404, text="lesson audio not found") return web.FileResponse(path=disk, headers={"Content-Type": "audio/mpeg"}) def public_root(self) -> str: return lesson_audio_public_root(self.config) def log_ready(self) -> None: ensure_org_dirs(self.config) self.logger.bind(tag=TAG).info( f"课文音频 HTTP: {self.public_root()}/{{org_id}}/english_learning/..." ) self.logger.bind(tag=TAG).info( f"课文音频目录: {org_audio_dir(self.config, DEFAULT_ORG_ID)} (空 org 用 {DEFAULT_ORG_ID})" )