refactor: reformat code

This commit is contained in:
maxDorninger
2025-05-29 13:54:18 +02:00
parent b7c32f24b9
commit a51716db7d
28 changed files with 806 additions and 337 deletions

View File

@@ -5,17 +5,18 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
class AuthConfig(BaseSettings):
# to get a signing key run:
# openssl rand -hex 32
model_config = SettingsConfigDict(env_prefix='AUTH_')
model_config = SettingsConfigDict(env_prefix="AUTH_")
token_secret: str
session_lifetime: int = 60 * 60 * 24
admin_email: str | list[str]
@property
def jwt_signing_key(self):
return self._jwt_signing_key
class OAuth2Config(BaseSettings):
model_config = SettingsConfigDict(env_prefix='OAUTH_')
model_config = SettingsConfigDict(env_prefix="OAUTH_")
client_id: str
client_secret: str
authorize_endpoint: str

View File

@@ -1,7 +1,11 @@
from collections.abc import AsyncGenerator
from fastapi import Depends
from fastapi_users.db import SQLAlchemyBaseUserTableUUID, SQLAlchemyUserDatabase, SQLAlchemyBaseOAuthAccountTableUUID
from fastapi_users.db import (
SQLAlchemyBaseUserTableUUID,
SQLAlchemyUserDatabase,
SQLAlchemyBaseOAuthAccountTableUUID,
)
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import Mapped, relationship

View File

@@ -16,7 +16,11 @@ if oauth_enabled:
oauth_config = OAuth2Config()
@users_router.get("/users/all", status_code=status.HTTP_200_OK, dependencies=[Depends(current_superuser)])
@users_router.get(
"/users/all",
status_code=status.HTTP_200_OK,
dependencies=[Depends(current_superuser)],
)
def get_all_users(db: DbSessionDependency) -> list[UserRead]:
stmt = select(User)
result = db.execute(stmt).scalars().unique()

View File

@@ -8,7 +8,8 @@ from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin, models
from fastapi_users.authentication import (
AuthenticationBackend,
BearerTransport,
CookieTransport, JWTStrategy,
CookieTransport,
JWTStrategy,
)
from fastapi_users.db import SQLAlchemyUserDatabase
from httpx_oauth.oauth2 import OAuth2
@@ -34,15 +35,17 @@ class GenericOAuth2(OAuth2):
userinfo_endpoint = self.user_info_endpoint
async with httpx.AsyncClient() as client:
resp = await client.get(
userinfo_endpoint,
headers={"Authorization": f"Bearer {token}"}
userinfo_endpoint, headers={"Authorization": f"Bearer {token}"}
)
resp.raise_for_status()
data = resp.json()
return data["sub"], data["email"]
if os.getenv("OAUTH_ENABLED") is not None and os.getenv("OAUTH_ENABLED").upper() == "TRUE":
if (
os.getenv("OAUTH_ENABLED") is not None
and os.getenv("OAUTH_ENABLED").upper() == "TRUE"
):
oauth2_config = OAuth2Config()
oauth_client = GenericOAuth2(
client_id=oauth2_config.client_id,
@@ -72,7 +75,9 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
):
print(f"User {user.id} has forgot their password. Reset token: {token}")
async def on_after_reset_password(self, user: User, request: Optional[Request] = None):
async def on_after_reset_password(
self, user: User, request: Optional[Request] = None
):
print(f"User {user.id} has reset their password.")
async def on_after_request_verify(
@@ -80,9 +85,7 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
):
print(f"Verification requested for user {user.id}. Verification token: {token}")
async def on_after_verify(
self, user: User, request: Optional[Request] = None
):
async def on_after_verify(self, user: User, request: Optional[Request] = None):
print(f"User {user.id} has been verified")
@@ -98,7 +101,10 @@ def get_jwt_strategy() -> JWTStrategy[models.UP, models.ID]:
# thus the user would be stuck on the OAuth Providers "redirecting" page
class RedirectingCookieTransport(CookieTransport):
async def get_login_response(self, token: str) -> Response:
response = RedirectResponse(str(BasicConfig().FRONTEND_URL) + "dashboard", status_code=status.HTTP_302_FOUND)
response = RedirectResponse(
str(BasicConfig().FRONTEND_URL) + "dashboard",
status_code=status.HTTP_302_FOUND,
)
return self._set_login_cookie(response, token)
@@ -122,7 +128,11 @@ oauth_cookie_auth_backend = AuthenticationBackend(
get_strategy=get_jwt_strategy,
)
fastapi_users = FastAPIUsers[User, uuid.UUID](get_user_manager, [bearer_auth_backend, cookie_auth_backend])
fastapi_users = FastAPIUsers[User, uuid.UUID](
get_user_manager, [bearer_auth_backend, cookie_auth_backend]
)
current_active_user = fastapi_users.current_user(active=True, verified=True)
current_superuser = fastapi_users.current_user(active=True, verified=True, superuser=True)
current_superuser = fastapi_users.current_user(
active=True, verified=True, superuser=True
)

View File

@@ -12,8 +12,19 @@ from backend.src.database.config import DbConfig
log = logging.getLogger(__name__)
config = DbConfig()
db_url = "postgresql+psycopg" + "://" + config.USER + ":" + config.PASSWORD + "@" + config.HOST + ":" + str(
config.PORT) + "/" + config.DBNAME
db_url = (
"postgresql+psycopg"
+ "://"
+ config.USER
+ ":"
+ config.PASSWORD
+ "@"
+ config.HOST
+ ":"
+ str(config.PORT)
+ "/"
+ config.DBNAME
)
engine = create_engine(db_url, echo=False)
log.debug("initializing sqlalchemy declarative base")
@@ -21,7 +32,6 @@ Base = declarative_base()
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def init_db() -> None:
log.debug("initializing database with following tables")
for table in Base.metadata.tables:
@@ -41,7 +51,7 @@ def get_session() -> Generator[Session, Any, None]:
db.close()
db_session: ContextVar[Session] = ContextVar('db_session')
db_session: ContextVar[Session] = ContextVar("db_session")
DbSessionDependency = Annotated[Session, Depends(get_session)]

View File

@@ -2,7 +2,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
class DbConfig(BaseSettings):
model_config = SettingsConfigDict(env_prefix='DB_')
model_config = SettingsConfigDict(env_prefix="DB_")
HOST: str = "localhost"
PORT: int = 5432
USER: str = "MediaManager"

View File

@@ -8,7 +8,7 @@ class GenericIndexer(object):
if name:
self.name = name
else:
raise ValueError('indexer name must not be None')
raise ValueError("indexer name must not be None")
def get_search_results(self, query: str) -> list[IndexerQueryResult]:
"""

View File

@@ -17,7 +17,7 @@ class Prowlarr(GenericIndexer):
:param api_key: The API key for authenticating requests to Prowlarr.
:param kwargs: Additional keyword arguments to pass to the superclass constructor.
"""
super().__init__(name='prowlarr')
super().__init__(name="prowlarr")
config = ProwlarrConfig()
self.api_key = config.api_key
self.url = config.url
@@ -25,31 +25,29 @@ class Prowlarr(GenericIndexer):
def get_search_results(self, query: str) -> list[IndexerQueryResult]:
log.debug("Searching for " + query)
url = self.url + '/api/v1/search'
headers = {
'accept': 'application/json',
'X-Api-Key': self.api_key
}
url = self.url + "/api/v1/search"
headers = {"accept": "application/json", "X-Api-Key": self.api_key}
params = {
'query': query,
"query": query,
}
response = requests.get(url, headers=headers, params=params)
if response.status_code == 200:
result_list: list[IndexerQueryResult] = []
for result in response.json():
if result['protocol'] == 'torrent':
if result["protocol"] == "torrent":
log.debug("torrent result: " + result.__str__())
result_list.append(
IndexerQueryResult(
download_url=result['downloadUrl'],
title=result['sortTitle'],
seeders=result['seeders'],
flags=result['indexerFlags'],
size=result['size'], )
download_url=result["downloadUrl"],
title=result["sortTitle"],
seeders=result["seeders"],
flags=result["indexerFlags"],
size=result["size"],
)
)
return result_list
else:
log.error(f'Prowlarr Error: {response.status_code}')
log.error(f"Prowlarr Error: {response.status_code}")
return []

View File

@@ -8,7 +8,7 @@ from backend.src.torrent.schemas import Quality
class IndexerQueryResult(Base):
__tablename__ = 'indexer_query_result'
__tablename__ = "indexer_query_result"
id: Mapped[IndexerQueryResultId] = mapped_column(primary_key=True)
title: Mapped[str]
download_url: Mapped[str]

View File

