feat: add Settings tab with provider management and language preferences
- Implemented AppSettings model and table using SQLModel. - Created Settings router with endpoints for preferences and provider toggling. - Added Settings tab to the UI with real-time health status of providers. - Integrated language and provider filtering into anime and series search logic. - Updated templates to respect user-defined settings.
This commit is contained in:
@@ -10,8 +10,9 @@ from app.routers.router_recommendations import router as recommendations_router
|
||||
from app.routers.router_watchlist import router as watchlist_router
|
||||
from app.routers.router_sonarr import router as sonarr_router
|
||||
from app.routers.router_player import router as player_router
|
||||
from app.routers.router_static import router as static_router
|
||||
from app.routers.router_root import router as root_router
|
||||
from .router_static import router as static_router
|
||||
from .router_root import router as root_router
|
||||
from .router_settings import router as settings_router
|
||||
|
||||
__all__ = [
|
||||
"auth_router",
|
||||
@@ -24,4 +25,6 @@ __all__ = [
|
||||
"player_router",
|
||||
"static_router",
|
||||
"root_router",
|
||||
"settings_router",
|
||||
]
|
||||
|
||||
|
||||
@@ -11,7 +11,12 @@ import hashlib
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, Request, Response
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.database import get_session
|
||||
from app.models.settings import AppSettingsTable
|
||||
from app.routers.router_auth import get_current_user_from_token
|
||||
from app.models.auth import User
|
||||
from app.download_manager import DownloadManager
|
||||
from app.downloaders import (
|
||||
AnimeSamaDownloader,
|
||||
@@ -67,6 +72,8 @@ async def search_anime_unified(
|
||||
lang: str = "vostfr",
|
||||
include_metadata: bool = False,
|
||||
html: bool = Query(False),
|
||||
current_user: User = Depends(get_current_user_from_token),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""
|
||||
Search across all anime providers.
|
||||
@@ -75,6 +82,11 @@ async def search_anime_unified(
|
||||
print(f"\n[SEARCH] Starting search for '{q}'. html={html}")
|
||||
start_time = time.time()
|
||||
|
||||
# Get user settings for disabled providers
|
||||
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 []
|
||||
|
||||
results = {}
|
||||
|
||||
# 1. Prepare search tasks (Generic + Legacy)
|
||||
@@ -84,8 +96,9 @@ async def search_anime_unified(
|
||||
# Generic YAML providers
|
||||
active_generic = providers_manager.get_active_providers()
|
||||
for provider in active_generic:
|
||||
search_tasks.append(provider.search(q))
|
||||
task_metadata.append({"id": provider.id, "type": "generic"})
|
||||
if provider.id not in disabled_providers:
|
||||
search_tasks.append(provider.search(q))
|
||||
task_metadata.append({"id": provider.id, "type": "generic"})
|
||||
|
||||
# Legacy providers
|
||||
legacy_downloaders = {
|
||||
@@ -94,8 +107,9 @@ async def search_anime_unified(
|
||||
"vostfree": VostfreeDownloader(),
|
||||
}
|
||||
for pid, dl in legacy_downloaders.items():
|
||||
search_tasks.append(dl.search_anime(q, lang, include_metadata=False))
|
||||
task_metadata.append({"id": pid, "type": "legacy"})
|
||||
if pid not in disabled_providers:
|
||||
search_tasks.append(dl.search_anime(q, lang, include_metadata=False))
|
||||
task_metadata.append({"id": pid, "type": "legacy"})
|
||||
|
||||
# 2. Run searches in parallel
|
||||
all_raw_results = await asyncio.gather(*search_tasks, return_exceptions=True)
|
||||
@@ -163,7 +177,11 @@ async def search_anime_unified(
|
||||
print("[SEARCH] Returning HTML response")
|
||||
return templates.TemplateResponse(
|
||||
"components/anime_search_results.html",
|
||||
{"request": request, "results": results}
|
||||
{
|
||||
"request": request,
|
||||
"results": results,
|
||||
"settings": settings_obj
|
||||
}
|
||||
)
|
||||
|
||||
print("[SEARCH] Returning JSON response")
|
||||
@@ -181,6 +199,8 @@ async def search_series_unified(
|
||||
q: str,
|
||||
lang: str = "vf",
|
||||
html: bool = Query(False),
|
||||
current_user: User = Depends(get_current_user_from_token),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""
|
||||
Search across all TV series providers (FS7, etc.)
|
||||
@@ -191,6 +211,12 @@ async def search_series_unified(
|
||||
|
||||
print(f"\n[SERIES SEARCH] Starting search for '{q}' in {lang}. html={html}")
|
||||
start_time = time.time()
|
||||
|
||||
# Get user settings for disabled providers
|
||||
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 []
|
||||
|
||||
results = {}
|
||||
series_downloaders = {
|
||||
"fs7": FS7Downloader(),
|
||||
@@ -200,7 +226,7 @@ async def search_series_unified(
|
||||
provider_ids = []
|
||||
|
||||
for provider_id, provider in get_series_providers().items():
|
||||
if provider_id in series_downloaders:
|
||||
if provider_id in series_downloaders and provider_id not in disabled_providers:
|
||||
downloader = series_downloaders[provider_id]
|
||||
search_tasks.append(downloader.search_anime(q, lang))
|
||||
provider_ids.append(provider_id)
|
||||
@@ -221,7 +247,11 @@ async def search_series_unified(
|
||||
if html or request.headers.get("HX-Request"):
|
||||
return templates.TemplateResponse(
|
||||
"components/series_search_results.html",
|
||||
{"request": request, "results": results}
|
||||
{
|
||||
"request": request,
|
||||
"results": results,
|
||||
"settings": settings_obj
|
||||
}
|
||||
)
|
||||
|
||||
return {"query": q, "lang": lang, "results": results}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
"""Application settings routes for Ohm Stream Downloader API"""
|
||||
import json
|
||||
from typing import List, Dict, Any
|
||||
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
|
||||
from app.providers import get_anime_providers, get_series_providers
|
||||
from app.providers_manager import providers_manager
|
||||
|
||||
router = APIRouter(prefix="/api/settings", tags=["settings"])
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
|
||||
@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
|
||||
)
|
||||
|
||||
|
||||
@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
|
||||
|
||||
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 AppSettings(
|
||||
default_lang=settings_obj.default_lang,
|
||||
theme=settings_obj.theme,
|
||||
disabled_providers=settings_obj.disabled_providers
|
||||
)
|
||||
|
||||
|
||||
@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: User = Depends(get_current_user_from_token),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""Return the settings UI fragment for HTMX"""
|
||||
# Reuse existing endpoints logic
|
||||
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
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user