Files
ohm_streaming/app/routers/router_settings.py
root 87f245d3fc
CI / Test (Python 3.11) (pull_request) Has been cancelled
CI / Test (Python 3.12) (pull_request) Has been cancelled
CI / Lint (pull_request) Has been cancelled
CI / Type Check (pull_request) Has been cancelled
CI / Summary (pull_request) Has been cancelled
feat: flat design Sunset Glitch + download manager + settings + recommendations overhaul
- Sunset Glitch color palette applied to all templates
- Font Awesome icons throughout UI
- Download manager with parallel queue and progress tracking
- Settings page with dynamic configuration
- Recommendations router enhanced with scoring
- Local vendor libs (Alpine.js, HTMX) for offline support
- Auto test suite with screenshots
- Series releases list component
- New download model
2026-04-11 19:30:32 +00:00

300 lines
11 KiB
Python

"""Application settings routes for Ohm Stream Downloader API"""
import json
import logging
from pathlib import Path
from typing import List, Dict, Any, Optional
from fastapi import APIRouter, Depends, HTTPException, Request, Response
from fastapi.templating import Jinja2Templates
from sqlmodel import Session, select
from app.database import get_session
from app.models.auth import User, UserTable
from app.models.settings import AppSettings, AppSettingsTable, AppSettingsUpdate
from app.routers.router_auth import get_current_user_from_token, get_optional_user
from app.providers import get_anime_providers, get_series_providers
from app.providers_manager import providers_manager
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/settings", tags=["settings"])
templates = Jinja2Templates(directory="templates")
def _compute_auto_weights(download_dir: str) -> Dict[str, Any]:
"""Analyze downloaded files to compute anime vs series ratio.
Uses filename conventions:
- Series: contains "Saison" or "Season" keywords
- Anime: everything else in the downloads folder
Returns dict with counts and computed weights.
"""
base = Path(download_dir)
if not base.exists():
return {"anime_count": 0, "series_count": 0, "anime_weight": 1, "series_weight": 1, "total": 0}
anime_count = 0
series_count = 0
for f in base.rglob("*"):
if not f.is_file():
continue
if f.suffix.lower() not in (".mp4", ".mkv", ".avi", ".wmv", ".flv", ".mov"):
continue
name = f.stem.lower()
# Heuristic: series TV files often have "Saison" or "Season" + number
# Anime files rarely use this format (they use "Episode" or "S01E01")
import re
if re.search(r'(?:saison|season)\s*\d+', name):
series_count += 1
else:
anime_count += 1
total = anime_count + series_count
if total == 0:
return {"anime_count": 0, "series_count": 0, "anime_weight": 1, "series_weight": 1, "total": 0}
# Compute weights: proportional to download count, minimum 1
if anime_count == 0:
aw, sw = 0, 1
elif series_count == 0:
aw, sw = 1, 0
else:
# Keep weights small (max 5) for reasonable interleaving
ratio = anime_count / series_count
if ratio >= 4:
aw, sw = 4, 1
elif ratio >= 2:
aw, sw = 2, 1
elif ratio >= 1:
aw, sw = 1, 1
elif ratio >= 0.5:
aw, sw = 1, 2
else:
aw, sw = 1, 4
return {
"anime_count": anime_count,
"series_count": series_count,
"anime_weight": aw,
"series_weight": sw,
"total": total,
}
@router.get("", response_model=AppSettings)
async def get_settings(
current_user: User = Depends(get_current_user_from_token),
session: Session = Depends(get_session),
):
"""Get current application settings for the user"""
statement = select(AppSettingsTable).where(
AppSettingsTable.user_id == current_user.id
)
settings_obj = session.exec(statement).first()
if not settings_obj:
# Create default settings if they don't exist
settings_obj = AppSettingsTable(user_id=current_user.id)
session.add(settings_obj)
session.commit()
session.refresh(settings_obj)
return AppSettings(
default_lang=settings_obj.default_lang,
theme=settings_obj.theme,
disabled_providers=settings_obj.disabled_providers,
recommendations_filter=getattr(settings_obj, 'recommendations_filter', 'all'),
releases_filter=getattr(settings_obj, 'releases_filter', 'all'),
anime_enabled=getattr(settings_obj, 'anime_enabled', True),
series_enabled=getattr(settings_obj, 'series_enabled', True),
download_dir=getattr(settings_obj, 'download_dir', 'downloads'),
content_weight_mode=getattr(settings_obj, 'content_weight_mode', 'auto'),
content_weight_anime=getattr(settings_obj, 'content_weight_anime', 2),
content_weight_series=getattr(settings_obj, 'content_weight_series', 1),
)
@router.patch("", response_model=AppSettings)
async def update_settings(
update_data: AppSettingsUpdate,
response: Response,
current_user: User = Depends(get_current_user_from_token),
session: Session = Depends(get_session),
):
"""Update application settings for the user"""
statement = select(AppSettingsTable).where(
AppSettingsTable.user_id == current_user.id
)
settings_obj = session.exec(statement).first()
if not settings_obj:
settings_obj = AppSettingsTable(user_id=current_user.id)
session.add(settings_obj)
if update_data.default_lang is not None:
settings_obj.default_lang = update_data.default_lang
if update_data.theme is not None:
settings_obj.theme = update_data.theme
if update_data.disabled_providers is not None:
settings_obj.disabled_providers = update_data.disabled_providers
if update_data.recommendations_filter is not None:
settings_obj.recommendations_filter = update_data.recommendations_filter
if update_data.releases_filter is not None:
settings_obj.releases_filter = update_data.releases_filter
if update_data.anime_enabled is not None:
# Prevent disabling both categories
if not update_data.anime_enabled and not getattr(settings_obj, 'series_enabled', True):
raise HTTPException(status_code=400, detail="Au moins une categorie doit rester active")
settings_obj.anime_enabled = update_data.anime_enabled
if update_data.series_enabled is not None:
# Prevent disabling both categories
if not update_data.series_enabled and not getattr(settings_obj, 'anime_enabled', True):
raise HTTPException(status_code=400, detail="Au moins une categorie doit rester active")
settings_obj.series_enabled = update_data.series_enabled
if update_data.download_dir is not None:
settings_obj.download_dir = update_data.download_dir
if update_data.content_weight_mode is not None:
settings_obj.content_weight_mode = update_data.content_weight_mode
if update_data.content_weight_anime is not None:
settings_obj.content_weight_anime = update_data.content_weight_anime
if update_data.content_weight_series is not None:
settings_obj.content_weight_series = update_data.content_weight_series
session.add(settings_obj)
session.commit()
session.refresh(settings_obj)
response.headers["HX-Trigger"] = json.dumps(
{"show-toast": {"message": "Paramètres enregistrés", "type": "success"}}
)
return settings_obj
@router.get("/content-weight")
async def get_content_weight(
current_user: User = Depends(get_current_user_from_token),
session: Session = Depends(get_session),
):
"""Get current effective content weights (auto-computed or manual)"""
statement = select(AppSettingsTable).where(
AppSettingsTable.user_id == current_user.id
)
settings_obj = session.exec(statement).first()
download_dir = getattr(settings_obj, 'download_dir', 'downloads') if settings_obj else 'downloads'
mode = getattr(settings_obj, 'content_weight_mode', 'auto') if settings_obj else 'auto'
if mode == "auto":
weights = _compute_auto_weights(download_dir)
weights["mode"] = "auto"
return weights
else:
return {
"mode": "manual",
"anime_weight": getattr(settings_obj, 'content_weight_anime', 2),
"series_weight": getattr(settings_obj, 'content_weight_series', 1),
"anime_count": None,
"series_count": None,
"total": None,
}
@router.get("/providers/availability")
async def get_providers_availability(
current_user: User = Depends(get_current_user_from_token),
session: Session = Depends(get_session),
):
"""Get list of providers with their availability and enabled status"""
# Get user settings
statement = select(AppSettingsTable).where(
AppSettingsTable.user_id == current_user.id
)
settings_obj = session.exec(statement).first()
disabled_providers = settings_obj.disabled_providers if settings_obj else []
# Get health status
health_status = providers_manager.get_all_status()
# Combine anime and series providers
all_providers = {**get_anime_providers(), **get_series_providers()}
result = []
for pid, info in all_providers.items():
status_info = health_status.get(pid, {"status": "unknown"})
result.append(
{
"id": pid,
"name": info["name"],
"icon": info.get("icon", "🎬"),
"status": status_info["status"],
"enabled": pid not in disabled_providers,
"error": status_info.get("error"),
}
)
return result
@router.post("/providers/{provider_id}/toggle")
async def toggle_provider(
provider_id: str,
response: Response,
current_user: User = Depends(get_current_user_from_token),
session: Session = Depends(get_session),
):
"""Toggle a provider's enabled/disabled status"""
statement = select(AppSettingsTable).where(
AppSettingsTable.user_id == current_user.id
)
settings_obj = session.exec(statement).first()
if not settings_obj:
settings_obj = AppSettingsTable(user_id=current_user.id)
session.add(settings_obj)
disabled = settings_obj.disabled_providers
if provider_id in disabled:
disabled.remove(provider_id)
enabled = True
else:
disabled.append(provider_id)
enabled = False
settings_obj.disabled_providers = disabled
session.add(settings_obj)
session.commit()
status_text = "activé" if enabled else "désactivé"
response.headers["HX-Trigger"] = json.dumps(
{
"show-toast": {
"message": f"Fournisseur {provider_id} {status_text}",
"type": "success",
}
}
)
return {"id": provider_id, "enabled": enabled}
@router.get("/ui")
async def get_settings_ui(
request: Request,
current_user: Optional[User] = Depends(get_optional_user),
session: Session = Depends(get_session),
):
if current_user is None:
return templates.TemplateResponse(
"components/login_prompt.html", {"request": request}
)
settings = await get_settings(current_user, session)
providers = await get_providers_availability(current_user, session)
return templates.TemplateResponse(
"components/settings_section.html",
{"request": request, "settings": settings, "providers": providers},
)