From 32de886dbef90aa44db1a83d04a4dd9e76fef766 Mon Sep 17 00:00:00 2001 From: maxDorninger <97409287+maxDorninger@users.noreply.github.com> Date: Sun, 8 Jun 2025 16:59:27 +0200 Subject: [PATCH 01/11] refactor indexer module to implement dependency injection --- media_manager/indexer/dependencies.py | 24 ++++++++++++++++ media_manager/indexer/repository.py | 26 ++++++++--------- media_manager/indexer/schemas.py | 1 - media_manager/indexer/service.py | 40 +++++++++++++++++---------- media_manager/torrent/dependencies.py | 7 +++-- media_manager/torrent/router.py | 12 ++++---- media_manager/tv/dependencies.py | 13 ++++++--- media_manager/tv/service.py | 5 ++-- 8 files changed, 85 insertions(+), 43 deletions(-) create mode 100644 media_manager/indexer/dependencies.py diff --git a/media_manager/indexer/dependencies.py b/media_manager/indexer/dependencies.py new file mode 100644 index 0000000..ca52fb2 --- /dev/null +++ b/media_manager/indexer/dependencies.py @@ -0,0 +1,24 @@ +from typing import Annotated + +from fastapi import Depends, Path + +from indexer.repository import IndexerRepository +from indexer.service import IndexerService +from media_manager.database import DbSessionDependency +from media_manager.tv.service import TvService + + +def get_indexer_repository(db_session: DbSessionDependency) -> IndexerRepository: + return IndexerRepository(db_session) + + +indexer_repository_dep = Annotated[IndexerRepository, Depends(get_indexer_repository)] + + +def get_indexer_service( + indexer_repository: IndexerRepository = indexer_repository_dep, +) -> IndexerService: + return IndexerService(indexer_repository) + + +indexer_service_dep = Annotated[TvService, Depends(get_indexer_service)] \ No newline at end of file diff --git a/media_manager/indexer/repository.py b/media_manager/indexer/repository.py index 1f0cb42..8672e02 100644 --- a/media_manager/indexer/repository.py +++ b/media_manager/indexer/repository.py @@ -10,18 +10,18 @@ from media_manager.indexer.schemas import ( log = logging.getLogger(__name__) -def get_result( - result_id: IndexerQueryResultId, db: Session -) -> IndexerQueryResultSchema: - return IndexerQueryResultSchema.model_validate( - db.get(IndexerQueryResult, result_id) - ) +class IndexerRepository: + def __init__(self, db: Session): + self.db = db -def save_result( - result: IndexerQueryResultSchema, db: Session -) -> IndexerQueryResultSchema: - log.debug("Saving indexer query result: %s", result) - db.add(IndexerQueryResult(**result.model_dump())) - db.commit() - return result + def get_result(self, result_id: IndexerQueryResultId) -> IndexerQueryResultSchema: + return IndexerQueryResultSchema.model_validate( + self.db.get(IndexerQueryResult, result_id) + ) + + def save_result(self, result: IndexerQueryResultSchema) -> IndexerQueryResultSchema: + log.debug("Saving indexer query result: %s", result) + self.db.add(IndexerQueryResult(**result.model_dump())) + self.db.commit() + return result diff --git a/media_manager/indexer/schemas.py b/media_manager/indexer/schemas.py index dc685be..362cc99 100644 --- a/media_manager/indexer/schemas.py +++ b/media_manager/indexer/schemas.py @@ -64,7 +64,6 @@ 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 2c2580a..6e15408 100644 --- a/media_manager/indexer/service.py +++ b/media_manager/indexer/service.py @@ -2,24 +2,36 @@ from sqlalchemy.orm import Session import media_manager.indexer.repository from media_manager.indexer import log, indexers -from media_manager.indexer.repository import save_result from media_manager.indexer.schemas import IndexerQueryResultId, IndexerQueryResult +from media_manager.tv.schemas import Show +from media_manager.indexer.repository import IndexerRepository -def search(query: str, db: Session) -> list[IndexerQueryResult]: - results = [] +class IndexerService: + def __init__(self, indexer_repository: IndexerRepository): + self.repository = indexer_repository - log.debug(f"Searching for Torrent: {query}") + def get_result(self, result_id: IndexerQueryResultId) -> IndexerQueryResult: + return self.repository.get_result(result_id=result_id) - for i in indexers: - results.extend(i.search(query)) - for result in results: - save_result(result=result, db=db) - log.debug(f"Found Torrents: {results}") - return results + def search( + self, query: str + ) -> list[IndexerQueryResult]: + """ + Search for results using the indexers based on a query. + :param query: The search query. + :param db: The database session. + :return: A list of search results. + """ + log.debug(f"Searching for: {query}") + results = [] -def get_indexer_query_result( - result_id: IndexerQueryResultId, db: Session -) -> IndexerQueryResult: - return media_manager.indexer.repository.get_result(result_id=result_id, db=db) + for indexer in indexers: + results.extend(indexer.search(query)) + + for result in results: + self.repository.save_result(result=result) + + log.debug(f"Found torrents: {results}") + return results \ No newline at end of file diff --git a/media_manager/torrent/dependencies.py b/media_manager/torrent/dependencies.py index adc1582..8738873 100644 --- a/media_manager/torrent/dependencies.py +++ b/media_manager/torrent/dependencies.py @@ -10,10 +10,11 @@ from media_manager.torrent.repository import TorrentRepository def get_torrent_repository(db: DbSessionDependency) -> TorrentRepository: return TorrentRepository(db=db) -TorrentRepositoryDependency = Annotated[TorrentRepository, Depends(get_torrent_repository)] -def get_torrent_service(torrent_repository: TorrentRepositoryDependency) -> TorrentService: +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) -TorrentServiceDependency = Annotated[TorrentService, Depends(get_torrent_service)] +tv_service_dep = Annotated[TorrentService, Depends(get_torrent_service)] diff --git a/media_manager/torrent/router.py b/media_manager/torrent/router.py index 1b9d6de..7a82ad3 100644 --- a/media_manager/torrent/router.py +++ b/media_manager/torrent/router.py @@ -3,14 +3,14 @@ from fastapi import status from fastapi.params import Depends from media_manager.auth.users import current_active_user, current_superuser -from media_manager.torrent.dependencies import TorrentServiceDependency +from media_manager.torrent.dependencies import tv_service_dep from media_manager.torrent.schemas import TorrentId, Torrent router = APIRouter() @router.get("/{torrent_id}", status_code=status.HTTP_200_OK, response_model=Torrent) -def get_torrent(service: TorrentServiceDependency, torrent_id: TorrentId): +def get_torrent(service: tv_service_dep, torrent_id: TorrentId): return service.get_torrent_by_id(id=torrent_id) @@ -20,7 +20,7 @@ def get_torrent(service: TorrentServiceDependency, torrent_id: TorrentId): dependencies=[Depends(current_active_user)], response_model=list[Torrent], ) -def get_all_torrents(service: TorrentServiceDependency): +def get_all_torrents(service: tv_service_dep): return service.get_all_torrents() @@ -30,7 +30,7 @@ def get_all_torrents(service: TorrentServiceDependency): dependencies=[Depends(current_active_user)], response_model=Torrent, ) -def import_torrent(service: TorrentServiceDependency, torrent_id: TorrentId): +def import_torrent(service: tv_service_dep, torrent_id: TorrentId): return service.import_torrent(service.get_torrent_by_id(id=torrent_id)) @@ -40,7 +40,7 @@ def import_torrent(service: TorrentServiceDependency, torrent_id: TorrentId): dependencies=[Depends(current_active_user)], response_model=list[Torrent], ) -def import_all_torrents(service: TorrentServiceDependency): +def import_all_torrents(service: tv_service_dep): return service.import_all_torrents() @@ -49,5 +49,5 @@ def import_all_torrents(service: TorrentServiceDependency): status_code=status.HTTP_200_OK, dependencies=[Depends(current_superuser)], ) -def delete_torrent(service: TorrentServiceDependency, torrent_id: TorrentId): +def delete_torrent(service: tv_service_dep, torrent_id: TorrentId): service.delete_torrent(torrent_id=torrent_id) diff --git a/media_manager/tv/dependencies.py b/media_manager/tv/dependencies.py index b0dd782..805e03d 100644 --- a/media_manager/tv/dependencies.py +++ b/media_manager/tv/dependencies.py @@ -8,8 +8,8 @@ from media_manager.tv.schemas import Show, ShowId, SeasonId, Season from media_manager.tv.service import TvService from media_manager.tv.exceptions import NotFoundError from fastapi import HTTPException - -from media_manager.torrent.dependencies import TorrentServiceDependency +from media_manager.indexer.dependencies import indexer_service_dep +from media_manager.torrent.dependencies import tv_service_dep def get_tv_repository(db_session: DbSessionDependency) -> TvRepository: @@ -21,9 +21,14 @@ tv_repository_dep = Annotated[TvRepository, Depends(get_tv_repository)] def get_tv_service( tv_repository: tv_repository_dep, - torrent_service: TorrentServiceDependency + torrent_service: tv_service_dep, + indexer_service: indexer_service_dep, ) -> TvService: - return TvService(tv_repository, torrent_service) + return TvService( + tv_repository=tv_repository, + torrent_service=torrent_service, + indexer_service=indexer_service, + ) tv_service_dep = Annotated[TvService, Depends(get_tv_service)] diff --git a/media_manager/tv/service.py b/media_manager/tv/service.py index 4d36f5f..709f2ca 100644 --- a/media_manager/tv/service.py +++ b/media_manager/tv/service.py @@ -37,12 +37,13 @@ from pathlib import Path from media_manager.config import BasicConfig from media_manager.torrent.repository import TorrentRepository from media_manager.torrent.utils import import_file, import_torrent - +from media_manager.indexer.service import IndexerService class TvService: - def __init__(self, tv_repository: TvRepository, torrent_service: TorrentService): + def __init__(self, tv_repository: TvRepository, torrent_service: TorrentService, indexer_service: IndexerService): self.tv_repository = tv_repository self.torrent_service = torrent_service + self.indexer_service = indexer_service def add_show(self, external_id: int, metadata_provider: str) -> Show | None: """ From b42b86e99b56b7a8130a7c52857b9416f1906f56 Mon Sep 17 00:00:00 2001 From: maxDorninger <97409287+maxDorninger@users.noreply.github.com> Date: Sun, 8 Jun 2025 17:03:36 +0200 Subject: [PATCH 02/11] refactor TvService to use IndexerService --- media_manager/tv/service.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/media_manager/tv/service.py b/media_manager/tv/service.py index 709f2ca..0773677 100644 --- a/media_manager/tv/service.py +++ b/media_manager/tv/service.py @@ -6,6 +6,7 @@ from sqlalchemy.orm import Session import media_manager.indexer.service import media_manager.metadataProvider import media_manager.torrent.repository +from indexer.repository import IndexerRepository from media_manager.database import SessionLocal from media_manager.indexer.schemas import IndexerQueryResult from media_manager.indexer.schemas import IndexerQueryResultId @@ -39,8 +40,14 @@ from media_manager.torrent.repository import TorrentRepository from media_manager.torrent.utils import import_file, import_torrent from media_manager.indexer.service import IndexerService + class TvService: - def __init__(self, tv_repository: TvRepository, torrent_service: TorrentService, indexer_service: IndexerService): + def __init__( + self, + tv_repository: TvRepository, + torrent_service: TorrentService, + indexer_service: IndexerService, + ): self.tv_repository = tv_repository self.torrent_service = torrent_service self.indexer_service = indexer_service @@ -174,8 +181,8 @@ 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] = self.indexer_service.search( + query=search_query ) if search_query_override: @@ -385,8 +392,8 @@ class TvService: :param override_show_file_path_suffix: Optional override for the file path suffix. :return: The downloaded torrent. """ - indexer_result = media_manager.indexer.service.get_indexer_query_result( - db=self.tv_repository.db, result_id=public_indexer_result_id + indexer_result = self.indexer_service.get_result( + result_id=public_indexer_result_id ) show_torrent = self.torrent_service.download(indexer_result=indexer_result) @@ -569,7 +576,12 @@ def auto_download_all_approved_season_requests() -> None: db: Session = SessionLocal() tv_repository = TvRepository(db=db) torrent_service = TorrentService(torrent_repository=TorrentRepository(db=db)) - tv_service = TvService(tv_repository=tv_repository, torrent_service=torrent_service) + indexer_service = IndexerService(indexer_repository=IndexerRepository(db=db)) + tv_service = TvService( + tv_repository=tv_repository, + torrent_service=torrent_service, + indexer_service=indexer_service, + ) log.info("Auto downloading all approved season requests") season_requests = tv_repository.get_season_requests() From a13a7d5bb306a7700ede781f11f204e31862b5c1 Mon Sep 17 00:00:00 2001 From: maxDorninger <97409287+maxDorninger@users.noreply.github.com> Date: Sun, 8 Jun 2025 17:09:03 +0200 Subject: [PATCH 03/11] adjust existing tests --- media_manager/tv/service.py | 2 +- tests/tv/test_service.py | 34 ++++++++++++++++++---------------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/media_manager/tv/service.py b/media_manager/tv/service.py index 0773677..eb35470 100644 --- a/media_manager/tv/service.py +++ b/media_manager/tv/service.py @@ -6,7 +6,7 @@ from sqlalchemy.orm import Session import media_manager.indexer.service import media_manager.metadataProvider import media_manager.torrent.repository -from indexer.repository import IndexerRepository +from media_manager.indexer.repository import IndexerRepository from media_manager.database import SessionLocal from media_manager.indexer.schemas import IndexerQueryResult from media_manager.indexer.schemas import IndexerQueryResultId diff --git a/tests/tv/test_service.py b/tests/tv/test_service.py index a46bd68..131f918 100644 --- a/tests/tv/test_service.py +++ b/tests/tv/test_service.py @@ -20,10 +20,17 @@ 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): - return TvService(tv_repository=mock_tv_repository, torrent_service=mock_torrent_service) +def tv_service(mock_tv_repository, mock_torrent_service, mock_indexer_service): + return TvService( + tv_repository=mock_tv_repository, + torrent_service=mock_torrent_service, + indexer_service=mock_indexer_service, + ) def test_add_show(tv_service, mock_tv_repository, mock_torrent_service): @@ -354,7 +361,7 @@ def test_season_file_exists_on_file_with_none_not_imported(monkeypatch, tv_servi def test_get_all_available_torrents_for_a_season_no_override( - tv_service, mock_tv_repository, mock_torrent_service, monkeypatch + tv_service, mock_tv_repository, mock_torrent_service, mock_indexer_service ): show_id = ShowId(uuid.uuid4()) season_number = 1 @@ -411,18 +418,15 @@ def test_get_all_available_torrents_for_a_season_no_override( size=100, ) # Different season - mock_search = MagicMock( - return_value=[torrent1, torrent2, torrent3, torrent4, torrent5] - ) - monkeypatch.setattr("media_manager.indexer.service.search", mock_search) + 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 ) mock_tv_repository.get_show_by_id.assert_called_once_with(show_id=show_id) - mock_search.assert_called_once_with( - query=f"{show_name} s{str(season_number).zfill(2)}", db=mock_tv_repository.db + mock_indexer_service.search.assert_called_once_with( + query=f"{show_name} s{str(season_number).zfill(2)}" ) assert len(results) == 3 assert torrent1 in results @@ -436,7 +440,7 @@ def test_get_all_available_torrents_for_a_season_no_override( def test_get_all_available_torrents_for_a_season_with_override( - tv_service, mock_tv_repository, mock_torrent_service, monkeypatch + tv_service, mock_tv_repository, mock_torrent_service, mock_indexer_service ): show_id = ShowId(uuid.uuid4()) season_number = 1 @@ -461,8 +465,7 @@ def test_get_all_available_torrents_for_a_season_with_override( size=100, season=[1], ) - mock_search = MagicMock(return_value=[torrent1]) - monkeypatch.setattr("media_manager.indexer.service.search", mock_search) + mock_indexer_service.search.return_value = [torrent1] results = tv_service.get_all_available_torrents_for_a_season( season_number=season_number, @@ -470,12 +473,12 @@ def test_get_all_available_torrents_for_a_season_with_override( search_query_override=override_query, ) - mock_search.assert_called_once_with(query=override_query, db=mock_tv_repository.db) + mock_indexer_service.search.assert_called_once_with(query=override_query) assert results == [torrent1] def test_get_all_available_torrents_for_a_season_no_results( - tv_service, mock_tv_repository, mock_torrent_service, monkeypatch + tv_service, mock_tv_repository, mock_torrent_service, mock_indexer_service ): show_id = ShowId(uuid.uuid4()) season_number = 1 @@ -490,8 +493,7 @@ def test_get_all_available_torrents_for_a_season_no_results( ) mock_tv_repository.get_show_by_id.return_value = mock_show - mock_search = MagicMock(return_value=[]) - monkeypatch.setattr("media_manager.indexer.service.search", mock_search) + mock_indexer_service.search.return_value = [] results = tv_service.get_all_available_torrents_for_a_season( season_number=season_number, show_id=show_id From b5e68f194efcb9f8b34919ad7aa2a4465ac474be Mon Sep 17 00:00:00 2001 From: maxDorninger <97409287+maxDorninger@users.noreply.github.com> Date: Sun, 8 Jun 2025 17:30:23 +0200 Subject: [PATCH 04/11] add tests for indexer module --- media_manager/indexer/schemas.py | 8 +- tests/indexer/__init__.py | 0 tests/indexer/test_repository.py | 65 ++++++++++ tests/indexer/test_schemas.py | 210 +++++++++++++++++++++++++++++++ tests/indexer/test_service.py | 61 +++++++++ 5 files changed, 340 insertions(+), 4 deletions(-) create mode 100644 tests/indexer/__init__.py create mode 100644 tests/indexer/test_repository.py create mode 100644 tests/indexer/test_schemas.py create mode 100644 tests/indexer/test_service.py diff --git a/media_manager/indexer/schemas.py b/media_manager/indexer/schemas.py index 362cc99..146fd12 100644 --- a/media_manager/indexer/schemas.py +++ b/media_manager/indexer/schemas.py @@ -56,13 +56,13 @@ class IndexerQueryResult(BaseModel): def __gt__(self, other) -> bool: if self.quality.value != other.quality.value: - return self.quality.value > other.quality.value - return self.seeders < other.seeders + return self.quality.value < other.quality.value + return self.seeders > other.seeders def __lt__(self, other) -> bool: if self.quality.value != other.quality.value: - return self.quality.value < other.quality.value - return self.seeders > other.seeders + return self.quality.value > other.quality.value + return self.seeders < other.seeders class PublicIndexerQueryResult(BaseModel): title: str diff --git a/tests/indexer/__init__.py b/tests/indexer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/indexer/test_repository.py b/tests/indexer/test_repository.py new file mode 100644 index 0000000..ca78b05 --- /dev/null +++ b/tests/indexer/test_repository.py @@ -0,0 +1,65 @@ +import uuid +import pytest +from unittest.mock import MagicMock +from media_manager.indexer.schemas import IndexerQueryResult, IndexerQueryResultId +from media_manager.indexer.repository import IndexerRepository + + +class DummyDB: + def __init__(self): + self._storage = {} + self.added = [] + self.committed = False + + def get(self, model, result_id): + return self._storage.get(result_id) + + def add(self, obj): + self.added.append(obj) + self._storage[obj.id] = obj + + def commit(self): + self.committed = True + + +@pytest.fixture +def dummy_db(): + return DummyDB() + + +@pytest.fixture +def repo(dummy_db): + return IndexerRepository(db=dummy_db) + + +def test_save_and_get_result(repo, dummy_db): + result_id = IndexerQueryResultId(uuid.uuid4()) + result = IndexerQueryResult( + id=result_id, + title="Test Title", + download_url="http://example.com", + seeders=5, + flags=["flag1"], + size=1234, + ) + saved = repo.save_result(result) + assert saved == result + assert dummy_db.committed + fetched = repo.get_result(result_id) + assert fetched.id == result_id + assert fetched.title == "Test Title" + + +def test_save_result_calls_db_methods(repo, dummy_db): + result = IndexerQueryResult( + id=IndexerQueryResultId(uuid.uuid4()), + title="Another Title", + download_url="http://example.com/2", + seeders=2, + flags=[], + size=5678, + ) + 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 new file mode 100644 index 0000000..4876db1 --- /dev/null +++ b/tests/indexer/test_schemas.py @@ -0,0 +1,210 @@ +import pytest +from media_manager.indexer.schemas import IndexerQueryResult +from media_manager.torrent.models import Quality + + +def test_quality_computed_field(): + assert ( + IndexerQueryResult( + title="Show S01 4K", download_url="", seeders=1, flags=[], size=1 + ).quality + == Quality.uhd + ) + assert ( + IndexerQueryResult( + title="Show S01 1080p", download_url="", seeders=1, flags=[], size=1 + ).quality + == Quality.fullhd + ) + assert ( + IndexerQueryResult( + title="Show S01 720p", download_url="", seeders=1, flags=[], size=1 + ).quality + == Quality.hd + ) + assert ( + IndexerQueryResult( + title="Show S01 480p", download_url="", seeders=1, flags=[], size=1 + ).quality + == Quality.sd + ) + assert ( + IndexerQueryResult( + title="Show S01", download_url="", seeders=1, flags=[], size=1 + ).quality + == Quality.unknown + ) + + +def test_quality_computed_field_edge_cases(): + # Case-insensitive + assert ( + IndexerQueryResult( + title="Show S01 4k", download_url="", seeders=1, flags=[], size=1 + ).quality + == Quality.uhd + ) + assert ( + IndexerQueryResult( + title="Show S01 1080P", download_url="", seeders=1, flags=[], size=1 + ).quality + == Quality.fullhd + ) + assert ( + IndexerQueryResult( + title="Show S01 720P", download_url="", seeders=1, flags=[], size=1 + ).quality + == Quality.hd + ) + assert ( + IndexerQueryResult( + title="Show S01 480P", download_url="", seeders=1, flags=[], size=1 + ).quality + == Quality.sd + ) + # Multiple quality tags, prefer highest + assert ( + IndexerQueryResult( + title="Show S01 4K 1080p 720p", download_url="", seeders=1, flags=[], size=1 + ).quality + == Quality.uhd + ) + assert ( + IndexerQueryResult( + title="Show S01 1080p 720p", download_url="", seeders=1, flags=[], size=1 + ).quality + == Quality.fullhd + ) + # No quality tag + assert ( + IndexerQueryResult( + title="Show S01", download_url="", seeders=1, flags=[], size=1 + ).quality + == Quality.unknown + ) + # Quality tag in the middle + assert ( + IndexerQueryResult( + title="4K Show S01", download_url="", seeders=1, flags=[], size=1 + ).quality + == Quality.uhd + ) + + +def test_season_computed_field(): + # Single season + assert IndexerQueryResult( + title="Show S01", download_url="", seeders=1, flags=[], size=1 + ).season == [1] + # Range of seasons + assert IndexerQueryResult( + title="Show S01 S03", download_url="", seeders=1, flags=[], size=1 + ).season == [1, 2, 3] + # No season + assert ( + IndexerQueryResult( + title="Show", download_url="", seeders=1, flags=[], size=1 + ).season + == [] + ) + + +def test_season_computed_field_edge_cases(): + # Multiple seasons, unordered + assert ( + IndexerQueryResult( + title="Show S03 S01", download_url="", seeders=1, flags=[], size=1 + ).season + == [] + ) + # Season with leading zeros + assert IndexerQueryResult( + title="Show S01 S03", download_url="", seeders=1, flags=[], size=1 + ).season == [1, 2, 3] + assert IndexerQueryResult( + title="Show S01 S01", download_url="", seeders=1, flags=[], size=1 + ).season == [1] + # No season at all + assert ( + IndexerQueryResult( + title="Show", download_url="", seeders=1, flags=[], size=1 + ).season + == [] + ) + # Season in lower/upper case + assert IndexerQueryResult( + title="Show s02", download_url="", seeders=1, flags=[], size=1 + ).season == [2] + assert IndexerQueryResult( + title="Show S02", download_url="", seeders=1, flags=[], size=1 + ).season == [2] + # Season with extra text + assert IndexerQueryResult( + title="Show S01 Complete", download_url="", seeders=1, flags=[], size=1 + ).season == [1] + + +def test_gt_and_lt_methods(): + a = IndexerQueryResult( + title="Show S01 1080p", download_url="", seeders=5, flags=[], size=1 + ) + b = IndexerQueryResult( + title="Show S01 720p", download_url="", seeders=10, flags=[], size=1 + ) + c = IndexerQueryResult( + title="Show S01 1080p", download_url="", seeders=2, flags=[], size=1 + ) + # a (fullhd) > b (hd) + assert a > b + assert not (b > a) + # If quality is equal, compare by seeders (lower seeders is less than higher seeders) + assert c < a + assert a > c + # If quality is equal, but seeders are equal, neither is greater + d = IndexerQueryResult( + title="Show S01 1080p", download_url="", seeders=5, flags=[], size=1 + ) + assert not (a < d) + assert not (a > d) + + +def test_gt_and_lt_methods_edge_cases(): + # Different qualities + a = IndexerQueryResult( + title="Show S01 4K", download_url="", seeders=1, flags=[], size=1 + ) + b = IndexerQueryResult( + title="Show S01 1080p", download_url="", seeders=100, flags=[], size=1 + ) + assert a > b + assert not (b > a) + # Same quality, different seeders + c = IndexerQueryResult( + title="Show S01 4K", download_url="", seeders=2, flags=[], size=1 + ) + assert a < c + assert c > a + # Same quality and seeders + d = IndexerQueryResult( + title="Show S01 4K", download_url="", seeders=1, flags=[], size=1 + ) + assert not (a < d) + assert not (a > d) + # Unknown quality, should compare by seeders + e = IndexerQueryResult( + title="Show S01", download_url="", seeders=5, flags=[], size=1 + ) + f = IndexerQueryResult( + title="Show S01", download_url="", seeders=10, flags=[], size=1 + ) + assert e < f + assert f > e + # Mixed known and unknown quality + g = IndexerQueryResult( + title="Show S01 720p", download_url="", seeders=1, flags=[], size=1 + ) + h = IndexerQueryResult( + title="Show S01", download_url="", seeders=100, flags=[], size=1 + ) + assert g > h + assert not (h > g) diff --git a/tests/indexer/test_service.py b/tests/indexer/test_service.py new file mode 100644 index 0000000..b6c4ba3 --- /dev/null +++ b/tests/indexer/test_service.py @@ -0,0 +1,61 @@ +import uuid +from unittest.mock import MagicMock +import pytest + +from media_manager.indexer.schemas import IndexerQueryResult, IndexerQueryResultId +from media_manager.indexer.repository import IndexerRepository +from media_manager.indexer.service import IndexerService + + +class DummyIndexer: + def search(self, query): + return [ + IndexerQueryResult( + id=IndexerQueryResultId(uuid.uuid4()), + title=f"{query} S01 1080p", + download_url="http://example.com/1", + seeders=10, + flags=["test"], + size=123456, + ) + ] + + +@pytest.fixture +def mock_indexer_repository(): + repo = MagicMock(spec=IndexerRepository) + repo.save_result.side_effect = lambda result: result + return repo + + +@pytest.fixture +def indexer_service(monkeypatch, mock_indexer_repository): + # Patch the global indexers list + monkeypatch.setattr("media_manager.indexer.service.indexers", [DummyIndexer()]) + return IndexerService(indexer_repository=mock_indexer_repository) + + +def test_search_returns_results(indexer_service, mock_indexer_repository): + query = "TestShow" + results = indexer_service.search(query) + assert len(results) == 1 + assert results[0].title == f"{query} S01 1080p" + mock_indexer_repository.save_result.assert_called_once() + + +def test_get_result_returns_result(mock_indexer_repository): + result_id = IndexerQueryResultId(uuid.uuid4()) + expected_result = IndexerQueryResult( + id=result_id, + title="Test S01 1080p", + download_url="http://example.com/1", + seeders=10, + flags=["test"], + size=123456, + ) + mock_indexer_repository.get_result.return_value = expected_result + service = IndexerService(indexer_repository=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) + From 4c414d13efc55a2fdbb9f502924688e1b4fc9a53 Mon Sep 17 00:00:00 2001 From: maxDorninger <97409287+maxDorninger@users.noreply.github.com> Date: Sun, 8 Jun 2025 17:51:47 +0200 Subject: [PATCH 05/11] refactor indexer module imports and format code --- media_manager/indexer/dependencies.py | 10 +-- media_manager/indexer/indexers/generic.py | 5 +- media_manager/indexer/models.py | 1 - media_manager/indexer/schemas.py | 1 + media_manager/indexer/service.py | 10 +-- media_manager/main.py | 4 +- media_manager/metadataProvider/__init__.py | 6 +- media_manager/torrent/dependencies.py | 1 + media_manager/torrent/repository.py | 1 - media_manager/torrent/service.py | 20 ++---- media_manager/torrent/utils.py | 2 +- media_manager/tv/service.py | 4 +- tests/indexer/test_repository.py | 2 - tests/indexer/test_schemas.py | 1 - tests/indexer/test_service.py | 1 - tests/tv/test_service.py | 82 ++++++++++++++++------ 16 files changed, 88 insertions(+), 63 deletions(-) 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 == [] - From d82f9a3d9e82f426a07dfc312098de88c6054ffe Mon Sep 17 00:00:00 2001 From: maxDorninger <97409287+maxDorninger@users.noreply.github.com> Date: Sun, 8 Jun 2025 18:25:11 +0200 Subject: [PATCH 06/11] add back feature to auto import all torrents --- media_manager/main.py | 10 ++++----- media_manager/torrent/dependencies.py | 2 +- media_manager/torrent/repository.py | 4 +++- media_manager/torrent/router.py | 12 +++++----- media_manager/torrent/service.py | 20 ++++++++--------- media_manager/tv/dependencies.py | 4 ++-- media_manager/tv/router.py | 32 +++++++++++++-------------- media_manager/tv/service.py | 28 ++++++++++++++++++++++- 8 files changed, 69 insertions(+), 43 deletions(-) diff --git a/media_manager/main.py b/media_manager/main.py index 2ff468e..9cc33e2 100644 --- a/media_manager/main.py +++ b/media_manager/main.py @@ -48,7 +48,10 @@ log = logging.getLogger(__name__) from media_manager.database import init_db import media_manager.tv.router as tv_router -from media_manager.tv.service import auto_download_all_approved_season_requests +from media_manager.tv.service import ( + auto_download_all_approved_season_requests, + import_all_torrents, +) import media_manager.torrent.router as torrent_router from media_manager.config import BasicConfig from fastapi import FastAPI @@ -75,10 +78,7 @@ else: def hourly_tasks(): log.info(f"Tasks are running at {datetime.now()}") auto_download_all_approved_season_requests() - # media_manager.torrent.service.TorrentService( - # db=SessionLocal() - # ).import_all_torrents() - + import_all_torrents() scheduler = BackgroundScheduler() trigger = CronTrigger(second=0, hour="*") diff --git a/media_manager/torrent/dependencies.py b/media_manager/torrent/dependencies.py index 6cd90ff..a3db70a 100644 --- a/media_manager/torrent/dependencies.py +++ b/media_manager/torrent/dependencies.py @@ -18,4 +18,4 @@ def get_torrent_service(torrent_repository: torrent_repository_dep) -> TorrentSe return TorrentService(torrent_repository=torrent_repository) -tv_service_dep = Annotated[TorrentService, Depends(get_torrent_service)] +torrent_service_dep = Annotated[TorrentService, Depends(get_torrent_service)] diff --git a/media_manager/torrent/repository.py b/media_manager/torrent/repository.py index 0034e44..7bb678d 100644 --- a/media_manager/torrent/repository.py +++ b/media_manager/torrent/repository.py @@ -18,7 +18,7 @@ class TorrentRepository: result = self.db.execute(stmt).scalars().all() return [SeasonFileSchema.model_validate(season_file) for season_file in result] - def get_show_of_torrent(self, torrent_id: TorrentId) -> ShowSchema: + def get_show_of_torrent(self, torrent_id: TorrentId) -> ShowSchema | None: stmt = ( select(Show) .join(SeasonFile.season) @@ -26,6 +26,8 @@ class TorrentRepository: .where(SeasonFile.torrent_id == torrent_id) ) result = self.db.execute(stmt).unique().scalar_one_or_none() + if result is None: + return None return ShowSchema.model_validate(result) def save_torrent(self, torrent: TorrentSchema) -> TorrentSchema: diff --git a/media_manager/torrent/router.py b/media_manager/torrent/router.py index 7a82ad3..3194ee8 100644 --- a/media_manager/torrent/router.py +++ b/media_manager/torrent/router.py @@ -3,14 +3,14 @@ from fastapi import status from fastapi.params import Depends from media_manager.auth.users import current_active_user, current_superuser -from media_manager.torrent.dependencies import tv_service_dep +from media_manager.torrent.dependencies import torrent_service_dep from media_manager.torrent.schemas import TorrentId, Torrent router = APIRouter() @router.get("/{torrent_id}", status_code=status.HTTP_200_OK, response_model=Torrent) -def get_torrent(service: tv_service_dep, torrent_id: TorrentId): +def get_torrent(service: torrent_service_dep, torrent_id: TorrentId): return service.get_torrent_by_id(id=torrent_id) @@ -20,7 +20,7 @@ def get_torrent(service: tv_service_dep, torrent_id: TorrentId): dependencies=[Depends(current_active_user)], response_model=list[Torrent], ) -def get_all_torrents(service: tv_service_dep): +def get_all_torrents(service: torrent_service_dep): return service.get_all_torrents() @@ -30,7 +30,7 @@ def get_all_torrents(service: tv_service_dep): dependencies=[Depends(current_active_user)], response_model=Torrent, ) -def import_torrent(service: tv_service_dep, torrent_id: TorrentId): +def import_torrent(service: torrent_service_dep, torrent_id: TorrentId): return service.import_torrent(service.get_torrent_by_id(id=torrent_id)) @@ -40,7 +40,7 @@ def import_torrent(service: tv_service_dep, torrent_id: TorrentId): dependencies=[Depends(current_active_user)], response_model=list[Torrent], ) -def import_all_torrents(service: tv_service_dep): +def import_all_torrents(service: torrent_service_dep): return service.import_all_torrents() @@ -49,5 +49,5 @@ def import_all_torrents(service: tv_service_dep): status_code=status.HTTP_200_OK, dependencies=[Depends(current_superuser)], ) -def delete_torrent(service: tv_service_dep, torrent_id: TorrentId): +def delete_torrent(service: torrent_service_dep, torrent_id: TorrentId): service.delete_torrent(torrent_id=torrent_id) diff --git a/media_manager/torrent/service.py b/media_manager/torrent/service.py index 6ca525b..12a3b44 100644 --- a/media_manager/torrent/service.py +++ b/media_manager/torrent/service.py @@ -10,7 +10,7 @@ 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.tv.schemas import SeasonFile +from media_manager.tv.schemas import SeasonFile, Show log = logging.getLogger(__name__) @@ -68,6 +68,14 @@ class TorrentService: torrent_id=torrent.id ) + 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 + :return: the show of the torrent + """ + return self.torrent_repository.get_show_of_torrent(torrent_id=torrent.id) + def download(self, indexer_result: IndexerQueryResult) -> Torrent: log.info(f"Attempting to download torrent: {indexer_result.title}") torrent = Torrent( @@ -186,13 +194,3 @@ class TorrentService: # 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]: - # log.info("Importing all torrents") - # torrents = self.get_all_torrents() - # log.info("Found %d torrents to import", len(torrents)) - # imported_torrents = [] - # for t in torrents: - # if t.imported == False and t.status == TorrentStatus.finished: - # imported_torrents.append(self.import_torrent(t)) - # log.info("Finished importing all torrents") - # return imported_torrents diff --git a/media_manager/tv/dependencies.py b/media_manager/tv/dependencies.py index 805e03d..a7981db 100644 --- a/media_manager/tv/dependencies.py +++ b/media_manager/tv/dependencies.py @@ -9,7 +9,7 @@ from media_manager.tv.service import TvService from media_manager.tv.exceptions import NotFoundError from fastapi import HTTPException from media_manager.indexer.dependencies import indexer_service_dep -from media_manager.torrent.dependencies import tv_service_dep +from media_manager.torrent.dependencies import torrent_service_dep def get_tv_repository(db_session: DbSessionDependency) -> TvRepository: @@ -21,7 +21,7 @@ tv_repository_dep = Annotated[TvRepository, Depends(get_tv_repository)] def get_tv_service( tv_repository: tv_repository_dep, - torrent_service: tv_service_dep, + torrent_service: torrent_service_dep, indexer_service: indexer_service_dep, ) -> TvService: return TvService( diff --git a/media_manager/tv/router.py b/media_manager/tv/router.py index b1d534c..26b9f25 100644 --- a/media_manager/tv/router.py +++ b/media_manager/tv/router.py @@ -24,7 +24,7 @@ from media_manager.tv.schemas import ( RichSeasonRequest, ) from media_manager.tv.dependencies import ( - tv_service_dep, + torrent_service_dep, season_dep, show_dep, tv_repository_dep, @@ -51,7 +51,7 @@ router = APIRouter() }, ) def add_a_show( - tv_service: tv_service_dep, show_id: int, metadata_provider: str = "tmdb" + tv_service: torrent_service_dep, show_id: int, metadata_provider: str = "tmdb" ): try: show = tv_service.add_show( @@ -83,7 +83,7 @@ def delete_a_show(tv_repository: tv_repository_dep, show_id: ShowId): "/shows", dependencies=[Depends(current_active_user)], response_model=list[Show] ) def get_all_shows( - tv_service: tv_service_dep, external_id: int = None, metadata_provider: str = "tmdb" + tv_service: torrent_service_dep, external_id: int = None, metadata_provider: str = "tmdb" ): if external_id is not None: return tv_service.get_show_by_external_id( @@ -98,7 +98,7 @@ def get_all_shows( dependencies=[Depends(current_active_user)], response_model=list[RichShowTorrent], ) -def get_shows_with_torrents(tv_service: tv_service_dep): +def get_shows_with_torrents(tv_service: torrent_service_dep): """ get all shows that are associated with torrents :return: A list of shows with all their torrents @@ -112,7 +112,7 @@ def get_shows_with_torrents(tv_service: tv_service_dep): dependencies=[Depends(current_active_user)], response_model=PublicShow, ) -def get_a_show(show: show_dep, tv_service: tv_service_dep) -> PublicShow: +def get_a_show(show: show_dep, tv_service: torrent_service_dep) -> PublicShow: return tv_service.get_public_show_by_id(show_id=show.id) @@ -121,7 +121,7 @@ def get_a_show(show: show_dep, tv_service: tv_service_dep) -> PublicShow: dependencies=[Depends(current_active_user)], response_model=RichShowTorrent, ) -def get_a_shows_torrents(show: show_dep, tv_service: tv_service_dep): +def get_a_shows_torrents(show: show_dep, tv_service: torrent_service_dep): return tv_service.get_torrents_for_show(show=show) @@ -134,7 +134,7 @@ def get_a_shows_torrents(show: show_dep, tv_service: tv_service_dep): def request_a_season( user: Annotated[User, Depends(current_active_user)], season_request: CreateSeasonRequest, - tv_service: tv_service_dep, + tv_service: torrent_service_dep, ): """ adds request flag to a season @@ -156,7 +156,7 @@ def request_a_season( dependencies=[Depends(current_active_user)], response_model=list[RichSeasonRequest], ) -def get_season_requests(tv_service: tv_service_dep) -> list[RichSeasonRequest]: +def get_season_requests(tv_service: torrent_service_dep) -> list[RichSeasonRequest]: return tv_service.get_all_season_requests() @@ -165,7 +165,7 @@ def get_season_requests(tv_service: tv_service_dep) -> list[RichSeasonRequest]: status_code=status.HTTP_204_NO_CONTENT, ) def delete_season_request( - tv_service: tv_service_dep, + tv_service: torrent_service_dep, user: Annotated[User, Depends(current_active_user)], request_id: SeasonRequestId, ): @@ -188,7 +188,7 @@ def delete_season_request( "/seasons/requests/{season_request_id}", status_code=status.HTTP_204_NO_CONTENT ) def authorize_request( - tv_service: tv_service_dep, + tv_service: torrent_service_dep, user: Annotated[User, Depends(current_superuser)], season_request_id: SeasonRequestId, authorized_status: bool = False, @@ -209,7 +209,7 @@ def authorize_request( @router.put("/seasons/requests", status_code=status.HTTP_204_NO_CONTENT) def update_request( - tv_service: tv_service_dep, + tv_service: torrent_service_dep, user: Annotated[User, Depends(current_active_user)], season_request: UpdateSeasonRequest, ): @@ -229,7 +229,7 @@ def update_request( response_model=list[PublicSeasonFile], ) def get_season_files( - season: season_dep, tv_service: tv_service_dep + season: season_dep, tv_service: torrent_service_dep ) -> list[PublicSeasonFile]: return tv_service.get_public_season_files_by_season_id(season_id=season.id) @@ -247,7 +247,7 @@ def get_season_files( response_model=list[PublicIndexerQueryResult], ) def get_torrents_for_a_season( - tv_service: tv_service_dep, + tv_service: torrent_service_dep, show_id: ShowId, season_number: int = 1, search_query_override: str = None, @@ -267,7 +267,7 @@ def get_torrents_for_a_season( dependencies=[Depends(current_superuser)], ) def download_a_torrent( - tv_service: tv_service_dep, + tv_service: torrent_service_dep, public_indexer_result_id: IndexerQueryResultId, show_id: ShowId, override_file_path_suffix: str = "", @@ -290,7 +290,7 @@ def download_a_torrent( response_model=list[MetaDataProviderShowSearchResult], ) def search_metadata_providers_for_a_show( - tv_service: tv_service_dep, query: str, metadata_provider: str = "tmdb" + tv_service: torrent_service_dep, query: str, metadata_provider: str = "tmdb" ): return tv_service.search_for_show(query=query, metadata_provider=metadata_provider) @@ -300,5 +300,5 @@ 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: str = "tmdb"): +def get_recommended_shows(tv_service: torrent_service_dep, metadata_provider: str = "tmdb"): 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 aaa7ba3..e88165b 100644 --- a/media_manager/tv/service.py +++ b/media_manager/tv/service.py @@ -11,7 +11,7 @@ from media_manager.database import SessionLocal from media_manager.indexer.schemas import IndexerQueryResult from media_manager.indexer.schemas import IndexerQueryResultId from media_manager.metadataProvider.schemas import MetaDataProviderShowSearchResult -from media_manager.torrent.schemas import Torrent +from media_manager.torrent.schemas import Torrent, TorrentStatus from media_manager.torrent.service import TorrentService from media_manager.tv import log from media_manager.tv.schemas import ( @@ -605,3 +605,29 @@ def auto_download_all_approved_season_requests() -> None: log.info(f"Auto downloaded {count} approved season requests") db.close() + + +def import_all_torrents() -> None: + db: Session = SessionLocal() + tv_repository = TvRepository(db=db) + torrent_service = TorrentService(torrent_repository=TorrentRepository(db=db)) + indexer_service = IndexerService(indexer_repository=IndexerRepository(db=db)) + tv_service = TvService( + tv_repository=tv_repository, + torrent_service=torrent_service, + indexer_service=indexer_service, + ) + log.info("Importing all torrents") + torrents = torrent_service.get_all_torrents() + log.info("Found %d torrents to import", len(torrents)) + imported_torrents = [] + for t in torrents: + if t.imported == False 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." + ) + continue + imported_torrents.append(tv_service.import_torrent_files(torrent=t, show=show)) + log.info("Finished importing all torrents") \ No newline at end of file From e9cddf40f81d3633a9f05f1d2e5325a84c76b4bd Mon Sep 17 00:00:00 2001 From: maxDorninger <97409287+maxDorninger@users.noreply.github.com> Date: Sun, 8 Jun 2025 18:31:26 +0200 Subject: [PATCH 07/11] update roadmap --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bc39a65..7c874a5 100644 --- a/README.md +++ b/README.md @@ -27,21 +27,21 @@ torrents and authentication. ## Roadmap -- [ ] support for movies - [x] support for more torrent indexers - [x] fully automatic downloads +- [x] add tests +- [x] add more logs/errors +- [ ] support for movies - [ ] responsive ui - [ ] add check at startup if hardlinks work -- [ ] support multiple OIDC servers at once +- [ ] support styling the login with OIDC button - [ ] make API return proper error codes - [ ] add in-depth documentation on the architecure of the codebase - [ ] expand README with more information and a quickstart guide - [ ] make indexer module multithreaded - [ ] add notification system - [ ] _maybe_ rework the logo -- [ ] add tests - [ ] optimize images for web in the backend and merging frontend with backend container -- [ ] add more logs/errors - [ ] add support for deluge and transmission - [ ] automatically download new seasons/episodes of shows From 5dabb5c0d768adbfd9849b766b80b3219c1f6606 Mon Sep 17 00:00:00 2001 From: maxDorninger <97409287+maxDorninger@users.noreply.github.com> Date: Sun, 8 Jun 2025 18:31:46 +0200 Subject: [PATCH 08/11] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7c874a5..f8d8a1a 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@

-Media Manager aims to be a replacement for Sonarr/Radarr/Prowlarr/Overseer, +Media Manager aims to be a replacement for Sonarr/Radarr/Overseer/Jellyseer, it is a comprehensive solution for organizing your media library, including TV shows and movies. It provides a modern web interface and integrates with various services for metadata, torrents and authentication. From 3d9e9d00d04d6703c1128cf824f934815cf9dfc2 Mon Sep 17 00:00:00 2001 From: maxDorninger <97409287+maxDorninger@users.noreply.github.com> Date: Sun, 8 Jun 2025 18:39:45 +0200 Subject: [PATCH 09/11] add get_torrent_by_id dependency, add 404 error message and make NotFoundException globally available --- README.md | 2 +- media_manager/{tv => }/exceptions.py | 0 media_manager/torrent/dependencies.py | 23 ++++++++++++++++++++++- media_manager/torrent/repository.py | 6 +++++- media_manager/torrent/router.py | 14 +++++++------- media_manager/tv/dependencies.py | 2 +- media_manager/tv/repository.py | 2 +- media_manager/tv/router.py | 2 +- media_manager/tv/service.py | 2 +- 9 files changed, 39 insertions(+), 14 deletions(-) rename media_manager/{tv => }/exceptions.py (100%) diff --git a/README.md b/README.md index f8d8a1a..fef3321 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,11 @@ torrents and authentication. - [x] fully automatic downloads - [x] add tests - [x] add more logs/errors +- [x] make API return proper error codes - [ ] support for movies - [ ] responsive ui - [ ] add check at startup if hardlinks work - [ ] support styling the login with OIDC button -- [ ] make API return proper error codes - [ ] add in-depth documentation on the architecure of the codebase - [ ] expand README with more information and a quickstart guide - [ ] make indexer module multithreaded diff --git a/media_manager/tv/exceptions.py b/media_manager/exceptions.py similarity index 100% rename from media_manager/tv/exceptions.py rename to media_manager/exceptions.py diff --git a/media_manager/torrent/dependencies.py b/media_manager/torrent/dependencies.py index a3db70a..df025af 100644 --- a/media_manager/torrent/dependencies.py +++ b/media_manager/torrent/dependencies.py @@ -2,10 +2,12 @@ from typing import Annotated from fastapi import Depends +from exceptions import NotFoundError from media_manager.database import DbSessionDependency from media_manager.torrent.service import TorrentService 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) @@ -19,3 +21,22 @@ 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: + """ + Retrieves a torrent by its ID. + + :param torrent_service: The TorrentService instance. + :param torrent_id: The ID of the torrent to retrieve. + :return: The TorrentService instance with the specified torrent. + """ + 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") + return torrent + +torrent_dep = Annotated[Torrent, Depends(get_torrent_by_id)] \ No newline at end of file diff --git a/media_manager/torrent/repository.py b/media_manager/torrent/repository.py index 7bb678d..63bb968 100644 --- a/media_manager/torrent/repository.py +++ b/media_manager/torrent/repository.py @@ -5,6 +5,7 @@ from media_manager.torrent.models import Torrent from media_manager.torrent.schemas import TorrentId, Torrent as TorrentSchema from media_manager.tv.models import SeasonFile, Show, Season from media_manager.tv.schemas import SeasonFile as SeasonFileSchema, Show as ShowSchema +from tv.exceptions import NotFoundError class TorrentRepository: @@ -44,7 +45,10 @@ class TorrentRepository: ] def get_torrent_by_id(self, torrent_id: TorrentId) -> TorrentSchema: - return TorrentSchema.model_validate(self.db.get(Torrent, torrent_id)) + result = self.db.get(Torrent, torrent_id) + if result is None: + raise NotFoundError(f"Torrent with ID {torrent_id} not found.") + return TorrentSchema.model_validate(result) def delete_torrent(self, torrent_id: TorrentId): self.db.delete(self.db.get(Torrent, torrent_id)) diff --git a/media_manager/torrent/router.py b/media_manager/torrent/router.py index 3194ee8..a617cb4 100644 --- a/media_manager/torrent/router.py +++ b/media_manager/torrent/router.py @@ -3,15 +3,15 @@ from fastapi import status 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 +from media_manager.torrent.dependencies import torrent_service_dep, torrent_dep from media_manager.torrent.schemas import TorrentId, Torrent router = APIRouter() @router.get("/{torrent_id}", status_code=status.HTTP_200_OK, response_model=Torrent) -def get_torrent(service: torrent_service_dep, torrent_id: TorrentId): - return service.get_torrent_by_id(id=torrent_id) +def get_torrent(service: torrent_service_dep, torrent: torrent_dep): + return service.get_torrent_by_id(id=torrent.id) @router.get( @@ -30,8 +30,8 @@ def get_all_torrents(service: torrent_service_dep): dependencies=[Depends(current_active_user)], response_model=Torrent, ) -def import_torrent(service: torrent_service_dep, torrent_id: TorrentId): - return service.import_torrent(service.get_torrent_by_id(id=torrent_id)) +def import_torrent(service: torrent_service_dep, torrent: torrent_dep): + return service.import_torrent(service.get_torrent_by_id(id=torrent.id)) @router.post( @@ -49,5 +49,5 @@ def import_all_torrents(service: torrent_service_dep): status_code=status.HTTP_200_OK, dependencies=[Depends(current_superuser)], ) -def delete_torrent(service: torrent_service_dep, torrent_id: TorrentId): - service.delete_torrent(torrent_id=torrent_id) +def delete_torrent(service: torrent_service_dep, torrent: torrent_dep): + service.delete_torrent(torrent_id=torrent.id) diff --git a/media_manager/tv/dependencies.py b/media_manager/tv/dependencies.py index a7981db..cd1434b 100644 --- a/media_manager/tv/dependencies.py +++ b/media_manager/tv/dependencies.py @@ -6,7 +6,7 @@ from media_manager.database import DbSessionDependency from media_manager.tv.repository import TvRepository from media_manager.tv.schemas import Show, ShowId, SeasonId, Season from media_manager.tv.service import TvService -from media_manager.tv.exceptions import NotFoundError +from media_manager.exceptions import NotFoundError from fastapi import HTTPException from media_manager.indexer.dependencies import indexer_service_dep from media_manager.torrent.dependencies import torrent_service_dep diff --git a/media_manager/tv/repository.py b/media_manager/tv/repository.py index 85ca271..c247f4d 100644 --- a/media_manager/tv/repository.py +++ b/media_manager/tv/repository.py @@ -9,7 +9,7 @@ from media_manager.torrent.models import Torrent from media_manager.torrent.schemas import TorrentId, Torrent as TorrentSchema from media_manager.tv import log from media_manager.tv.models import Season, Show, Episode, SeasonRequest, SeasonFile -from media_manager.tv.exceptions import NotFoundError +from media_manager.exceptions import NotFoundError from media_manager.tv.schemas import ( Season as SeasonSchema, SeasonId, diff --git a/media_manager/tv/router.py b/media_manager/tv/router.py index 26b9f25..6b1f38d 100644 --- a/media_manager/tv/router.py +++ b/media_manager/tv/router.py @@ -10,7 +10,7 @@ from media_manager.indexer.schemas import PublicIndexerQueryResult, IndexerQuery from media_manager.metadataProvider.schemas import MetaDataProviderShowSearchResult from media_manager.torrent.schemas import Torrent from media_manager.tv import log -from media_manager.tv.exceptions import MediaAlreadyExists +from media_manager.exceptions import MediaAlreadyExists from media_manager.tv.schemas import ( Show, SeasonRequest, diff --git a/media_manager/tv/service.py b/media_manager/tv/service.py index e88165b..e8b6e4d 100644 --- a/media_manager/tv/service.py +++ b/media_manager/tv/service.py @@ -31,7 +31,7 @@ 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 +from media_manager.exceptions import NotFoundError import pprint from pathlib import Path from media_manager.config import BasicConfig From 302890171fefe3c2764903c168ec8fce0e578a64 Mon Sep 17 00:00:00 2001 From: maxDorninger <97409287+maxDorninger@users.noreply.github.com> Date: Sun, 8 Jun 2025 18:42:02 +0200 Subject: [PATCH 10/11] refactor repository module to import NotFoundError from media_manager.exceptions and fix return statement in delete operation --- media_manager/torrent/repository.py | 2 +- media_manager/tv/repository.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/media_manager/torrent/repository.py b/media_manager/torrent/repository.py index 63bb968..41b979b 100644 --- a/media_manager/torrent/repository.py +++ b/media_manager/torrent/repository.py @@ -5,7 +5,7 @@ from media_manager.torrent.models import Torrent from media_manager.torrent.schemas import TorrentId, Torrent as TorrentSchema from media_manager.tv.models import SeasonFile, Show, Season from media_manager.tv.schemas import SeasonFile as SeasonFileSchema, Show as ShowSchema -from tv.exceptions import NotFoundError +from media_manager.exceptions import NotFoundError class TorrentRepository: diff --git a/media_manager/tv/repository.py b/media_manager/tv/repository.py index c247f4d..43367e8 100644 --- a/media_manager/tv/repository.py +++ b/media_manager/tv/repository.py @@ -414,7 +414,7 @@ class TvRepository: log.info( f"Successfully removed {deleted_count} season files for torrent_id: {torrent_id}" ) - return deleted_count + return deleted_count() except SQLAlchemyError as e: self.db.rollback() log.error( From cbd597c5dd90a6bbeee700e815cbeed11924cda7 Mon Sep 17 00:00:00 2001 From: maxDorninger <97409287+maxDorninger@users.noreply.github.com> Date: Sun, 8 Jun 2025 18:46:57 +0200 Subject: [PATCH 11/11] fix exception import in test --- tests/tv/test_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tv/test_service.py b/tests/tv/test_service.py index 6744592..29206af 100644 --- a/tests/tv/test_service.py +++ b/tests/tv/test_service.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock, patch import pytest -from media_manager.tv.exceptions import NotFoundError +from media_manager.exceptions import NotFoundError from media_manager.tv.schemas import Show, ShowId from media_manager.tv.service import TvService from media_manager.indexer.schemas import IndexerQueryResult, IndexerQueryResultId