mirror of
https://github.com/maxdorninger/MediaManager.git
synced 2026-04-17 15:13:24 +02:00
refactor: reformat code
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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]:
|
||||
"""
|
||||
|
||||
@@ -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 []
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
class AbstractMetadataProvider(ABC):
|
||||
storage_path = config.BasicConfig().image_directory
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def name(self) -> str:
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user