run linter and formatter

This commit is contained in:
maxDorninger
2025-06-07 13:23:00 +02:00
parent d31658a82f
commit 1fab5d8056
7 changed files with 157 additions and 110 deletions

View File

@@ -8,8 +8,6 @@ Create Date: 2025-05-27 21:36:18.532068
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "93fb07842385"

View File

@@ -13,6 +13,4 @@ class JackettConfig(BaseSettings):
enabled: bool | None = False
api_key: str | None = None
url: str = "http://localhost:9696"
indexers: list[str] = [
"all"
]
indexers: list[str] = ["all"]

View File

@@ -1,7 +1,6 @@
from typing import Annotated
from fastapi import Depends, Path
from sqlalchemy.orm import Session
from media_manager.database import DbSessionDependency
from media_manager.tv.repository import TvRepository
@@ -27,7 +26,7 @@ tv_service_dep = Annotated[TvService, Depends(get_tv_service)]
def get_show_by_id(
tv_service: tv_service_dep,
show_id: ShowId = Path(..., description="The ID of the show")
show_id: ShowId = Path(..., description="The ID of the show"),
) -> Show:
show = tv_service.get_show_by_id(show_id)
return show
@@ -38,7 +37,7 @@ show_dep = Annotated[Show, Depends(get_show_by_id)]
def get_season_by_id(
tv_service: tv_service_dep,
season_id: SeasonId = Path(..., description="The ID of the season")
season_id: SeasonId = Path(..., description="The ID of the season"),
) -> Season:
return tv_service.get_season(season_id=season_id)

View File

@@ -4,4 +4,5 @@ class MediaAlreadyExists(ValueError):
class NotFoundError(Exception):
"""Custom exception for when an entity is not found."""
pass

View File

