diff --git a/media_manager/indexer/dependencies.py b/media_manager/indexer/dependencies.py index ca52fb2..29b763a 100644 --- a/media_manager/indexer/dependencies.py +++ b/media_manager/indexer/dependencies.py @@ -1,9 +1,9 @@ from typing import Annotated -from fastapi import Depends, Path +from fastapi import Depends -from indexer.repository import IndexerRepository -from indexer.service import IndexerService +from media_manager.indexer.repository import IndexerRepository +from media_manager.indexer.service import IndexerService from media_manager.database import DbSessionDependency from media_manager.tv.service import TvService @@ -16,9 +16,9 @@ indexer_repository_dep = Annotated[IndexerRepository, Depends(get_indexer_reposi def get_indexer_service( - indexer_repository: IndexerRepository = indexer_repository_dep, + indexer_repository: indexer_repository_dep, ) -> IndexerService: return IndexerService(indexer_repository) -indexer_service_dep = Annotated[TvService, Depends(get_indexer_service)] \ No newline at end of file +indexer_service_dep = Annotated[TvService, Depends(get_indexer_service)] diff --git a/media_manager/indexer/indexers/generic.py b/media_manager/indexer/indexers/generic.py index e0b1fa6..0238645 100644 --- a/media_manager/indexer/indexers/generic.py +++ b/media_manager/indexer/indexers/generic.py @@ -1,3 +1,6 @@ +from media_manager.indexer.schemas import IndexerQueryResult + + class GenericIndexer(object): name: str @@ -7,7 +10,7 @@ class GenericIndexer(object): else: raise ValueError("indexer name must not be None") - def search(self, query: str) -> list["IndexerQueryResult"]: + def search(self, query: str) -> list[IndexerQueryResult]: """ Sends a search request to the Indexer and returns the results. diff --git a/media_manager/indexer/models.py b/media_manager/indexer/models.py index 1b5ab6b..d362ca1 100644 --- a/media_manager/indexer/models.py +++ b/media_manager/indexer/models.py @@ -6,7 +6,6 @@ from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.sql.sqltypes import BigInteger from media_manager.database import Base -from media_manager.indexer.schemas import IndexerQueryResultId from media_manager.torrent.schemas import Quality diff --git a/media_manager/indexer/schemas.py b/media_manager/indexer/schemas.py index 146fd12..e3bfe32 100644 --- a/media_manager/indexer/schemas.py +++ b/media_manager/indexer/schemas.py @@ -64,6 +64,7 @@ class IndexerQueryResult(BaseModel): return self.quality.value > other.quality.value return self.seeders < other.seeders + class PublicIndexerQueryResult(BaseModel): title: str quality: Quality diff --git a/media_manager/indexer/service.py b/media_manager/indexer/service.py index 6e15408..9e25541 100644 --- a/media_manager/indexer/service.py +++ b/media_manager/indexer/service.py @@ -1,9 +1,5 @@ -from sqlalchemy.orm import Session - -import media_manager.indexer.repository from media_manager.indexer import log, indexers from media_manager.indexer.schemas import IndexerQueryResultId, IndexerQueryResult -from media_manager.tv.schemas import Show from media_manager.indexer.repository import IndexerRepository @@ -14,9 +10,7 @@ class IndexerService: def get_result(self, result_id: IndexerQueryResultId) -> IndexerQueryResult: return self.repository.get_result(result_id=result_id) - def search( - self, query: str - ) -> list[IndexerQueryResult]: + def search(self, query: str) -> list[IndexerQueryResult]: """ Search for results using the indexers based on a query. @@ -34,4 +28,4 @@ class IndexerService: self.repository.save_result(result=result) log.debug(f"Found torrents: {results}") - return results \ No newline at end of file + return results diff --git a/media_manager/main.py b/media_manager/main.py index 2928adb..2ff468e 100644 --- a/media_manager/main.py +++ b/media_manager/main.py @@ -57,8 +57,6 @@ from datetime import datetime from contextlib import asynccontextmanager from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.cron import CronTrigger -import media_manager.torrent.service -from media_manager.database import SessionLocal init_db() log.info("Database initialized") @@ -79,7 +77,7 @@ def hourly_tasks(): auto_download_all_approved_season_requests() # media_manager.torrent.service.TorrentService( # db=SessionLocal() - #).import_all_torrents() + # ).import_all_torrents() scheduler = BackgroundScheduler() diff --git a/media_manager/metadataProvider/__init__.py b/media_manager/metadataProvider/__init__.py index 99b9e5c..c2c6d24 100644 --- a/media_manager/metadataProvider/__init__.py +++ b/media_manager/metadataProvider/__init__.py @@ -1,12 +1,14 @@ import logging from cachetools import TTLCache, cached -import media_manager.metadataProvider.tmdb -import media_manager.metadataProvider.tvdb +import media_manager.metadataProvider.tmdb as tmdb +import media_manager.metadataProvider.tvdb as tvdb from media_manager.metadataProvider.abstractMetaDataProvider import metadata_providers from media_manager.metadataProvider.schemas import MetaDataProviderShowSearchResult from media_manager.tv.schemas import Show +_ = tvdb, tmdb + log = logging.getLogger(__name__) search_show_cache = TTLCache(maxsize=128, ttl=24 * 60 * 60) # Cache for 24 hours diff --git a/media_manager/torrent/dependencies.py b/media_manager/torrent/dependencies.py index 8738873..6cd90ff 100644 --- a/media_manager/torrent/dependencies.py +++ b/media_manager/torrent/dependencies.py @@ -13,6 +13,7 @@ def get_torrent_repository(db: DbSessionDependency) -> TorrentRepository: torrent_repository_dep = Annotated[TorrentRepository, Depends(get_torrent_repository)] + def get_torrent_service(torrent_repository: torrent_repository_dep) -> TorrentService: return TorrentService(torrent_repository=torrent_repository) diff --git a/media_manager/torrent/repository.py b/media_manager/torrent/repository.py index eb1238c..0034e44 100644 --- a/media_manager/torrent/repository.py +++ b/media_manager/torrent/repository.py @@ -1,5 +1,4 @@ from sqlalchemy import select -from sqlalchemy.orm import Session from media_manager.database import DbSessionDependency from media_manager.torrent.models import Torrent diff --git a/media_manager/torrent/service.py b/media_manager/torrent/service.py index 69f3b96..6ca525b 100644 --- a/media_manager/torrent/service.py +++ b/media_manager/torrent/service.py @@ -1,26 +1,15 @@ import hashlib import logging -import mimetypes -import pprint -import re -from pathlib import Path import bencoder import qbittorrentapi import requests from pydantic_settings import BaseSettings, SettingsConfigDict -from sqlalchemy.orm import Session -import media_manager.torrent.repository from media_manager.config import BasicConfig from media_manager.indexer.schemas import IndexerQueryResult from media_manager.torrent.repository import TorrentRepository from media_manager.torrent.schemas import Torrent, TorrentStatus, TorrentId -from media_manager.torrent.utils import ( - list_files_recursively, - get_torrent_filepath, - extract_archives, -) from media_manager.tv.schemas import SeasonFile log = logging.getLogger(__name__) @@ -75,7 +64,9 @@ class TorrentService: :param torrent: the torrent to get the season files of :return: list of season files """ - return self.torrent_repository.get_seasons_files_of_torrent(torrent_id=torrent.id) + return self.torrent_repository.get_seasons_files_of_torrent( + torrent_id=torrent.id + ) def download(self, indexer_result: IndexerQueryResult) -> Torrent: log.info(f"Attempting to download torrent: {indexer_result.title}") @@ -186,15 +177,16 @@ class TorrentService: return self.get_torrent_status( self.torrent_repository.get_torrent_by_id(torrent_id=torrent_id) ) + # TODO: extract deletion logic to tv module - #def delete_torrent(self, torrent_id: TorrentId): + # def delete_torrent(self, torrent_id: TorrentId): # t = self.torrent_repository.get_torrent_by_id(torrent_id=torrent_id) # if not t.imported: # 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) - #def import_all_torrents(self) -> list[Torrent]: + # def import_all_torrents(self) -> list[Torrent]: # log.info("Importing all torrents") # torrents = self.get_all_torrents() # log.info("Found %d torrents to import", len(torrents)) diff --git a/media_manager/torrent/utils.py b/media_manager/torrent/utils.py index e8daa6e..b0bcfbb 100644 --- a/media_manager/torrent/utils.py +++ b/media_manager/torrent/utils.py @@ -76,4 +76,4 @@ def import_torrent(torrent: Torrent) -> (list[Path], list[Path], list[Path]): log.info( f"Found {len(all_files)} files ({len(video_files)} video files, {len(subtitle_files)} subtitle files) for further processing." ) - return video_files, subtitle_files, all_files \ No newline at end of file + return video_files, subtitle_files, all_files diff --git a/media_manager/tv/service.py b/media_manager/tv/service.py index eb35470..aaa7ba3 100644 --- a/media_manager/tv/service.py +++ b/media_manager/tv/service.py @@ -32,7 +32,6 @@ from media_manager.tv.schemas import ( from media_manager.torrent.schemas import QualityStrings from media_manager.tv.repository import TvRepository from media_manager.tv.exceptions import NotFoundError -import mimetypes import pprint from pathlib import Path from media_manager.config import BasicConfig @@ -596,7 +595,8 @@ def auto_download_all_approved_season_requests() -> None: season_id=season_request.season_id ) if tv_service.download_approved_season_request( - season_request=season_request, show=show): + season_request=season_request, show=show + ): count += 1 else: log.warning( diff --git a/tests/indexer/test_repository.py b/tests/indexer/test_repository.py index ca78b05..6855981 100644 --- a/tests/indexer/test_repository.py +++ b/tests/indexer/test_repository.py @@ -1,6 +1,5 @@ import uuid import pytest -from unittest.mock import MagicMock from media_manager.indexer.schemas import IndexerQueryResult, IndexerQueryResultId from media_manager.indexer.repository import IndexerRepository @@ -62,4 +61,3 @@ def test_save_result_calls_db_methods(repo, dummy_db): repo.save_result(result) assert dummy_db.added[0].title == "Another Title" assert dummy_db.committed - diff --git a/tests/indexer/test_schemas.py b/tests/indexer/test_schemas.py index 4876db1..09592b9 100644 --- a/tests/indexer/test_schemas.py +++ b/tests/indexer/test_schemas.py @@ -1,4 +1,3 @@ -import pytest from media_manager.indexer.schemas import IndexerQueryResult from media_manager.torrent.models import Quality diff --git a/tests/indexer/test_service.py b/tests/indexer/test_service.py index b6c4ba3..cd90438 100644 --- a/tests/indexer/test_service.py +++ b/tests/indexer/test_service.py @@ -58,4 +58,3 @@ def test_get_result_returns_result(mock_indexer_repository): result = service.get_result(result_id) assert result == expected_result mock_indexer_repository.get_result.assert_called_once_with(result_id=result_id) - diff --git a/tests/tv/test_service.py b/tests/tv/test_service.py index 131f918..6744592 100644 --- a/tests/tv/test_service.py +++ b/tests/tv/test_service.py @@ -4,11 +4,10 @@ from unittest.mock import MagicMock, patch import pytest from media_manager.tv.exceptions import NotFoundError -from media_manager.tv.schemas import Show, ShowId, SeasonId +from media_manager.tv.schemas import Show, ShowId from media_manager.tv.service import TvService from media_manager.indexer.schemas import IndexerQueryResult, IndexerQueryResultId from media_manager.metadataProvider.schemas import MetaDataProviderShowSearchResult -from media_manager.torrent.models import Quality @pytest.fixture @@ -20,10 +19,12 @@ def mock_tv_repository(): def mock_torrent_service(): return MagicMock() + @pytest.fixture def mock_indexer_service(): return MagicMock() + @pytest.fixture def tv_service(mock_tv_repository, mock_torrent_service, mock_indexer_service): return TvService( @@ -61,7 +62,9 @@ def test_add_show(tv_service, mock_tv_repository, mock_torrent_service): assert result == show_data -def test_add_show_with_invalid_metadata(monkeypatch, tv_service, mock_tv_repository, mock_torrent_service): +def test_add_show_with_invalid_metadata( + monkeypatch, tv_service, mock_tv_repository, mock_torrent_service +): external_id = 123 metadata_provider = "tmdb" # Simulate metadata provider returning None @@ -75,7 +78,9 @@ def test_add_show_with_invalid_metadata(monkeypatch, tv_service, mock_tv_reposit assert result is None -def test_check_if_show_exists_by_external_id(tv_service, mock_tv_repository, mock_torrent_service): +def test_check_if_show_exists_by_external_id( + tv_service, mock_tv_repository, mock_torrent_service +): external_id = 123 metadata_provider = "tmdb" mock_tv_repository.get_show_by_external_id.return_value = "show_obj" @@ -92,7 +97,9 @@ def test_check_if_show_exists_by_external_id(tv_service, mock_tv_repository, moc ) -def test_check_if_show_exists_by_show_id(tv_service, mock_tv_repository, mock_torrent_service): +def test_check_if_show_exists_by_show_id( + tv_service, mock_tv_repository, mock_torrent_service +): show_id = ShowId(uuid.uuid4()) mock_tv_repository.get_show_by_id.return_value = "show_obj" assert tv_service.check_if_show_exists(show_id=show_id) @@ -102,7 +109,9 @@ def test_check_if_show_exists_by_show_id(tv_service, mock_tv_repository, mock_to assert not tv_service.check_if_show_exists(show_id=show_id) -def test_check_if_show_exists_with_invalid_uuid(tv_service, mock_tv_repository, mock_torrent_service): +def test_check_if_show_exists_with_invalid_uuid( + tv_service, mock_tv_repository, mock_torrent_service +): # Simulate NotFoundError for a random UUID show_id = uuid.uuid4() mock_tv_repository.get_show_by_id.side_effect = NotFoundError @@ -196,7 +205,9 @@ def test_get_show_by_external_id(tv_service, mock_tv_repository, mock_torrent_se assert result == show -def test_get_show_by_external_id_not_found(tv_service, mock_tv_repository, mock_torrent_service): +def test_get_show_by_external_id_not_found( + tv_service, mock_tv_repository, mock_torrent_service +): external_id = 123 metadata_provider = "tmdb" mock_tv_repository.get_show_by_external_id.side_effect = NotFoundError @@ -274,14 +285,18 @@ def test_get_public_season_files_by_season_id_not_downloaded( assert result[0].downloaded is False -def test_get_public_season_files_by_season_id_empty(tv_service, mock_tv_repository, mock_torrent_service): +def test_get_public_season_files_by_season_id_empty( + tv_service, mock_tv_repository, mock_torrent_service +): season_id = uuid.uuid4() mock_tv_repository.get_season_files_by_season_id.return_value = [] result = tv_service.get_public_season_files_by_season_id(season_id) assert result == [] -def test_is_season_downloaded_true(monkeypatch, tv_service, mock_tv_repository, mock_torrent_service): +def test_is_season_downloaded_true( + monkeypatch, tv_service, mock_tv_repository, mock_torrent_service +): season_id = MagicMock() season_file = MagicMock() mock_tv_repository.get_season_files_by_season_id.return_value = [season_file] @@ -291,7 +306,9 @@ def test_is_season_downloaded_true(monkeypatch, tv_service, mock_tv_repository, assert tv_service.is_season_downloaded(season_id) is True -def test_is_season_downloaded_false(monkeypatch, tv_service, mock_tv_repository, mock_torrent_service): +def test_is_season_downloaded_false( + monkeypatch, tv_service, mock_tv_repository, mock_torrent_service +): season_id = MagicMock() season_file = MagicMock() mock_tv_repository.get_season_files_by_season_id.return_value = [season_file] @@ -301,7 +318,9 @@ def test_is_season_downloaded_false(monkeypatch, tv_service, mock_tv_repository, assert tv_service.is_season_downloaded(season_id) is False -def test_is_season_downloaded_with_no_files(tv_service, mock_tv_repository, mock_torrent_service): +def test_is_season_downloaded_with_no_files( + tv_service, mock_tv_repository, mock_torrent_service +): season_id = uuid.uuid4() mock_tv_repository.get_season_files_by_season_id.return_value = [] assert tv_service.is_season_downloaded(season_id) is False @@ -313,16 +332,22 @@ def test_season_file_exists_on_file_none(monkeypatch, tv_service, mock_torrent_s assert tv_service.season_file_exists_on_file(season_file) is True -def test_season_file_exists_on_file_imported(monkeypatch, tv_service, mock_torrent_service): +def test_season_file_exists_on_file_imported( + monkeypatch, tv_service, mock_torrent_service +): season_file = MagicMock() season_file.torrent_id = "torrent_id" torrent_file = MagicMock(imported=True) # Patch the repository method on the torrent_service instance - tv_service.torrent_service.torrent_repository.get_torrent_by_id = MagicMock(return_value=torrent_file) + tv_service.torrent_service.torrent_repository.get_torrent_by_id = MagicMock( + return_value=torrent_file + ) assert tv_service.season_file_exists_on_file(season_file) is True -def test_season_file_exists_on_file_not_imported(monkeypatch, tv_service, mock_torrent_service): +def test_season_file_exists_on_file_not_imported( + monkeypatch, tv_service, mock_torrent_service +): season_file = MagicMock() season_file.torrent_id = "torrent_id" torrent_file = MagicMock() @@ -332,7 +357,9 @@ def test_season_file_exists_on_file_not_imported(monkeypatch, tv_service, mock_t assert tv_service.season_file_exists_on_file(season_file) is False -def test_season_file_exists_on_file_with_none_imported(monkeypatch, tv_service, mock_torrent_service): +def test_season_file_exists_on_file_with_none_imported( + monkeypatch, tv_service, mock_torrent_service +): class DummyFile: def __init__(self): self.torrent_id = uuid.uuid4() @@ -342,11 +369,15 @@ def test_season_file_exists_on_file_with_none_imported(monkeypatch, tv_service, class DummyTorrent: imported = True - tv_service.torrent_service.torrent_repository.get_torrent_by_id = MagicMock(return_value=DummyTorrent()) + tv_service.torrent_service.torrent_repository.get_torrent_by_id = MagicMock( + return_value=DummyTorrent() + ) assert tv_service.season_file_exists_on_file(dummy_file) is True -def test_season_file_exists_on_file_with_none_not_imported(monkeypatch, tv_service, mock_torrent_service): +def test_season_file_exists_on_file_with_none_not_imported( + monkeypatch, tv_service, mock_torrent_service +): class DummyFile: def __init__(self): self.torrent_id = uuid.uuid4() @@ -356,7 +387,9 @@ def test_season_file_exists_on_file_with_none_not_imported(monkeypatch, tv_servi class DummyTorrent: imported = False - tv_service.torrent_service.get_torrent_by_id = MagicMock(return_value=DummyTorrent()) + tv_service.torrent_service.get_torrent_by_id = MagicMock( + return_value=DummyTorrent() + ) assert tv_service.season_file_exists_on_file(dummy_file) is False @@ -418,7 +451,13 @@ def test_get_all_available_torrents_for_a_season_no_override( size=100, ) # Different season - mock_indexer_service.search.return_value = [torrent1, torrent2, torrent3, torrent4, torrent5] + mock_indexer_service.search.return_value = [ + torrent1, + torrent2, + torrent3, + torrent4, + torrent5, + ] results = tv_service.get_all_available_torrents_for_a_season( season_number=season_number, show_id=show_id @@ -627,11 +666,12 @@ def test_get_popular_shows_all_added(tv_service, mock_torrent_service, monkeypat assert results == [] -def test_get_popular_shows_empty_from_provider(tv_service, mock_torrent_service, monkeypatch): +def test_get_popular_shows_empty_from_provider( + tv_service, mock_torrent_service, monkeypatch +): metadata_provider = "tmdb" mock_search_show = MagicMock(return_value=[]) monkeypatch.setattr("media_manager.metadataProvider.search_show", mock_search_show) results = tv_service.get_popular_shows(metadata_provider=metadata_provider) assert results == [] -