Refactored clips module into layered architecture.
CI / Formatting (push) Successful in 5s
CI / Linting (push) Successful in 6s
CI / Tests (Python 3.12) (push) Successful in 15s
CI / Tests (Python 3.13) (push) Successful in 18s
CI / Tests (Python 3.14) (push) Successful in 12s
CI / Type Checking (push) Successful in 10s
CI / Spelling (push) Successful in 5s

This commit is contained in:
2026-04-13 13:11:20 -04:00
parent 5e712d2059
commit d3e3460ce8
10 changed files with 1142 additions and 909 deletions
+16 -94
View File
@@ -20,27 +20,17 @@ stream in real time and provides a browser-based clip editor.
from __future__ import annotations
import asyncio
import contextlib
import shutil
from pathlib import Path
from owlbot.api import (
EventContext,
EventType,
ModuleContext,
StreamStartedEvent,
StreamStoppedEvent,
on_event,
on_setup,
on_teardown,
)
from owlbot.api import ModuleContext, on_setup, on_teardown
from .cache import start_caching, stop_caching
from .commands import clip_command, clips_command, delclip_command
from .processing import ProcessingManager
from .events import on_stream_started, on_stream_stopped
from .manager import ClipManager, get_manager
from .processing import VideoProcessor
from .repository import ClipRepository
from .routes import (
cleanup_session,
clip_page,
clip_thumbnail,
clip_video,
@@ -50,7 +40,6 @@ from .routes import (
editor_preview_video,
editor_submit,
)
from .types import ModuleState, get_state
__all__ = [
"clip_command",
@@ -89,16 +78,6 @@ async def setup(ctx: ModuleContext) -> None:
)
raise RuntimeError(msg)
await ctx.storage.execute("""
CREATE TABLE IF NOT EXISTS clips (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
creator TEXT NOT NULL,
created_at TEXT NOT NULL,
duration REAL
)
""")
ctx.config.register_defaults(
{
"cache_duration": 300,
@@ -110,92 +89,35 @@ async def setup(ctx: ModuleContext) -> None:
}
)
# Create clips directory if it doesn't exist.
repo = ClipRepository(ctx.storage)
await repo.setup()
processor = VideoProcessor(ctx.logger)
clips_dir = Path(str(ctx.config.get("clips_dir")))
clips_dir.mkdir(parents=True, exist_ok=True) # noqa: ASYNC240
# Initialize runtime state.
ctx.state["clips"] = ModuleState()
module_state = get_state(ctx)
module_state.manager = ProcessingManager(ctx.logger)
manager = ClipManager(ctx, repo, processor, clips_dir)
ctx.state["manager"] = manager
# Check if stream is already live.
try:
status = await ctx.owncast_client.get_status()
if status.get("online", False):
ctx.logger.info("Stream is already live. Starting HLS cache.")
await start_caching(ctx)
await manager.start_caching()
except Exception:
ctx.logger.warning(
"Could not check Owncast status during setup.", exc_info=True
)
@on_event(EventType.STREAM_STARTED)
async def on_stream_started(ctx: EventContext[StreamStartedEvent]) -> None:
"""Start HLS caching when the stream goes live.
:param ctx: The event context.
"""
# If there's a grace period task running, cancel it and wait for it to finish.
module_state = get_state(ctx.module)
if module_state.grace_task is not None:
ctx.module.logger.debug("Cancelling active grace period task.")
module_state.grace_task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await module_state.grace_task
module_state.grace_task = None
# Wipe old cache before starting fresh.
await stop_caching(ctx.module)
await start_caching(ctx.module)
@on_event(EventType.STREAM_STOPPED)
async def on_stream_stopped(ctx: EventContext[StreamStoppedEvent]) -> None:
"""Begin grace period when the stream goes offline.
:param ctx: The event context.
"""
async def _grace_period(module_ctx: ModuleContext) -> None:
grace = int(module_ctx.config.get("grace_period"))
module_ctx.logger.info(f"Stream stopped. Grace period: {grace}s.")
await asyncio.sleep(grace)
await stop_caching(module_ctx)
module_ctx.logger.info("Grace period ended. Cache cleaned up.")
ctx.module.logger.debug("Stream stopped event received. Scheduling grace period.")
get_state(ctx.module).grace_task = asyncio.create_task(_grace_period(ctx.module))
@on_teardown
async def teardown(ctx: ModuleContext) -> None:
"""Clean up the clips module.
Cancels grace/expiry tasks and cleans up temp files.
:param ctx: Module context.
"""
module_state = get_state(ctx)
# 1. Cancel grace period task if running.
if module_state.grace_task is not None:
ctx.logger.debug("Cancelling grace period task during teardown.")
module_state.grace_task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await module_state.grace_task
# 2. Stop caching.
await stop_caching(ctx)
# 3. Cancel all session expiry tasks (prevents races during drain).
for session in module_state.sessions.values():
if session.expiry_task is not None:
session.expiry_task.cancel()
# 4. Clean up editor sessions and their working directories.
for token in list(module_state.sessions):
cleanup_session(module_state.sessions, token)
ctx.logger.info("Clips module cleaned up.")
manager = get_manager(ctx)
await manager.teardown()
ctx.state["manager"] = None