CI / Formatting (push) Successful in 5s
CI / Linting (push) Successful in 5s
CI / Tests (Python 3.12) (push) Successful in 14s
CI / Tests (Python 3.13) (push) Successful in 14s
CI / Tests (Python 3.14) (push) Successful in 11s
CI / Type Checking (push) Successful in 10s
CI / Spelling (push) Successful in 5s
125 lines
3.5 KiB
Python
125 lines
3.5 KiB
Python
# Copyright 2026 Logan Fick
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""Clips module for Owlbot.
|
|
|
|
Allows chat users to create clips from the live Owncast stream. Caches the HLS
|
|
stream in real time and provides a browser-based clip editor.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
from owlbot.api import ModuleContext, on_setup, on_teardown
|
|
|
|
from .commands import clip_command, clips_command, delclip_command
|
|
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 (
|
|
clip_page,
|
|
clip_thumbnail,
|
|
clip_video,
|
|
clips_list_page,
|
|
editor_js,
|
|
editor_page,
|
|
editor_preview_video,
|
|
editor_submit,
|
|
)
|
|
|
|
__all__ = [
|
|
"clip_command",
|
|
"clip_page",
|
|
"clip_thumbnail",
|
|
"clip_video",
|
|
"clips_command",
|
|
"clips_list_page",
|
|
"delclip_command",
|
|
"editor_js",
|
|
"editor_page",
|
|
"editor_preview_video",
|
|
"editor_submit",
|
|
"on_stream_started",
|
|
"on_stream_stopped",
|
|
"setup",
|
|
"teardown",
|
|
]
|
|
|
|
|
|
@on_setup
|
|
async def setup(ctx: ModuleContext) -> None:
|
|
"""Initialize the clips module.
|
|
|
|
Validates ffmpeg availability, creates the database schema, registers
|
|
config defaults, creates the clips storage directory, and starts caching
|
|
if a stream is already live.
|
|
|
|
:param ctx: Module context with config, storage, and other services.
|
|
"""
|
|
for binary in ("ffmpeg", "ffprobe"):
|
|
if shutil.which(binary) is None:
|
|
msg = (
|
|
f"'{binary}' not found in PATH. "
|
|
f"The clips module requires ffmpeg to be installed."
|
|
)
|
|
raise RuntimeError(msg)
|
|
|
|
ctx.config.register_defaults(
|
|
{
|
|
"cache_duration": 300,
|
|
"min_clip_length": 5,
|
|
"max_clip_length": 120,
|
|
"clips_dir": "data/clips",
|
|
"session_expiry": 900,
|
|
"grace_period": 120,
|
|
}
|
|
)
|
|
|
|
repo = ClipRepository(ctx.storage)
|
|
await repo.setup()
|
|
|
|
processor = VideoProcessor(ctx.logger)
|
|
|
|
clips_dir = Path(str(ctx.config.get("clips_dir")))
|
|
await asyncio.to_thread(clips_dir.mkdir, parents=True, exist_ok=True)
|
|
|
|
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 manager.start_caching()
|
|
except Exception: # noqa: BLE001 # best-effort; non-fatal if Owncast is unreachable during setup
|
|
ctx.logger.warning(
|
|
"Could not check Owncast status during setup.", exc_info=True
|
|
)
|
|
|
|
|
|
@on_teardown
|
|
async def teardown(ctx: ModuleContext) -> None:
|
|
"""Clean up the clips module.
|
|
|
|
:param ctx: Module context.
|
|
"""
|
|
manager = get_manager(ctx)
|
|
await manager.teardown()
|
|
ctx.state["manager"] = None
|