Merge branch 'main' of github.com:mhdzumair/MediaFusion

This commit is contained in:
mhdzumair
2025-03-24 01:05:49 +05:30
3 changed files with 222 additions and 1 deletions
+216
View File
@@ -0,0 +1,216 @@
import math
from typing import Dict, List, Optional, Any, Literal
from fastapi import APIRouter, Depends, Request, HTTPException, BackgroundTasks
from pydantic import BaseModel
from db import crud, schemas
from db.config import settings
from utils import const
from utils.network import get_user_data, get_user_public_ip
# Create a router with more appropriate naming for the frontend
router = APIRouter(prefix="/api/v1", tags=["frontend"])
# ---- Data Models ----
class AppConfig(BaseModel):
"""Base configuration data for the application"""
addon_name: str
logo_url: str
host_url: str
poster_host_url: Optional[str]
version: str
description: str
branding_description: str
is_public_instance: bool
disabled_providers: List[str]
authentication_required: bool
class UserConfig(BaseModel):
"""User-specific configuration data"""
user_data: Dict[str, Any]
configured_fields: List[str]
class ValidationResult(BaseModel):
"""Data model for validation results"""
status: str
message: Optional[str] = None
class StreamData(BaseModel):
"""Data model for stream information"""
title: str
year: int
poster: str
background: str
description: str
streams: List[Dict[str, Any]]
catalog_type: str
season: Optional[int] = None
episode: Optional[int] = None
video_id: str
secret_str: str
series_data: Optional[str] = None
# ---- API Endpoints ----
@router.get("/app-config", response_model=AppConfig)
async def get_app_config():
"""
Get basic application configuration.
Replaces the former home endpoint with more specific naming.
"""
return {
"addon_name": settings.addon_name,
"logo_url": settings.logo_url,
"host_url": settings.host_url,
"poster_host_url": settings.poster_host_url,
"version": settings.version,
"description": settings.description,
"branding_description": settings.branding_description,
"is_public_instance": settings.is_public_instance,
"disabled_providers": settings.disabled_providers,
"authentication_required": settings.api_password is not None and not settings.is_public_instance,
}
@router.get("/user-config", response_model=UserConfig)
async def get_user_config(
user_data: schemas.UserData = Depends(get_user_data),
secret_str: Optional[str] = None,
):
"""
Get user-specific configuration.
Returns only user configuration without default values from constants.
"""
configured_fields = []
# Handle sensitive data masking
if user_data.streaming_provider:
user_data.streaming_provider.password = "••••••••"
user_data.streaming_provider.token = "••••••••"
configured_fields.extend(["provider_token", "password"])
if user_data.streaming_provider.qbittorrent_config:
user_data.streaming_provider.qbittorrent_config.qbittorrent_password = "••••••••"
user_data.streaming_provider.qbittorrent_config.webdav_password = "••••••••"
configured_fields.extend(["qbittorrent_password", "webdav_password"])
if user_data.mediaflow_config:
user_data.mediaflow_config.api_password = "••••••••"
configured_fields.append("mediaflow_api_password")
if user_data.rpdb_config:
user_data.rpdb_config.api_key = "••••••••"
configured_fields.append("rpdb_api_key")
user_data.api_password = None
return {
"user_data": user_data.model_dump(),
"configured_fields": configured_fields,
}
@router.get("/system-constants")
async def get_system_constants():
"""
Get all system constants needed by the frontend.
Renamed from 'constants' to be more descriptive.
"""
return {
"CATALOG_DATA": const.CATALOG_DATA,
"RESOLUTIONS": const.RESOLUTIONS,
"TORRENT_SORTING_PRIORITY_OPTIONS": const.TORRENT_SORTING_PRIORITY_OPTIONS,
"SUPPORTED_LANGUAGES": const.SUPPORTED_LANGUAGES,
"QUALITY_GROUPS": const.QUALITY_GROUPS,
}
@router.get("/download/{secret_str}/{catalog_type}/{video_id}", response_model=StreamData)
@router.get("/download/{secret_str}/{catalog_type}/{video_id}/{season}/{episode}", response_model=StreamData)
async def get_download_info(
request: Request,
secret_str: str,
catalog_type: Literal["movie", "series"],
video_id: str,
user_data: schemas.UserData = Depends(get_user_data),
background_tasks: BackgroundTasks = BackgroundTasks(),
season: int = None,
episode: int = None,
):
"""
Get download information for the React frontend.
Replaces the Jinja template rendering for the download page.
"""
if (
not user_data.streaming_provider
or not user_data.streaming_provider.download_via_browser
):
raise HTTPException(
status_code=403,
detail="Download option is not enabled or no streaming provider configured",
)
metadata = (
await crud.get_movie_data_by_id(video_id)
if catalog_type == "movie"
else await crud.get_series_data_by_id(video_id)
)
if not metadata:
raise HTTPException(status_code=404, detail="Metadata not found")
user_ip = await get_user_public_ip(request, user_data)
if catalog_type == "movie":
streams = await crud.get_movie_streams(
user_data, secret_str, video_id, user_ip, background_tasks
)
else:
streams = await crud.get_series_streams(
user_data, secret_str, video_id, season, episode, user_ip, background_tasks
)
streaming_provider_path = f"{settings.host_url}/streaming_provider/"
downloadable_streams = [
stream.model_dump()
for stream in streams
if stream.url and stream.url.startswith(streaming_provider_path)
]
if video_id.startswith("tt") and user_data.rpdb_config:
_poster = f"https://api.ratingposterdb.com/{user_data.rpdb_config.api_key}/imdb/poster-default/{video_id}.jpg?fallback=true"
else:
_poster = f"{settings.poster_host_url}/poster/{catalog_type}/{video_id}.jpg"
background = (
metadata.background
if metadata.background
else f"{settings.host_url}/static/images/background.jpg"
)
return {
"title": metadata.title,
"year": metadata.year,
"poster": _poster,
"background": background,
"description": metadata.description,
"streams": downloadable_streams,
"catalog_type": catalog_type,
"season": season,
"episode": episode,
"video_id": video_id,
"secret_str": secret_str,
"series_data": (
metadata.model_dump_json(
include={"episodes": {"__all__": {"season_number", "episode_number"}}}
)
if catalog_type == "series"
else None
),
}
+5
View File
@@ -29,6 +29,7 @@ from db.redis_database import REDIS_ASYNC_CLIENT
from db.schemas import SortingOption
from kodi.routes import kodi_router
from metrics.routes import metrics_router
from api.frontend_api import router as frontend_api_router
from scrapers.routes import router as scrapers_router
from scrapers.rpdb import update_rpdb_posters, update_rpdb_poster
from streaming_providers import mapper
@@ -123,6 +124,7 @@ app.add_middleware(middleware.SecureLoggingMiddleware)
app.mount("/static", StaticFiles(directory="resources"), name="static")
# Keep the existing template-based routes for backward compatibility
@app.get("/", tags=["home"])
async def get_home(request: Request):
return TEMPLATES.TemplateResponse(
@@ -957,3 +959,6 @@ app.include_router(scrapers_router, prefix="/scraper", tags=["scraper"])
app.include_router(metrics_router, prefix="/metrics", tags=["metrics"])
app.include_router(kodi_router, prefix="/kodi", tags=["kodi"])
# Add the frontend API router
app.include_router(frontend_api_router)
+1 -1
View File
@@ -72,7 +72,7 @@ class UserDataMiddleware(BaseHTTPMiddleware):
if encoded_user_data:
user_data = crypto_utils.decode_user_data(encoded_user_data)
else:
secret_str = request.path_params.get("secret_str")
secret_str = request.path_params.get("secret_str") or request.query_params.get("secret_str")
user_data = await crypto_utils.decrypt_user_data(secret_str)
except (ValueError, ValidationError):
# check if the endpoint is for /streams