@@ -1,5 +1,8 @@
from sqlalchemy import select, delete
from sqlalchemy.exc import IntegrityError, SQLAlchemyError # Keep SQLAlchemyError for broader exception handling
from sqlalchemy.exc import (
IntegrityError,
SQLAlchemyError,
) # Keep SQLAlchemyError for broader exception handling
from sqlalchemy.orm import Session, joinedload
from media_manager.torrent.models import Torrent
@@ -174,7 +177,9 @@ class TvRepository:
except IntegrityError as e:
self.db.rollback()
log.error(f"Integrity error while saving show {show.name}: {e}")
raise ValueError(f"Show with this primary key or unique constraint violation: {e.orig}")
raise ValueError(
f"Show with this primary key or unique constraint violation: {e.orig}"
)
except SQLAlchemyError as e:
self.db.rollback()
log.error(f"Database error while saving show {show.name}: {e}")
@@ -275,16 +280,23 @@ class TvRepository:
try:
request_to_delete = self.db.get(SeasonRequest, season_request_id)
if not request_to_delete:
log.warning(f"Season request with id {season_request_id} not found for deletion.")
raise NotFoundError(f"Season request with id {season_request_id} not found.")
log.warning(
f"Season request with id {season_request_id} not found for deletion."
)
raise NotFoundError(
f"Season request with id {season_request_id} not found."
)
stmt = delete(SeasonRequest).where(SeasonRequest.id == season_request_id)
result = self.db.execute(stmt)
if result.rowcount == 0:
log.warning(
f"Season request with id {season_request_id} not found during delete execution (rowcount 0).")
f"Season request with id {season_request_id} not found during delete execution (rowcount 0)."
)
self.db.commit()
log.info(f"Successfully deleted season request with id: {season_request_id}")
log.info(
f"Successfully deleted season request with id: {season_request_id}"
)
except SQLAlchemyError as e:
self.db.rollback()
log.error(
@@ -292,9 +304,7 @@ class TvRepository:
)
raise
def get_season_by_number(
self, season_number: int, show_id: ShowId
) -> SeasonSchema:
def get_season_by_number(self, season_number: int, show_id: ShowId) -> SeasonSchema:
"""
Retrieve a season by its number and show ID.
@@ -348,16 +358,12 @@ class TvRepository:
)
results = self.db.execute(stmt).scalars().unique().all()
log.info(f"Successfully retrieved {len(results)} season requests.")
return [
RichSeasonRequestSchema.model_validate(x) for x in results
]
return [RichSeasonRequestSchema.model_validate(x) for x in results]
except SQLAlchemyError as e:
log.error(f"Database error while retrieving season requests: {e}")
raise
def add_season_file(
self, season_file: SeasonFileSchema
) -> SeasonFileSchema:
def add_season_file(self, season_file: SeasonFileSchema) -> SeasonFileSchema:
"""
Adds a season file record to the database.
@@ -374,7 +380,9 @@ class TvRepository:
self.db.refresh(db_model)
# Assuming SeasonFile model has an 'id' attribute after refresh for logging.
# If not, this line or the model needs adjustment.
log.info(f"Successfully added season file. Torrent ID: {db_model.torrent_id}, Path: {db_model.file_path}")
log.info(
f"Successfully added season file. Torrent ID: {db_model.torrent_id}, Path: {db_model.file_path}"
)
return SeasonFileSchema.model_validate(db_model)
except IntegrityError as e:
self.db.rollback()
@@ -393,9 +401,7 @@ class TvRepository:
:return: The number of season files removed.
:raises SQLAlchemyError: If a database error occurs.
"""
log.debug(
f"Attempting to remove season files for torrent_id: {torrent_id}"
)
log.debug(f"Attempting to remove season files for torrent_id: {torrent_id}")
try:
stmt = delete(SeasonFile).where(SeasonFile.torrent_id == torrent_id)
result = self.db.execute(stmt)
@@ -429,18 +435,14 @@ class TvRepository:
log.info(
f"Successfully retrieved {len(results)} season files for season_id: {season_id}"
)
return [
SeasonFileSchema.model_validate(sf) for sf in results
]
return [SeasonFileSchema.model_validate(sf) for sf in results]
except SQLAlchemyError as e:
log.error(
f"Database error retrieving season files for season_id {season_id}: {e}"
)
raise
def get_torrents_by_show_id(
self, show_id: ShowId
) -> list[TorrentSchema]:
def get_torrents_by_show_id(self, show_id: ShowId) -> list[TorrentSchema]:
"""
Retrieve all torrents associated with a given show ID.
@@ -463,9 +465,7 @@ class TvRepository:
)
return [TorrentSchema.model_validate(torrent) for torrent in results]
except SQLAlchemyError as e:
log.error(
f"Database error retrieving torrents for show_id {show_id}: {e}"
)
log.error(f"Database error retrieving torrents for show_id {show_id}: {e}")
raise
def get_all_shows_with_torrents(self) -> list[ShowSchema]:
@@ -493,9 +493,7 @@ class TvRepository:
log.error(f"Database error retrieving all shows with torrents: {e}")
raise
def get_seasons_by_torrent_id(
self, torrent_id: TorrentId
) -> list[SeasonNumber]:
def get_seasons_by_torrent_id(self, torrent_id: TorrentId) -> list[SeasonNumber]:
"""
Retrieve season numbers associated with a given torrent ID.
@@ -533,15 +531,11 @@ class TvRepository:
:raises NotFoundError: If the season request is not found.
:raises SQLAlchemyError: If a database error occurs.
"""
log.debug(
f"Attempting to retrieve season request with id: {season_request_id}"
)
log.debug(f"Attempting to retrieve season request with id: {season_request_id}")
try:
request = self.db.get(SeasonRequest, season_request_id)
if not request:
log.warning(
f"Season request with id {season_request_id} not found."
)
log.warning(f"Season request with id {season_request_id} not found.")
raise NotFoundError(
f"Season request with id {season_request_id} not found."
)
@@ -579,7 +573,5 @@ class TvRepository:
log.info(f"Successfully retrieved show for season_id: {season_id}")
return ShowSchema.model_validate(result)
except SQLAlchemyError as e:
log.error(
f"Database error retrieving show by season_id {season_id}: {e}"
)
log.error(f"Database error retrieving show by season_id {season_id}: {e}")
raise

View File

