mirror of
https://github.com/maxdorninger/MediaManager.git
synced 2026-04-27 19:25:40 +02:00
small fix
This commit is contained in:
@@ -3,16 +3,11 @@ from collections.abc import AsyncGenerator
|
||||
from fastapi import Depends
|
||||
from fastapi_users.db import SQLAlchemyBaseUserTableUUID, SQLAlchemyUserDatabase
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
import database
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class User(SQLAlchemyBaseUserTableUUID, Base):
|
||||
class User(SQLAlchemyBaseUserTableUUID, database.Base):
|
||||
pass
|
||||
|
||||
|
||||
@@ -20,11 +15,6 @@ engine = create_async_engine(database.db_url, echo=False)
|
||||
async_session_maker = async_sessionmaker(engine, expire_on_commit=False)
|
||||
|
||||
|
||||
async def create_db_and_tables():
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
|
||||
async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
|
||||
async with async_session_maker() as session:
|
||||
yield session
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import logging
|
||||
from contextvars import ContextVar
|
||||
from typing import Annotated, Any, Generator
|
||||
|
||||
from fastapi import Depends
|
||||
from sqlmodel import SQLModel, Session, create_engine
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session, declarative_base, sessionmaker
|
||||
|
||||
from database.config import DbConfig
|
||||
|
||||
@@ -13,15 +15,28 @@ db_url = "postgresql+psycopg" + "://" + config.USER + ":" + config.PASSWORD + "@
|
||||
config.PORT) + "/" + config.DBNAME
|
||||
|
||||
engine = create_engine(db_url, echo=False)
|
||||
Base = declarative_base()
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
|
||||
|
||||
def init_db() -> None:
|
||||
SQLModel.metadata.create_all(engine)
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
|
||||
def get_session() -> Generator[Session, Any, None]:
|
||||
with Session(engine) as session:
|
||||
yield session
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
log.critical(f"error occurred: {e}")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
db_session: ContextVar[Session] = ContextVar('db_session')
|
||||
|
||||
|
||||
DbSessionDependency = Annotated[Session, Depends(get_session)]
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import uuid
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import ForeignKey, ForeignKeyConstraint, Integer, String, UniqueConstraint
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class Show(Base):
|
||||
__tablename__ = "show"
|
||||
__table_args__ = (UniqueConstraint("external_id", "metadata_provider", "version"),)
|
||||
|
||||
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
||||
external_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
metadata_provider: Mapped[str] = mapped_column(String, nullable=False)
|
||||
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||
overview: Mapped[str] = mapped_column(String, nullable=False)
|
||||
year: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
version: Mapped[str] = mapped_column(String, default="")
|
||||
|
||||
seasons: Mapped[list["Season"]] = relationship(back_populates="show", cascade="all, delete")
|
||||
|
||||
|
||||
class Season(Base):
|
||||
__tablename__ = "season"
|
||||
__table_args__ = (UniqueConstraint("show_id", "number"),)
|
||||
|
||||
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
||||
show_id: Mapped[UUID] = mapped_column(ForeignKey("show.id", ondelete="CASCADE"), nullable=False)
|
||||
number: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
external_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||
overview: Mapped[str] = mapped_column(String, nullable=False)
|
||||
|
||||
show: Mapped[Show] = relationship(back_populates="seasons")
|
||||
episodes: Mapped[list["Episode"]] = relationship(back_populates="season", cascade="all, delete")
|
||||
|
||||
|
||||
class Episode(Base):
|
||||
__tablename__ = "episode"
|
||||
__table_args__ = (
|
||||
ForeignKeyConstraint(["show_id", "season_number"], ["season.show_id", "season.number"], ondelete="CASCADE"),
|
||||
)
|
||||
|
||||
show_id: Mapped[UUID] = mapped_column(ForeignKey("show.id"), primary_key=True)
|
||||
season_number: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
number: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
external_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
title: Mapped[str] = mapped_column(String, nullable=False)
|
||||
|
||||
season: Mapped[Season] = relationship(back_populates="episodes")
|
||||
@@ -1,3 +0,0 @@
|
||||
|
||||
|
||||
|
||||
@@ -1,26 +1,11 @@
|
||||
import logging
|
||||
|
||||
from database.tv import Season
|
||||
from indexer.config import ProwlarrConfig
|
||||
from indexer.generic import GenericIndexer, IndexerQueryResult
|
||||
from indexer.prowlarr import Prowlarr
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def search(query: str | Season) -> list[IndexerQueryResult]:
|
||||
results = []
|
||||
|
||||
if isinstance(query, Season):
|
||||
query = query.show.name + " s" + query.number.__str__()
|
||||
log.debug(f"Searching for Season {query}")
|
||||
|
||||
for indexer in indexers:
|
||||
results.extend(indexer.get_search_results(query))
|
||||
|
||||
return results
|
||||
|
||||
|
||||
indexers: list[GenericIndexer] = []
|
||||
|
||||
if ProwlarrConfig().enabled:
|
||||
|
||||
@@ -1,57 +1,4 @@
|
||||
import re
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import pydantic
|
||||
from pydantic import BaseModel, computed_field
|
||||
|
||||
from database.torrents import QualityMixin, Torrent
|
||||
|
||||
|
||||
# TODO: use something like strategy pattern to make sorting more user customizable
|
||||
class IndexerQueryResult(BaseModel, QualityMixin):
|
||||
id: UUID = pydantic.Field(default_factory=uuid4)
|
||||
title: str
|
||||
_download_url: str
|
||||
seeders: int
|
||||
flags: set[str]
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def season(self) -> set[int]:
|
||||
pattern = r"\b[sS](\d+)\b"
|
||||
matches = re.findall(pattern, self.title, re.IGNORECASE)
|
||||
if matches.__len__() == 2:
|
||||
result = set()
|
||||
for i in range(int(matches[0]), int(matches[1]) + 1):
|
||||
result.add(i)
|
||||
elif matches.__len__() == 1:
|
||||
result = {int(matches[0])}
|
||||
else:
|
||||
result = {}
|
||||
return result
|
||||
|
||||
def __gt__(self, other) -> bool:
|
||||
if self.quality.value != other.quality.value:
|
||||
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
|
||||
|
||||
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(torrent_title=self.title)
|
||||
from indexer.schemas import IndexerQueryResult
|
||||
|
||||
|
||||
class GenericIndexer(object):
|
||||
|
||||
16
backend/src/indexer/models.py
Normal file
16
backend/src/indexer/models.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from database import Base
|
||||
from indexer.schemas import IndexerQueryResultId
|
||||
from torrent.schemas import Quality
|
||||
|
||||
|
||||
class IndexerQueryResult(Base):
|
||||
__tablename__ = 'indexer_query_result'
|
||||
id: Mapped[IndexerQueryResultId] = mapped_column(primary_key=True)
|
||||
title: Mapped[str]
|
||||
download_url: Mapped[str]
|
||||
seeders: Mapped[int]
|
||||
flags: Mapped[set[str]]
|
||||
quality: Mapped[Quality | None]
|
||||
season: Mapped[set[int]]
|
||||
@@ -40,7 +40,7 @@ class Prowlarr(GenericIndexer):
|
||||
log.debug("torrent result: " + result.__str__())
|
||||
result_list.append(
|
||||
IndexerQueryResult(
|
||||
_download_url=result['downloadUrl'],
|
||||
download_url=result['downloadUrl'],
|
||||
title=result['sortTitle'],
|
||||
seeders=result['seeders'],
|
||||
flags=result['indexerFlags'],
|
||||
|
||||
13
backend/src/indexer/repository.py
Normal file
13
backend/src/indexer/repository.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from indexer.models import IndexerQueryResult
|
||||
from indexer.schemas import IndexerQueryResultId, IndexerQueryResult as IndexerQueryResultSchema
|
||||
|
||||
|
||||
def get_result(result_id: IndexerQueryResultId, db: Session) -> IndexerQueryResultSchema:
|
||||
return IndexerQueryResultSchema(**db.get(IndexerQueryResult, result_id).__dict__)
|
||||
|
||||
|
||||
def save_result(result: IndexerQueryResultSchema, db: Session) -> IndexerQueryResultSchema:
|
||||
db.add(IndexerQueryResult(**result.__dict__))
|
||||
return result
|
||||
58
backend/src/indexer/schemas.py
Normal file
58
backend/src/indexer/schemas.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import re
|
||||
import typing
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import pydantic
|
||||
from pydantic import BaseModel, computed_field
|
||||
|
||||
from torrent.models import Quality, Torrent
|
||||
|
||||
IndexerQueryResultId = typing.NewType('IndexerQueryResultId', UUID)
|
||||
|
||||
|
||||
# TODO: use something like strategy pattern to make sorting more user customizable
|
||||
class IndexerQueryResult(BaseModel):
|
||||
id: IndexerQueryResultId = pydantic.Field(default_factory=uuid4)
|
||||
title: str
|
||||
download_url: str
|
||||
seeders: int
|
||||
flags: set[str]
|
||||
quality: Quality | None
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def season(self) -> set[int]:
|
||||
pattern = r"\b[sS](\d+)\b"
|
||||
matches = re.findall(pattern, self.title, re.IGNORECASE)
|
||||
if matches.__len__() == 2:
|
||||
result = set()
|
||||
for i in range(int(matches[0]), int(matches[1]) + 1):
|
||||
result.add(i)
|
||||
elif matches.__len__() == 1:
|
||||
result = {int(matches[0])}
|
||||
else:
|
||||
result = {}
|
||||
return result
|
||||
|
||||
def __gt__(self, other) -> bool:
|
||||
if self.quality.value != other.quality.value:
|
||||
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
|
||||
|
||||
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)
|
||||
21
backend/src/indexer/service.py
Normal file
21
backend/src/indexer/service.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from indexer import IndexerQueryResult, log, indexers
|
||||
from indexer.repository import save_result
|
||||
from indexer.schemas import IndexerQueryResultId
|
||||
|
||||
|
||||
def search(query: str, db: Session) -> list[IndexerQueryResult]:
|
||||
results = []
|
||||
|
||||
log.debug(f"Searching for Torrent: {query}")
|
||||
|
||||
for indexer in indexers:
|
||||
results.extend(indexer.get_search_results(query))
|
||||
for result in results:
|
||||
save_result(result=result, db=db)
|
||||
return results
|
||||
|
||||
|
||||
def get_indexer_query_result(result_id: IndexerQueryResultId, db: Session) -> IndexerQueryResult:
|
||||
return get_indexer_query_result(result_id=result_id, db=db)
|
||||
@@ -1,10 +1,8 @@
|
||||
import logging
|
||||
import sys
|
||||
from contextlib import asynccontextmanager
|
||||
from logging.config import dictConfig
|
||||
|
||||
import database
|
||||
from auth.db import create_db_and_tables
|
||||
from auth.schemas import UserCreate, UserRead, UserUpdate
|
||||
from auth.users import bearer_auth_backend, fastapi_users
|
||||
|
||||
@@ -45,16 +43,9 @@ LOGGING_CONFIG = {
|
||||
# Apply logging config
|
||||
dictConfig(LOGGING_CONFIG)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Not needed if you setup a migration system like Alembic
|
||||
await create_db_and_tables()
|
||||
yield
|
||||
|
||||
|
||||
database.init_db()
|
||||
app = FastAPI(root_path="/api/v1", lifespan=lifespan)
|
||||
|
||||
app = FastAPI(root_path="/api/v1")
|
||||
app.include_router(
|
||||
fastapi_users.get_auth_router(bearer_auth_backend),
|
||||
prefix="/auth/jwt",
|
||||
@@ -88,7 +79,9 @@ app.include_router(
|
||||
|
||||
app.include_router(
|
||||
tv.router.router,
|
||||
tags=["tv"])
|
||||
prefix="/tv",
|
||||
tags=["tv"]
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(app, host="127.0.0.1", port=5049, log_config=LOGGING_CONFIG)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import logging
|
||||
|
||||
import metadataProvider.tmdb
|
||||
from database.tv import Show
|
||||
from metadataProvider.abstractMetaDataProvider import metadata_providers
|
||||
from tv.schemas import Show
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
import config
|
||||
from database.tv import Show
|
||||
from tv.schemas import Show
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ import tmdbsimple
|
||||
from pydantic_settings import BaseSettings
|
||||
from tmdbsimple import TV, TV_Seasons
|
||||
|
||||
from database.tv import Episode, Season, Show
|
||||
from metadataProvider.abstractMetaDataProvider import AbstractMetadataProvider, register_metadata_provider
|
||||
from tv.schemas import Episode, Season, Show
|
||||
|
||||
|
||||
class TmdbConfig(BaseSettings):
|
||||
@@ -51,7 +51,8 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
|
||||
name=season_metadata["name"],
|
||||
overview=season_metadata["overview"],
|
||||
number=int(season_metadata["season_number"]),
|
||||
episodes=episode_list
|
||||
episodes=episode_list,
|
||||
|
||||
)
|
||||
)
|
||||
|
||||
@@ -76,7 +77,7 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
|
||||
content_type = res.headers["content-type"]
|
||||
file_extension = mimetypes.guess_extension(content_type)
|
||||
if res.status_code == 200:
|
||||
with open(f"{self.storage_path}/images/{show.id}{file_extension}", 'wb') as f:
|
||||
with open(f"{self.storage_path}/{show.id}{file_extension}", 'wb') as f:
|
||||
f.write(res.content)
|
||||
log.info(f"image for show {show.name} successfully downloaded")
|
||||
|
||||
|
||||
0
backend/src/torrent/__init__.py
Normal file
0
backend/src/torrent/__init__.py
Normal file
@@ -1,19 +1,12 @@
|
||||
import re
|
||||
from enum import Enum
|
||||
from typing import Literal
|
||||
from uuid import UUID, uuid4
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import computed_field
|
||||
from sqlalchemy import Column, String
|
||||
from sqlmodel import Field, SQLModel
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
|
||||
class Quality(Enum):
|
||||
high = 1
|
||||
medium = 2
|
||||
low = 3
|
||||
very_low = 4
|
||||
unknown = 5
|
||||
from database import Base
|
||||
from torrent.schemas import Quality
|
||||
|
||||
|
||||
class QualityMixin:
|
||||
@@ -38,17 +31,11 @@ class QualityMixin:
|
||||
else:
|
||||
return Quality.unknown
|
||||
|
||||
class TorrentMixin:
|
||||
torrent_id: UUID | None = Field(default=None, foreign_key="torrent.id")
|
||||
|
||||
class Torrent(Base):
|
||||
__tablename__ = "torrent"
|
||||
|
||||
class Torrent(SQLModel, QualityMixin, table=True):
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
torrent_status: Literal["downloading", "finished", "error"] | None = Field(default=None,
|
||||
sa_column=Column(String))
|
||||
torrent_title: str = Field(default=None)
|
||||
|
||||
@property
|
||||
@computed_field
|
||||
def torrent_filepath(self) -> str:
|
||||
return f"{self.id}.torrent"
|
||||
id: Mapped[UUID] = mapped_column(primary_key=True)
|
||||
status: Mapped[Literal["downloading", "finished", "error"] | None]
|
||||
title: Mapped[str]
|
||||
quality: Mapped[Quality | None]
|
||||
9
backend/src/torrent/schemas.py
Normal file
9
backend/src/torrent/schemas.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class Quality(Enum):
|
||||
high = 1
|
||||
medium = 2
|
||||
low = 3
|
||||
very_low = 4
|
||||
unknown = 5
|
||||
6
backend/src/tv/exceptions.py
Normal file
6
backend/src/tv/exceptions.py
Normal file
@@ -0,0 +1,6 @@
|
||||
class MediaAlreadyExists(ValueError):
|
||||
'''Raised when a show already exists'''
|
||||
|
||||
|
||||
class MediaDoesNotExist(ValueError):
|
||||
'''Raised when a does not show exist'''
|
||||
@@ -1,25 +1,22 @@
|
||||
import uuid
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import ForeignKey, ForeignKeyConstraint, Integer, String, UniqueConstraint
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||
from sqlalchemy import ForeignKey, PrimaryKeyConstraint, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
from database import Base
|
||||
from torrent.models import Quality
|
||||
|
||||
|
||||
class Show(Base):
|
||||
__tablename__ = "show"
|
||||
__table_args__ = (UniqueConstraint("external_id", "metadata_provider", "version"),)
|
||||
__table_args__ = (UniqueConstraint("external_id", "metadata_provider"),)
|
||||
|
||||
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
||||
external_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
metadata_provider: Mapped[str] = mapped_column(String, nullable=False)
|
||||
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||
overview: Mapped[str] = mapped_column(String, nullable=False)
|
||||
year: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
version: Mapped[str] = mapped_column(String, default="")
|
||||
id: Mapped[UUID] = mapped_column(primary_key=True)
|
||||
external_id: Mapped[int]
|
||||
metadata_provider: Mapped[str]
|
||||
name: Mapped[str]
|
||||
overview: Mapped[str]
|
||||
year: Mapped[int | None]
|
||||
|
||||
seasons: Mapped[list["Season"]] = relationship(back_populates="show", cascade="all, delete")
|
||||
|
||||
@@ -28,30 +25,47 @@ class Season(Base):
|
||||
__tablename__ = "season"
|
||||
__table_args__ = (UniqueConstraint("show_id", "number"),)
|
||||
|
||||
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
||||
show_id: Mapped[UUID] = mapped_column(ForeignKey(column="show.id", ondelete="CASCADE"), nullable=False)
|
||||
number: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
external_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||
overview: Mapped[str] = mapped_column(String, nullable=False)
|
||||
torrent_id: Mapped[UUID] = mapped_column(ForeignKey(column="torrent.id"), nullable=False)
|
||||
id: Mapped[UUID] = mapped_column(primary_key=True)
|
||||
show_id: Mapped[UUID] = mapped_column(ForeignKey(column="show.id", ondelete="CASCADE"), )
|
||||
number: Mapped[int]
|
||||
external_id: Mapped[int]
|
||||
name: Mapped[str]
|
||||
overview: Mapped[str]
|
||||
|
||||
show: Mapped[Show] = relationship(back_populates="seasons")
|
||||
show: Mapped["Show"] = relationship(back_populates="seasons")
|
||||
episodes: Mapped[list["Episode"]] = relationship(back_populates="season", cascade="all, delete")
|
||||
|
||||
|
||||
class Episode(Base):
|
||||
__tablename__ = "episode"
|
||||
__table_args__ = (
|
||||
ForeignKeyConstraint(columns=["show_id", "season_number"],
|
||||
refcolumns=["season.show_id", "season.number"],
|
||||
ondelete="CASCADE"),
|
||||
UniqueConstraint("season_id", "number"),
|
||||
)
|
||||
id: Mapped[UUID] = mapped_column(primary_key=True)
|
||||
season_id: Mapped[UUID] = mapped_column(ForeignKey("season.id", ondelete="CASCADE"), )
|
||||
number: Mapped[int]
|
||||
external_id: Mapped[int]
|
||||
title: Mapped[str]
|
||||
|
||||
show_id: Mapped[UUID] = mapped_column(ForeignKey("show.id"), primary_key=True)
|
||||
season_number: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
number: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
external_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
title: Mapped[str] = mapped_column(String, nullable=False)
|
||||
season: Mapped["Season"] = relationship(back_populates="episodes")
|
||||
|
||||
season: Mapped[Season] = relationship(back_populates="episodes")
|
||||
|
||||
class SeasonFile(Base):
|
||||
__tablename__ = "season_file"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("season_id", "file_path"),
|
||||
)
|
||||
season_id: Mapped[UUID] = mapped_column(ForeignKey(column="season.id", ondelete="CASCADE"), )
|
||||
torrent_id: Mapped[UUID | None] = mapped_column(ForeignKey(column="torrent.id", ondelete="SET NULL"), )
|
||||
file_path: Mapped[str]
|
||||
quality: Mapped[Quality]
|
||||
|
||||
|
||||
class SeasonRequest(Base):
|
||||
__tablename__ = "season_torrent_relation"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("season_id", "wanted_quality"),
|
||||
)
|
||||
season_id: Mapped[UUID] = mapped_column(ForeignKey(column="season.id", ondelete="CASCADE"), )
|
||||
wanted_quality: Mapped[Quality]
|
||||
min_quality: Mapped[Quality]
|
||||
|
||||
196
backend/src/tv/repository.py
Normal file
196
backend/src/tv/repository.py
Normal file
@@ -0,0 +1,196 @@
|
||||
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, \
|
||||
SeasonRequest as SeasonRequestSchema
|
||||
|
||||
|
||||
def get_show(show_id: ShowId, db: Session) -> ShowSchema | None:
|
||||
"""
|
||||
Retrieve a show by its ID, including seasons and episodes.
|
||||
|
||||
:param show_id: The ID of the show to retrieve.
|
||||
:param db: The database session.
|
||||
:return: A ShowSchema object if found, otherwise None.
|
||||
"""
|
||||
stmt = (
|
||||
select(Show)
|
||||
.where(Show.id == show_id)
|
||||
.options(
|
||||
joinedload(Show.seasons).joinedload(Season.episodes) # Load relationships
|
||||
)
|
||||
)
|
||||
|
||||
result = db.execute(stmt).unique().scalar_one_or_none()
|
||||
if not result:
|
||||
return None
|
||||
|
||||
return ShowSchema(
|
||||
**result.__dict__
|
||||
)
|
||||
|
||||
|
||||
def get_show_by_external_id(external_id: int, db: Session, metadata_provider: str) -> ShowSchema | None:
|
||||
"""
|
||||
Retrieve a show by its external ID, including nested seasons and episodes.
|
||||
|
||||
:param external_id: The ID of the show to retrieve.
|
||||
:param metadata_provider: The metadata provider associated with the ID.
|
||||
:param db: The database session.
|
||||
:return: A ShowSchema object if found, otherwise None.
|
||||
"""
|
||||
stmt = (
|
||||
select(Show)
|
||||
.where(Show.external_id == external_id)
|
||||
.where(Show.metadata_provider == metadata_provider)
|
||||
.options(
|
||||
joinedload(Show.seasons).joinedload(Season.episodes) # Load relationships
|
||||
)
|
||||
)
|
||||
|
||||
result = db.execute(stmt).unique().scalar_one_or_none()
|
||||
if not result:
|
||||
return None
|
||||
|
||||
return ShowSchema(
|
||||
**result.__dict__
|
||||
)
|
||||
|
||||
|
||||
def get_shows(db: Session) -> list[ShowSchema]:
|
||||
"""
|
||||
Retrieve all shows from the database, including nested seasons and episodes.
|
||||
|
||||
:param db: The database session.
|
||||
:return: A list of ShowSchema objects.
|
||||
"""
|
||||
stmt = select(Show)
|
||||
|
||||
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
|
||||
]
|
||||
|
||||
|
||||
def save_show(show: ShowSchema, db: Session) -> ShowSchema:
|
||||
"""
|
||||
Save a new show to the database, including its seasons and episodes.
|
||||
|
||||
:param show: The ShowSchema object to save.
|
||||
:param db: The database session.
|
||||
:return: The saved ShowSchema object.
|
||||
:raises ValueError: If a show with the same primary key already exists.
|
||||
"""
|
||||
db_show = Show(
|
||||
id=show.id,
|
||||
external_id=show.external_id,
|
||||
metadata_provider=show.metadata_provider,
|
||||
name=show.name,
|
||||
overview=show.overview,
|
||||
year=show.year,
|
||||
seasons=[
|
||||
Season(
|
||||
id=season.id,
|
||||
show_id=show.id, # Correctly linking to the parent show
|
||||
number=season.number,
|
||||
external_id=season.external_id,
|
||||
name=season.name,
|
||||
overview=season.overview,
|
||||
episodes=[
|
||||
Episode(
|
||||
id=episode.id,
|
||||
season_id=season.id, # Correctly linking to the parent season
|
||||
number=episode.number,
|
||||
external_id=episode.external_id,
|
||||
title=episode.title
|
||||
) for episode in season.episodes # Convert episodes properly
|
||||
]
|
||||
) for season in show.seasons # Convert seasons properly
|
||||
]
|
||||
)
|
||||
|
||||
db.add(db_show)
|
||||
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
|
||||
]
|
||||
)
|
||||
except IntegrityError:
|
||||
db.rollback()
|
||||
raise ValueError("Show with this primary key already exists.")
|
||||
|
||||
|
||||
def delete_show(show_id: ShowId, db: Session) -> None:
|
||||
"""
|
||||
Delete a show by its ID.
|
||||
|
||||
:param show_id: The ID of the show to delete.
|
||||
:param db: The database session.
|
||||
:return: The deleted ShowSchema object if found, otherwise None.
|
||||
"""
|
||||
show = db.get(Show, show_id)
|
||||
db.delete(show)
|
||||
db.commit()
|
||||
|
||||
|
||||
def get_season(season_id: SeasonId, db: Session) -> SeasonSchema:
|
||||
"""
|
||||
|
||||
:param season_id: The ID of the season to get.
|
||||
:param db: The database session.
|
||||
:return: a Season object.
|
||||
"""
|
||||
return SeasonSchema(**db.get(Season(), season_id).__dict__)
|
||||
|
||||
|
||||
def add_season_to_requested_list(season_request: SeasonRequestSchema, db: Session) -> None:
|
||||
"""
|
||||
Adds a Season to the SeasonRequest table, which marks it as requested.
|
||||
|
||||
"""
|
||||
db.add(SeasonRequest(**season_request.__dict__))
|
||||
db.commit()
|
||||
|
||||
|
||||
def remove_season_from_requested_list(season_request: SeasonRequestSchema, db: Session) -> None:
|
||||
"""
|
||||
Removes a Season from the SeasonRequest table, which removes it from the 'requested' list.
|
||||
|
||||
"""
|
||||
db.delete(SeasonRequest(**season_request.__dict__))
|
||||
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))
|
||||
@@ -1,197 +1,51 @@
|
||||
import json
|
||||
import pprint
|
||||
from uuid import UUID
|
||||
|
||||
import psycopg.errors
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
from sqlmodel import select
|
||||
|
||||
import dowloadClients
|
||||
import indexer
|
||||
import metadataProvider
|
||||
import tv.service
|
||||
from auth.users import current_active_user
|
||||
from database import DbSessionDependency
|
||||
from database.torrents import Torrent
|
||||
from database.tv import Season, Show
|
||||
from indexer import IndexerQueryResult
|
||||
from tv import log
|
||||
from tv.exceptions import MediaAlreadyExists
|
||||
from tv.schemas import Show, SeasonRequest, ShowId
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/tv",
|
||||
)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class ShowDetails(BaseModel):
|
||||
show: Show
|
||||
seasons: list[Season]
|
||||
# --------------------------------
|
||||
# CREATE AND DELETE SHOWS
|
||||
# --------------------------------
|
||||
|
||||
|
||||
@router.post("/show", status_code=status.HTTP_201_CREATED, dependencies=[Depends(current_active_user)],
|
||||
@router.post("/shows", status_code=status.HTTP_201_CREATED, dependencies=[Depends(current_active_user)],
|
||||
responses={
|
||||
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", version: str = ""):
|
||||
res = db.exec(select(Show).
|
||||
where(Show.external_id == show_id).
|
||||
where(Show.metadata_provider == metadata_provider).
|
||||
where(Show.version == version)).first()
|
||||
|
||||
if res is not None:
|
||||
return JSONResponse(status_code=status.HTTP_409_CONFLICT, content={"message": "Show already exists"})
|
||||
|
||||
show = metadataProvider.get_show_metadata(id=show_id, provider=metadata_provider)
|
||||
show.version = version
|
||||
log.info("Adding show: " + json.dumps(show.model_dump(), default=str))
|
||||
db.add(show)
|
||||
def add_show(db: DbSessionDependency, show_id: int, metadata_provider: str = "tmdb"):
|
||||
try:
|
||||
db.commit()
|
||||
db.refresh(show)
|
||||
except psycopg.errors.UniqueViolation as e:
|
||||
log.debug(e)
|
||||
log.info("Show already exists " + show.__str__())
|
||||
return JSONResponse(status_code=status.HTTP_409_CONFLICT, content={"message": "Show already exists"})
|
||||
|
||||
show = tv.service.add_show(db=db, external_id=show_id, metadata_provider=metadata_provider, )
|
||||
except MediaAlreadyExists as e:
|
||||
return JSONResponse(status_code=status.HTTP_409_CONFLICT, content={"message": str(e)})
|
||||
return show
|
||||
|
||||
|
||||
@router.delete("/{show_id}", status_code=status.HTTP_200_OK, dependencies=[Depends(current_active_user)])
|
||||
def delete_show(db: DbSessionDependency, show_id: UUID):
|
||||
@router.delete("/shows/{show_id}", status_code=status.HTTP_200_OK, dependencies=[Depends(current_active_user)])
|
||||
def delete_show(db: DbSessionDependency, show_id: ShowId):
|
||||
db.delete(db.get(Show, show_id))
|
||||
db.commit()
|
||||
|
||||
|
||||
@router.patch("/{show_id}/{season_id}", status_code=status.HTTP_200_OK, dependencies=[Depends(current_active_user)],
|
||||
response_model=Season)
|
||||
def add_season(db: DbSessionDependency, season_id: UUID):
|
||||
"""
|
||||
adds requested flag to a season
|
||||
"""
|
||||
season = db.get(Season, season_id)
|
||||
season.requested = True
|
||||
db.add(season)
|
||||
db.commit()
|
||||
db.refresh(season)
|
||||
# --------------------------------
|
||||
# GET SHOW INFORMATION
|
||||
# --------------------------------
|
||||
|
||||
return season
|
||||
|
||||
|
||||
@router.delete("/{show_id}/{season_id}", status_code=status.HTTP_200_OK, dependencies=[Depends(current_active_user)],
|
||||
response_model=Show)
|
||||
def delete_season(db: DbSessionDependency, show_id: UUID, season: int):
|
||||
"""
|
||||
removes requested flag from a season
|
||||
"""
|
||||
season = db.get(Season, (show_id, season))
|
||||
season.requested = False
|
||||
db.add(season)
|
||||
db.commit()
|
||||
db.refresh(season)
|
||||
return season
|
||||
|
||||
|
||||
@router.get("/{show_id}/{season_id}/torrent", status_code=status.HTTP_200_OK, dependencies=[Depends(
|
||||
current_active_user)],
|
||||
response_model=list[IndexerQueryResult])
|
||||
def get_season_torrents(db: DbSessionDependency, show_id: UUID, season_id: UUID):
|
||||
season = db.get(Season, season_id)
|
||||
|
||||
if season is None:
|
||||
return JSONResponse(status_code=status.HTTP_404_NOT_FOUND, content={"message": "Season not found"})
|
||||
|
||||
torrents: list[IndexerQueryResult] = indexer.search(season)
|
||||
result = []
|
||||
for torrent in torrents:
|
||||
if season.number in torrent.season:
|
||||
result.append(torrent)
|
||||
|
||||
db.commit()
|
||||
if len(result) == 0:
|
||||
return result
|
||||
result.sort()
|
||||
|
||||
log.info(f"Found {torrents.__len__()} torrents for show {season.show.name} season {season.number}, of which "
|
||||
f"{result.__len__()} torrents fit the query")
|
||||
log.debug(f"unfiltered torrents: \n{pprint.pformat(torrents)}\nfiltered torrents: \n{pprint.pformat(result)}")
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/{show_id}/torrent", status_code=status.HTTP_200_OK, dependencies=[Depends(
|
||||
current_active_user)], response_model=list[Season])
|
||||
def download_seasons_torrent(db: DbSessionDependency, show_id: UUID, torrent_id: UUID):
|
||||
"""
|
||||
downloads torrents for a show season, links the torrent for all seasons the torrent contains
|
||||
|
||||
"""
|
||||
torrent = db.get(Torrent, torrent_id)
|
||||
|
||||
if torrent is None:
|
||||
return JSONResponse(status_code=status.HTTP_404_NOT_FOUND, content={"message": "Torrent not found"})
|
||||
|
||||
seasons = []
|
||||
for season_number in torrent.season:
|
||||
seasons.append(
|
||||
db.exec(select(Season)
|
||||
.where(Season.show_id == show_id)
|
||||
.where(Season.number == season_number)
|
||||
).first()
|
||||
)
|
||||
|
||||
torrent = torrent.download()
|
||||
|
||||
dowloadClients.client.download(Torrent)
|
||||
|
||||
for season in seasons:
|
||||
season.requested = True
|
||||
season.torrent_id = torrent.id
|
||||
|
||||
return seasons
|
||||
|
||||
|
||||
@router.post("/{show_id}/{season_id}/torrent", status_code=status.HTTP_200_OK, dependencies=[Depends(
|
||||
current_active_user)], response_model=list[Season])
|
||||
def delete_seasons_torrent(db: DbSessionDependency, show_id: UUID, season_id: UUID, torrent_id: UUID):
|
||||
"""
|
||||
downloads torrents for a season, links the torrent only to the specified season
|
||||
this means that multiple torrents can contain a season but you can choose from one which the content should be
|
||||
imported
|
||||
|
||||
"""
|
||||
torrent = db.get(Torrent, torrent_id)
|
||||
|
||||
if torrent is None:
|
||||
return JSONResponse(status_code=status.HTTP_404_NOT_FOUND, content={"message": "Torrent not found"})
|
||||
|
||||
seasons = []
|
||||
for season_number in torrent.season:
|
||||
seasons.append(
|
||||
db.exec(select(Season)
|
||||
.where(Season.show_id == show_id)
|
||||
.where(Season.number == season_number)
|
||||
).first()
|
||||
)
|
||||
|
||||
torrent = torrent.download()
|
||||
|
||||
dowloadClients.client.download(Torrent)
|
||||
|
||||
for season in seasons:
|
||||
season.requested = True
|
||||
season.torrent_id = torrent.id
|
||||
|
||||
return seasons
|
||||
|
||||
|
||||
@router.get("/", dependencies=[Depends(current_active_user)], response_model=list[Show])
|
||||
@router.get("/shows", dependencies=[Depends(current_active_user)], response_model=list[Show])
|
||||
def get_shows(db: DbSessionDependency):
|
||||
""""""
|
||||
return db.exec(select(Show)).unique().fetchall()
|
||||
return tv.service.get_all_shows(db=db)
|
||||
|
||||
|
||||
@router.get("/{show_id}", dependencies=[Depends(current_active_user)], response_model=ShowDetails)
|
||||
def get_show(db: DbSessionDependency, show_id: UUID):
|
||||
@router.get("/shows/{show_id}", dependencies=[Depends(current_active_user)], response_model=Show)
|
||||
def get_show(db: DbSessionDependency, show_id: ShowId):
|
||||
"""
|
||||
|
||||
:param show_id:
|
||||
@@ -199,15 +53,60 @@ def get_show(db: DbSessionDependency, show_id: UUID):
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
shows = db.execute(select(Show, Season).where(Show.id == show_id).join(Season).order_by(Season.number)).fetchall()
|
||||
seasons = []
|
||||
for show in shows:
|
||||
seasons.append(show[1])
|
||||
|
||||
shows = db.execute(select(Show, Season).where(Show.id == show_id).join(Season).order_by(Season.number))
|
||||
return tv.service.get_show_by_id(db=db, show_id=show_id)
|
||||
|
||||
return ShowDetails(show=shows.first()[0], seasons=seasons)
|
||||
|
||||
# --------------------------------
|
||||
# CREATE AND DELETE SHOW REQUESTS
|
||||
# --------------------------------
|
||||
|
||||
@router.post("/season", status_code=status.HTTP_200_OK, dependencies=[Depends(current_active_user)])
|
||||
def add_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)
|
||||
|
||||
|
||||
@router.delete("/season/request", status_code=status.HTTP_200_OK, dependencies=[Depends(current_active_user)])
|
||||
def unrequest_season(db: DbSessionDependency, request: SeasonRequest):
|
||||
tv.service.unrequest_season(db=db, season_request=request)
|
||||
|
||||
|
||||
# --------------------------------
|
||||
# MANAGE TORRENTS
|
||||
# --------------------------------
|
||||
|
||||
# 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):
|
||||
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/")
|
||||
# --------------------------------
|
||||
# SEARCH SHOWS ON METADATA PROVIDERS
|
||||
# --------------------------------
|
||||
|
||||
@router.get("/search", dependencies=[Depends(current_active_user)])
|
||||
def search_show(query: str, metadata_provider: str = "tmdb"):
|
||||
|
||||
67
backend/src/tv/schemas.py
Normal file
67
backend/src/tv/schemas.py
Normal file
@@ -0,0 +1,67 @@
|
||||
import typing
|
||||
import uuid
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
|
||||
from torrent.models import Quality
|
||||
|
||||
ShowId = typing.NewType("ShowId", UUID)
|
||||
SeasonId = typing.NewType("SeasonId", UUID)
|
||||
EpisodeId = typing.NewType("EpisodeId", UUID)
|
||||
|
||||
SeasonNumber = typing.NewType("SeasonNumber", int)
|
||||
EpisodeNumber = typing.NewType("EpisodeNumber", int)
|
||||
|
||||
|
||||
class Episode(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: EpisodeId = Field(default_factory=uuid.uuid4)
|
||||
number: EpisodeNumber
|
||||
external_id: int
|
||||
title: str
|
||||
|
||||
|
||||
class Season(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: SeasonId = Field(default_factory=uuid.uuid4)
|
||||
number: SeasonNumber
|
||||
|
||||
name: str
|
||||
overview: str
|
||||
|
||||
external_id: int
|
||||
|
||||
episodes: list[Episode]
|
||||
|
||||
|
||||
class Show(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: ShowId = Field(default_factory=uuid.uuid4)
|
||||
|
||||
name: str
|
||||
overview: str
|
||||
year: int
|
||||
|
||||
external_id: int
|
||||
metadata_provider: str
|
||||
|
||||
seasons: list[Season]
|
||||
|
||||
|
||||
class SeasonRequest(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
season_id: SeasonId
|
||||
min_quality: Quality
|
||||
wanted_quality: Quality
|
||||
|
||||
|
||||
class SeasonFile(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
season_id: SeasonId
|
||||
quality: Quality
|
||||
torrent_id: UUID
|
||||
file_path: str
|
||||
88
backend/src/tv/service.py
Normal file
88
backend/src/tv/service.py
Normal file
@@ -0,0 +1,88 @@
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
import database
|
||||
import indexer
|
||||
# import indexer
|
||||
import metadataProvider
|
||||
import tv.repository
|
||||
from indexer import IndexerQueryResult
|
||||
from tv.exceptions import MediaAlreadyExists
|
||||
from tv.repository import get_show_by_external_id
|
||||
from tv.schemas import Show, ShowId, SeasonRequest
|
||||
|
||||
|
||||
def add_show(db: Session, external_id: int, metadata_provider: str) -> Show | None:
|
||||
if check_if_show_exists(db=db, external_id=external_id, metadata_provider=metadata_provider):
|
||||
raise MediaAlreadyExists(f"Show with external ID {external_id} and" +
|
||||
f" metadata provider {metadata_provider} already exists")
|
||||
show_with_metadata = metadataProvider.get_show_metadata(id=external_id, provider=metadata_provider)
|
||||
saved_show = tv.repository.save_show(db=db, show=show_with_metadata)
|
||||
return saved_show
|
||||
|
||||
|
||||
def request_season(db: Session, season_request: SeasonRequest) -> None:
|
||||
tv.repository.add_season_to_requested_list(db=db, season_request=season_request)
|
||||
|
||||
|
||||
def unrequest_season(db: Session, season_request: SeasonRequest) -> None:
|
||||
tv.repository.remove_season_from_requested_list(db=db, season_request=season_request)
|
||||
|
||||
|
||||
def check_if_show_exists(db: Session,
|
||||
external_id: int = None,
|
||||
metadata_provider: str = None,
|
||||
show_id: ShowId = None) -> bool:
|
||||
if external_id and metadata_provider:
|
||||
if tv.repository.get_show_by_external_id(external_id=external_id, metadata_provider=metadata_provider, db=db):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
elif show_id:
|
||||
if tv.repository.get_show(show_id=show_id, db=db):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
raise ValueError("External ID and metadata provider or Show ID must be provided")
|
||||
|
||||
|
||||
def get_all_available_torrents_for_a_season(db: Session, season_number: int, show_id: ShowId) -> list[
|
||||
IndexerQueryResult]:
|
||||
show = tv.repository.get_show(show_id=show_id, db=db)
|
||||
torrents: list[IndexerQueryResult] = indexer.search(show.name + " S" + str(season_number))
|
||||
result = []
|
||||
for torrent in torrents:
|
||||
if season.number in torrent.season:
|
||||
result.append(torrent)
|
||||
return result
|
||||
|
||||
|
||||
def get_all_shows(db: Session) -> list[Show]:
|
||||
return tv.repository.get_shows(db=db)
|
||||
|
||||
|
||||
def get_show_by_id(db: Session, show_id: ShowId) -> Show | None:
|
||||
return tv.repository.get_show(show_id=show_id, db=db)
|
||||
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user