diff --git a/.github/workflows/sub-audio-check.yml b/.github/workflows/sub-audio-check.yml index fea1603..ab40737 100644 --- a/.github/workflows/sub-audio-check.yml +++ b/.github/workflows/sub-audio-check.yml @@ -54,12 +54,12 @@ jobs: - name: Run sub-audio lint working-directory: ./sub-audio run: | - rye run lint + rye lint - name: Run sub-audio test working-directory: ./sub-audio run: | - rye run test + rye test - name: Build image uses: ./.github/actions/build diff --git a/sub-audio/main.py b/sub-audio/main.py index 67418b3..f3fa6b2 100644 --- a/sub-audio/main.py +++ b/sub-audio/main.py @@ -1,142 +1,142 @@ -import asyncio -import logging -import os -import subprocess -import urllib.parse -from secrets import token_urlsafe -from tempfile import NamedTemporaryFile - -import fastapi -from dotenv import load_dotenv -from pydantic import BaseModel -from redis import asyncio as aioredis - -logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - -load_dotenv() -load_dotenv(dotenv_path="../.env") - -HOSTS_BACKEND = os.getenv("HOSTS_BACKEND") -if HOSTS_BACKEND is None: - raise Exception("HOSTS_BACKEND is not set") -print(f"HOSTS_BACKEND = {HOSTS_BACKEND}") - -if sentry_dsn := os.getenv("SENTRY_DSN_SUB_AUDIO"): - import sentry_sdk - - traits_sample_rate = float(os.getenv("SENTRY_TRACE_SAMPLE_RATE", "0.01")) - sentry_sdk.init(sentry_dsn, traces_sample_rate=traits_sample_rate) - -app = fastapi.FastAPI() -redis = aioredis.from_url(os.getenv("REDIS_URL"), decode_responses=True) - - -@app.get("/") -async def read_root(): - return {"code": "ok"} - - -class ConvertParam(BaseModel): - url: str - - -@app.post("/convert") -async def convert(param: ConvertParam): - url = urllib.parse.urljoin(HOSTS_BACKEND, param.url) - - logger.info(f"convert: url={url}") - dist_bgm_file = NamedTemporaryFile(delete=False, suffix=".mp3") - dist_bgm_file.close() - dist_preview_file = NamedTemporaryFile(delete=False, suffix=".mp3") - dist_preview_file.close() - - bgm_process = subprocess.Popen( - [ - "ffmpeg", - "-y", - "-i", - url, - "-c:a", - "libmp3lame", - "-b:a", - "192k", - "-ac", - "2", - "-ar", - "44100", - dist_bgm_file.name, - ] - ) - preview_process = subprocess.Popen( - [ - "ffmpeg", - "-y", - "-i", - url, - "-c:a", - "libmp3lame", - "-b:a", - "96k", - "-ac", - "1", - "-ar", - "24000", - "-af", - "atrim=start=0:end=15,afade=t=out:st=13:d=2", - dist_preview_file.name, - ] - ) - while bgm_process.poll() is None or preview_process.poll() is None: - await asyncio.sleep(0.1) - logger.info( - f"convert: bgm_process={bgm_process.returncode}, preview_process={preview_process.returncode}" - ) - if bgm_process.returncode != 0 or preview_process.returncode != 0: - raise Exception( - f"ffmpeg failed: bgm_process={bgm_process.returncode}, preview_process={preview_process.returncode}" - ) - - nonce = token_urlsafe(16) - - await redis.set(f"audio:{nonce}:bgm", dist_bgm_file.name) - await redis.set(f"audio:{nonce}:preview", dist_preview_file.name) - - return { - "code": "ok", - "id": nonce, - } - - -@app.get("/download/{nonce}:{type}") -async def download(nonce: str, type: str): - if path := await redis.get(f"audio:{nonce}:{type}"): - logger.info(f"download: nonce={nonce}, type={type}") - return fastapi.responses.FileResponse(path) - else: - return {"code": "error"}, 404 - - -@app.delete("/download/{nonce}:{type}") -async def delete(nonce: str, type: str): - if path := await redis.get(f"audio:{nonce}:{type}"): - logger.info(f"delete: nonce={nonce}, type={type}") - os.remove(path) - await redis.delete("image:" + nonce) - return {"code": "ok"} - else: - return {"code": "error"}, 404 - - -@app.exception_handler(Exception) -async def exception_handler(request, exc): - return fastapi.responses.JSONResponse( - status_code=500, - content={"code": "error", "message": str(exc)}, - ) - - -if __name__ == "__main__": - import uvicorn - - uvicorn.run("main:app", port=3202, reload=True) +import asyncio +import logging +import os +import subprocess +import urllib.parse +from secrets import token_urlsafe +from tempfile import NamedTemporaryFile + +import fastapi +from dotenv import load_dotenv +from pydantic import BaseModel +from redis import asyncio as aioredis + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + +load_dotenv() +load_dotenv(dotenv_path="../.env") + +HOSTS_BACKEND = os.getenv("HOSTS_BACKEND") +if HOSTS_BACKEND is None: + raise Exception("HOSTS_BACKEND is not set") +print(f"HOSTS_BACKEND = {HOSTS_BACKEND}") + +if sentry_dsn := os.getenv("SENTRY_DSN_SUB_AUDIO"): + import sentry_sdk + + traits_sample_rate = float(os.getenv("SENTRY_TRACE_SAMPLE_RATE", "0.01")) + sentry_sdk.init(sentry_dsn, traces_sample_rate=traits_sample_rate) + +app = fastapi.FastAPI() +redis = aioredis.from_url(os.getenv("REDIS_URL"), decode_responses=True) + + +@app.get("/") +async def read_root(): + return {"code": "ok"} + + +class ConvertParam(BaseModel): + url: str + + +@app.post("/convert") +async def convert(param: ConvertParam): + url = urllib.parse.urljoin(HOSTS_BACKEND, param.url) + + logger.info(f"convert: url={url}") + dist_bgm_file = NamedTemporaryFile(delete=False, suffix=".mp3") + dist_bgm_file.close() + dist_preview_file = NamedTemporaryFile(delete=False, suffix=".mp3") + dist_preview_file.close() + + bgm_process = subprocess.Popen( + [ + "ffmpeg", + "-y", + "-i", + url, + "-c:a", + "libmp3lame", + "-b:a", + "192k", + "-ac", + "2", + "-ar", + "44100", + dist_bgm_file.name, + ] + ) + preview_process = subprocess.Popen( + [ + "ffmpeg", + "-y", + "-i", + url, + "-c:a", + "libmp3lame", + "-b:a", + "96k", + "-ac", + "1", + "-ar", + "24000", + "-af", + "atrim=start=0:end=15,afade=t=out:st=13:d=2", + dist_preview_file.name, + ] + ) + while bgm_process.poll() is None or preview_process.poll() is None: + await asyncio.sleep(0.1) + logger.info( + f"convert: bgm_process={bgm_process.returncode}, preview_process={preview_process.returncode}" + ) + if bgm_process.returncode != 0 or preview_process.returncode != 0: + raise Exception( + f"ffmpeg failed: bgm_process={bgm_process.returncode}, preview_process={preview_process.returncode}" + ) + + nonce = token_urlsafe(16) + + await redis.set(f"audio:{nonce}:bgm", dist_bgm_file.name) + await redis.set(f"audio:{nonce}:preview", dist_preview_file.name) + + return { + "code": "ok", + "id": nonce, + } + + +@app.get("/download/{nonce}:{type}") +async def download(nonce: str, type: str): + if path := await redis.get(f"audio:{nonce}:{type}"): + logger.info(f"download: nonce={nonce}, type={type}") + return fastapi.responses.FileResponse(path) + else: + return {"code": "error"}, 404 + + +@app.delete("/download/{nonce}:{type}") +async def delete(nonce: str, type: str): + if path := await redis.get(f"audio:{nonce}:{type}"): + logger.info(f"delete: nonce={nonce}, type={type}") + os.remove(path) + await redis.delete("image:" + nonce) + return {"code": "ok"} + else: + return {"code": "error"}, 404 + + +@app.exception_handler(Exception) +async def exception_handler(request, exc): + return fastapi.responses.JSONResponse( + status_code=500, + content={"code": "error", "message": str(exc)}, + ) + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run("main:app", port=3202, reload=True) diff --git a/sub-audio/pyproject.toml b/sub-audio/pyproject.toml index 418b11f..af3b47f 100644 --- a/sub-audio/pyproject.toml +++ b/sub-audio/pyproject.toml @@ -26,8 +26,6 @@ dev-dependencies = [ ] [tool.rye.scripts] -test = "pytest" -lint = "ruff check ." start = "gunicorn -w 2 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:3202 main:app" dev = "uvicorn main:app --reload --port 3202" diff --git a/sub-audio/test_main.py b/sub-audio/test_main.py index 4f421b7..25aca01 100644 --- a/sub-audio/test_main.py +++ b/sub-audio/test_main.py @@ -18,7 +18,10 @@ def test_read_main(): @pytest.mark.skip("SEGV on GitHub Actions") def test_process_bgm(simple_server): - response = client.post("/convert", json={"url": f"http://localhost:{simple_server.server_port}/bgm.mp3"}) + response = client.post( + "/convert", + json={"url": f"http://localhost:{simple_server.server_port}/bgm.mp3"}, + ) assert response.status_code == 200 assert response.json()["code"] == "ok" @@ -27,7 +30,9 @@ class TestServer(http.server.SimpleHTTPRequestHandler): __test__ = False def __init__(self, *args, **kwargs): - super().__init__(*args, directory=os.path.dirname(__file__) + "/test_server", **kwargs) + super().__init__( + *args, directory=os.path.dirname(__file__) + "/test_server", **kwargs + ) @pytest.fixture()