@@ -23,7 +23,12 @@ from media_manager.tv.schemas import (
UpdateSeasonRequest,
RichSeasonRequest,
)
from media_manager.tv.dependencies import tv_service_dep, season_dep, show_dep, tv_repository_dep
from media_manager.tv.dependencies import (
tv_service_dep,
season_dep,
show_dep,
tv_repository_dep,
)
router = APIRouter()
@@ -45,7 +50,9 @@ router = APIRouter()
status.HTTP_409_CONFLICT: {"model": str, "description": "Show already exists"},
},
)
def add_a_show(tv_service: tv_service_dep, show_id: int, metadata_provider: str = "tmdb"):
def add_a_show(
tv_service: tv_service_dep, show_id: int, metadata_provider: str = "tmdb"
):
try:
show = tv_service.add_show(
external_id=show_id,
@@ -118,9 +125,6 @@ def get_a_shows_torrents(show: show_dep, tv_service: tv_service_dep):
return tv_service.get_torrents_for_show(show=show)
# --------------------------------
# MANAGE REQUESTS
# --------------------------------
@@ -163,13 +167,9 @@ def delete_season_request(
user: Annotated[User, Depends(current_active_user)],
request_id: SeasonRequestId,
):
request = tv_service.get_season_request_by_id(
season_request_id=request_id
)
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
)
tv_service.delete_season_request(season_request_id=request_id)
log.info(f"User {user.id} deleted season request {request_id}.")
else:
log.warning(
@@ -216,21 +216,21 @@ def update_request(
)
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
)
tv_service.update_season_request(season_request=updated_season_request)
return
@router.get(
"/seasons/{season_id}/files",
dependencies=[Depends(current_active_user)],
response_model=list[PublicSeasonFile]
response_model=list[PublicSeasonFile],
)
def get_season_files(
season: season_dep, tv_service: tv_service_dep
) -> list[PublicSeasonFile]:
return tv_service.get_public_season_files_by_season_id(season_id=season.id)
# --------------------------------
# MANAGE TORRENTS
# --------------------------------
@@ -289,9 +289,7 @@ def download_a_torrent(
def search_metadata_providers_for_a_show(
tv_service: tv_service_dep, query: str, metadata_provider: str = "tmdb"
):
return tv_service.search_for_show(
query=query, metadata_provider=metadata_provider
)
return tv_service.search_for_show(query=query, metadata_provider=metadata_provider)
@router.get(
@@ -300,6 +298,4 @@ def search_metadata_providers_for_a_show(
response_model=list[MetaDataProviderShowSearchResult],
)
def get_recommended_shows(tv_service: tv_service_dep, metadata_provider: str = "tmdb"):
return tv_service.get_popular_shows(
metadata_provider=metadata_provider
)
return tv_service.get_popular_shows(metadata_provider=metadata_provider)

View File

@@ -11,7 +11,6 @@ from media_manager.torrent.repository import get_seasons_files_of_torrent
from media_manager.torrent.schemas import Torrent
from media_manager.torrent.service import TorrentService
from media_manager.tv import log
from media_manager.tv.exceptions import MediaAlreadyExists
from media_manager.tv.schemas import (
Show,
ShowId,
@@ -24,12 +23,12 @@ from media_manager.tv.schemas import (
PublicSeason,
PublicShow,
PublicSeasonFile,
SeasonNumber,
SeasonRequestId,
RichSeasonRequest,
)
from media_manager.torrent.schemas import QualityStrings
from media_manager.tv.repository import TvRepository
from tv.exceptions import NotFoundError
class TvService:
@@ -59,14 +58,18 @@ class TvService:
"""
return self.tv_repository.add_season_request(season_request=season_request)
def get_season_request_by_id(self, season_request_id: SeasonRequestId) -> SeasonRequest | None:
def get_season_request_by_id(
self, season_request_id: SeasonRequestId
) -> SeasonRequest | None:
"""
Get a season request by its ID.
:param season_request_id: The ID of the season request.
:return: The season request or None if not found.
"""
return self.tv_repository.get_season_request(season_request_id=season_request_id)
return self.tv_repository.get_season_request(
season_request_id=season_request_id
)
def update_season_request(self, season_request: SeasonRequest) -> SeasonRequest:
"""
@@ -86,14 +89,18 @@ class TvService:
"""
self.tv_repository.delete_season_request(season_request_id=season_request_id)
def get_public_season_files_by_season_id(self, season_id: SeasonId) -> list[PublicSeasonFile]:
def get_public_season_files_by_season_id(
self, season_id: SeasonId
) -> list[PublicSeasonFile]:
"""
Get all public season files for a given season ID.
:param season_id: The ID of the season.
:return: A list of public season files.
"""
season_files = self.tv_repository.get_season_files_by_season_id(season_id=season_id)
season_files = self.tv_repository.get_season_files_by_season_id(
season_id=season_id
)
public_season_files = [PublicSeasonFile.model_validate(x) for x in season_files]
result = []
for season_file in public_season_files:
@@ -102,8 +109,12 @@ class TvService:
result.append(season_file)
return result
def check_if_show_exists(self, external_id: int = None, metadata_provider: str = None,
show_id: ShowId = None) -> bool:
def check_if_show_exists(
self,
external_id: int = None,
metadata_provider: str = None,
show_id: ShowId = None,
) -> bool:
"""
Check if a show exists in the database.
@@ -115,21 +126,26 @@ class TvService:
"""
if external_id and metadata_provider:
try:
self.tv_repository.get_show_by_external_id(external_id=external_id, metadata_provider=metadata_provider)
self.tv_repository.get_show_by_external_id(
external_id=external_id, metadata_provider=metadata_provider
)
return True
except:
except NotFoundError:
return False
elif show_id:
try:
self.tv_repository.get_show_by_id(show_id=show_id)
return True
except:
except NotFoundError:
return False
else:
raise ValueError("External ID and metadata provider or Show ID must be provided")
raise ValueError(
"External ID and metadata provider or Show ID must be provided"
)
def get_all_available_torrents_for_a_season(self, season_number: int, show_id: ShowId,
search_query_override: str = None) -> list[IndexerQueryResult]:
def get_all_available_torrents_for_a_season(
self, season_number: int, show_id: ShowId, search_query_override: str = None
) -> list[IndexerQueryResult]:
"""
Get all available torrents for a given season.
@@ -138,7 +154,9 @@ class TvService:
:param search_query_override: Optional override for the search query.
:return: A list of indexer query results.
"""
log.debug(f"getting all available torrents for season {season_number} for show {show_id}")
log.debug(
f"getting all available torrents for season {season_number} for show {show_id}"
)
show = self.tv_repository.get_show_by_id(show_id=show_id)
if search_query_override:
search_query = search_query_override
@@ -146,11 +164,14 @@ class TvService:
# TODO: add more Search query strings and combine all the results, like "season 3", "s03", "s3"
search_query = show.name + " s" + str(season_number).zfill(2)
torrents: list[IndexerQueryResult] = media_manager.indexer.service.search(query=search_query,
db=self.tv_repository.db)
torrents: list[IndexerQueryResult] = media_manager.indexer.service.search(
query=search_query, db=self.tv_repository.db
)
if search_query_override:
log.debug(f"Found with search query override {torrents.__len__()} torrents: {torrents}")
log.debug(
f"Found with search query override {torrents.__len__()} torrents: {torrents}"
)
return torrents
result: list[IndexerQueryResult] = []
@@ -168,7 +189,9 @@ class TvService:
"""
return self.tv_repository.get_shows()
def search_for_show(self, query: str, metadata_provider: str) -> list[MetaDataProviderShowSearchResult]:
def search_for_show(
self, query: str, metadata_provider: str
) -> list[MetaDataProviderShowSearchResult]:
"""
Search for shows using a given query.
@@ -178,11 +201,15 @@ class TvService:
"""
results = media_manager.metadataProvider.search_show(query, metadata_provider)
for result in results:
if self.check_if_show_exists(external_id=result.external_id, metadata_provider=metadata_provider):
if self.check_if_show_exists(
external_id=result.external_id, metadata_provider=metadata_provider
):
result.added = True
return results
def get_popular_shows(self, metadata_provider: str) -> list[MetaDataProviderShowSearchResult]:
def get_popular_shows(
self, metadata_provider: str
) -> list[MetaDataProviderShowSearchResult]:
"""
Get popular shows from a given metadata provider.
@@ -195,7 +222,9 @@ class TvService:
filtered_results = []
for result in results:
if not self.check_if_show_exists(external_id=result.external_id, metadata_provider=metadata_provider):
if not self.check_if_show_exists(
external_id=result.external_id, metadata_provider=metadata_provider
):
filtered_results.append(result)
return filtered_results
@@ -231,7 +260,9 @@ class TvService:
:param season_id: The ID of the season.
:return: True if the season is downloaded, False otherwise.
"""
season_files = self.tv_repository.get_season_files_by_season_id(season_id=season_id)
season_files = self.tv_repository.get_season_files_by_season_id(
season_id=season_id
)
for season_file in season_files:
if self.season_file_exists_on_file(season_file=season_file):
return True
@@ -254,7 +285,9 @@ class TvService:
return True
return False
def get_show_by_external_id(self, external_id: int, metadata_provider: str) -> Show | None:
def get_show_by_external_id(
self, external_id: int, metadata_provider: str
) -> Show | None:
"""
Get a show by its external ID and metadata provider.
@@ -293,8 +326,12 @@ class TvService:
show_torrents = self.tv_repository.get_torrents_by_show_id(show_id=show.id)
rich_season_torrents = []
for show_torrent in show_torrents:
seasons = self.tv_repository.get_seasons_by_torrent_id(torrent_id=show_torrent.id)
season_files = get_seasons_files_of_torrent(db=self.tv_repository.db, torrent_id=show_torrent.id)
seasons = self.tv_repository.get_seasons_by_torrent_id(
torrent_id=show_torrent.id
)
season_files = get_seasons_files_of_torrent(
db=self.tv_repository.db, torrent_id=show_torrent.id
)
file_path_suffix = season_files[0].file_path_suffix if season_files else ""
season_torrent = RichSeasonTorrent(
torrent_id=show_torrent.id,
@@ -323,8 +360,12 @@ class TvService:
shows = self.tv_repository.get_all_shows_with_torrents()
return [self.get_torrents_for_show(show=show) for show in shows]
def download_torrent(self, public_indexer_result_id: IndexerQueryResultId, show_id: ShowId,
override_show_file_path_suffix: str = "") -> Torrent:
def download_torrent(
self,
public_indexer_result_id: IndexerQueryResultId,
show_id: ShowId,
override_show_file_path_suffix: str = "",
) -> Torrent:
"""
Download a torrent for a given indexer result and show.
@@ -336,10 +377,14 @@ class TvService:
indexer_result = media_manager.indexer.service.get_indexer_query_result(
db=self.tv_repository.db, result_id=public_indexer_result_id
)
show_torrent = TorrentService(db=self.tv_repository.db).download(indexer_result=indexer_result)
show_torrent = TorrentService(db=self.tv_repository.db).download(
indexer_result=indexer_result
)
for season_number in indexer_result.season:
season = self.tv_repository.get_season_by_number(season_number=season_number, show_id=show_id)
season = self.tv_repository.get_season_by_number(
season_number=season_number, show_id=show_id
)
season_file = SeasonFile(
season_id=season.id,
quality=indexer_result.quality,
@@ -349,7 +394,9 @@ class TvService:
self.tv_repository.add_season_file(season_file=season_file)
return show_torrent
def download_approved_season_request(self, season_request: SeasonRequest, show: Show) -> bool:
def download_approved_season_request(
self, season_request: SeasonRequest, show: Show
) -> bool:
"""
Download an approved season request.
@@ -359,19 +406,27 @@ class TvService:
:raises ValueError: If the season request is not authorized.
"""
if not season_request.authorized:
log.error(f"Season request {season_request.id} is not authorized for download")
raise ValueError(f"Season request {season_request.id} is not authorized for download")
log.error(
f"Season request {season_request.id} is not authorized for download"
)
raise ValueError(
f"Season request {season_request.id} is not authorized for download"
)
log.info(f"Downloading approved season request {season_request.id}")
season = self.get_season(season_id=season_request.season_id)
torrents = self.get_all_available_torrents_for_a_season(season_number=season.number, show_id=show.id)
torrents = self.get_all_available_torrents_for_a_season(
season_number=season.number, show_id=show.id
)
available_torrents: list[IndexerQueryResult] = []
for torrent in torrents:
if (torrent.quality > season_request.wanted_quality or
torrent.quality < season_request.min_quality or
torrent.seeders < 3):
if (
torrent.quality > season_request.wanted_quality
or torrent.quality < season_request.min_quality
or torrent.seeders < 3
):
log.info(
f"Skipping torrent {torrent.title} with quality {torrent.quality} for season {season.id}, because it does not match the requested quality {season_request.wanted_quality}"
)
@@ -393,7 +448,9 @@ class TvService:
available_torrents.sort()
torrent = TorrentService(db=self.tv_repository.db).download(indexer_result=available_torrents[0])
torrent = TorrentService(db=self.tv_repository.db).download(
indexer_result=available_torrents[0]
)
season_file = SeasonFile(
season_id=season.id,
quality=torrent.quality,
@@ -421,11 +478,17 @@ def auto_download_all_approved_season_requests() -> None:
for season_request in season_requests:
if season_request.authorized:
log.info(f"Processing season request {season_request.id} for download")
show = tv_repository.get_show_by_season_id(season_id=season_request.season_id)
if tv_service.download_approved_season_request(season_request=season_request, show_id=show.id):
show = tv_repository.get_show_by_season_id(
season_id=season_request.season_id
)
if tv_service.download_approved_season_request(
season_request=season_request, show_id=show.id
):
count += 1
else:
log.warning(f"Failed to download season request {season_request.id} for show {show.name}")
log.warning(
f"Failed to download season request {season_request.id} for show {show.name}"
)
log.info(f"Auto downloaded {count} approved season requests")
db.close()