mirror of
https://github.com/maxdorninger/MediaManager.git
synced 2026-02-19 15:35:18 -05:00
Merge pull request #12 from maxdorninger/rework-indexer-module
Rework indexer module
This commit is contained in:
12
README.md
12
README.md
@@ -17,7 +17,7 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
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.
|
||||
@@ -27,21 +27,21 @@ torrents and authentication.
|
||||
<!-- ROADMAP -->
|
||||
## Roadmap
|
||||
|
||||
- [ ] support for movies
|
||||
- [x] support for more torrent indexers
|
||||
- [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 multiple OIDC servers at once
|
||||
- [x] make API return proper error codes
|
||||
- [ ] support styling the login with OIDC button
|
||||
- [ ] 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
|
||||
- [x] add more logs/errors
|
||||
- [ ] add support for deluge and transmission
|
||||
- [ ] automatically download new seasons/episodes of shows
|
||||
|
||||
|
||||
24
media_manager/indexer/dependencies.py
Normal file
24
media_manager/indexer/dependencies.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends
|
||||
|
||||
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
|
||||
|
||||
|
||||
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: indexer_repository_dep,
|
||||
) -> IndexerService:
|
||||
return IndexerService(indexer_repository)
|
||||
|
||||
|
||||
indexer_service_dep = Annotated[TvService, Depends(get_indexer_service)]
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
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.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
|
||||
|
||||
@@ -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
|
||||
@@ -57,8 +60,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")
|
||||
@@ -77,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="*")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -2,18 +2,41 @@ 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)
|
||||
|
||||
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)]
|
||||
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)]
|
||||
@@ -1,11 +1,11 @@
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from media_manager.database import DbSessionDependency
|
||||
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 media_manager.exceptions import NotFoundError
|
||||
|
||||
|
||||
class TorrentRepository:
|
||||
@@ -19,7 +19,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)
|
||||
@@ -27,6 +27,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:
|
||||
@@ -43,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))
|
||||
|
||||
@@ -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 TorrentServiceDependency
|
||||
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: TorrentServiceDependency, 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(
|
||||
@@ -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: torrent_service_dep):
|
||||
return service.get_all_torrents()
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ def get_all_torrents(service: TorrentServiceDependency):
|
||||
dependencies=[Depends(current_active_user)],
|
||||
response_model=Torrent,
|
||||
)
|
||||
def import_torrent(service: TorrentServiceDependency, 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(
|
||||
@@ -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: torrent_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):
|
||||
service.delete_torrent(torrent_id=torrent_id)
|
||||
def delete_torrent(service: torrent_service_dep, torrent: torrent_dep):
|
||||
service.delete_torrent(torrent_id=torrent.id)
|
||||
|
||||
@@ -1,27 +1,16 @@
|
||||
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
|
||||
from media_manager.tv.schemas import SeasonFile, Show
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -75,7 +64,17 @@ 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 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}")
|
||||
@@ -186,21 +185,12 @@ 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]:
|
||||
# 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
|
||||
|
||||
@@ -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
|
||||
return video_files, subtitle_files, all_files
|
||||
|
||||
@@ -6,10 +6,10 @@ 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.torrent.dependencies import TorrentServiceDependency
|
||||
from media_manager.indexer.dependencies import indexer_service_dep
|
||||
from media_manager.torrent.dependencies import torrent_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: torrent_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)]
|
||||
|
||||
@@ -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,
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
@@ -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)
|
||||
|
||||
@@ -6,11 +6,12 @@ from sqlalchemy.orm import Session
|
||||
import media_manager.indexer.service
|
||||
import media_manager.metadataProvider
|
||||
import media_manager.torrent.repository
|
||||
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
|
||||
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 (
|
||||
@@ -30,19 +31,25 @@ 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
|
||||
from media_manager.exceptions import NotFoundError
|
||||
import pprint
|
||||
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:
|
||||
"""
|
||||
@@ -173,8 +180,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:
|
||||
@@ -384,8 +391,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)
|
||||
|
||||
@@ -568,7 +575,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()
|
||||
@@ -583,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(
|
||||
@@ -592,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")
|
||||
0
tests/indexer/__init__.py
Normal file
0
tests/indexer/__init__.py
Normal file
63
tests/indexer/test_repository.py
Normal file
63
tests/indexer/test_repository.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import uuid
|
||||
import pytest
|
||||
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
|
||||
209
tests/indexer/test_schemas.py
Normal file
209
tests/indexer/test_schemas.py
Normal file
@@ -0,0 +1,209 @@
|
||||
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)
|
||||
60
tests/indexer/test_service.py
Normal file
60
tests/indexer/test_service.py
Normal file
@@ -0,0 +1,60 @@
|
||||
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)
|
||||
@@ -3,12 +3,11 @@ 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.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
|
||||
from media_manager.metadataProvider.schemas import MetaDataProviderShowSearchResult
|
||||
from media_manager.torrent.models import Quality
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -22,8 +21,17 @@ def mock_torrent_service():
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tv_service(mock_tv_repository, mock_torrent_service):
|
||||
return TvService(tv_repository=mock_tv_repository, torrent_service=mock_torrent_service)
|
||||
def mock_indexer_service():
|
||||
return MagicMock()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
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):
|
||||
@@ -54,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
|
||||
@@ -68,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"
|
||||
@@ -85,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)
|
||||
@@ -95,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
|
||||
@@ -189,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
|
||||
@@ -267,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]
|
||||
@@ -284,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]
|
||||
@@ -294,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
|
||||
@@ -306,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()
|
||||
@@ -325,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()
|
||||
@@ -335,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()
|
||||
@@ -349,12 +387,14 @@ 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
|
||||
|
||||
|
||||
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 +451,21 @@ 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 +479,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 +504,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 +512,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 +532,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
|
||||
@@ -625,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 == []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user