Files
MediaManager/media_manager/tv/router.py
Marcel Hellwig 96b84d45db Adding some more new lints (#393)
Enable `UP` and `TRY` lint
2026-02-01 18:04:15 +01:00

478 lines
14 KiB
Python

from pathlib import Path
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from media_manager.auth.db import User
from media_manager.auth.schemas import UserRead
from media_manager.auth.users import current_active_user, current_superuser
from media_manager.config import LibraryItem, MediaManagerConfig
from media_manager.exceptions import MediaAlreadyExistsError, NotFoundError
from media_manager.indexer.schemas import (
IndexerQueryResult,
IndexerQueryResultId,
)
from media_manager.metadataProvider.dependencies import metadata_provider_dep
from media_manager.metadataProvider.schemas import MetaDataProviderSearchResult
from media_manager.schemas import MediaImportSuggestion
from media_manager.torrent.schemas import Torrent
from media_manager.torrent.utils import get_importable_media_directories
from media_manager.tv import log
from media_manager.tv.dependencies import (
season_dep,
show_dep,
tv_service_dep,
)
from media_manager.tv.schemas import (
CreateSeasonRequest,
PublicSeasonFile,
PublicShow,
RichSeasonRequest,
RichShowTorrent,
Season,
SeasonRequest,
SeasonRequestId,
Show,
ShowId,
UpdateSeasonRequest,
)
router = APIRouter()
# -----------------------------------------------------------------------------
# METADATA & SEARCH
# -----------------------------------------------------------------------------
@router.get(
"/search",
dependencies=[Depends(current_active_user)],
)
def search_metadata_providers_for_a_show(
tv_service: tv_service_dep, query: str, metadata_provider: metadata_provider_dep
) -> list[MetaDataProviderSearchResult]:
"""
Search for a show on the configured metadata provider.
"""
return tv_service.search_for_show(query=query, metadata_provider=metadata_provider)
@router.get(
"/recommended",
dependencies=[Depends(current_active_user)],
)
def get_recommended_shows(
tv_service: tv_service_dep, metadata_provider: metadata_provider_dep
) -> list[MetaDataProviderSearchResult]:
"""
Get a list of recommended/popular shows from the metadata provider.
"""
return tv_service.get_popular_shows(metadata_provider=metadata_provider)
# -----------------------------------------------------------------------------
# IMPORTING
# -----------------------------------------------------------------------------
@router.get(
"/importable",
status_code=status.HTTP_200_OK,
dependencies=[Depends(current_superuser)],
)
def get_all_importable_shows(
tv_service: tv_service_dep, metadata_provider: metadata_provider_dep
) -> list[MediaImportSuggestion]:
"""
Get a list of unknown shows that were detected in the TV directory and are importable.
"""
return tv_service.get_importable_tv_shows(metadata_provider=metadata_provider)
@router.post(
"/importable/{show_id}",
dependencies=[Depends(current_superuser)],
status_code=status.HTTP_204_NO_CONTENT,
)
def import_detected_show(
tv_service: tv_service_dep, tv_show: show_dep, directory: str
) -> None:
"""
Import a detected show from the specified directory into the library.
"""
source_directory = Path(directory)
if source_directory not in get_importable_media_directories(
MediaManagerConfig().misc.tv_directory
):
raise HTTPException(status.HTTP_400_BAD_REQUEST, "No such directory")
tv_service.import_existing_tv_show(
tv_show=tv_show, source_directory=source_directory
)
# -----------------------------------------------------------------------------
# SHOWS
# -----------------------------------------------------------------------------
@router.get(
"/shows",
dependencies=[Depends(current_active_user)],
)
def get_all_shows(tv_service: tv_service_dep) -> list[Show]:
"""
Get all shows in the library.
"""
return tv_service.get_all_shows()
@router.post(
"/shows",
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(current_active_user)],
responses={
status.HTTP_201_CREATED: {
"model": Show,
"description": "Successfully created show",
}
},
)
def add_a_show(
tv_service: tv_service_dep,
metadata_provider: metadata_provider_dep,
show_id: int,
language: str | None = None,
) -> Show:
"""
Add a new show to the library.
"""
try:
show = tv_service.add_show(
external_id=show_id,
metadata_provider=metadata_provider,
language=language,
)
except MediaAlreadyExistsError:
show = tv_service.get_show_by_external_id(
show_id, metadata_provider=metadata_provider.name
)
if not show:
raise NotFoundError from MediaAlreadyExistsError
return show
@router.get(
"/shows/torrents",
dependencies=[Depends(current_active_user)],
)
def get_shows_with_torrents(tv_service: tv_service_dep) -> list[RichShowTorrent]:
"""
Get all shows that are associated with torrents.
"""
return tv_service.get_all_shows_with_torrents()
@router.get(
"/shows/libraries",
dependencies=[Depends(current_active_user)],
)
def get_available_libraries() -> list[LibraryItem]:
"""
Get available TV libraries from configuration.
"""
return MediaManagerConfig().misc.tv_libraries
# -----------------------------------------------------------------------------
# SHOWS - INDIVIDUAL
# -----------------------------------------------------------------------------
@router.get(
"/shows/{show_id}",
dependencies=[Depends(current_active_user)],
)
def get_a_show(show: show_dep, tv_service: tv_service_dep) -> PublicShow:
"""
Get details for a specific show.
"""
return tv_service.get_public_show_by_id(show=show)
@router.delete(
"/shows/{show_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(current_superuser)],
)
def delete_a_show(
tv_service: tv_service_dep,
show: show_dep,
delete_files_on_disk: bool = False,
delete_torrents: bool = False,
) -> None:
"""
Delete a show from the library.
"""
tv_service.delete_show(
show=show,
delete_files_on_disk=delete_files_on_disk,
delete_torrents=delete_torrents,
)
@router.post(
"/shows/{show_id}/metadata",
dependencies=[Depends(current_active_user)],
)
def update_shows_metadata(
show: show_dep, tv_service: tv_service_dep, metadata_provider: metadata_provider_dep
) -> PublicShow:
"""
Update a show's metadata from the provider.
"""
tv_service.update_show_metadata(db_show=show, metadata_provider=metadata_provider)
return tv_service.get_public_show_by_id(show=show)
@router.post(
"/shows/{show_id}/continuousDownload",
dependencies=[Depends(current_superuser)],
)
def set_continuous_download(
show: show_dep, tv_service: tv_service_dep, continuous_download: bool
) -> PublicShow:
"""
Toggle whether future seasons of a show will be automatically downloaded.
"""
tv_service.set_show_continuous_download(
show=show, continuous_download=continuous_download
)
return tv_service.get_public_show_by_id(show=show)
@router.post(
"/shows/{show_id}/library",
dependencies=[Depends(current_superuser)],
status_code=status.HTTP_204_NO_CONTENT,
)
def set_library(
show: show_dep,
tv_service: tv_service_dep,
library: str,
) -> None:
"""
Set the library path for a Show.
"""
tv_service.set_show_library(show=show, library=library)
return
@router.get(
"/shows/{show_id}/torrents",
dependencies=[Depends(current_active_user)],
)
def get_a_shows_torrents(show: show_dep, tv_service: tv_service_dep) -> RichShowTorrent:
"""
Get torrents associated with a specific show.
"""
return tv_service.get_torrents_for_show(show=show)
# -----------------------------------------------------------------------------
# SEASONS - REQUESTS
# -----------------------------------------------------------------------------
@router.get(
"/seasons/requests",
status_code=status.HTTP_200_OK,
dependencies=[Depends(current_active_user)],
)
def get_season_requests(tv_service: tv_service_dep) -> list[RichSeasonRequest]:
"""
Get all season requests.
"""
return tv_service.get_all_season_requests()
@router.post("/seasons/requests", status_code=status.HTTP_204_NO_CONTENT)
def request_a_season(
user: Annotated[User, Depends(current_active_user)],
season_request: CreateSeasonRequest,
tv_service: tv_service_dep,
) -> None:
"""
Create a new season request.
"""
request: SeasonRequest = SeasonRequest.model_validate(season_request)
request.requested_by = UserRead.model_validate(user)
if user.is_superuser:
request.authorized = True
request.authorized_by = UserRead.model_validate(user)
tv_service.add_season_request(request)
return
@router.put("/seasons/requests", status_code=status.HTTP_204_NO_CONTENT)
def update_request(
tv_service: tv_service_dep,
user: Annotated[User, Depends(current_active_user)],
season_request: UpdateSeasonRequest,
) -> None:
"""
Update an existing season request.
"""
updated_season_request: SeasonRequest = SeasonRequest.model_validate(season_request)
request = tv_service.get_season_request_by_id(
season_request_id=updated_season_request.id
)
if request.requested_by.id == user.id or user.is_superuser:
updated_season_request.requested_by = UserRead.model_validate(user)
tv_service.update_season_request(season_request=updated_season_request)
return
@router.patch(
"/seasons/requests/{season_request_id}", status_code=status.HTTP_204_NO_CONTENT
)
def authorize_request(
tv_service: tv_service_dep,
user: Annotated[User, Depends(current_superuser)],
season_request_id: SeasonRequestId,
authorized_status: bool = False,
) -> None:
"""
Authorize or de-authorize a season request.
"""
season_request = tv_service.get_season_request_by_id(
season_request_id=season_request_id
)
if not season_request:
raise NotFoundError
season_request.authorized_by = UserRead.model_validate(user)
season_request.authorized = authorized_status
if not authorized_status:
season_request.authorized_by = None
tv_service.update_season_request(season_request=season_request)
@router.delete(
"/seasons/requests/{request_id}",
status_code=status.HTTP_204_NO_CONTENT,
)
def delete_season_request(
tv_service: tv_service_dep,
user: Annotated[User, Depends(current_active_user)],
request_id: SeasonRequestId,
) -> None:
"""
Delete a season request.
"""
request = tv_service.get_season_request_by_id(season_request_id=request_id)
if user.is_superuser or request.requested_by.id == user.id:
tv_service.delete_season_request(season_request_id=request_id)
log.info(f"User {user.id} deleted season request {request_id}.")
return
log.warning(
f"User {user.id} tried to delete season request {request_id} but is not authorized."
)
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to delete this request",
)
# -----------------------------------------------------------------------------
# SEASONS
# -----------------------------------------------------------------------------
@router.get(
"/seasons/{season_id}",
dependencies=[Depends(current_active_user)],
)
def get_season(season: season_dep) -> Season:
"""
Get details for a specific season.
"""
return season
@router.get(
"/seasons/{season_id}/files",
dependencies=[Depends(current_active_user)],
)
def get_season_files(
season: season_dep, tv_service: tv_service_dep
) -> list[PublicSeasonFile]:
"""
Get files associated with a specific season.
"""
return tv_service.get_public_season_files_by_season_id(season=season)
# -----------------------------------------------------------------------------
# TORRENTS
# -----------------------------------------------------------------------------
@router.get(
"/torrents",
status_code=status.HTTP_200_OK,
dependencies=[Depends(current_superuser)],
)
def get_torrents_for_a_season(
tv_service: tv_service_dep,
show_id: ShowId,
season_number: int = 1,
search_query_override: str | None = None,
) -> list[IndexerQueryResult]:
"""
Search for torrents for a specific season of a show.
Default season_number is 1 because it often returns multi-season torrents.
"""
return tv_service.get_all_available_torrents_for_a_season(
season_number=season_number,
show_id=show_id,
search_query_override=search_query_override,
)
@router.post(
"/torrents",
status_code=status.HTTP_200_OK,
dependencies=[Depends(current_superuser)],
)
def download_a_torrent(
tv_service: tv_service_dep,
public_indexer_result_id: IndexerQueryResultId,
show_id: ShowId,
override_file_path_suffix: str = "",
) -> Torrent:
"""
Trigger a download for a specific torrent.
"""
return tv_service.download_torrent(
public_indexer_result_id=public_indexer_result_id,
show_id=show_id,
override_show_file_path_suffix=override_file_path_suffix,
)
# -----------------------------------------------------------------------------
# STATISTICS
# -----------------------------------------------------------------------------
@router.get(
"/episodes/count",
status_code=status.HTTP_200_OK,
description="Total number of episodes downloaded",
dependencies=[Depends(current_active_user)],
)
def get_total_count_of_downloaded_episodes(tv_service: tv_service_dep) -> int:
"""
Get the total count of downloaded episodes across all shows.
"""
return tv_service.get_total_downloaded_episoded_count()