Twitch stream event platform with OBS browser source overlay. Features: - Twitch OAuth login (streamer + mod access) - EventSub WebSocket for subs, gift subs, bits, channel points, follows, raids - Chat commands via tmi.js in the overlay - Gap audio — tone plays on new chat message after silence, volume/pitch scale with gap length - Self-hosted media library (upload sounds and videos) - Action configuration — map any event to sound, video, or alert - Per-action cooldowns, test button, enable/disable toggle - Multiple actions per event (all matching actions fire) - Activity log dashboard with EventSub connection status - Layout editor — iframe-based WYSIWYG with drag handles, live style preview - Custom CSS and custom JS injection into overlay - Custom DOM events (bashyoverlay:sub, bashyoverlay:raid, etc.) for custom JS hooks - deploy.sh — one-shot setup and launch script Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
79 lines
2.2 KiB
Python
79 lines
2.2 KiB
Python
import os
|
|
import uuid
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile
|
|
from fastapi.responses import HTMLResponse
|
|
from sqlmodel import Session, select
|
|
|
|
from app.auth import require_user
|
|
from app.database import get_session
|
|
from app.models import MediaFile
|
|
from app.templates import templates
|
|
|
|
router = APIRouter(prefix="/media")
|
|
|
|
MEDIA_DIR = "static/media"
|
|
AUDIO_EXTENSIONS = {".mp3", ".ogg", ".wav", ".m4a"}
|
|
VIDEO_EXTENSIONS = {".mp4", ".webm", ".mov"}
|
|
|
|
|
|
@router.get("", response_class=HTMLResponse)
|
|
async def media_page(request: Request, session: Session = Depends(get_session), user=Depends(require_user)):
|
|
files = session.exec(select(MediaFile)).all()
|
|
return templates.TemplateResponse("media.html", {
|
|
"request": request,
|
|
"user": user,
|
|
"files": files,
|
|
})
|
|
|
|
|
|
@router.post("", response_class=HTMLResponse)
|
|
async def upload_file(
|
|
request: Request,
|
|
file: UploadFile,
|
|
session: Session = Depends(get_session),
|
|
user=Depends(require_user),
|
|
):
|
|
ext = os.path.splitext(file.filename)[1].lower()
|
|
if ext in AUDIO_EXTENSIONS:
|
|
file_type = "audio"
|
|
elif ext in VIDEO_EXTENSIONS:
|
|
file_type = "video"
|
|
else:
|
|
raise HTTPException(status_code=400, detail="Unsupported file type")
|
|
|
|
filename = f"{uuid.uuid4().hex}{ext}"
|
|
path = os.path.join(MEDIA_DIR, filename)
|
|
|
|
content = await file.read()
|
|
with open(path, "wb") as f:
|
|
f.write(content)
|
|
|
|
media = MediaFile(filename=filename, original_name=file.filename, file_type=file_type)
|
|
session.add(media)
|
|
session.commit()
|
|
session.refresh(media)
|
|
|
|
return templates.TemplateResponse("partials/media_item.html", {
|
|
"request": request,
|
|
"file": media,
|
|
})
|
|
|
|
|
|
@router.delete("/{file_id}", response_class=HTMLResponse)
|
|
async def delete_file(
|
|
file_id: int,
|
|
session: Session = Depends(get_session),
|
|
user=Depends(require_user),
|
|
):
|
|
media = session.get(MediaFile, file_id)
|
|
if not media:
|
|
raise HTTPException(status_code=404)
|
|
|
|
path = os.path.join(MEDIA_DIR, media.filename)
|
|
if os.path.exists(path):
|
|
os.remove(path)
|
|
|
|
session.delete(media)
|
|
session.commit()
|
|
return HTMLResponse("")
|