wangzhibo
2026-10-08 220e4fab27b7f419b3e89e2f2d28ae789aac09d7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
"""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})"
        )