mirror of
https://github.com/HyPnoTiiK/stream-fusion.git
synced 2026-07-28 23:12:46 +00:00
Async correctness: - Wrap blocking time.sleep/requests.get calls with asyncio.to_thread() in torrent_service.py and torr9_service.py - Run cache_container_items() as fire-and-forget background task via asyncio.create_task(asyncio.to_thread(...)) in search/views.py - Make RDTokenManager.get_access_token() and new_access_token() async, replace requests.post with aiohttp (token_manager.py) - Make RealDebrid.get_headers() async, await all callers (realdebrid.py) Security: - Remove API key, device code, and PIN values from log output (auth/views.py, admin/views.py) - Add WARNING on startup when SESSION_KEY uses the insecure default value (settings.py) - Make admin template dir configurable via settings.admin_template_dir Performance: - Parallelize TMDB metadata fetches with asyncio.gather() (catalog/views.py) - Add tmdb_language setting instead of hardcoded "fr-FR" Deduplication: - Extract ensure_uuid() to stream_fusion/web/api/utils.py - Extract get_client_ip() to stream_fusion/web/utils.py - Replace 8-way if/elif debrid factory with _SERVICE_MAP dict + _build_service() helper (get_debrid_service.py) - Extract _model_to_schema() in APIKeyDAO to remove 6 repeated blocks - Simplify ProxyStreamer: remove manual BytesIO buffer, stream directly via iter_any() (stream/views.py) Inspired by commits 823bc77 and e306eb5 from limedrive/develop. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
270 lines
No EOL
10 KiB
Python
270 lines
No EOL
10 KiB
Python
from datetime import datetime, timezone
|
|
from typing import List, Optional
|
|
from uuid import UUID
|
|
import aiohttp
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
|
|
from stream_fusion.services.postgresql.dao.apikey_dao import APIKeyDAO
|
|
from stream_fusion.services.postgresql.schemas.apikey_schemas import APIKeyCreate, APIKeyUpdate
|
|
from stream_fusion.services.rd_conn.token_manager import RealDebridService
|
|
from stream_fusion.settings import settings
|
|
from stream_fusion.utils.security import secret_based_security
|
|
from stream_fusion.web.api.auth.schemas import UsageLog, UsageLogs
|
|
from stream_fusion.logging_config import logger
|
|
from stream_fusion.web.api.utils import ensure_uuid
|
|
|
|
router = APIRouter()
|
|
rd_service = RealDebridService()
|
|
|
|
@router.post(
|
|
"/new",
|
|
dependencies=[Depends(secret_based_security)],
|
|
include_in_schema=settings.security_hide_docs,
|
|
)
|
|
async def create_new_api_key(
|
|
name: str = Query(None, description="Set API key name"),
|
|
never_expires: bool = Query(False, description="If set, the created API key will never be considered expired"),
|
|
apikey_dao: APIKeyDAO = Depends()
|
|
) -> str:
|
|
logger.info(f"Creating new API key. Name: {name}, Never expires: {never_expires}")
|
|
api_key = await apikey_dao.create_key(APIKeyCreate(
|
|
name=name,
|
|
never_expire=never_expires
|
|
))
|
|
logger.info("New API key created successfully.")
|
|
return api_key.api_key
|
|
|
|
@router.get(
|
|
"/get/{api_key}",
|
|
dependencies=[Depends(secret_based_security)],
|
|
include_in_schema=settings.security_hide_docs,
|
|
)
|
|
async def get_api_key(
|
|
api_key: str,
|
|
apikey_dao: APIKeyDAO = Depends()
|
|
):
|
|
api_key_uuid = ensure_uuid(api_key)
|
|
logger.info(f"Retrieving API key: {api_key_uuid}")
|
|
key = await apikey_dao.get_key_by_uuid(api_key_uuid)
|
|
if not key:
|
|
logger.warning(f"API key not found: {api_key_uuid}")
|
|
raise HTTPException(status_code=404, detail="API key not found")
|
|
return key.dict()
|
|
|
|
@router.get(
|
|
"/get_by_name/{name}",
|
|
dependencies=[Depends(secret_based_security)],
|
|
include_in_schema=settings.security_hide_docs,
|
|
)
|
|
async def get_api_keys_by_name(
|
|
name: str,
|
|
apikey_dao: APIKeyDAO = Depends()
|
|
):
|
|
logger.info(f"Retrieving API keys with name: {name}")
|
|
keys = await apikey_dao.get_keys_by_name(name)
|
|
return [key.dict() for key in keys]
|
|
|
|
@router.put(
|
|
"/revoke/{api_key}",
|
|
dependencies=[Depends(secret_based_security)],
|
|
include_in_schema=settings.security_hide_docs,
|
|
)
|
|
async def revoke_api_key(
|
|
api_key: str,
|
|
apikey_dao: APIKeyDAO = Depends()
|
|
):
|
|
api_key_uuid = ensure_uuid(api_key)
|
|
logger.info(f"Attempting to revoke API key: {api_key_uuid}")
|
|
result = await apikey_dao.revoke_key(api_key_uuid)
|
|
if not result:
|
|
logger.warning(f"API key not found for revocation: {api_key_uuid}")
|
|
raise HTTPException(status_code=404, detail="API key not found")
|
|
logger.info(f"API key successfully revoked: {api_key_uuid}")
|
|
return {"message": "API key successfully revoked"}
|
|
|
|
@router.put(
|
|
"/renew/{api_key}",
|
|
dependencies=[Depends(secret_based_security)],
|
|
include_in_schema=settings.security_hide_docs,
|
|
)
|
|
async def renew_api_key(
|
|
api_key: str,
|
|
apikey_dao: APIKeyDAO = Depends()
|
|
):
|
|
api_key_uuid = ensure_uuid(api_key)
|
|
logger.info(f"Attempting to renew API key: {api_key_uuid}")
|
|
try:
|
|
updated_key = await apikey_dao.renew_key(api_key_uuid)
|
|
logger.info(f"API key renewed successfully: {api_key_uuid}")
|
|
return updated_key.dict()
|
|
except HTTPException as e:
|
|
logger.error(f"HTTP exception occurred while renewing API key {api_key_uuid}: {str(e)}")
|
|
raise e
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error occurred while renewing API key {api_key_uuid}: {str(e)}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@router.get(
|
|
"/logs",
|
|
dependencies=[Depends(secret_based_security)],
|
|
response_model=UsageLogs,
|
|
include_in_schema=settings.security_hide_docs,
|
|
)
|
|
async def get_api_key_usage_logs(
|
|
apikey_dao: APIKeyDAO = Depends()
|
|
):
|
|
logger.info("Retrieving usage logs for all API keys")
|
|
usage_stats = await apikey_dao.get_usage_stats()
|
|
logger.info(f"Retrieved usage stats for {len(usage_stats)} API keys")
|
|
|
|
current_time = datetime.now(timezone.utc)
|
|
|
|
return UsageLogs(
|
|
logs=[
|
|
UsageLog(
|
|
api_key=key.api_key,
|
|
# Update is_active based on expiration status
|
|
is_active=key.is_active and (key.never_expire or (key.expiration_date and key.expiration_date > current_time)),
|
|
never_expire=key.never_expire,
|
|
expiration_date=key.expiration_date,
|
|
latest_query_date=key.latest_query_date,
|
|
total_queries=key.total_queries,
|
|
name=key.name,
|
|
)
|
|
for key in usage_stats
|
|
],
|
|
)
|
|
|
|
@router.get(
|
|
"/list",
|
|
dependencies=[Depends(secret_based_security)],
|
|
include_in_schema=settings.security_hide_docs,
|
|
)
|
|
async def list_active_api_keys(
|
|
apikey_dao: APIKeyDAO = Depends()
|
|
) -> List[dict]:
|
|
logger.info("Retrieving list of active API keys")
|
|
active_keys = await apikey_dao.list_active_keys()
|
|
logger.info(f"Retrieved {len(active_keys)} active API keys")
|
|
return [key.dict() for key in active_keys]
|
|
|
|
@router.put(
|
|
"/update/{api_key}",
|
|
dependencies=[Depends(secret_based_security)],
|
|
include_in_schema=settings.security_hide_docs,
|
|
)
|
|
async def update_api_key(
|
|
api_key: str,
|
|
name: Optional[str] = Query(None, description="New name for the API key"),
|
|
is_active: Optional[bool] = Query(None, description="Set the active status of the API key"),
|
|
expiration_date: Optional[datetime] = Query(None, description="New expiration date for the API key"),
|
|
apikey_dao: APIKeyDAO = Depends()
|
|
):
|
|
api_key_uuid = ensure_uuid(api_key)
|
|
logger.info(f"Attempting to update API key: {api_key_uuid}")
|
|
update_data = APIKeyUpdate(
|
|
name=name,
|
|
is_active=is_active,
|
|
expiration_date=expiration_date
|
|
)
|
|
try:
|
|
updated_key = await apikey_dao.update_key(api_key_uuid, update_data)
|
|
logger.info(f"API key updated successfully: {api_key_uuid}")
|
|
return updated_key.dict()
|
|
except HTTPException as e:
|
|
logger.error(f"HTTP exception occurred while updating API key {api_key_uuid}: {str(e)}")
|
|
raise e
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error occurred while updating API key {api_key_uuid}: {str(e)}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@router.delete(
|
|
"/delete/{api_key}",
|
|
dependencies=[Depends(secret_based_security)],
|
|
include_in_schema=settings.security_hide_docs,
|
|
)
|
|
async def delete_api_key(
|
|
api_key: str,
|
|
apikey_dao: APIKeyDAO = Depends()
|
|
):
|
|
api_key_uuid = ensure_uuid(api_key)
|
|
logger.info(f"Attempting to delete API key: {api_key_uuid}")
|
|
try:
|
|
result = await apikey_dao.delete_key(api_key_uuid)
|
|
if result:
|
|
logger.info(f"API key deleted successfully: {api_key_uuid}")
|
|
return {"message": "API key successfully deleted"}
|
|
else:
|
|
logger.warning(f"API key not found for deletion: {api_key_uuid}")
|
|
raise HTTPException(status_code=404, detail="API key not found")
|
|
except Exception as e:
|
|
logger.error(f"An error occurred while deleting API key {api_key_uuid}: {str(e)}")
|
|
raise HTTPException(status_code=500, detail=f"An error occurred while deleting the API key: {str(e)}")
|
|
|
|
@router.get(
|
|
"/check/{api_key}",
|
|
dependencies=[Depends(secret_based_security)],
|
|
include_in_schema=settings.security_hide_docs,
|
|
)
|
|
async def check_api_key(
|
|
api_key: str,
|
|
apikey_dao: APIKeyDAO = Depends()
|
|
):
|
|
api_key_uuid = ensure_uuid(api_key)
|
|
logger.info(f"Checking API key: {api_key_uuid}")
|
|
is_valid = await apikey_dao.check_key(api_key_uuid)
|
|
return {"is_valid": is_valid}
|
|
|
|
|
|
@router.post(
|
|
"/realdebrid/device_code",
|
|
include_in_schema=settings.security_hide_docs,
|
|
)
|
|
async def get_device_code():
|
|
logger.info("Requesting device code from RealDebrid")
|
|
results = await rd_service.get_device_code()
|
|
logger.info("Device code received successfully")
|
|
return results
|
|
|
|
@router.post("/realdebrid/credentials",
|
|
include_in_schema=settings.security_hide_docs
|
|
)
|
|
async def get_credentials(device_code: str):
|
|
logger.info("Requesting credentials for device code")
|
|
results = await rd_service.get_credentials(device_code)
|
|
logger.info("Credentials received successfully")
|
|
return results
|
|
|
|
@router.post("/realdebrid/token",
|
|
include_in_schema=settings.security_hide_docs
|
|
)
|
|
async def get_token(client_id: str, client_secret: str, device_code: str):
|
|
logger.info(f"Requesting token for device code: {device_code}")
|
|
results = await rd_service.get_token(client_id, client_secret, device_code)
|
|
logger.info("Token received successfully")
|
|
return results
|
|
|
|
|
|
@router.get("/alldebrid/pin/get")
|
|
async def get_alldebrid_pin():
|
|
logger.info("Requesting PIN code for AllDebrid")
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.get(f"https://api.alldebrid.com/v4/pin/get?agent={settings.ad_user_app}") as response:
|
|
if response.status != 200:
|
|
logger.error(f"Error retrieving AllDebrid PIN: {response.status}")
|
|
raise HTTPException(status_code=response.status, detail="Error retrieving AllDebrid PIN")
|
|
data = await response.json()
|
|
logger.info("AllDebrid PIN code received successfully")
|
|
return data
|
|
|
|
@router.get("/alldebrid/pin/check")
|
|
async def check_alldebrid_pin(check: str, pin: str):
|
|
logger.info("Verifying AllDebrid PIN code")
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.get(f"https://api.alldebrid.com/v4/pin/check?check={check}&pin={pin}&agent={settings.ad_user_app}") as response:
|
|
if response.status != 200:
|
|
logger.error(f"Error verifying AllDebrid PIN: {response.status}")
|
|
raise HTTPException(status_code=response.status, detail="Error verifying AllDebrid PIN")
|
|
data = await response.json()
|
|
logger.info("AllDebrid PIN code verification successful")
|
|
return data |