diff --git a/backend/src/database/__init__.py b/backend/src/database/__init__.py index 388fb00..aa82af5 100644 --- a/backend/src/database/__init__.py +++ b/backend/src/database/__init__.py @@ -32,6 +32,7 @@ def get_session() -> Generator[Session, Any, None]: except Exception as e: db.rollback() log.critical(f"error occurred: {e}") + print("OIDA OIDA OIDA OIDA OIDA", e) finally: db.close() diff --git a/backend/src/indexer/__init__.py b/backend/src/indexer/__init__.py index 20d84ae..0c102de 100644 --- a/backend/src/indexer/__init__.py +++ b/backend/src/indexer/__init__.py @@ -1,8 +1,8 @@ import logging from indexer.config import ProwlarrConfig -from indexer.generic import GenericIndexer, IndexerQueryResult -from indexer.prowlarr import Prowlarr +from indexer.indexers.generic import GenericIndexer, IndexerQueryResult +from indexer.indexers.prowlarr import Prowlarr log = logging.getLogger(__name__) diff --git a/backend/src/indexer/indexers/__init__.py b/backend/src/indexer/indexers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/indexer/generic.py b/backend/src/indexer/indexers/generic.py similarity index 100% rename from backend/src/indexer/generic.py rename to backend/src/indexer/indexers/generic.py diff --git a/backend/src/indexer/prowlarr.py b/backend/src/indexer/indexers/prowlarr.py similarity index 87% rename from backend/src/indexer/prowlarr.py rename to backend/src/indexer/indexers/prowlarr.py index 8c76a7b..021b060 100644 --- a/backend/src/indexer/prowlarr.py +++ b/backend/src/indexer/indexers/prowlarr.py @@ -2,8 +2,9 @@ import logging import requests -from indexer import GenericIndexer, IndexerQueryResult +from indexer import GenericIndexer from indexer.config import ProwlarrConfig +from indexer.schemas import IndexerQueryResult log = logging.getLogger(__name__) @@ -20,8 +21,10 @@ class Prowlarr(GenericIndexer): config = ProwlarrConfig() self.api_key = config.api_key self.url = config.url + log.debug("Registering Prowlarr as Indexer") def get_search_results(self, query: str) -> list[IndexerQueryResult]: + log.debug("Searching for " + query) url = self.url + '/api/v1/search' headers = { 'accept': 'application/json', @@ -48,5 +51,5 @@ class Prowlarr(GenericIndexer): ) return result_list else: - print(f'Error: {response.status_code}') + log.error(f'Prowlarr Error: {response.status_code}') return [] diff --git a/backend/src/indexer/models.py b/backend/src/indexer/models.py index 761fae9..7c52479 100644 --- a/backend/src/indexer/models.py +++ b/backend/src/indexer/models.py @@ -1,3 +1,5 @@ +from sqlalchemy import String, Integer +from sqlalchemy.dialects.postgresql import ARRAY from sqlalchemy.orm import Mapped, mapped_column from database import Base @@ -11,6 +13,6 @@ class IndexerQueryResult(Base): title: Mapped[str] download_url: Mapped[str] seeders: Mapped[int] - flags: Mapped[set[str]] - quality: Mapped[Quality | None] - season: Mapped[set[int]] + flags = mapped_column(ARRAY(String)) + quality: Mapped[Quality] + season = mapped_column(ARRAY(Integer)) diff --git a/backend/src/indexer/repository.py b/backend/src/indexer/repository.py index 3c9afdd..c377fb1 100644 --- a/backend/src/indexer/repository.py +++ b/backend/src/indexer/repository.py @@ -5,9 +5,9 @@ from indexer.schemas import IndexerQueryResultId, IndexerQueryResult as IndexerQ def get_result(result_id: IndexerQueryResultId, db: Session) -> IndexerQueryResultSchema: - return IndexerQueryResultSchema(**db.get(IndexerQueryResult, result_id).__dict__) + return IndexerQueryResultSchema.model_validate(db.get(IndexerQueryResult, result_id)) def save_result(result: IndexerQueryResultSchema, db: Session) -> IndexerQueryResultSchema: - db.add(IndexerQueryResult(**result.__dict__)) + db.add(IndexerQueryResult(**result.model_dump())) return result diff --git a/backend/src/indexer/schemas.py b/backend/src/indexer/schemas.py index c83e794..645422f 100644 --- a/backend/src/indexer/schemas.py +++ b/backend/src/indexer/schemas.py @@ -5,7 +5,7 @@ from uuid import UUID, uuid4 import pydantic from pydantic import BaseModel, computed_field -from torrent.models import Quality, Torrent +from torrent.models import Quality IndexerQueryResultId = typing.NewType('IndexerQueryResultId', UUID) @@ -16,22 +16,40 @@ class IndexerQueryResult(BaseModel): title: str download_url: str seeders: int - flags: set[str] - quality: Quality | None + flags: list[str] - @computed_field + @computed_field(return_type=Quality) @property - def season(self) -> set[int]: + def quality(self) -> Quality: + high_quality_pattern = r'\b(4k|4K)\b' + medium_quality_pattern = r'\b(1080p|1080P)\b' + low_quality_pattern = r'\b(720p|720P)\b' + very_low_quality_pattern = r'\b(480p|480P|360p|360P)\b' + + if re.search(high_quality_pattern, self.title): + return Quality.high + elif re.search(medium_quality_pattern, self.title): + return Quality.medium + elif re.search(low_quality_pattern, self.title): + return Quality.low + elif re.search(very_low_quality_pattern, self.title): + return Quality.very_low + + return Quality.unknown + + @computed_field(return_type=list[int]) + @property + def season(self) -> list[int]: pattern = r"\b[sS](\d+)\b" matches = re.findall(pattern, self.title, re.IGNORECASE) if matches.__len__() == 2: - result = set() + result = [] for i in range(int(matches[0]), int(matches[1]) + 1): - result.add(i) + result.append(i) elif matches.__len__() == 1: - result = {int(matches[0])} + result = [int(matches[0])] else: - result = {} + result = [] return result def __gt__(self, other) -> bool: @@ -44,15 +62,23 @@ class IndexerQueryResult(BaseModel): return self.quality.value < other.quality.value return self.seeders > other.seeders - def download(self) -> Torrent: - """ - downloads a torrent file and returns the filepath - """ - import requests - url = self.download_url - torrent_filepath = self.title + ".torrent" - with open(torrent_filepath, 'wb') as out_file: - content = requests.get(url).content - out_file.write(content) + # def download(self) -> Torrent: + # """ + # downloads a torrent file and returns the filepath + # """ + # import requests + # url = self.download_url + # torrent_filepath = self.title + ".torrent" + # with open(torrent_filepath, 'wb') as out_file: + # content = requests.get(url).content + # out_file.write(content) + # return Torrent(status=None, title=self.title, quality=self.quality, id=self.id) - return Torrent(status=None, title=self.title, quality=self.quality, id=self.id) + +class PublicIndexerQueryResult(BaseModel): + title: str + quality: Quality + id: IndexerQueryResultId + seeders: int + flags: list[str] + season: list[int] diff --git a/backend/src/torrent/models.py b/backend/src/torrent/models.py index fa239e1..36724d4 100644 --- a/backend/src/torrent/models.py +++ b/backend/src/torrent/models.py @@ -1,41 +1,16 @@ -import re from typing import Literal from uuid import UUID -from pydantic import computed_field from sqlalchemy.orm import Mapped, mapped_column from database import Base from torrent.schemas import Quality -class QualityMixin: - title: str - - @computed_field - @property - def quality(self) -> Quality: - high_quality_pattern = r'\b(4k|4K)\b' - medium_quality_pattern = r'\b(1080p|1080P)\b' - low_quality_pattern = r'\b(720p|720P)\b' - very_low_quality_pattern = r'\b(480p|480P|360p|360P)\b' - - if re.search(high_quality_pattern, self.title): - return Quality.high - elif re.search(medium_quality_pattern, self.title): - return Quality.medium - elif re.search(low_quality_pattern, self.title): - return Quality.low - elif re.search(very_low_quality_pattern, self.title): - return Quality.very_low - else: - return Quality.unknown - - class Torrent(Base): __tablename__ = "torrent" id: Mapped[UUID] = mapped_column(primary_key=True) status: Mapped[Literal["downloading", "finished", "error"] | None] title: Mapped[str] - quality: Mapped[Quality | None] + quality: Mapped[Quality] diff --git a/backend/src/tv/repository.py b/backend/src/tv/repository.py index b5cebf9..e0792f0 100644 --- a/backend/src/tv/repository.py +++ b/backend/src/tv/repository.py @@ -2,9 +2,8 @@ from sqlalchemy import select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session, joinedload -import database from tv.models import Season, Show, Episode, SeasonRequest -from tv.schemas import Episode as EpisodeSchema, Season as SeasonSchema, SeasonId, Show as ShowSchema, ShowId, \ +from tv.schemas import Season as SeasonSchema, SeasonId, Show as ShowSchema, ShowId, \ SeasonRequest as SeasonRequestSchema @@ -28,10 +27,7 @@ def get_show(show_id: ShowId, db: Session) -> ShowSchema | None: if not result: return None - return ShowSchema( - **result.__dict__ - ) - + return ShowSchema.model_validate(result) def get_show_by_external_id(external_id: int, db: Session, metadata_provider: str) -> ShowSchema | None: """ @@ -71,19 +67,7 @@ def get_shows(db: Session) -> list[ShowSchema]: results = db.execute(stmt).scalars().all() - return [ - ShowSchema( - **show.__dict__, - seasons=[ - SeasonSchema( - **season.__dict__, - episodes=[EpisodeSchema(**episode.__dict__) for episode in season.episodes] - ) - for season in show.seasons - ] - ) - for show in results - ] + return [ShowSchema.model_validate(show) for show in results] def save_show(show: ShowSchema, db: Session) -> ShowSchema: @@ -127,15 +111,7 @@ def save_show(show: ShowSchema, db: Session) -> ShowSchema: try: db.commit() db.refresh(db_show) - return ShowSchema( - **db_show.__dict__, - seasons=[ - SeasonSchema( - **season.__dict__, - episodes=[EpisodeSchema(**episode.__dict__) for episode in season.episodes] - ) for season in db_show.seasons - ] - ) + return ShowSchema.model_validate(db_show) except IntegrityError: db.rollback() raise ValueError("Show with this primary key already exists.") @@ -161,7 +137,7 @@ def get_season(season_id: SeasonId, db: Session) -> SeasonSchema: :param db: The database session. :return: a Season object. """ - return SeasonSchema(**db.get(Season(), season_id).__dict__) + return SeasonSchema.model_validate(db.get(Season(), season_id)) def add_season_to_requested_list(season_request: SeasonRequestSchema, db: Session) -> None: @@ -169,7 +145,7 @@ def add_season_to_requested_list(season_request: SeasonRequestSchema, db: Sessio Adds a Season to the SeasonRequest table, which marks it as requested. """ - db.add(SeasonRequest(**season_request.__dict__)) + db.add(SeasonRequest(**season_request.model_dump())) db.commit() @@ -178,19 +154,11 @@ def remove_season_from_requested_list(season_request: SeasonRequestSchema, db: S Removes a Season from the SeasonRequest table, which removes it from the 'requested' list. """ - db.delete(SeasonRequest(**season_request.__dict__)) + db.delete(SeasonRequest(**season_request.model_dump())) db.commit() def get_season_requests(db: Session) -> list[SeasonRequestSchema]: stmt = select(SeasonRequest) result = db.execute(stmt).scalars().all() - return [SeasonRequestSchema(**season.__dict__) for season in result] - - -if __name__ == "__main__": - database.init_db() - session = database.SessionLocal() - print(get_show(show_id=ShowId('2fc11032-d614-4651-877d-c8b11aa63aef'), db=session)) - print(get_shows(db=session)) - print(delete_show(show_id=ShowId('2fc11032-d614-4651-877d-c8b11aa63aef'), db=session)) + return [SeasonRequestSchema.model_validate(season) for season in result] diff --git a/backend/src/tv/router.py b/backend/src/tv/router.py index 21db283..8c42ab7 100644 --- a/backend/src/tv/router.py +++ b/backend/src/tv/router.py @@ -5,6 +5,7 @@ import metadataProvider import tv.service from auth.users import current_active_user from database import DbSessionDependency +from indexer.schemas import PublicIndexerQueryResult from tv.exceptions import MediaAlreadyExists from tv.schemas import Show, SeasonRequest, ShowId @@ -20,7 +21,7 @@ router = APIRouter() status.HTTP_201_CREATED: {"model": Show, "description": "Successfully created show"}, status.HTTP_409_CONFLICT: {"model": str, "description": "Show already exists"}, }) -def add_show(db: DbSessionDependency, show_id: int, metadata_provider: str = "tmdb"): +def add_a_show(db: DbSessionDependency, show_id: int, metadata_provider: str = "tmdb"): try: show = tv.service.add_show(db=db, external_id=show_id, metadata_provider=metadata_provider, ) except MediaAlreadyExists as e: @@ -29,7 +30,7 @@ def add_show(db: DbSessionDependency, show_id: int, metadata_provider: str = "tm @router.delete("/shows/{show_id}", status_code=status.HTTP_200_OK, dependencies=[Depends(current_active_user)]) -def delete_show(db: DbSessionDependency, show_id: ShowId): +def delete_a_show(db: DbSessionDependency, show_id: ShowId): db.delete(db.get(Show, show_id)) db.commit() @@ -39,13 +40,13 @@ def delete_show(db: DbSessionDependency, show_id: ShowId): # -------------------------------- @router.get("/shows", dependencies=[Depends(current_active_user)], response_model=list[Show]) -def get_shows(db: DbSessionDependency): +def get_all_shows(db: DbSessionDependency): """""" return tv.service.get_all_shows(db=db) @router.get("/shows/{show_id}", dependencies=[Depends(current_active_user)], response_model=Show) -def get_show(db: DbSessionDependency, show_id: ShowId): +def get_a_show(db: DbSessionDependency, show_id: ShowId): """ :param show_id: @@ -58,30 +59,16 @@ def get_show(db: DbSessionDependency, show_id: ShowId): # -------------------------------- -# CREATE AND DELETE SHOW REQUESTS +# MANAGE REQUESTS # -------------------------------- -@router.post("/season", status_code=status.HTTP_200_OK, dependencies=[Depends(current_active_user)]) -def add_season(db: DbSessionDependency, season_request: SeasonRequest): +@router.post("/season/request", status_code=status.HTTP_200_OK, dependencies=[Depends(current_active_user)]) +def request_a_season(db: DbSessionDependency, season_request: SeasonRequest): """ adds request flag to a season """ tv.service.request_season(db=db, season_request=season_request) - -@router.delete("/season", status_code=status.HTTP_200_OK, dependencies=[Depends(current_active_user)], - response_model=Show) -def delete_season(db: DbSessionDependency, season_request: SeasonRequest): - """ - removes request for a season - """ - tv.service.unrequest_season(db=db, season_request=season_request) - - -# -------------------------------- -# MANAGE REQUESTS -# -------------------------------- - @router.get("/season/request", status_code=status.HTTP_200_OK, dependencies=[Depends(current_active_user)]) def get_requested_seasons(db: DbSessionDependency) -> list[SeasonRequest]: return tv.service.get_all_requested_seasons(db=db) @@ -97,17 +84,18 @@ def unrequest_season(db: DbSessionDependency, request: SeasonRequest): # -------------------------------- # 1 is the default for season_number because it returns multi season torrents -@router.get("/torrents/{show_id}", status_code=status.HTTP_200_OK, dependencies=[Depends(current_active_user)]) -def get_torrents(db: DbSessionDependency, show_id: ShowId, season_number: int = 1): +@router.get("/torrents", status_code=status.HTTP_200_OK, dependencies=[Depends(current_active_user)], + response_model=list[PublicIndexerQueryResult]) +def get_torrents_for_a_season(db: DbSessionDependency, show_id: ShowId, season_number: int = 1): return tv.service.get_all_available_torrents_for_a_season(db=db, season_number=season_number, show_id=show_id) - # download a torrent -@router.post("/torrents/") +# @router.post("/torrents") + # -------------------------------- # SEARCH SHOWS ON METADATA PROVIDERS # -------------------------------- @router.get("/search", dependencies=[Depends(current_active_user)]) -def search_show(query: str, metadata_provider: str = "tmdb"): +def search_metadata_providers_for_a_show(query: str, metadata_provider: str = "tmdb"): return metadataProvider.search_show(query, metadata_provider) diff --git a/backend/src/tv/schemas.py b/backend/src/tv/schemas.py index 13de1ca..5658387 100644 --- a/backend/src/tv/schemas.py +++ b/backend/src/tv/schemas.py @@ -13,7 +13,6 @@ EpisodeId = typing.NewType("EpisodeId", UUID) SeasonNumber = typing.NewType("SeasonNumber", int) EpisodeNumber = typing.NewType("EpisodeNumber", int) - class Episode(BaseModel): model_config = ConfigDict(from_attributes=True) diff --git a/backend/src/tv/service.py b/backend/src/tv/service.py index 137dc62..d28498a 100644 --- a/backend/src/tv/service.py +++ b/backend/src/tv/service.py @@ -1,13 +1,11 @@ from sqlalchemy.orm import Session -import database -import indexer -# import indexer +import indexer.service import metadataProvider import tv.repository from indexer import IndexerQueryResult +from tv import log from tv.exceptions import MediaAlreadyExists -from tv.repository import get_show_by_external_id from tv.schemas import Show, ShowId, SeasonRequest @@ -48,12 +46,14 @@ def check_if_show_exists(db: Session, def get_all_available_torrents_for_a_season(db: Session, season_number: int, show_id: ShowId) -> list[ IndexerQueryResult]: + log.debug(f"getting all available torrents for season {season_number} for show {show_id}") show = tv.repository.get_show(show_id=show_id, db=db) - torrents: list[IndexerQueryResult] = indexer.search(show.name + " S" + str(season_number)) - result = [] + torrents: list[IndexerQueryResult] = indexer.service.search(query=show.name + " S" + str(season_number), db=db) + result: list[IndexerQueryResult] = [] for torrent in torrents: - if season.number in torrent.season: + if season_number in torrent.season: result.append(torrent) + result.sort() return result @@ -68,21 +68,3 @@ def get_show_by_id(db: Session, show_id: ShowId) -> Show | None: def get_all_requested_seasons(db: Session) -> list[SeasonRequest]: return tv.repository.get_season_requests(db=db) - -if __name__ == "__main__": - session = database.SessionLocal() - - try: - show = add_show(db=session, external_id=1418, metadata_provider="tmdb") - except MediaAlreadyExists as e: - print(e) - show = get_show_by_external_id(db=session, external_id=1418, metadata_provider="tmdb") - - print(show) - print(show.name) - for season in show.seasons: - print(season) - # print(season.number) - for episode in season.episodes: - print(episode) - # print(episode.title)