@@ -1,13 +1,22 @@
from sqlalchemy.orm import Session
from indexer.models import IndexerQueryResult
from indexer.schemas import IndexerQueryResultId, IndexerQueryResult as IndexerQueryResultSchema
from indexer.schemas import (
IndexerQueryResultId,
IndexerQueryResult as IndexerQueryResultSchema,
)
def get_result(result_id: IndexerQueryResultId, db: Session) -> IndexerQueryResultSchema:
return IndexerQueryResultSchema.model_validate(db.get(IndexerQueryResult, result_id))
def get_result(
result_id: IndexerQueryResultId, db: Session
) -> IndexerQueryResultSchema:
return IndexerQueryResultSchema.model_validate(
db.get(IndexerQueryResult, result_id)
)
def save_result(result: IndexerQueryResultSchema, db: Session) -> IndexerQueryResultSchema:
def save_result(
result: IndexerQueryResultSchema, db: Session
) -> IndexerQueryResultSchema:
db.add(IndexerQueryResult(**result.model_dump()))
return result

View File

@@ -7,7 +7,7 @@ from pydantic import BaseModel, computed_field, ConfigDict
from backend.src.torrent.models import Quality
IndexerQueryResultId = typing.NewType('IndexerQueryResultId', UUID)
IndexerQueryResultId = typing.NewType("IndexerQueryResultId", UUID)
# TODO: use something like strategy pattern to make sorting more user customizable
@@ -24,10 +24,10 @@ class IndexerQueryResult(BaseModel):
@computed_field(return_type=Quality)
@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'
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

View File

@@ -19,5 +19,7 @@ def search(query: str, db: Session) -> list[IndexerQueryResult]:
return results
def get_indexer_query_result(result_id: IndexerQueryResultId, db: Session) -> IndexerQueryResult:
def get_indexer_query_result(
result_id: IndexerQueryResultId, db: Session
) -> IndexerQueryResult:
return indexer.repository.get_result(result_id=result_id, db=db)

View File

