diff --git a/alembic/env.py b/alembic/env.py index 1e3dae9..b15956f 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -3,12 +3,12 @@ import sys sys.path = ["", ".."] + sys.path[1:] -from logging.config import fileConfig +from logging.config import fileConfig # noqa: E402 -from alembic import context -from pydantic_settings import BaseSettings, SettingsConfigDict -from sqlalchemy import engine_from_config -from sqlalchemy import pool +from alembic import context # noqa: E402 +from pydantic_settings import BaseSettings, SettingsConfigDict # noqa: E402 +from sqlalchemy import engine_from_config # noqa: E402 +from sqlalchemy import pool # noqa: E402 # this is the Alembic Config object, which provides # access to the values within the .ini file in use. @@ -24,12 +24,12 @@ if config.config_file_name is not None: # from myapp import mymodel # target_metadata = mymodel.Base.metadata -from media_manager.auth.db import User, OAuthAccount -from media_manager.indexer.models import IndexerQueryResult -from media_manager.torrent.models import Torrent -from media_manager.tv.models import Show, Season, Episode, SeasonFile, SeasonRequest +from media_manager.auth.db import User, OAuthAccount # noqa: E402 +from media_manager.indexer.models import IndexerQueryResult # noqa: E402 +from media_manager.torrent.models import Torrent # noqa: E402 +from media_manager.tv.models import Show, Season, Episode, SeasonFile, SeasonRequest # noqa: E402 -from media_manager.database import Base +from media_manager.database import Base # noqa: E402 target_metadata = Base.metadata diff --git a/media_manager/database/__init__.py b/media_manager/database/__init__.py index aa621a5..a369ff6 100644 --- a/media_manager/database/__init__.py +++ b/media_manager/database/__init__.py @@ -1,10 +1,8 @@ import logging -import pprint from contextvars import ContextVar from typing import Annotated, Any, Generator from fastapi import Depends -from jsonschema.validators import extend from sqlalchemy import create_engine from sqlalchemy.orm import Session, declarative_base, sessionmaker diff --git a/media_manager/exceptions.py b/media_manager/exceptions.py index be9ba88..cc800a2 100644 --- a/media_manager/exceptions.py +++ b/media_manager/exceptions.py @@ -13,4 +13,4 @@ class NotFoundError(Exception): class InvalidConfigError(Exception): """Custom exception for when an entity is not found.""" - pass \ No newline at end of file + pass diff --git a/media_manager/main.py b/media_manager/main.py index 21a6fd0..79c9653 100644 --- a/media_manager/main.py +++ b/media_manager/main.py @@ -56,7 +56,7 @@ from media_manager.tv.service import ( # noqa: E402 ) from media_manager.config import BasicConfig # noqa: E402 -import shutil # noqa: E402 +import shutil # noqa: E402 import media_manager.torrent.router as torrent_router # noqa: E402 from fastapi import FastAPI # noqa: E402 @@ -237,7 +237,9 @@ try: log.critical("Hardlink creation failed!") log.info("Successfully created test hardlink in TV directory") except OSError as e: - log.error(f"Hardlink creation failed, falling back to copying files. Error: {e}") + log.error( + f"Hardlink creation failed, falling back to copying files. Error: {e}" + ) shutil.copy(src=test_torrent_file, dst=test_hardlink) finally: test_hardlink.unlink() diff --git a/media_manager/metadataProvider/dependencies.py b/media_manager/metadataProvider/dependencies.py index 019f638..834d3e7 100644 --- a/media_manager/metadataProvider/dependencies.py +++ b/media_manager/metadataProvider/dependencies.py @@ -33,4 +33,4 @@ def get_metadata_provider( metadata_provider_dep = Annotated[ AbstractMetadataProvider, Depends(get_metadata_provider) -] \ No newline at end of file +] diff --git a/media_manager/metadataProvider/tmdb.py b/media_manager/metadataProvider/tmdb.py index c398814..9caac26 100644 --- a/media_manager/metadataProvider/tmdb.py +++ b/media_manager/metadataProvider/tmdb.py @@ -51,7 +51,6 @@ class TmdbMetadataProvider(AbstractMetadataProvider): return False return True - def get_show_metadata(self, id: int = None) -> Show: """ @@ -112,9 +111,9 @@ class TmdbMetadataProvider(AbstractMetadataProvider): If no query is provided, it will return the most popular shows. """ if query is None: - result_factory = lambda page: tmdbsimple.Trending(media_type="tv").info() + result_factory = lambda page: tmdbsimple.Trending(media_type="tv").info() # noqa: E731 else: - result_factory = lambda page: tmdbsimple.Search().tv( + result_factory = lambda page: tmdbsimple.Search().tv( # noqa: E731 page=page, query=query, include_adult=True ) @@ -152,4 +151,4 @@ class TmdbMetadataProvider(AbstractMetadataProvider): ) except Exception as e: log.warning(f"Error processing search result {result}: {e}") - return formatted_results \ No newline at end of file + return formatted_results diff --git a/media_manager/metadataProvider/tvdb.py b/media_manager/metadataProvider/tvdb.py index 2c7ddad..6a26696 100644 --- a/media_manager/metadataProvider/tvdb.py +++ b/media_manager/metadataProvider/tvdb.py @@ -1,5 +1,3 @@ -import pprint - import tvdb_v4_official import logging @@ -9,7 +7,6 @@ import media_manager.metadataProvider.utils from media_manager.exceptions import InvalidConfigError from media_manager.metadataProvider.abstractMetaDataProvider import ( AbstractMetadataProvider, - register_metadata_provider, ) from media_manager.metadataProvider.schemas import MetaDataProviderShowSearchResult from media_manager.tv.schemas import Episode, Season, Show, SeasonNumber @@ -136,4 +133,4 @@ class TvdbMetadataProvider(AbstractMetadataProvider): ) except Exception as e: log.warning(f"Error processing search result {result}: {e}") - return formatted_results \ No newline at end of file + return formatted_results diff --git a/media_manager/metadataProvider/utils.py b/media_manager/metadataProvider/utils.py index b533f87..d6df112 100644 --- a/media_manager/metadataProvider/utils.py +++ b/media_manager/metadataProvider/utils.py @@ -1,7 +1,7 @@ from PIL import Image -import pillow_avif import requests + def get_year_from_first_air_date(first_air_date: str | None) -> int | None: if first_air_date: return int(first_air_date.split("-")[0]) @@ -13,12 +13,12 @@ def download_poster_image(storage_path=None, poster_url=None, show=None) -> bool res = requests.get(poster_url, stream=True) if res.status_code == 200: image_file_path = storage_path.joinpath(str(show.id)) - with open( str(image_file_path)+".jpg", "wb") as f: + with open(str(image_file_path) + ".jpg", "wb") as f: f.write(res.content) - original_image = Image.open(str(image_file_path)+".jpg") - original_image.save(str(image_file_path)+".avif", quality=50) - original_image.save(str(image_file_path)+".webp", quality=50) + original_image = Image.open(str(image_file_path) + ".jpg") + original_image.save(str(image_file_path) + ".avif", quality=50) + original_image.save(str(image_file_path) + ".webp", quality=50) return True else: return False diff --git a/media_manager/torrent/dependencies.py b/media_manager/torrent/dependencies.py index 76bb96b..e9673b4 100644 --- a/media_manager/torrent/dependencies.py +++ b/media_manager/torrent/dependencies.py @@ -9,6 +9,7 @@ from media_manager.torrent.repository import TorrentRepository from media_manager.torrent.schemas import TorrentId, Torrent from fastapi.exceptions import HTTPException + def get_torrent_repository(db: DbSessionDependency) -> TorrentRepository: return TorrentRepository(db=db) @@ -22,9 +23,9 @@ def get_torrent_service(torrent_repository: torrent_repository_dep) -> TorrentSe torrent_service_dep = Annotated[TorrentService, Depends(get_torrent_service)] + def get_torrent_by_id( - torrent_service: torrent_service_dep, - torrent_id: TorrentId + torrent_service: torrent_service_dep, torrent_id: TorrentId ) -> Torrent: """ Retrieves a torrent by its ID. @@ -36,7 +37,10 @@ def get_torrent_by_id( try: torrent = torrent_service.get_torrent_by_id(torrent_id=torrent_id) except NotFoundError: - raise HTTPException(status_code=404, detail=f"Torrent with ID {torrent_id} not found") + raise HTTPException( + status_code=404, detail=f"Torrent with ID {torrent_id} not found" + ) return torrent -torrent_dep = Annotated[Torrent, Depends(get_torrent_by_id)] \ No newline at end of file + +torrent_dep = Annotated[Torrent, Depends(get_torrent_by_id)] diff --git a/media_manager/torrent/router.py b/media_manager/torrent/router.py index a617cb4..502dc3d 100644 --- a/media_manager/torrent/router.py +++ b/media_manager/torrent/router.py @@ -4,7 +4,7 @@ from fastapi.params import Depends from media_manager.auth.users import current_active_user, current_superuser from media_manager.torrent.dependencies import torrent_service_dep, torrent_dep -from media_manager.torrent.schemas import TorrentId, Torrent +from media_manager.torrent.schemas import Torrent router = APIRouter() diff --git a/media_manager/torrent/service.py b/media_manager/torrent/service.py index 12a3b44..b00d7e4 100644 --- a/media_manager/torrent/service.py +++ b/media_manager/torrent/service.py @@ -68,7 +68,7 @@ class TorrentService: torrent_id=torrent.id ) - def get_show_of_torrent(self, torrent: Torrent) -> Show|None: + def get_show_of_torrent(self, torrent: Torrent) -> Show | None: """ Returns the show of a torrent :param torrent: the torrent to get the show of @@ -193,4 +193,3 @@ class TorrentService: # from media_manager.tv.repository import remove_season_files_by_torrent_id # remove_season_files_by_torrent_id(db=self.db, torrent_id=torrent_id) # media_manager.torrent.repository.delete_torrent(db=self.db, torrent_id=t.id) - diff --git a/media_manager/tv/models.py b/media_manager/tv/models.py index 21b4c64..8f7cbb2 100644 --- a/media_manager/tv/models.py +++ b/media_manager/tv/models.py @@ -39,7 +39,6 @@ class Season(Base): name: Mapped[str] overview: Mapped[str] - show: Mapped["Show"] = relationship(back_populates="seasons") episodes: Mapped[list["Episode"]] = relationship( back_populates="season", cascade="all, delete" diff --git a/media_manager/tv/repository.py b/media_manager/tv/repository.py index bee2eb3..753b438 100644 --- a/media_manager/tv/repository.py +++ b/media_manager/tv/repository.py @@ -583,7 +583,9 @@ class TvRepository: log.error(f"Database error retrieving show by season_id {season_id}: {e}") raise - def add_season_to_show(self, show_id: ShowId, season_data: SeasonSchema) -> SeasonSchema: + def add_season_to_show( + self, show_id: ShowId, season_data: SeasonSchema + ) -> SeasonSchema: """ Adds a new season and its episodes to a show. If the season number already exists for the show, it returns the existing season. @@ -600,10 +602,16 @@ class TvRepository: log.warning(f"Show with id {show_id} not found when trying to add season.") raise NotFoundError(f"Show with id {show_id} not found.") - stmt = select(Season).where(Season.show_id == show_id).where(Season.number == season_data.number) + stmt = ( + select(Season) + .where(Season.show_id == show_id) + .where(Season.number == season_data.number) + ) existing_db_season = self.db.execute(stmt).scalar_one_or_none() if existing_db_season: - log.info(f"Season {season_data.number} already exists for show {show_id} (ID: {existing_db_season.id}). Skipping add.") + log.info( + f"Season {season_data.number} already exists for show {show_id} (ID: {existing_db_season.id}). Skipping add." + ) return SeasonSchema.model_validate(existing_db_season) db_season = Season( @@ -620,18 +628,22 @@ class TvRepository: number=ep_schema.number, external_id=ep_schema.external_id, title=ep_schema.title, - ) for ep_schema in season_data.episodes - ] + ) + for ep_schema in season_data.episodes + ], ) - self.db.add(db_season) self.db.commit() self.db.refresh(db_season) - log.info(f"Successfully added season {db_season.number} (ID: {db_season.id}) to show {show_id}.") + log.info( + f"Successfully added season {db_season.number} (ID: {db_season.id}) to show {show_id}." + ) return SeasonSchema.model_validate(db_season) - def add_episode_to_season(self, season_id: SeasonId, episode_data: EpisodeSchema) -> EpisodeSchema: + def add_episode_to_season( + self, season_id: SeasonId, episode_data: EpisodeSchema + ) -> EpisodeSchema: """ Adds a new episode to a season. If the episode number already exists for the season, it returns the existing episode. @@ -642,13 +654,21 @@ class TvRepository: :raises NotFoundError: If the season is not found. :raises SQLAlchemyError: If a database error occurs. """ - log.debug(f"Attempting to add episode {episode_data.number} to season {season_id}") + log.debug( + f"Attempting to add episode {episode_data.number} to season {season_id}" + ) db_season = self.db.get(Season, season_id) if not db_season: - log.warning(f"Season with id {season_id} not found when trying to add episode.") + log.warning( + f"Season with id {season_id} not found when trying to add episode." + ) raise NotFoundError(f"Season with id {season_id} not found.") - stmt = select(Episode).where(Episode.season_id == season_id).where(Episode.number == episode_data.number) + stmt = ( + select(Episode) + .where(Episode.season_id == season_id) + .where(Episode.number == episode_data.number) + ) existing_db_episode = self.db.execute(stmt).scalar_one_or_none() if existing_db_episode: log.info( @@ -667,10 +687,20 @@ class TvRepository: self.db.add(db_episode) self.db.commit() self.db.refresh(db_episode) - log.info(f"Successfully added episode {db_episode.number} (ID: {db_episode.id}) to season {season_id}.") + log.info( + f"Successfully added episode {db_episode.number} (ID: {db_episode.id}) to season {season_id}." + ) return EpisodeSchema.model_validate(db_episode) - def update_show_attributes(self, show_id: ShowId, name: str | None = None, overview: str | None = None, year: int | None = None, ended: bool|None = None, continuous_download: bool|None = None) -> ShowSchema: # Removed poster_url from params + def update_show_attributes( + self, + show_id: ShowId, + name: str | None = None, + overview: str | None = None, + year: int | None = None, + ended: bool | None = None, + continuous_download: bool | None = None, + ) -> ShowSchema: # Removed poster_url from params """ Update attributes of an existing show. @@ -700,7 +730,10 @@ class TvRepository: if ended is not None and db_show.ended != ended: db_show.ended = ended updated = True - if continuous_download is not None and db_show.continuous_download != continuous_download: + if ( + continuous_download is not None + and db_show.continuous_download != continuous_download + ): db_show.continuous_download = continuous_download updated = True @@ -712,7 +745,9 @@ class TvRepository: log.info(f"No attribute changes needed for show ID: {show_id}") return ShowSchema.model_validate(db_show) - def update_season_attributes(self, season_id: SeasonId, name: str | None = None, overview: str | None = None) -> SeasonSchema: + def update_season_attributes( + self, season_id: SeasonId, name: str | None = None, overview: str | None = None + ) -> SeasonSchema: """ Update attributes of an existing season. @@ -746,8 +781,9 @@ class TvRepository: log.info(f"No attribute changes needed for season ID: {season_id}") return SeasonSchema.model_validate(db_season) - - def update_episode_attributes(self, episode_id: EpisodeId, title: str | None = None) -> EpisodeSchema: + def update_episode_attributes( + self, episode_id: EpisodeId, title: str | None = None + ) -> EpisodeSchema: """ Update attributes of an existing episode. diff --git a/media_manager/tv/router.py b/media_manager/tv/router.py index 195491a..b8443e2 100644 --- a/media_manager/tv/router.py +++ b/media_manager/tv/router.py @@ -255,7 +255,7 @@ def update_request( dependencies=[Depends(current_active_user)], response_model=Season, ) -def get_season_files(season: season_dep) -> Season: +def get_season(season: season_dep) -> Season: return season @@ -336,5 +336,7 @@ def search_metadata_providers_for_a_show( dependencies=[Depends(current_active_user)], response_model=list[MetaDataProviderShowSearchResult], ) -def get_recommended_shows(tv_service: tv_service_dep, metadata_provider: metadata_provider_dep): +def get_recommended_shows( + tv_service: tv_service_dep, metadata_provider: metadata_provider_dep +): return tv_service.get_popular_shows(metadata_provider=metadata_provider) diff --git a/media_manager/tv/service.py b/media_manager/tv/service.py index 6f13ae2..7dd26c8 100644 --- a/media_manager/tv/service.py +++ b/media_manager/tv/service.py @@ -3,8 +3,6 @@ import re from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session -import media_manager.indexer.service -import media_manager.torrent.repository from media_manager.exceptions import InvalidConfigError from media_manager.indexer.repository import IndexerRepository from media_manager.database import SessionLocal @@ -604,7 +602,9 @@ class TvService: overview=fresh_show_data.overview, year=fresh_show_data.year, ended=fresh_show_data.ended, - continuous_download=db_show.continuous_download if fresh_show_data.ended is False else False, + continuous_download=db_show.continuous_download + if fresh_show_data.ended is False + else False, ) # Process seasons and episodes @@ -704,6 +704,7 @@ class TvService: show_id=show_id, continuous_download=continuous_download ) + def auto_download_all_approved_season_requests() -> None: """ Auto download all approved season requests. @@ -760,7 +761,7 @@ def import_all_torrents() -> None: log.info("Found %d torrents to import", len(torrents)) imported_torrents = [] for t in torrents: - if t.imported == False and t.status == TorrentStatus.finished: + if not t.imported and t.status == TorrentStatus.finished: show = torrent_service.get_show_of_torrent(torrent=t) if show is None: log.warning(f"torrent {t.title} is not a tv torrent, skipping import.") @@ -820,12 +821,21 @@ def update_all_non_ended_shows_metadata() -> None: log.info( f"Automatically adding season requeest for new season {new_season.number} of show {updated_show.name}" ) - tv_service.add_season_request(SeasonRequest(min_quality=Quality.sd, wanted_quality=Quality.uhd, season_id=new_season.id, authorized=True)) + tv_service.add_season_request( + SeasonRequest( + min_quality=Quality.sd, + wanted_quality=Quality.uhd, + season_id=new_season.id, + authorized=True, + ) + ) if updated_show: log.info(f"Successfully updated metadata for show: {updated_show.name}") - log.debug(f"Added new seasons: {len(new_seasons)} to show: {updated_show.name}") + log.debug( + f"Added new seasons: {len(new_seasons)} to show: {updated_show.name}" + ) else: log.warning(f"Failed to update metadata for show: {show.name}") db.commit() - db.close() \ No newline at end of file + db.close() diff --git a/tests/tv/test_service.py b/tests/tv/test_service.py index c7cbe9f..1a7f37d 100644 --- a/tests/tv/test_service.py +++ b/tests/tv/test_service.py @@ -1,5 +1,5 @@ import uuid -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock import pytest @@ -57,7 +57,9 @@ def test_add_show(tv_service, mock_tv_repository, mock_torrent_service): mock_metadata_provider.get_show_metadata.assert_called_once_with(id=external_id) mock_tv_repository.save_show.assert_called_once_with(show=show_data) - mock_metadata_provider.download_show_poster_image.assert_called_once_with(show=show_data) + mock_metadata_provider.download_show_poster_image.assert_called_once_with( + show=show_data + ) assert result == show_data @@ -362,9 +364,12 @@ def test_season_file_exists_on_file_with_none_imported( class DummySeasonFile: def __init__(self): self.torrent_id = uuid.uuid4() + dummy_file = DummySeasonFile() + class DummyTorrent: imported = True + tv_service.torrent_service.torrent_repository.get_torrent_by_id = MagicMock( return_value=DummyTorrent() ) @@ -377,9 +382,12 @@ def test_season_file_exists_on_file_with_none_not_imported( class DummySeasonFile: def __init__(self): self.torrent_id = uuid.uuid4() + dummy_file = DummySeasonFile() + class DummyTorrent: imported = False + tv_service.torrent_service.get_torrent_by_id = MagicMock( return_value=DummyTorrent() ) @@ -637,9 +645,7 @@ def test_get_popular_shows_all_added(tv_service, mock_torrent_service): assert results == [] -def test_get_popular_shows_empty_from_provider( - tv_service, mock_torrent_service -): +def test_get_popular_shows_empty_from_provider(tv_service, mock_torrent_service): mock_metadata_provider = MagicMock() mock_metadata_provider.search_show.return_value = [] tv_service.check_if_show_exists = MagicMock()