@@ -14,8 +14,7 @@ LOGGING_CONFIG = {
},
"json": {
"()": JsonFormatter,
}
},
},
"handlers": {
"console": {
@@ -29,7 +28,7 @@ LOGGING_CONFIG = {
"filename": "./log.txt",
"maxBytes": 10485760,
"backupCount": 5,
}
},
},
"loggers": {
"uvicorn": {"handlers": ["console", "file"], "level": "DEBUG"},
@@ -39,16 +38,16 @@ LOGGING_CONFIG = {
}
dictConfig(LOGGING_CONFIG)
logging.basicConfig(level=logging.DEBUG,
format="%(asctime)s - %(levelname)s - %(name)s - %(funcName)s(): %(message)s",
stream=sys.stdout,
)
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(levelname)s - %(name)s - %(funcName)s(): %(message)s",
stream=sys.stdout,
)
log = logging.getLogger(__name__)
from backend.src.database import init_db
import tv.router
import torrent.router
import auth.db
init_db()
log.info("Database initialized")
@@ -62,9 +61,15 @@ from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from auth.schemas import UserCreate, UserRead, UserUpdate
from auth.users import bearer_auth_backend, fastapi_users, cookie_auth_backend, oauth_cookie_auth_backend
from auth.users import (
bearer_auth_backend,
fastapi_users,
cookie_auth_backend,
oauth_cookie_auth_backend,
)
from auth.router import users_router as custom_users_router
from auth.router import auth_metadata_router
basic_config = BasicConfig()
if basic_config.DEVELOPMENT:
basic_config.torrent_directory.mkdir(parents=True, exist_ok=True)
@@ -94,12 +99,12 @@ if basic_config.DEVELOPMENT:
app.include_router(
fastapi_users.get_auth_router(bearer_auth_backend),
prefix="/auth/jwt",
tags=["auth"]
tags=["auth"],
)
app.include_router(
fastapi_users.get_auth_router(cookie_auth_backend),
prefix="/auth/cookie",
tags=["auth"]
tags=["auth"],
)
app.include_router(
fastapi_users.get_register_router(UserRead, UserCreate),
@@ -117,15 +122,9 @@ app.include_router(
tags=["auth"],
)
# All users route router
app.include_router(
custom_users_router,
tags=["users"]
)
app.include_router(custom_users_router, tags=["users"])
# OAuth Metadata Router
app.include_router(
auth_metadata_router,
tags=["oauth"]
)
app.include_router(auth_metadata_router, tags=["oauth"])
# User Router
app.include_router(
fastapi_users.get_users_router(UserRead, UserUpdate),
@@ -135,29 +134,26 @@ app.include_router(
# OAuth2 Routers
if oauth_client is not None:
app.include_router(
fastapi_users.get_oauth_router(oauth_client,
oauth_cookie_auth_backend,
auth.users.SECRET,
associate_by_email=True,
is_verified_by_default=True,
),
fastapi_users.get_oauth_router(
oauth_client,
oauth_cookie_auth_backend,
auth.users.SECRET,
associate_by_email=True,
is_verified_by_default=True,
),
prefix=f"/auth/cookie/{oauth_client.name}",
tags=["oauth"],
)
app.include_router(
tv.router.router,
prefix="/tv",
tags=["tv"]
)
app.include_router(
torrent.router.router,
prefix="/torrent",
tags=["torrent"]
)
app.include_router(tv.router.router, prefix="/tv", tags=["tv"])
app.include_router(torrent.router.router, prefix="/torrent", tags=["torrent"])
# static file routers
app.mount("/static/image", StaticFiles(directory=basic_config.image_directory), name="static-images")
app.mount(
"/static/image",
StaticFiles(directory=basic_config.image_directory),
name="static-images",
)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=5049, log_config=LOGGING_CONFIG)

View File

@@ -18,9 +18,10 @@ def get_show_metadata(id: int = None, provider: str = "tmdb") -> Show:
@cached(search_show_cache)
def search_show(query: str | None = None, provider: str = "tmdb") -> list[MetaDataProviderShowSearchResult]:
def search_show(
query: str | None = None, provider: str = "tmdb"
) -> list[MetaDataProviderShowSearchResult]:
"""
If no query is provided, it will return the most popular shows.
"""
return metadata_providers[provider].search_show(query)

View File

@@ -10,6 +10,7 @@ log = logging.getLogger(__name__)
class AbstractMetadataProvider(ABC):
storage_path = config.BasicConfig().image_directory
@property
@abstractmethod
def name(self) -> str:

View File

@@ -7,7 +7,10 @@ from pydantic_settings import BaseSettings
from tmdbsimple import TV, TV_Seasons
import metadataProvider.utils
from metadataProvider.abstractMetaDataProvider import AbstractMetadataProvider, register_metadata_provider
from metadataProvider.abstractMetaDataProvider import (
AbstractMetadataProvider,
register_metadata_provider,
)
from metadataProvider.schemas import MetaDataProviderShowSearchResult
from tv.schemas import Episode, Season, Show, SeasonNumber, EpisodeNumber
@@ -35,7 +38,9 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
season_list = []
# inserting all the metadata into the objects
for season in show_metadata["seasons"]:
season_metadata = TV_Seasons(tv_id=show_metadata["id"], season_number=season["season_number"]).info()
season_metadata = TV_Seasons(
tv_id=show_metadata["id"], season_number=season["season_number"]
).info()
episode_list = []
for episode in season_metadata["episodes"]:
@@ -43,7 +48,7 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
Episode(
external_id=int(episode["id"]),
title=episode["name"],
number=EpisodeNumber(episode["episode_number"])
number=EpisodeNumber(episode["episode_number"]),
)
)
@@ -54,11 +59,12 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
overview=season_metadata["overview"],
number=SeasonNumber(season_metadata["season_number"]),
episodes=episode_list,
)
)
year = metadataProvider.utils.get_year_from_first_air_date(show_metadata["first_air_date"])
year = metadataProvider.utils.get_year_from_first_air_date(
show_metadata["first_air_date"]
)
show = Show(
external_id=id,
@@ -72,9 +78,12 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
# downloading the poster
# all pictures from TMDB should already be jpeg, so no need to convert
if show_metadata["poster_path"] is not None:
poster_url = "https://image.tmdb.org/t/p/original" + show_metadata["poster_path"]
if metadataProvider.utils.download_poster_image(storage_path=self.storage_path, poster_url=poster_url,
show=show):
poster_url = (
"https://image.tmdb.org/t/p/original" + show_metadata["poster_path"]
)
if metadataProvider.utils.download_poster_image(
storage_path=self.storage_path, poster_url=poster_url, show=show
):
log.info("Successfully downloaded poster image for show " + show.name)
else:
log.warning(f"download for image of show {show.name} failed")
@@ -83,7 +92,9 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
return show
def search_show(self, query: str | None = None, max_pages: int = 5) -> list[MetaDataProviderShowSearchResult]:
def search_show(
self, query: str | None = None, max_pages: int = 5
) -> list[MetaDataProviderShowSearchResult]:
"""
Search for shows using TMDB API.
If no query is provided, it will return the most popular shows.
@@ -91,7 +102,9 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
if query is None:
result_factory = lambda page: tmdbsimple.Trending(media_type="tv").info()
else:
result_factory = lambda page: tmdbsimple.Search().tv(page=page, query=query, include_adult=True)
result_factory = lambda page: tmdbsimple.Search().tv(
page=page, query=query, include_adult=True
)
results = []
for i in range(1, max_pages + 1):
@@ -106,7 +119,9 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
for result in results:
try:
if result["poster_path"] is not None:
poster_url = "https://image.tmdb.org/t/p/original" + result["poster_path"]
poster_url = (
"https://image.tmdb.org/t/p/original" + result["poster_path"]
)
else:
poster_url = None
formatted_results.append(
@@ -115,7 +130,9 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
overview=result["overview"],
name=result["name"],
external_id=result["id"],
year=metadataProvider.utils.get_year_from_first_air_date(result["first_air_date"]),
year=metadataProvider.utils.get_year_from_first_air_date(
result["first_air_date"]
),
metadata_provider=self.name,
added=False,
vote_average=result["vote_average"],
@@ -131,4 +148,6 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
if config.TMDB_API_KEY is not None:
log.info("Registering TMDB as metadata provider")
register_metadata_provider(metadata_provider=TmdbMetadataProvider(config.TMDB_API_KEY))
register_metadata_provider(
metadata_provider=TmdbMetadataProvider(config.TMDB_API_KEY)
)

View File

@@ -11,7 +11,10 @@ from pydantic_settings import BaseSettings
from tmdbsimple import TV, TV_Seasons
import metadataProvider.utils
from metadataProvider.abstractMetaDataProvider import AbstractMetadataProvider, register_metadata_provider
from metadataProvider.abstractMetaDataProvider import (
AbstractMetadataProvider,
register_metadata_provider,
)
from metadataProvider.schemas import MetaDataProviderShowSearchResult
from tv.schemas import Episode, Season, Show, SeasonNumber, EpisodeNumber
@@ -43,27 +46,48 @@ class TvdbMetadataProvider(AbstractMetadataProvider):
seasons = []
for season in series["seasons"]:
s = self.tvdb_client.get_season_extended(season["id"])
episodes = [Episode(number=episode['number'], external_id=episode['id'], title=episode['name']) for episode
in s["episodes"]]
seasons.append(Season(number=s['number'], name="TVDB doesn't provide Season Names",
overview="TVDB doesn't provide Season Overviews", external_id=s['id'],
episodes=episodes))
episodes = [
Episode(
number=episode["number"],
external_id=episode["id"],
title=episode["name"],
)
for episode in s["episodes"]
]
seasons.append(
Season(
number=s["number"],
name="TVDB doesn't provide Season Names",
overview="TVDB doesn't provide Season Overviews",
external_id=s["id"],
episodes=episodes,
)
)
try:
year = series['year']
year = series["year"]
except KeyError:
year = None
show = Show(name=series['name'], overview=series['overview'], year=year,
external_id=series['id'], metadata_provider=self.name, seasons=seasons)
show = Show(
name=series["name"],
overview=series["overview"],
year=year,
external_id=series["id"],
metadata_provider=self.name,
seasons=seasons,
)
if series["image"] is not None:
metadataProvider.utils.download_poster_image(storage_path=self.storage_path,
poster_url=series['image'], show=show)
metadataProvider.utils.download_poster_image(
storage_path=self.storage_path, poster_url=series["image"], show=show
)
else:
log.warning(f"image for show {show.name} could not be downloaded")
return show
def search_show(self, query: str | None = None) -> list[MetaDataProviderShowSearchResult]:
def search_show(
self, query: str | None = None
) -> list[MetaDataProviderShowSearchResult]:
if query is None:
results = self.tvdb_client.get_all_series()
else:
@@ -71,9 +95,9 @@ class TvdbMetadataProvider(AbstractMetadataProvider):
formatted_results = []
for result in results:
try:
if result['type'] == 'series':
if result["type"] == "series":
try:
year = result['year']
year = result["year"]
except KeyError:
year = None
@@ -86,7 +110,7 @@ class TvdbMetadataProvider(AbstractMetadataProvider):
year=year,
metadata_provider=self.name,
added=False,
vote_average=None
vote_average=None,
)
)
except Exception as e:
@@ -96,7 +120,9 @@ class TvdbMetadataProvider(AbstractMetadataProvider):
if config.TVDB_API_KEY is not None:
log.info("Registering TVDB as metadata provider")
register_metadata_provider(metadata_provider=TvdbMetadataProvider(config.TVDB_API_KEY))
register_metadata_provider(
metadata_provider=TvdbMetadataProvider(config.TVDB_API_KEY)
)
if __name__ == "__main__":
tvdb = TvdbMetadataProvider(config.TVDB_API_KEY)

View File

@@ -3,10 +3,9 @@ import mimetypes
import requests
def get_year_from_first_air_date(first_air_date: str | None) -> int | None:
if first_air_date:
return int(first_air_date.split('-')[0])
return int(first_air_date.split("-")[0])
else:
return None
@@ -16,7 +15,7 @@ def download_poster_image(storage_path=None, poster_url=None, show=None) -> bool
content_type = res.headers["content-type"]
file_extension = mimetypes.guess_extension(content_type)
if res.status_code == 200:
with open(storage_path.joinpath(str(show.id) + file_extension), 'wb') as f:
with open(storage_path.joinpath(str(show.id) + file_extension), "wb") as f:
f.write(res.content)
return True
else:

View File

@@ -7,7 +7,9 @@ from tv.models import SeasonFile, Show, Season
from tv.schemas import SeasonFile as SeasonFileSchema, Show as ShowSchema
def get_seasons_files_of_torrent(db: Session, torrent_id: TorrentId) -> list[SeasonFileSchema]:
def get_seasons_files_of_torrent(
db: Session, torrent_id: TorrentId
) -> list[SeasonFileSchema]:
stmt = select(SeasonFile).where(SeasonFile.torrent_id == torrent_id)
result = db.execute(stmt).scalars().all()
return [SeasonFileSchema.model_validate(season_file) for season_file in result]
@@ -15,10 +17,10 @@ def get_seasons_files_of_torrent(db: Session, torrent_id: TorrentId) -> list[Sea
def get_show_of_torrent(db: Session, torrent_id: TorrentId) -> ShowSchema:
stmt = (
select(Show).
join(SeasonFile.season).
join(Season.show).
where(SeasonFile.torrent_id == torrent_id)
select(Show)
.join(SeasonFile.season)
.join(Season.show)
.where(SeasonFile.torrent_id == torrent_id)
)
result = db.execute(stmt).unique().scalar_one_or_none()
return ShowSchema.model_validate(result)

View File

@@ -14,24 +14,40 @@ def get_torrent(service: TorrentServiceDependency, torrent_id: TorrentId):
return service.get_torrent_by_id(id=torrent_id)
@router.get("/", status_code=status.HTTP_200_OK, dependencies=[Depends(current_active_user)],
response_model=list[Torrent])
@router.get(
"/",
status_code=status.HTTP_200_OK,
dependencies=[Depends(current_active_user)],
response_model=list[Torrent],
)
def get_all_torrents(service: TorrentServiceDependency):
return service.get_all_torrents()
@router.post("/{torrent_id}", status_code=status.HTTP_200_OK, dependencies=[Depends(current_active_user)],
response_model=Torrent)
@router.post(
"/{torrent_id}",
status_code=status.HTTP_200_OK,
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))
@router.post("/", status_code=status.HTTP_200_OK, dependencies=[Depends(current_active_user)],
response_model=list[Torrent])
@router.post(
"/",
status_code=status.HTTP_200_OK,
dependencies=[Depends(current_active_user)],
response_model=list[Torrent],
)
def import_all_torrents(service: TorrentServiceDependency):
return service.import_all_torrents()
@router.delete("/{torrent_id}", status_code=status.HTTP_200_OK, dependencies=[Depends(current_superuser)])
@router.delete(
"/{torrent_id}",
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)

View File

@@ -17,16 +17,25 @@ import tv.repository
import tv.service
from config import BasicConfig
from indexer import IndexerQueryResult
from torrent.repository import get_seasons_files_of_torrent, get_show_of_torrent, save_torrent
from torrent.repository import (
get_seasons_files_of_torrent,
get_show_of_torrent,
save_torrent,
)
from torrent.schemas import Torrent, TorrentStatus, TorrentId
from torrent.utils import list_files_recursively, get_torrent_filepath, import_file, extract_archives
from torrent.utils import (
list_files_recursively,
get_torrent_filepath,
import_file,
extract_archives,
)
from tv.schemas import SeasonFile, Show
log = logging.getLogger(__name__)
class TorrentServiceConfig(BaseSettings):
model_config = SettingsConfigDict(env_prefix='QBITTORRENT_')
model_config = SettingsConfigDict(env_prefix="QBITTORRENT_")
host: str = "localhost"
port: int = 8080
username: str = "admin"
@@ -34,9 +43,25 @@ class TorrentServiceConfig(BaseSettings):
class TorrentService:
DOWNLOADING_STATE = ("allocating", "downloading", "metaDL", "pausedDL", "queuedDL", "stalledDL", "checkingDL",
"forcedDL", "moving")
FINISHED_STATE = ("uploading", "pausedUP", "queuedUP", "stalledUP", "checkingUP", "forcedUP")
DOWNLOADING_STATE = (
"allocating",
"downloading",
"metaDL",
"pausedDL",
"queuedDL",
"stalledDL",
"checkingDL",
"forcedDL",
"moving",
)
FINISHED_STATE = (
"uploading",
"pausedUP",
"queuedUP",
"stalledUP",
"checkingUP",
"forcedUP",
)
ERROR_STATE = ("missingFiles", "error", "checkingResumeData")
UNKNOWN_STATE = ("unknown",)
api_client = qbittorrentapi.Client(**TorrentServiceConfig().model_dump())
@@ -54,35 +79,42 @@ class TorrentService:
def download(self, indexer_result: IndexerQueryResult) -> Torrent:
log.info(f"Attempting to download torrent: {indexer_result.title}")
torrent = Torrent(status=TorrentStatus.unknown,
title=indexer_result.title,
quality=indexer_result.quality,
imported=False,
hash="")
torrent = Torrent(
status=TorrentStatus.unknown,
title=indexer_result.title,
quality=indexer_result.quality,
imported=False,
hash="",
)
url = indexer_result.download_url
torrent_filepath = BasicConfig().torrent_directory / f"{torrent.title}.torrent"
with open(torrent_filepath, 'wb') as file:
with open(torrent_filepath, "wb") as file:
content = requests.get(url).content
file.write(content)
with open(torrent_filepath, 'rb') as file:
with open(torrent_filepath, "rb") as file:
content = file.read()
try:
decoded_content = bencoder.decode(content)
except Exception as e:
log.error(f"Failed to decode torrent file: {e}")
raise e
torrent.hash = hashlib.sha1(bencoder.encode(decoded_content[b'info'])).hexdigest()
answer = self.api_client.torrents_add(category="MediaManager", torrent_files=content,
save_path=torrent.title)
torrent.hash = hashlib.sha1(
bencoder.encode(decoded_content[b"info"])
).hexdigest()
answer = self.api_client.torrents_add(
category="MediaManager", torrent_files=content, save_path=torrent.title
)
if answer == "Ok.":
log.info(f"Successfully added torrent: {torrent.title}")
return self.get_torrent_status(torrent=torrent)
else:
log.error(f"Failed to download torrent. API response: {answer}")
raise RuntimeError(f"Failed to download torrent, API-Answer isn't 'Ok.'; API Answer: {answer}")
raise RuntimeError(
f"Failed to download torrent, API-Answer isn't 'Ok.'; API Answer: {answer}"
)
def get_torrent_status(self, torrent: Torrent) -> Torrent:
log.info(f"Fetching status for torrent: {torrent.title}")
@@ -161,14 +193,25 @@ class TorrentService:
subtitle_files.append(file)
log.debug(f"File is a subtitle, it will be imported: {file}")
else:
log.debug(f"File is neither a video nor a subtitle, will not be imported: {file}")
log.info(f"Importing these {len(video_files)} files:\n" + pprint.pformat(video_files))
log.debug(
f"File is neither a video nor a subtitle, will not be imported: {file}"
)
log.info(
f"Importing these {len(video_files)} files:\n" + pprint.pformat(video_files)
)
# Fetch show and season information
show: Show = get_show_of_torrent(db=self.db, torrent_id=torrent.id)
show_file_path = BasicConfig().tv_directory / f"{show.name} ({show.year}) [{show.metadata_provider}id-{show.external_id}]"
season_files: list[SeasonFile] = get_seasons_files_of_torrent(db=self.db, torrent_id=torrent.id)
log.info(f"Found {len(season_files)} season files associated with torrent {torrent.title}")
show_file_path = (
BasicConfig().tv_directory
/ f"{show.name} ({show.year}) [{show.metadata_provider}id-{show.external_id}]"
)
season_files: list[SeasonFile] = get_seasons_files_of_torrent(
db=self.db, torrent_id=torrent.id
)
log.info(
f"Found {len(season_files)} season files associated with torrent {torrent.title}"
)
# creating directories and hard linking files
for season_file in season_files:
@@ -181,52 +224,82 @@ class TorrentService:
log.warning(f"Path already exists: {season_path}")
for episode in season.episodes:
episode_file_name = f"{show.name} S{season.number:02d}E{episode.number:02d}"
episode_file_name = (
f"{show.name} S{season.number:02d}E{episode.number:02d}"
)
if season_file.file_path_suffix != "":
episode_file_name += f" - {season_file.file_path_suffix}"
pattern = r'.*[.]S0?' + str(season.number) + r'E0?' + str(episode.number) + r"[.].*"
pattern = (
r".*[.]S0?"
+ str(season.number)
+ r"E0?"
+ str(episode.number)
+ r"[.].*"
)
subtitle_pattern = pattern + r"[.]([A-Za-z]{2})[.]srt"
target_file_name = season_path / episode_file_name
# import subtitles
for subtitle_file in subtitle_files:
log.debug(f"Searching for pattern {subtitle_pattern} in subtitle file: {subtitle_file.name}")
log.debug(
f"Searching for pattern {subtitle_pattern} in subtitle file: {subtitle_file.name}"
)
regex_result = re.search(subtitle_pattern, subtitle_file.name)
if regex_result:
language_code = regex_result.group(1)
log.debug(
f"Found matching pattern: {subtitle_pattern} in subtitle file: {subtitle_file.name}," +
f" extracted language code: {language_code}")
target_subtitle_file = target_file_name.with_suffix(f".{language_code}.srt")
import_file(target_file=target_subtitle_file, source_file=subtitle_file)
f"Found matching pattern: {subtitle_pattern} in subtitle file: {subtitle_file.name},"
+ f" extracted language code: {language_code}"
)
target_subtitle_file = target_file_name.with_suffix(
f".{language_code}.srt"
)
import_file(
target_file=target_subtitle_file, source_file=subtitle_file
)
else:
log.debug(f"Didn't find any pattern {subtitle_pattern} in subtitle file: {subtitle_file.name}")
log.debug(
f"Didn't find any pattern {subtitle_pattern} in subtitle file: {subtitle_file.name}"
)
# import episode videos
for file in video_files:
log.debug(f"Searching for pattern {pattern} in video file: {file.name}")
log.debug(
f"Searching for pattern {pattern} in video file: {file.name}"
)
if re.search(pattern, file.name):
log.debug(f"Found matching pattern: {pattern} in file {file.name}")
log.debug(
f"Found matching pattern: {pattern} in file {file.name}"
)
target_video_file = target_file_name.with_suffix(file.suffix)
import_file(target_file=target_video_file, source_file=file)
break
else:
log.warning(f"S{season.number}E{episode.number} in Torrent {torrent.title}'s files not found.")
log.warning(
f"S{season.number}E{episode.number} in Torrent {torrent.title}'s files not found."
)
torrent.imported = True
return self.get_torrent_status(torrent=torrent)
def get_all_torrents(self) -> list[Torrent]:
return [self.get_torrent_status(x) for x in torrent.repository.get_all_torrents(db=self.db)]
return [
self.get_torrent_status(x)
for x in torrent.repository.get_all_torrents(db=self.db)
]
def get_torrent_by_id(self, id: TorrentId) -> Torrent:
return self.get_torrent_status(torrent.repository.get_torrent_by_id(torrent_id=id, db=self.db))
return self.get_torrent_status(
torrent.repository.get_torrent_by_id(torrent_id=id, db=self.db)
)
def delete_torrent(self, torrent_id: TorrentId):
t = torrent.repository.get_torrent_by_id(torrent_id=torrent_id, db=self.db)
if not t.imported:
tv.repository.remove_season_files_by_torrent_id(db=self.db, torrent_id=torrent_id)
tv.repository.remove_season_files_by_torrent_id(
db=self.db, torrent_id=torrent_id
)
torrent.repository.delete_torrent(db=self.db, torrent_id=t.id)
@repeat_every(seconds=3600)

View File

@@ -9,6 +9,7 @@ from torrent.schemas import Torrent
log = logging.getLogger(__name__)
def list_files_recursively(path: Path = Path(".")) -> list[Path]:
files = list(path.glob("**/*"))
log.debug(f"Found {len(files)} entries via glob")
@@ -28,10 +29,13 @@ def extract_archives(files):
for file in files:
file_type = mimetypes.guess_type(file)
log.debug(f"File: {file}, Size: {file.stat().st_size} bytes, Type: {file_type}")
if file_type[0] == 'application/x-compressed':
log.debug(f"File {file} is a compressed file, extracting it into directory {file.parent}")
if file_type[0] == "application/x-compressed":
log.debug(
f"File {file} is a compressed file, extracting it into directory {file.parent}"
)
patoolib.extract_archive(str(file), outdir=str(file.parent))
def get_torrent_filepath(torrent: Torrent):
return BasicConfig().torrent_directory / torrent.title

View File

@@ -1,6 +1,6 @@
class MediaAlreadyExists(ValueError):
'''Raised when a show already exists'''
"""Raised when a show already exists"""
class MediaDoesNotExist(ValueError):
'''Raised when a does not show exist'''
"""Raised when a does not show exist"""

View File

@@ -19,7 +19,9 @@ class Show(Base):
overview: Mapped[str]
year: Mapped[int | None]
seasons: Mapped[list["Season"]] = relationship(back_populates="show", cascade="all, delete")
seasons: Mapped[list["Season"]] = relationship(
back_populates="show", cascade="all, delete"
)
class Season(Base):
@@ -27,26 +29,34 @@ class Season(Base):
__table_args__ = (UniqueConstraint("show_id", "number"),)
id: Mapped[UUID] = mapped_column(primary_key=True)
show_id: Mapped[UUID] = mapped_column(ForeignKey(column="show.id", ondelete="CASCADE"), )
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")
episodes: Mapped[list["Episode"]] = relationship(back_populates="season", cascade="all, delete")
episodes: Mapped[list["Episode"]] = relationship(
back_populates="season", cascade="all, delete"
)
season_files = relationship("SeasonFile", back_populates="season", cascade="all, delete")
season_requests = relationship("SeasonRequest", back_populates="season", cascade="all, delete")
season_files = relationship(
"SeasonFile", back_populates="season", cascade="all, delete"
)
season_requests = relationship(
"SeasonRequest", back_populates="season", cascade="all, delete"
)
class Episode(Base):
__tablename__ = "episode"
__table_args__ = (
UniqueConstraint("season_id", "number"),
)
__table_args__ = (UniqueConstraint("season_id", "number"),)
id: Mapped[UUID] = mapped_column(primary_key=True)
season_id: Mapped[UUID] = mapped_column(ForeignKey("season.id", ondelete="CASCADE"), )
season_id: Mapped[UUID] = mapped_column(
ForeignKey("season.id", ondelete="CASCADE"),
)
number: Mapped[int]
external_id: Mapped[int]
title: Mapped[str]
@@ -56,30 +66,41 @@ class Episode(Base):
class SeasonFile(Base):
__tablename__ = "season_file"
__table_args__ = (
PrimaryKeyConstraint("season_id", "file_path_suffix"),
__table_args__ = (PrimaryKeyConstraint("season_id", "file_path_suffix"),)
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"),
)
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_suffix: Mapped[str]
quality: Mapped[Quality]
torrent = relationship("Torrent", back_populates="season_files", uselist=False)
season = relationship("Season", back_populates="season_files", uselist=False)
class SeasonRequest(Base):
__tablename__ = "season_request"
__table_args__ = (
UniqueConstraint("season_id", "wanted_quality"),
)
__table_args__ = (UniqueConstraint("season_id", "wanted_quality"),)
id: Mapped[UUID] = mapped_column(primary_key=True)
season_id: Mapped[UUID] = mapped_column(ForeignKey(column="season.id", ondelete="CASCADE"), )
season_id: Mapped[UUID] = mapped_column(
ForeignKey(column="season.id", ondelete="CASCADE"),
)
wanted_quality: Mapped[Quality]
min_quality: Mapped[Quality]
requested_by_id: Mapped[UUID | None] = mapped_column(ForeignKey(column="user.id", ondelete="SET NULL"), )
requested_by_id: Mapped[UUID | None] = mapped_column(
ForeignKey(column="user.id", ondelete="SET NULL"),
)
authorized: Mapped[bool] = mapped_column(default=False)
authorized_by_id: Mapped[UUID | None] = mapped_column(ForeignKey(column="user.id", ondelete="SET NULL"), )
authorized_by_id: Mapped[UUID | None] = mapped_column(
ForeignKey(column="user.id", ondelete="SET NULL"),
)
requested_by: Mapped["User|None"] = relationship(foreign_keys=[requested_by_id], uselist=False)
authorized_by: Mapped["User|None"] = relationship(foreign_keys=[authorized_by_id], uselist=False)
requested_by: Mapped["User|None"] = relationship(
foreign_keys=[requested_by_id], uselist=False
)
authorized_by: Mapped["User|None"] = relationship(
foreign_keys=[authorized_by_id], uselist=False
)
season = relationship("Season", back_populates="season_requests", uselist=False)

View File

@@ -6,9 +6,17 @@ from torrent.models import Torrent
from torrent.schemas import TorrentId, Torrent as TorrentSchema
from tv import log
from tv.models import Season, Show, Episode, SeasonRequest, SeasonFile
from tv.schemas import Season as SeasonSchema, SeasonId, Show as ShowSchema, ShowId, \
SeasonRequest as SeasonRequestSchema, SeasonFile as SeasonFileSchema, SeasonNumber, SeasonRequestId, \
RichSeasonRequest as RichSeasonRequestSchema
from tv.schemas import (
Season as SeasonSchema,
SeasonId,
Show as ShowSchema,
ShowId,
SeasonRequest as SeasonRequestSchema,
SeasonFile as SeasonFileSchema,
SeasonNumber,
SeasonRequestId,
RichSeasonRequest as RichSeasonRequestSchema,
)
def get_show(show_id: ShowId, db: Session) -> ShowSchema | None:
@@ -19,7 +27,11 @@ def get_show(show_id: ShowId, db: Session) -> ShowSchema | None:
: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)))
stmt = (
select(Show)
.where(Show.id == show_id)
.options(joinedload(Show.seasons).joinedload(Season.episodes))
)
result = db.execute(stmt).unique().scalar_one_or_none()
if not result:
@@ -28,7 +40,9 @@ def get_show(show_id: ShowId, db: Session) -> ShowSchema | None:
return ShowSchema.model_validate(result)
def get_show_by_external_id(external_id: int, db: Session, metadata_provider: str) -> ShowSchema | None:
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.
@@ -38,8 +52,11 @@ def get_show_by_external_id(external_id: int, db: Session, metadata_provider: st
: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)))
select(Show)
.where(Show.external_id == external_id)
.where(Show.metadata_provider == metadata_provider)
.options(joinedload(Show.seasons).joinedload(Season.episodes))
)
result = db.execute(stmt).unique().scalar_one_or_none()
if not result:
@@ -71,12 +88,35 @@ def save_show(show: ShowSchema, db: Session) -> ShowSchema:
: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, number=season.number, external_id=season.external_id,
name=season.name, overview=season.overview, episodes=[
Episode(id=episode.id, season_id=season.id, number=episode.number, external_id=episode.external_id,
title=episode.title) for episode in season.episodes]) for season in show.seasons])
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,
number=season.number,
external_id=season.external_id,
name=season.name,
overview=season.overview,
episodes=[
Episode(
id=episode.id,
season_id=season.id,
number=episode.number,
external_id=episode.external_id,
title=episode.title,
)
for episode in season.episodes
],
)
for season in show.seasons
],
)
db.add(db_show)
try:
@@ -122,9 +162,13 @@ def add_season_request(season_request: SeasonRequestSchema, db: Session) -> None
season_id=season_request.season_id,
wanted_quality=season_request.wanted_quality,
min_quality=season_request.min_quality,
requested_by_id=season_request.requested_by.id if season_request.requested_by else None,
requested_by_id=season_request.requested_by.id
if season_request.requested_by
else None,
authorized=season_request.authorized,
authorized_by_id=season_request.authorized_by.id if season_request.authorized_by else None
authorized_by_id=season_request.authorized_by.id
if season_request.authorized_by
else None,
)
db.add(db_model)
db.commit()
@@ -140,23 +184,40 @@ def delete_season_request(season_request_id: SeasonRequestId, db: Session) -> No
db.commit()
def get_season_by_number(db: Session, season_number: int, show_id: ShowId) -> SeasonSchema:
stmt = (select(Season).where(Season.show_id == show_id).where(Season.number == season_number).options(
joinedload(Season.episodes), joinedload(Season.show)))
def get_season_by_number(
db: Session, season_number: int, show_id: ShowId
) -> SeasonSchema:
stmt = (
select(Season)
.where(Season.show_id == show_id)
.where(Season.number == season_number)
.options(joinedload(Season.episodes), joinedload(Season.show))
)
result = db.execute(stmt).unique().scalar_one_or_none()
return SeasonSchema.model_validate(result)
def get_season_requests(db: Session) -> list[RichSeasonRequestSchema]:
stmt = select(SeasonRequest).options(joinedload(SeasonRequest.requested_by),
joinedload(SeasonRequest.authorized_by),
joinedload(SeasonRequest.season).joinedload(Season.show))
stmt = select(SeasonRequest).options(
joinedload(SeasonRequest.requested_by),
joinedload(SeasonRequest.authorized_by),
joinedload(SeasonRequest.season).joinedload(Season.show),
)
result = db.execute(stmt).scalars().unique().all()
return [RichSeasonRequestSchema(min_quality=x.min_quality,
wanted_quality=x.wanted_quality, show=x.season.show, season=x.season,
requested_by=x.requested_by, authorized_by=x.authorized_by, authorized=x.authorized,
id=x.id, season_id=x.season.id)
for x in result]
return [
RichSeasonRequestSchema(
min_quality=x.min_quality,
wanted_quality=x.wanted_quality,
show=x.season.show,
season=x.season,
requested_by=x.requested_by,
authorized_by=x.authorized_by,
authorized=x.authorized,
id=x.id,
season_id=x.season.id,
)
for x in result
]
def add_season_file(db: Session, season_file: SeasonFileSchema) -> SeasonFileSchema:
@@ -166,22 +227,24 @@ def add_season_file(db: Session, season_file: SeasonFileSchema) -> SeasonFileSch
def remove_season_files_by_torrent_id(db: Session, torrent_id: TorrentId):
stmt = (delete(SeasonFile).where(SeasonFile.torrent_id == torrent_id))
stmt = delete(SeasonFile).where(SeasonFile.torrent_id == torrent_id)
db.execute(stmt)
def get_season_files_by_season_id(db: Session, season_id: SeasonId):
stmt = (select(SeasonFile).where(SeasonFile.season_id == season_id))
stmt = select(SeasonFile).where(SeasonFile.season_id == season_id)
result = db.execute(stmt).scalars().all()
return [SeasonFileSchema.model_validate(season_file) for season_file in result]
def get_torrents_by_show_id(db: Session, show_id: ShowId) -> list[TorrentSchema]:
stmt = (select(Torrent)
.distinct()
.join(SeasonFile, SeasonFile.torrent_id == Torrent.id)
.join(Season, Season.id == SeasonFile.season_id)
.where(Season.show_id == show_id))
stmt = (
select(Torrent)
.distinct()
.join(SeasonFile, SeasonFile.torrent_id == Torrent.id)
.join(Season, Season.id == SeasonFile.season_id)
.where(Season.show_id == show_id)
)
result = db.execute(stmt).scalars().unique().all()
return [TorrentSchema.model_validate(torrent) for torrent in result]
@@ -193,13 +256,15 @@ def get_all_shows_with_torrents(db: Session) -> list[ShowSchema]:
:param db: The database session.
:return: A list of ShowSchema objects.
"""
stmt = (select(Show)
.distinct()
.join(Season, Show.id == Season.show_id)
.join(SeasonFile, Season.id == SeasonFile.season_id)
.join(Torrent, SeasonFile.torrent_id == Torrent.id)
.options(joinedload(Show.seasons).joinedload(Season.episodes))
.order_by(Show.name))
stmt = (
select(Show)
.distinct()
.join(Season, Show.id == Season.show_id)
.join(SeasonFile, Season.id == SeasonFile.season_id)
.join(Torrent, SeasonFile.torrent_id == Torrent.id)
.options(joinedload(Show.seasons).joinedload(Season.episodes))
.order_by(Show.name)
)
results = db.execute(stmt).scalars().unique().all()
@@ -207,16 +272,20 @@ def get_all_shows_with_torrents(db: Session) -> list[ShowSchema]:
def get_seasons_by_torrent_id(db: Session, torrent_id: TorrentId) -> list[SeasonNumber]:
stmt = (select(Season.number)
.distinct()
.join(SeasonFile, SeasonFile.torrent_id == Torrent.id)
.join(Season, Season.id == SeasonFile.season_id).where(
Torrent.id == torrent_id).select_from(Torrent))
stmt = (
select(Season.number)
.distinct()
.join(SeasonFile, SeasonFile.torrent_id == Torrent.id)
.join(Season, Season.id == SeasonFile.season_id)
.where(Torrent.id == torrent_id)
.select_from(Torrent)
)
result = db.execute(stmt).scalars().unique().all()
return [SeasonNumber(x) for x in result]
def get_season_request(db: Session, season_request_id: SeasonRequestId) -> SeasonRequestSchema:
def get_season_request(
db: Session, season_request_id: SeasonRequestId
) -> SeasonRequestSchema:
return SeasonRequestSchema.model_validate(db.get(SeasonRequest, season_request_id))

View File

@@ -1,4 +1,3 @@
import logging
from typing import Annotated
from fastapi import APIRouter, Depends, status
@@ -15,8 +14,19 @@ from metadataProvider.schemas import MetaDataProviderShowSearchResult
from torrent.schemas import Torrent
from tv import log
from tv.exceptions import MediaAlreadyExists
from tv.schemas import Show, SeasonRequest, ShowId, RichShowTorrent, PublicShow, PublicSeasonFile, SeasonNumber, \
CreateSeasonRequest, SeasonRequestId, UpdateSeasonRequest, RichSeasonRequest
from tv.schemas import (
Show,
SeasonRequest,
ShowId,
RichShowTorrent,
PublicShow,
PublicSeasonFile,
SeasonNumber,
CreateSeasonRequest,
SeasonRequestId,
UpdateSeasonRequest,
RichSeasonRequest,
)
router = APIRouter()
@@ -25,18 +35,38 @@ router = APIRouter()
# CREATE AND DELETE SHOWS
# --------------------------------
@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"}, })
@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_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, )
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 JSONResponse(
status_code=status.HTTP_409_CONFLICT, content={"message": str(e)}
)
return show
@router.delete("/shows/{show_id}", status_code=status.HTTP_200_OK, dependencies=[Depends(current_active_user)])
@router.delete(
"/shows/{show_id}",
status_code=status.HTTP_200_OK,
dependencies=[Depends(current_active_user)],
)
def delete_a_show(db: DbSessionDependency, show_id: ShowId):
db.delete(db.get(Show, show_id))
db.commit()
@@ -46,15 +76,26 @@ def delete_a_show(db: DbSessionDependency, show_id: ShowId):
# GET SHOW INFORMATION
# --------------------------------
@router.get("/shows", dependencies=[Depends(current_active_user)], response_model=list[Show])
def get_all_shows(db: DbSessionDependency, external_id: int = None, metadata_provider: str = "tmdb"):
@router.get(
"/shows", dependencies=[Depends(current_active_user)], response_model=list[Show]
)
def get_all_shows(
db: DbSessionDependency, external_id: int = None, metadata_provider: str = "tmdb"
):
if external_id is not None:
return tv.service.get_show_by_external_id(db=db, external_id=external_id, metadata_provider=metadata_provider)
return tv.service.get_show_by_external_id(
db=db, external_id=external_id, metadata_provider=metadata_provider
)
else:
return tv.service.get_all_shows(db=db)
@router.get("/shows/torrents", dependencies=[Depends(current_active_user)], response_model=list[RichShowTorrent])
@router.get(
"/shows/torrents",
dependencies=[Depends(current_active_user)],
response_model=list[RichShowTorrent],
)
def get_shows_with_torrents(db: DbSessionDependency):
"""
get all shows that are associated with torrents
@@ -64,30 +105,51 @@ def get_shows_with_torrents(db: DbSessionDependency):
return result
@router.get("/shows/{show_id}", dependencies=[Depends(current_active_user)], response_model=PublicShow)
@router.get(
"/shows/{show_id}",
dependencies=[Depends(current_active_user)],
response_model=PublicShow,
)
def get_a_show(db: DbSessionDependency, show_id: ShowId):
return tv.service.get_public_show_by_id(db=db, show_id=show_id)
@router.get("/shows/{show_id}/torrents", dependencies=[Depends(current_active_user)], response_model=RichShowTorrent)
@router.get(
"/shows/{show_id}/torrents",
dependencies=[Depends(current_active_user)],
response_model=RichShowTorrent,
)
def get_a_shows_torrents(db: DbSessionDependency, show_id: ShowId):
return tv.service.get_torrents_for_show(db=db, show=tv.service.get_show_by_id(db=db, show_id=show_id))
return tv.service.get_torrents_for_show(
db=db, show=tv.service.get_show_by_id(db=db, show_id=show_id)
)
# TODO: replace by route with season_id rather than show_id and season_number
@router.get("/shows/{show_id}/{season_number}/files", status_code=status.HTTP_200_OK,
dependencies=[Depends(current_active_user)])
def get_season_files(db: DbSessionDependency, season_number: SeasonNumber, show_id: ShowId) -> list[PublicSeasonFile]:
return tv.service.get_public_season_files_by_season_number(db=db, season_number=season_number, show_id=show_id)
@router.get(
"/shows/{show_id}/{season_number}/files",
status_code=status.HTTP_200_OK,
dependencies=[Depends(current_active_user)],
)
def get_season_files(
db: DbSessionDependency, season_number: SeasonNumber, show_id: ShowId
) -> list[PublicSeasonFile]:
return tv.service.get_public_season_files_by_season_number(
db=db, season_number=season_number, show_id=show_id
)
# --------------------------------
# MANAGE REQUESTS
# --------------------------------
@router.post("/seasons/requests", status_code=status.HTTP_204_NO_CONTENT)
def request_a_season(db: DbSessionDependency, user: Annotated[User, Depends(current_active_user)],
season_request: CreateSeasonRequest):
def request_a_season(
db: DbSessionDependency,
user: Annotated[User, Depends(current_active_user)],
season_request: CreateSeasonRequest,
):
"""
adds request flag to a season
"""
@@ -100,33 +162,54 @@ def request_a_season(db: DbSessionDependency, user: Annotated[User, Depends(curr
return
@router.get("/seasons/requests", status_code=status.HTTP_200_OK, dependencies=[Depends(current_active_user)],
response_model=list[RichSeasonRequest])
@router.get(
"/seasons/requests",
status_code=status.HTTP_200_OK,
dependencies=[Depends(current_active_user)],
response_model=list[RichSeasonRequest],
)
def get_season_requests(db: DbSessionDependency) -> list[RichSeasonRequest]:
return tv.service.get_all_season_requests(db=db)
@router.delete("/seasons/requests/{request_id}", status_code=status.HTTP_204_NO_CONTENT, )
def delete_season_request(db: DbSessionDependency, user: Annotated[User, Depends(current_active_user)],
request_id: SeasonRequestId):
@router.delete(
"/seasons/requests/{request_id}",
status_code=status.HTTP_204_NO_CONTENT,
)
def delete_season_request(
db: DbSessionDependency,
user: Annotated[User, Depends(current_active_user)],
request_id: SeasonRequestId,
):
request = tv.service.get_season_request_by_id(db=db, season_request_id=request_id)
if user.is_superuser or request.requested_by.id == user.id:
tv.service.delete_season_request(db=db, season_request_id=request_id)
log.info(f"User {user.id} deleted season request {request_id}.")
else:
log.warning(f"User {user.id} tried to delete season request {request_id} but is not authorized.")
return JSONResponse(status_code=status.HTTP_403_FORBIDDEN,
content={"message": "Not authorized to delete this request."})
log.warning(
f"User {user.id} tried to delete season request {request_id} but is not authorized."
)
return JSONResponse(
status_code=status.HTTP_403_FORBIDDEN,
content={"message": "Not authorized to delete this request."},
)
@router.patch("/seasons/requests/{season_request_id}", status_code=status.HTTP_204_NO_CONTENT)
def authorize_request(db: DbSessionDependency, user: Annotated[User, Depends(current_superuser)],
season_request_id: SeasonRequestId, authorized_status: bool = False):
@router.patch(
"/seasons/requests/{season_request_id}", status_code=status.HTTP_204_NO_CONTENT
)
def authorize_request(
db: DbSessionDependency,
user: Annotated[User, Depends(current_superuser)],
season_request_id: SeasonRequestId,
authorized_status: bool = False,
):
"""
updates the request flag to true
"""
season_request: SeasonRequest = tv.repository.get_season_request(db=db, season_request_id=season_request_id)
season_request: SeasonRequest = tv.repository.get_season_request(
db=db, season_request_id=season_request_id
)
season_request.authorized_by = UserRead.model_validate(user)
season_request.authorized = authorized_status
if not authorized_status:
@@ -136,48 +219,92 @@ def authorize_request(db: DbSessionDependency, user: Annotated[User, Depends(cur
@router.put("/seasons/requests", status_code=status.HTTP_204_NO_CONTENT)
def update_request(db: DbSessionDependency, user: Annotated[User, Depends(current_active_user)],
season_request: UpdateSeasonRequest):
def update_request(
db: DbSessionDependency,
user: Annotated[User, Depends(current_active_user)],
season_request: UpdateSeasonRequest,
):
season_request: SeasonRequest = SeasonRequest.model_validate(season_request)
request = tv.service.get_season_request_by_id(db=db, season_request_id=season_request.id)
request = tv.service.get_season_request_by_id(
db=db, season_request_id=season_request.id
)
if request.requested_by.id == user.id or user.is_superuser:
season_request.requested_by = UserRead.model_validate(user)
tv.service.update_season_request(db=db, season_request=season_request)
return
# --------------------------------
# MANAGE TORRENTS
# --------------------------------
# 1 is the default for season_number because it returns multi season torrents
@router.get("/torrents", status_code=status.HTTP_200_OK, dependencies=[Depends(current_superuser)],
response_model=list[PublicIndexerQueryResult])
def get_torrents_for_a_season(db: DbSessionDependency, show_id: ShowId, season_number: int = 1,
search_query_override: str = None):
return tv.service.get_all_available_torrents_for_a_season(db=db, season_number=season_number, show_id=show_id,
search_query_override=search_query_override)
@router.get(
"/torrents",
status_code=status.HTTP_200_OK,
dependencies=[Depends(current_superuser)],
response_model=list[PublicIndexerQueryResult],
)
def get_torrents_for_a_season(
db: DbSessionDependency,
show_id: ShowId,
season_number: int = 1,
search_query_override: str = None,
):
return tv.service.get_all_available_torrents_for_a_season(
db=db,
season_number=season_number,
show_id=show_id,
search_query_override=search_query_override,
)
# download a torrent
@router.post("/torrents", status_code=status.HTTP_200_OK, response_model=Torrent,
dependencies=[Depends(current_superuser)])
def download_a_torrent(db: DbSessionDependency, public_indexer_result_id: IndexerQueryResultId, show_id: ShowId,
override_file_path_suffix: str = ""):
return tv.service.download_torrent(db=db, public_indexer_result_id=public_indexer_result_id, show_id=show_id,
override_show_file_path_suffix=override_file_path_suffix)
@router.post(
"/torrents",
status_code=status.HTTP_200_OK,
response_model=Torrent,
dependencies=[Depends(current_superuser)],
)
def download_a_torrent(
db: DbSessionDependency,
public_indexer_result_id: IndexerQueryResultId,
show_id: ShowId,
override_file_path_suffix: str = "",
):
return tv.service.download_torrent(
db=db,
public_indexer_result_id=public_indexer_result_id,
show_id=show_id,
override_show_file_path_suffix=override_file_path_suffix,
)
# --------------------------------
# SEARCH SHOWS ON METADATA PROVIDERS
# --------------------------------
@router.get("/search", dependencies=[Depends(current_active_user)],
response_model=list[MetaDataProviderShowSearchResult])
def search_metadata_providers_for_a_show(db: DbSessionDependency, query: str, metadata_provider: str = "tmdb"):
return tv.service.search_for_show(query=query, metadata_provider=metadata_provider, db=db)
@router.get(
"/search",
dependencies=[Depends(current_active_user)],
response_model=list[MetaDataProviderShowSearchResult],
)
def search_metadata_providers_for_a_show(
db: DbSessionDependency, query: str, metadata_provider: str = "tmdb"
):
return tv.service.search_for_show(
query=query, metadata_provider=metadata_provider, db=db
)
@router.get("/recommended", dependencies=[Depends(current_active_user)],
response_model=list[MetaDataProviderShowSearchResult])
def search_metadata_providers_for_a_show(db: DbSessionDependency, metadata_provider: str = "tmdb"):
@router.get(
"/recommended",
dependencies=[Depends(current_active_user)],
response_model=list[MetaDataProviderShowSearchResult],
)
def search_metadata_providers_for_a_show(
db: DbSessionDependency, metadata_provider: str = "tmdb"
):
return tv.service.get_popular_shows(metadata_provider=metadata_provider, db=db)

View File

@@ -17,6 +17,7 @@ SeasonNumber = typing.NewType("SeasonNumber", int)
EpisodeNumber = typing.NewType("EpisodeNumber", int)
SeasonRequestId = typing.NewType("SeasonRequestId", UUID)
class Episode(BaseModel):
model_config = ConfigDict(from_attributes=True)

View File

@@ -14,15 +14,35 @@ from torrent.service import TorrentService
from tv import log
from tv.exceptions import MediaAlreadyExists
from tv.repository import add_season_file, get_season_files_by_season_id
from tv.schemas import Show, ShowId, SeasonRequest, SeasonFile, SeasonId, Season, RichShowTorrent, RichSeasonTorrent, \
PublicSeason, PublicShow, PublicSeasonFile, SeasonNumber, SeasonRequestId, RichSeasonRequest
from tv.schemas import (
Show,
ShowId,
SeasonRequest,
SeasonFile,
SeasonId,
Season,
RichShowTorrent,
RichSeasonTorrent,
PublicSeason,
PublicShow,
PublicSeasonFile,
SeasonNumber,
SeasonRequestId,
RichSeasonRequest,
)
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)
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
@@ -31,7 +51,9 @@ def add_season_request(db: Session, season_request: SeasonRequest) -> None:
tv.repository.add_season_request(db=db, season_request=season_request)
def get_season_request_by_id(db: Session, season_request_id: SeasonRequestId) -> SeasonRequest | None:
def get_season_request_by_id(
db: Session, season_request_id: SeasonRequestId
) -> SeasonRequest | None:
return tv.repository.get_season_request(db=db, season_request_id=season_request_id)
@@ -44,7 +66,9 @@ def delete_season_request(db: Session, season_request_id: SeasonRequestId) -> No
tv.repository.delete_season_request(db=db, season_request_id=season_request_id)
def get_public_season_files_by_season_id(db: Session, season_id: SeasonId) -> list[PublicSeasonFile]:
def get_public_season_files_by_season_id(
db: Session, season_id: SeasonId
) -> list[PublicSeasonFile]:
season_files = get_season_files_by_season_id(db=db, season_id=season_id)
public_season_files = [PublicSeasonFile.model_validate(x) for x in season_files]
result = []
@@ -55,18 +79,25 @@ def get_public_season_files_by_season_id(db: Session, season_id: SeasonId) -> li
return result
def get_public_season_files_by_season_number(db: Session, season_number: SeasonNumber, show_id: ShowId) -> list[
PublicSeasonFile]:
season = tv.repository.get_season_by_number(db=db, season_number=season_number, show_id=show_id)
def get_public_season_files_by_season_number(
db: Session, season_number: SeasonNumber, show_id: ShowId
) -> list[PublicSeasonFile]:
season = tv.repository.get_season_by_number(
db=db, season_number=season_number, show_id=show_id
)
return get_public_season_files_by_season_id(db=db, season_id=season.id)
def check_if_show_exists(db: Session,
external_id: int = None,
metadata_provider: str = None,
show_id: ShowId = None) -> bool:
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):
if tv.repository.get_show_by_external_id(
external_id=external_id, metadata_provider=metadata_provider, db=db
):
return True
else:
return False
@@ -76,22 +107,30 @@ def check_if_show_exists(db: Session,
else:
return False
else:
raise ValueError("External ID and metadata provider or Show ID must be provided")
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,
search_query_override: str = None) -> list[
IndexerQueryResult]:
log.debug(f"getting all available torrents for season {season_number} for show {show_id}")
def get_all_available_torrents_for_a_season(
db: Session, season_number: int, show_id: ShowId, search_query_override: str = None
) -> 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)
if search_query_override:
search_query = search_query_override
else:
# 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] = indexer.service.search(query=search_query, db=db)
torrents: list[IndexerQueryResult] = indexer.service.search(
query=search_query, db=db
)
if search_query_override:
log.debug(f"Found with search query override {torrents.__len__()} torrents: {torrents}")
log.debug(
f"Found with search query override {torrents.__len__()} torrents: {torrents}"
)
return torrents
result: list[IndexerQueryResult] = []
for torrent in torrents:
@@ -105,19 +144,27 @@ def get_all_shows(db: Session) -> list[Show]:
return tv.repository.get_shows(db=db)
def search_for_show(query: str, metadata_provider: str, db: Session) -> list[MetaDataProviderShowSearchResult]:
def search_for_show(
query: str, metadata_provider: str, db: Session
) -> list[MetaDataProviderShowSearchResult]:
results = metadataProvider.search_show(query, metadata_provider)
for result in results:
if check_if_show_exists(db=db, external_id=result.external_id, metadata_provider=metadata_provider):
if check_if_show_exists(
db=db, external_id=result.external_id, metadata_provider=metadata_provider
):
result.added = True
return results
def get_popular_shows(metadata_provider: str, db: Session):
results: list[MetaDataProviderShowSearchResult] = metadataProvider.search_show(provider=metadata_provider)
results: list[MetaDataProviderShowSearchResult] = metadataProvider.search_show(
provider=metadata_provider
)
for result in results:
if check_if_show_exists(db=db, external_id=result.external_id, metadata_provider=metadata_provider):
if check_if_show_exists(
db=db, external_id=result.external_id, metadata_provider=metadata_provider
):
results.pop(results.index(result))
return results
@@ -149,15 +196,21 @@ def season_file_exists_on_file(db: Session, season_file: SeasonFile) -> bool:
if season_file.torrent_id is None:
return True
else:
torrent_file = torrent.repository.get_torrent_by_id(db=db, torrent_id=season_file.torrent_id)
torrent_file = torrent.repository.get_torrent_by_id(
db=db, torrent_id=season_file.torrent_id
)
if torrent_file.imported:
return True
return False
def get_show_by_external_id(db: Session, external_id: int, metadata_provider: str) -> Show | None:
return tv.repository.get_show_by_external_id(external_id=external_id, metadata_provider=metadata_provider, db=db)
def get_show_by_external_id(
db: Session, external_id: int, metadata_provider: str
) -> Show | None:
return tv.repository.get_show_by_external_id(
external_id=external_id, metadata_provider=metadata_provider, db=db
)
def get_season(db: Session, season_id: SeasonId) -> Season:
@@ -172,17 +225,28 @@ def get_torrents_for_show(db: Session, show: Show) -> RichShowTorrent:
show_torrents = tv.repository.get_torrents_by_show_id(db=db, show_id=show.id)
rich_season_torrents = []
for show_torrent in show_torrents:
seasons = tv.repository.get_seasons_by_torrent_id(db=db, torrent_id=show_torrent.id)
seasons = tv.repository.get_seasons_by_torrent_id(
db=db, torrent_id=show_torrent.id
)
season_files = get_seasons_files_of_torrent(db=db, torrent_id=show_torrent.id)
file_path_suffix = season_files[0].file_path_suffix
season_torrent = RichSeasonTorrent(torrent_id=show_torrent.id, torrent_title=show_torrent.title,
status=show_torrent.status, quality=show_torrent.quality,
imported=show_torrent.imported, seasons=seasons,
file_path_suffix=file_path_suffix
)
season_torrent = RichSeasonTorrent(
torrent_id=show_torrent.id,
torrent_title=show_torrent.title,
status=show_torrent.status,
quality=show_torrent.quality,
imported=show_torrent.imported,
seasons=seasons,
file_path_suffix=file_path_suffix,
)
rich_season_torrents.append(season_torrent)
return RichShowTorrent(show_id=show.id, name=show.name, year=show.year,
metadata_provider=show.metadata_provider, torrents=rich_season_torrents)
return RichShowTorrent(
show_id=show.id,
name=show.name,
year=show.year,
metadata_provider=show.metadata_provider,
torrents=rich_season_torrents,
)
def get_all_shows_with_torrents(db: Session) -> list[RichShowTorrent]:
@@ -190,14 +254,26 @@ def get_all_shows_with_torrents(db: Session) -> list[RichShowTorrent]:
return [get_torrents_for_show(show=show, db=db) for show in shows]
def download_torrent(db: Session, public_indexer_result_id: IndexerQueryResultId, show_id: ShowId,
override_show_file_path_suffix: str = "") -> Torrent:
indexer_result = indexer.service.get_indexer_query_result(db=db, result_id=public_indexer_result_id)
def download_torrent(
db: Session,
public_indexer_result_id: IndexerQueryResultId,
show_id: ShowId,
override_show_file_path_suffix: str = "",
) -> Torrent:
indexer_result = indexer.service.get_indexer_query_result(
db=db, result_id=public_indexer_result_id
)
show_torrent = TorrentService(db=db).download(indexer_result=indexer_result)
for season_number in indexer_result.season:
season = tv.repository.get_season_by_number(db=db, season_number=season_number, show_id=show_id)
season_file = SeasonFile(season_id=season.id, quality=indexer_result.quality, torrent_id=show_torrent.id,
file_path_suffix=override_show_file_path_suffix)
season = tv.repository.get_season_by_number(
db=db, season_number=season_number, show_id=show_id
)
season_file = SeasonFile(
season_id=season.id,
quality=indexer_result.quality,
torrent_id=show_torrent.id,
file_path_suffix=override_show_file_path_suffix,
)
add_season_file(db=db, season_file=season_file)
return show_torrent