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):
|
class AuthConfig(BaseSettings):
|
||||||
# to get a signing key run:
|
# to get a signing key run:
|
||||||
# openssl rand -hex 32
|
# openssl rand -hex 32
|
||||||
model_config = SettingsConfigDict(env_prefix='AUTH_')
|
model_config = SettingsConfigDict(env_prefix="AUTH_")
|
||||||
token_secret: str
|
token_secret: str
|
||||||
session_lifetime: int = 60 * 60 * 24
|
session_lifetime: int = 60 * 60 * 24
|
||||||
admin_email: str | list[str]
|
admin_email: str | list[str]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def jwt_signing_key(self):
|
def jwt_signing_key(self):
|
||||||
return self._jwt_signing_key
|
return self._jwt_signing_key
|
||||||
|
|
||||||
|
|
||||||
class OAuth2Config(BaseSettings):
|
class OAuth2Config(BaseSettings):
|
||||||
model_config = SettingsConfigDict(env_prefix='OAUTH_')
|
model_config = SettingsConfigDict(env_prefix="OAUTH_")
|
||||||
client_id: str
|
client_id: str
|
||||||
client_secret: str
|
client_secret: str
|
||||||
authorize_endpoint: str
|
authorize_endpoint: str
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
|
|
||||||
from fastapi import Depends
|
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.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||||
from sqlalchemy.orm import Mapped, relationship
|
from sqlalchemy.orm import Mapped, relationship
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,11 @@ if oauth_enabled:
|
|||||||
oauth_config = OAuth2Config()
|
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]:
|
def get_all_users(db: DbSessionDependency) -> list[UserRead]:
|
||||||
stmt = select(User)
|
stmt = select(User)
|
||||||
result = db.execute(stmt).scalars().unique()
|
result = db.execute(stmt).scalars().unique()
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin, models
|
|||||||
from fastapi_users.authentication import (
|
from fastapi_users.authentication import (
|
||||||
AuthenticationBackend,
|
AuthenticationBackend,
|
||||||
BearerTransport,
|
BearerTransport,
|
||||||
CookieTransport, JWTStrategy,
|
CookieTransport,
|
||||||
|
JWTStrategy,
|
||||||
)
|
)
|
||||||
from fastapi_users.db import SQLAlchemyUserDatabase
|
from fastapi_users.db import SQLAlchemyUserDatabase
|
||||||
from httpx_oauth.oauth2 import OAuth2
|
from httpx_oauth.oauth2 import OAuth2
|
||||||
@@ -34,15 +35,17 @@ class GenericOAuth2(OAuth2):
|
|||||||
userinfo_endpoint = self.user_info_endpoint
|
userinfo_endpoint = self.user_info_endpoint
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
resp = await client.get(
|
resp = await client.get(
|
||||||
userinfo_endpoint,
|
userinfo_endpoint, headers={"Authorization": f"Bearer {token}"}
|
||||||
headers={"Authorization": f"Bearer {token}"}
|
|
||||||
)
|
)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
return data["sub"], data["email"]
|
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()
|
oauth2_config = OAuth2Config()
|
||||||
oauth_client = GenericOAuth2(
|
oauth_client = GenericOAuth2(
|
||||||
client_id=oauth2_config.client_id,
|
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}")
|
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.")
|
print(f"User {user.id} has reset their password.")
|
||||||
|
|
||||||
async def on_after_request_verify(
|
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}")
|
print(f"Verification requested for user {user.id}. Verification token: {token}")
|
||||||
|
|
||||||
async def on_after_verify(
|
async def on_after_verify(self, user: User, request: Optional[Request] = None):
|
||||||
self, user: User, request: Optional[Request] = None
|
|
||||||
):
|
|
||||||
print(f"User {user.id} has been verified")
|
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
|
# thus the user would be stuck on the OAuth Providers "redirecting" page
|
||||||
class RedirectingCookieTransport(CookieTransport):
|
class RedirectingCookieTransport(CookieTransport):
|
||||||
async def get_login_response(self, token: str) -> Response:
|
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)
|
return self._set_login_cookie(response, token)
|
||||||
|
|
||||||
|
|
||||||
@@ -122,7 +128,11 @@ oauth_cookie_auth_backend = AuthenticationBackend(
|
|||||||
get_strategy=get_jwt_strategy,
|
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_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__)
|
log = logging.getLogger(__name__)
|
||||||
config = DbConfig()
|
config = DbConfig()
|
||||||
|
|
||||||
db_url = "postgresql+psycopg" + "://" + config.USER + ":" + config.PASSWORD + "@" + config.HOST + ":" + str(
|
db_url = (
|
||||||
config.PORT) + "/" + config.DBNAME
|
"postgresql+psycopg"
|
||||||
|
+ "://"
|
||||||
|
+ config.USER
|
||||||
|
+ ":"
|
||||||
|
+ config.PASSWORD
|
||||||
|
+ "@"
|
||||||
|
+ config.HOST
|
||||||
|
+ ":"
|
||||||
|
+ str(config.PORT)
|
||||||
|
+ "/"
|
||||||
|
+ config.DBNAME
|
||||||
|
)
|
||||||
|
|
||||||
engine = create_engine(db_url, echo=False)
|
engine = create_engine(db_url, echo=False)
|
||||||
log.debug("initializing sqlalchemy declarative base")
|
log.debug("initializing sqlalchemy declarative base")
|
||||||
@@ -21,7 +32,6 @@ Base = declarative_base()
|
|||||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def init_db() -> None:
|
def init_db() -> None:
|
||||||
log.debug("initializing database with following tables")
|
log.debug("initializing database with following tables")
|
||||||
for table in Base.metadata.tables:
|
for table in Base.metadata.tables:
|
||||||
@@ -41,7 +51,7 @@ def get_session() -> Generator[Session, Any, None]:
|
|||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
db_session: ContextVar[Session] = ContextVar('db_session')
|
db_session: ContextVar[Session] = ContextVar("db_session")
|
||||||
|
|
||||||
|
|
||||||
DbSessionDependency = Annotated[Session, Depends(get_session)]
|
DbSessionDependency = Annotated[Session, Depends(get_session)]
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
|||||||
|
|
||||||
|
|
||||||
class DbConfig(BaseSettings):
|
class DbConfig(BaseSettings):
|
||||||
model_config = SettingsConfigDict(env_prefix='DB_')
|
model_config = SettingsConfigDict(env_prefix="DB_")
|
||||||
HOST: str = "localhost"
|
HOST: str = "localhost"
|
||||||
PORT: int = 5432
|
PORT: int = 5432
|
||||||
USER: str = "MediaManager"
|
USER: str = "MediaManager"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class GenericIndexer(object):
|
|||||||
if name:
|
if name:
|
||||||
self.name = name
|
self.name = name
|
||||||
else:
|
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]:
|
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 api_key: The API key for authenticating requests to Prowlarr.
|
||||||
:param kwargs: Additional keyword arguments to pass to the superclass constructor.
|
:param kwargs: Additional keyword arguments to pass to the superclass constructor.
|
||||||
"""
|
"""
|
||||||
super().__init__(name='prowlarr')
|
super().__init__(name="prowlarr")
|
||||||
config = ProwlarrConfig()
|
config = ProwlarrConfig()
|
||||||
self.api_key = config.api_key
|
self.api_key = config.api_key
|
||||||
self.url = config.url
|
self.url = config.url
|
||||||
@@ -25,31 +25,29 @@ class Prowlarr(GenericIndexer):
|
|||||||
|
|
||||||
def get_search_results(self, query: str) -> list[IndexerQueryResult]:
|
def get_search_results(self, query: str) -> list[IndexerQueryResult]:
|
||||||
log.debug("Searching for " + query)
|
log.debug("Searching for " + query)
|
||||||
url = self.url + '/api/v1/search'
|
url = self.url + "/api/v1/search"
|
||||||
headers = {
|
headers = {"accept": "application/json", "X-Api-Key": self.api_key}
|
||||||
'accept': 'application/json',
|
|
||||||
'X-Api-Key': self.api_key
|
|
||||||
}
|
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
'query': query,
|
"query": query,
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.get(url, headers=headers, params=params)
|
response = requests.get(url, headers=headers, params=params)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
result_list: list[IndexerQueryResult] = []
|
result_list: list[IndexerQueryResult] = []
|
||||||
for result in response.json():
|
for result in response.json():
|
||||||
if result['protocol'] == 'torrent':
|
if result["protocol"] == "torrent":
|
||||||
log.debug("torrent result: " + result.__str__())
|
log.debug("torrent result: " + result.__str__())
|
||||||
result_list.append(
|
result_list.append(
|
||||||
IndexerQueryResult(
|
IndexerQueryResult(
|
||||||
download_url=result['downloadUrl'],
|
download_url=result["downloadUrl"],
|
||||||
title=result['sortTitle'],
|
title=result["sortTitle"],
|
||||||
seeders=result['seeders'],
|
seeders=result["seeders"],
|
||||||
flags=result['indexerFlags'],
|
flags=result["indexerFlags"],
|
||||||
size=result['size'], )
|
size=result["size"],
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return result_list
|
return result_list
|
||||||
else:
|
else:
|
||||||
log.error(f'Prowlarr Error: {response.status_code}')
|
log.error(f"Prowlarr Error: {response.status_code}")
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from backend.src.torrent.schemas import Quality
|
|||||||
|
|
||||||
|
|
||||||
class IndexerQueryResult(Base):
|
class IndexerQueryResult(Base):
|
||||||
__tablename__ = 'indexer_query_result'
|
__tablename__ = "indexer_query_result"
|
||||||
id: Mapped[IndexerQueryResultId] = mapped_column(primary_key=True)
|
id: Mapped[IndexerQueryResultId] = mapped_column(primary_key=True)
|
||||||
title: Mapped[str]
|
title: Mapped[str]
|
||||||
download_url: Mapped[str]
|
download_url: Mapped[str]
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from indexer.models import IndexerQueryResult
|
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:
|
def get_result(
|
||||||
return IndexerQueryResultSchema.model_validate(db.get(IndexerQueryResult, result_id))
|
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()))
|
db.add(IndexerQueryResult(**result.model_dump()))
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from pydantic import BaseModel, computed_field, ConfigDict
|
|||||||
|
|
||||||
from backend.src.torrent.models import Quality
|
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
|
# TODO: use something like strategy pattern to make sorting more user customizable
|
||||||
@@ -24,10 +24,10 @@ class IndexerQueryResult(BaseModel):
|
|||||||
@computed_field(return_type=Quality)
|
@computed_field(return_type=Quality)
|
||||||
@property
|
@property
|
||||||
def quality(self) -> Quality:
|
def quality(self) -> Quality:
|
||||||
high_quality_pattern = r'\b(4k|4K)\b'
|
high_quality_pattern = r"\b(4k|4K)\b"
|
||||||
medium_quality_pattern = r'\b(1080p|1080P)\b'
|
medium_quality_pattern = r"\b(1080p|1080P)\b"
|
||||||
low_quality_pattern = r'\b(720p|720P)\b'
|
low_quality_pattern = r"\b(720p|720P)\b"
|
||||||
very_low_quality_pattern = r'\b(480p|480P|360p|360P)\b'
|
very_low_quality_pattern = r"\b(480p|480P|360p|360P)\b"
|
||||||
|
|
||||||
if re.search(high_quality_pattern, self.title):
|
if re.search(high_quality_pattern, self.title):
|
||||||
return Quality.high
|
return Quality.high
|
||||||
|
|||||||
@@ -19,5 +19,7 @@ def search(query: str, db: Session) -> list[IndexerQueryResult]:
|
|||||||
return results
|
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)
|
return indexer.repository.get_result(result_id=result_id, db=db)
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ LOGGING_CONFIG = {
|
|||||||
},
|
},
|
||||||
"json": {
|
"json": {
|
||||||
"()": JsonFormatter,
|
"()": JsonFormatter,
|
||||||
}
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
"handlers": {
|
"handlers": {
|
||||||
"console": {
|
"console": {
|
||||||
@@ -29,7 +28,7 @@ LOGGING_CONFIG = {
|
|||||||
"filename": "./log.txt",
|
"filename": "./log.txt",
|
||||||
"maxBytes": 10485760,
|
"maxBytes": 10485760,
|
||||||
"backupCount": 5,
|
"backupCount": 5,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"loggers": {
|
"loggers": {
|
||||||
"uvicorn": {"handlers": ["console", "file"], "level": "DEBUG"},
|
"uvicorn": {"handlers": ["console", "file"], "level": "DEBUG"},
|
||||||
@@ -39,16 +38,16 @@ LOGGING_CONFIG = {
|
|||||||
}
|
}
|
||||||
dictConfig(LOGGING_CONFIG)
|
dictConfig(LOGGING_CONFIG)
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG,
|
logging.basicConfig(
|
||||||
format="%(asctime)s - %(levelname)s - %(name)s - %(funcName)s(): %(message)s",
|
level=logging.DEBUG,
|
||||||
stream=sys.stdout,
|
format="%(asctime)s - %(levelname)s - %(name)s - %(funcName)s(): %(message)s",
|
||||||
)
|
stream=sys.stdout,
|
||||||
|
)
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
from backend.src.database import init_db
|
from backend.src.database import init_db
|
||||||
import tv.router
|
import tv.router
|
||||||
import torrent.router
|
import torrent.router
|
||||||
import auth.db
|
|
||||||
|
|
||||||
init_db()
|
init_db()
|
||||||
log.info("Database initialized")
|
log.info("Database initialized")
|
||||||
@@ -62,9 +61,15 @@ from fastapi import FastAPI
|
|||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from auth.schemas import UserCreate, UserRead, UserUpdate
|
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 users_router as custom_users_router
|
||||||
from auth.router import auth_metadata_router
|
from auth.router import auth_metadata_router
|
||||||
|
|
||||||
basic_config = BasicConfig()
|
basic_config = BasicConfig()
|
||||||
if basic_config.DEVELOPMENT:
|
if basic_config.DEVELOPMENT:
|
||||||
basic_config.torrent_directory.mkdir(parents=True, exist_ok=True)
|
basic_config.torrent_directory.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -94,12 +99,12 @@ if basic_config.DEVELOPMENT:
|
|||||||
app.include_router(
|
app.include_router(
|
||||||
fastapi_users.get_auth_router(bearer_auth_backend),
|
fastapi_users.get_auth_router(bearer_auth_backend),
|
||||||
prefix="/auth/jwt",
|
prefix="/auth/jwt",
|
||||||
tags=["auth"]
|
tags=["auth"],
|
||||||
)
|
)
|
||||||
app.include_router(
|
app.include_router(
|
||||||
fastapi_users.get_auth_router(cookie_auth_backend),
|
fastapi_users.get_auth_router(cookie_auth_backend),
|
||||||
prefix="/auth/cookie",
|
prefix="/auth/cookie",
|
||||||
tags=["auth"]
|
tags=["auth"],
|
||||||
)
|
)
|
||||||
app.include_router(
|
app.include_router(
|
||||||
fastapi_users.get_register_router(UserRead, UserCreate),
|
fastapi_users.get_register_router(UserRead, UserCreate),
|
||||||
@@ -117,15 +122,9 @@ app.include_router(
|
|||||||
tags=["auth"],
|
tags=["auth"],
|
||||||
)
|
)
|
||||||
# All users route router
|
# All users route router
|
||||||
app.include_router(
|
app.include_router(custom_users_router, tags=["users"])
|
||||||
custom_users_router,
|
|
||||||
tags=["users"]
|
|
||||||
)
|
|
||||||
# OAuth Metadata Router
|
# OAuth Metadata Router
|
||||||
app.include_router(
|
app.include_router(auth_metadata_router, tags=["oauth"])
|
||||||
auth_metadata_router,
|
|
||||||
tags=["oauth"]
|
|
||||||
)
|
|
||||||
# User Router
|
# User Router
|
||||||
app.include_router(
|
app.include_router(
|
||||||
fastapi_users.get_users_router(UserRead, UserUpdate),
|
fastapi_users.get_users_router(UserRead, UserUpdate),
|
||||||
@@ -135,29 +134,26 @@ app.include_router(
|
|||||||
# OAuth2 Routers
|
# OAuth2 Routers
|
||||||
if oauth_client is not None:
|
if oauth_client is not None:
|
||||||
app.include_router(
|
app.include_router(
|
||||||
fastapi_users.get_oauth_router(oauth_client,
|
fastapi_users.get_oauth_router(
|
||||||
oauth_cookie_auth_backend,
|
oauth_client,
|
||||||
auth.users.SECRET,
|
oauth_cookie_auth_backend,
|
||||||
associate_by_email=True,
|
auth.users.SECRET,
|
||||||
is_verified_by_default=True,
|
associate_by_email=True,
|
||||||
),
|
is_verified_by_default=True,
|
||||||
|
),
|
||||||
prefix=f"/auth/cookie/{oauth_client.name}",
|
prefix=f"/auth/cookie/{oauth_client.name}",
|
||||||
tags=["oauth"],
|
tags=["oauth"],
|
||||||
)
|
)
|
||||||
|
|
||||||
app.include_router(
|
app.include_router(tv.router.router, prefix="/tv", tags=["tv"])
|
||||||
tv.router.router,
|
app.include_router(torrent.router.router, prefix="/torrent", tags=["torrent"])
|
||||||
prefix="/tv",
|
|
||||||
tags=["tv"]
|
|
||||||
)
|
|
||||||
app.include_router(
|
|
||||||
torrent.router.router,
|
|
||||||
prefix="/torrent",
|
|
||||||
tags=["torrent"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# static file routers
|
# 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__":
|
if __name__ == "__main__":
|
||||||
uvicorn.run(app, host="127.0.0.1", port=5049, log_config=LOGGING_CONFIG)
|
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)
|
@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.
|
If no query is provided, it will return the most popular shows.
|
||||||
"""
|
"""
|
||||||
return metadata_providers[provider].search_show(query)
|
return metadata_providers[provider].search_show(query)
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class AbstractMetadataProvider(ABC):
|
class AbstractMetadataProvider(ABC):
|
||||||
storage_path = config.BasicConfig().image_directory
|
storage_path = config.BasicConfig().image_directory
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ from pydantic_settings import BaseSettings
|
|||||||
from tmdbsimple import TV, TV_Seasons
|
from tmdbsimple import TV, TV_Seasons
|
||||||
|
|
||||||
import metadataProvider.utils
|
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 metadataProvider.schemas import MetaDataProviderShowSearchResult
|
||||||
from tv.schemas import Episode, Season, Show, SeasonNumber, EpisodeNumber
|
from tv.schemas import Episode, Season, Show, SeasonNumber, EpisodeNumber
|
||||||
|
|
||||||
@@ -35,7 +38,9 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
|
|||||||
season_list = []
|
season_list = []
|
||||||
# inserting all the metadata into the objects
|
# inserting all the metadata into the objects
|
||||||
for season in show_metadata["seasons"]:
|
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 = []
|
episode_list = []
|
||||||
|
|
||||||
for episode in season_metadata["episodes"]:
|
for episode in season_metadata["episodes"]:
|
||||||
@@ -43,7 +48,7 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
|
|||||||
Episode(
|
Episode(
|
||||||
external_id=int(episode["id"]),
|
external_id=int(episode["id"]),
|
||||||
title=episode["name"],
|
title=episode["name"],
|
||||||
number=EpisodeNumber(episode["episode_number"])
|
number=EpisodeNumber(episode["episode_number"]),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,11 +59,12 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
|
|||||||
overview=season_metadata["overview"],
|
overview=season_metadata["overview"],
|
||||||
number=SeasonNumber(season_metadata["season_number"]),
|
number=SeasonNumber(season_metadata["season_number"]),
|
||||||
episodes=episode_list,
|
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(
|
show = Show(
|
||||||
external_id=id,
|
external_id=id,
|
||||||
@@ -72,9 +78,12 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
|
|||||||
# downloading the poster
|
# downloading the poster
|
||||||
# all pictures from TMDB should already be jpeg, so no need to convert
|
# all pictures from TMDB should already be jpeg, so no need to convert
|
||||||
if show_metadata["poster_path"] is not None:
|
if show_metadata["poster_path"] is not None:
|
||||||
poster_url = "https://image.tmdb.org/t/p/original" + show_metadata["poster_path"]
|
poster_url = (
|
||||||
if metadataProvider.utils.download_poster_image(storage_path=self.storage_path, poster_url=poster_url,
|
"https://image.tmdb.org/t/p/original" + show_metadata["poster_path"]
|
||||||
show=show):
|
)
|
||||||
|
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)
|
log.info("Successfully downloaded poster image for show " + show.name)
|
||||||
else:
|
else:
|
||||||
log.warning(f"download for image of show {show.name} failed")
|
log.warning(f"download for image of show {show.name} failed")
|
||||||
@@ -83,7 +92,9 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
|
|||||||
|
|
||||||
return show
|
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.
|
Search for shows using TMDB API.
|
||||||
If no query is provided, it will return the most popular shows.
|
If no query is provided, it will return the most popular shows.
|
||||||
@@ -91,7 +102,9 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
|
|||||||
if query is None:
|
if query is None:
|
||||||
result_factory = lambda page: tmdbsimple.Trending(media_type="tv").info()
|
result_factory = lambda page: tmdbsimple.Trending(media_type="tv").info()
|
||||||
else:
|
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 = []
|
results = []
|
||||||
for i in range(1, max_pages + 1):
|
for i in range(1, max_pages + 1):
|
||||||
@@ -106,7 +119,9 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
|
|||||||
for result in results:
|
for result in results:
|
||||||
try:
|
try:
|
||||||
if result["poster_path"] is not None:
|
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:
|
else:
|
||||||
poster_url = None
|
poster_url = None
|
||||||
formatted_results.append(
|
formatted_results.append(
|
||||||
@@ -115,7 +130,9 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
|
|||||||
overview=result["overview"],
|
overview=result["overview"],
|
||||||
name=result["name"],
|
name=result["name"],
|
||||||
external_id=result["id"],
|
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,
|
metadata_provider=self.name,
|
||||||
added=False,
|
added=False,
|
||||||
vote_average=result["vote_average"],
|
vote_average=result["vote_average"],
|
||||||
@@ -131,4 +148,6 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
|
|||||||
|
|
||||||
if config.TMDB_API_KEY is not None:
|
if config.TMDB_API_KEY is not None:
|
||||||
log.info("Registering TMDB as metadata provider")
|
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
|
from tmdbsimple import TV, TV_Seasons
|
||||||
|
|
||||||
import metadataProvider.utils
|
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 metadataProvider.schemas import MetaDataProviderShowSearchResult
|
||||||
from tv.schemas import Episode, Season, Show, SeasonNumber, EpisodeNumber
|
from tv.schemas import Episode, Season, Show, SeasonNumber, EpisodeNumber
|
||||||
|
|
||||||
@@ -43,27 +46,48 @@ class TvdbMetadataProvider(AbstractMetadataProvider):
|
|||||||
seasons = []
|
seasons = []
|
||||||
for season in series["seasons"]:
|
for season in series["seasons"]:
|
||||||
s = self.tvdb_client.get_season_extended(season["id"])
|
s = self.tvdb_client.get_season_extended(season["id"])
|
||||||
episodes = [Episode(number=episode['number'], external_id=episode['id'], title=episode['name']) for episode
|
episodes = [
|
||||||
in s["episodes"]]
|
Episode(
|
||||||
seasons.append(Season(number=s['number'], name="TVDB doesn't provide Season Names",
|
number=episode["number"],
|
||||||
overview="TVDB doesn't provide Season Overviews", external_id=s['id'],
|
external_id=episode["id"],
|
||||||
episodes=episodes))
|
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:
|
try:
|
||||||
year = series['year']
|
year = series["year"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
year = None
|
year = None
|
||||||
show = Show(name=series['name'], overview=series['overview'], year=year,
|
show = Show(
|
||||||
external_id=series['id'], metadata_provider=self.name, seasons=seasons)
|
name=series["name"],
|
||||||
|
overview=series["overview"],
|
||||||
|
year=year,
|
||||||
|
external_id=series["id"],
|
||||||
|
metadata_provider=self.name,
|
||||||
|
seasons=seasons,
|
||||||
|
)
|
||||||
|
|
||||||
if series["image"] is not None:
|
if series["image"] is not None:
|
||||||
metadataProvider.utils.download_poster_image(storage_path=self.storage_path,
|
metadataProvider.utils.download_poster_image(
|
||||||
poster_url=series['image'], show=show)
|
storage_path=self.storage_path, poster_url=series["image"], show=show
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
log.warning(f"image for show {show.name} could not be downloaded")
|
log.warning(f"image for show {show.name} could not be downloaded")
|
||||||
|
|
||||||
return show
|
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:
|
if query is None:
|
||||||
results = self.tvdb_client.get_all_series()
|
results = self.tvdb_client.get_all_series()
|
||||||
else:
|
else:
|
||||||
@@ -71,9 +95,9 @@ class TvdbMetadataProvider(AbstractMetadataProvider):
|
|||||||
formatted_results = []
|
formatted_results = []
|
||||||
for result in results:
|
for result in results:
|
||||||
try:
|
try:
|
||||||
if result['type'] == 'series':
|
if result["type"] == "series":
|
||||||
try:
|
try:
|
||||||
year = result['year']
|
year = result["year"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
year = None
|
year = None
|
||||||
|
|
||||||
@@ -86,7 +110,7 @@ class TvdbMetadataProvider(AbstractMetadataProvider):
|
|||||||
year=year,
|
year=year,
|
||||||
metadata_provider=self.name,
|
metadata_provider=self.name,
|
||||||
added=False,
|
added=False,
|
||||||
vote_average=None
|
vote_average=None,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -96,7 +120,9 @@ class TvdbMetadataProvider(AbstractMetadataProvider):
|
|||||||
|
|
||||||
if config.TVDB_API_KEY is not None:
|
if config.TVDB_API_KEY is not None:
|
||||||
log.info("Registering TVDB as metadata provider")
|
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__":
|
if __name__ == "__main__":
|
||||||
tvdb = TvdbMetadataProvider(config.TVDB_API_KEY)
|
tvdb = TvdbMetadataProvider(config.TVDB_API_KEY)
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ import mimetypes
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_year_from_first_air_date(first_air_date: str | None) -> int | None:
|
def get_year_from_first_air_date(first_air_date: str | None) -> int | None:
|
||||||
if first_air_date:
|
if first_air_date:
|
||||||
return int(first_air_date.split('-')[0])
|
return int(first_air_date.split("-")[0])
|
||||||
else:
|
else:
|
||||||
return None
|
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"]
|
content_type = res.headers["content-type"]
|
||||||
file_extension = mimetypes.guess_extension(content_type)
|
file_extension = mimetypes.guess_extension(content_type)
|
||||||
if res.status_code == 200:
|
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)
|
f.write(res.content)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ from tv.models import SeasonFile, Show, Season
|
|||||||
from tv.schemas import SeasonFile as SeasonFileSchema, Show as ShowSchema
|
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)
|
stmt = select(SeasonFile).where(SeasonFile.torrent_id == torrent_id)
|
||||||
result = db.execute(stmt).scalars().all()
|
result = db.execute(stmt).scalars().all()
|
||||||
return [SeasonFileSchema.model_validate(season_file) for season_file in result]
|
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:
|
def get_show_of_torrent(db: Session, torrent_id: TorrentId) -> ShowSchema:
|
||||||
stmt = (
|
stmt = (
|
||||||
select(Show).
|
select(Show)
|
||||||
join(SeasonFile.season).
|
.join(SeasonFile.season)
|
||||||
join(Season.show).
|
.join(Season.show)
|
||||||
where(SeasonFile.torrent_id == torrent_id)
|
.where(SeasonFile.torrent_id == torrent_id)
|
||||||
)
|
)
|
||||||
result = db.execute(stmt).unique().scalar_one_or_none()
|
result = db.execute(stmt).unique().scalar_one_or_none()
|
||||||
return ShowSchema.model_validate(result)
|
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)
|
return service.get_torrent_by_id(id=torrent_id)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", status_code=status.HTTP_200_OK, dependencies=[Depends(current_active_user)],
|
@router.get(
|
||||||
response_model=list[Torrent])
|
"/",
|
||||||
|
status_code=status.HTTP_200_OK,
|
||||||
|
dependencies=[Depends(current_active_user)],
|
||||||
|
response_model=list[Torrent],
|
||||||
|
)
|
||||||
def get_all_torrents(service: TorrentServiceDependency):
|
def get_all_torrents(service: TorrentServiceDependency):
|
||||||
return service.get_all_torrents()
|
return service.get_all_torrents()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{torrent_id}", status_code=status.HTTP_200_OK, dependencies=[Depends(current_active_user)],
|
@router.post(
|
||||||
response_model=Torrent)
|
"/{torrent_id}",
|
||||||
|
status_code=status.HTTP_200_OK,
|
||||||
|
dependencies=[Depends(current_active_user)],
|
||||||
|
response_model=Torrent,
|
||||||
|
)
|
||||||
def import_torrent(service: TorrentServiceDependency, torrent_id: TorrentId):
|
def import_torrent(service: TorrentServiceDependency, torrent_id: TorrentId):
|
||||||
return service.import_torrent(service.get_torrent_by_id(id=torrent_id))
|
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)],
|
@router.post(
|
||||||
response_model=list[Torrent])
|
"/",
|
||||||
|
status_code=status.HTTP_200_OK,
|
||||||
|
dependencies=[Depends(current_active_user)],
|
||||||
|
response_model=list[Torrent],
|
||||||
|
)
|
||||||
def import_all_torrents(service: TorrentServiceDependency):
|
def import_all_torrents(service: TorrentServiceDependency):
|
||||||
return service.import_all_torrents()
|
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):
|
def delete_torrent(service: TorrentServiceDependency, torrent_id: TorrentId):
|
||||||
service.delete_torrent(torrent_id=torrent_id)
|
service.delete_torrent(torrent_id=torrent_id)
|
||||||
|
|||||||
@@ -17,16 +17,25 @@ import tv.repository
|
|||||||
import tv.service
|
import tv.service
|
||||||
from config import BasicConfig
|
from config import BasicConfig
|
||||||
from indexer import IndexerQueryResult
|
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.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
|
from tv.schemas import SeasonFile, Show
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TorrentServiceConfig(BaseSettings):
|
class TorrentServiceConfig(BaseSettings):
|
||||||
model_config = SettingsConfigDict(env_prefix='QBITTORRENT_')
|
model_config = SettingsConfigDict(env_prefix="QBITTORRENT_")
|
||||||
host: str = "localhost"
|
host: str = "localhost"
|
||||||
port: int = 8080
|
port: int = 8080
|
||||||
username: str = "admin"
|
username: str = "admin"
|
||||||
@@ -34,9 +43,25 @@ class TorrentServiceConfig(BaseSettings):
|
|||||||
|
|
||||||
|
|
||||||
class TorrentService:
|
class TorrentService:
|
||||||
DOWNLOADING_STATE = ("allocating", "downloading", "metaDL", "pausedDL", "queuedDL", "stalledDL", "checkingDL",
|
DOWNLOADING_STATE = (
|
||||||
"forcedDL", "moving")
|
"allocating",
|
||||||
FINISHED_STATE = ("uploading", "pausedUP", "queuedUP", "stalledUP", "checkingUP", "forcedUP")
|
"downloading",
|
||||||
|
"metaDL",
|
||||||
|
"pausedDL",
|
||||||
|
"queuedDL",
|
||||||
|
"stalledDL",
|
||||||
|
"checkingDL",
|
||||||
|
"forcedDL",
|
||||||
|
"moving",
|
||||||
|
)
|
||||||
|
FINISHED_STATE = (
|
||||||
|
"uploading",
|
||||||
|
"pausedUP",
|
||||||
|
"queuedUP",
|
||||||
|
"stalledUP",
|
||||||
|
"checkingUP",
|
||||||
|
"forcedUP",
|
||||||
|
)
|
||||||
ERROR_STATE = ("missingFiles", "error", "checkingResumeData")
|
ERROR_STATE = ("missingFiles", "error", "checkingResumeData")
|
||||||
UNKNOWN_STATE = ("unknown",)
|
UNKNOWN_STATE = ("unknown",)
|
||||||
api_client = qbittorrentapi.Client(**TorrentServiceConfig().model_dump())
|
api_client = qbittorrentapi.Client(**TorrentServiceConfig().model_dump())
|
||||||
@@ -54,35 +79,42 @@ class TorrentService:
|
|||||||
|
|
||||||
def download(self, indexer_result: IndexerQueryResult) -> Torrent:
|
def download(self, indexer_result: IndexerQueryResult) -> Torrent:
|
||||||
log.info(f"Attempting to download torrent: {indexer_result.title}")
|
log.info(f"Attempting to download torrent: {indexer_result.title}")
|
||||||
torrent = Torrent(status=TorrentStatus.unknown,
|
torrent = Torrent(
|
||||||
title=indexer_result.title,
|
status=TorrentStatus.unknown,
|
||||||
quality=indexer_result.quality,
|
title=indexer_result.title,
|
||||||
imported=False,
|
quality=indexer_result.quality,
|
||||||
hash="")
|
imported=False,
|
||||||
|
hash="",
|
||||||
|
)
|
||||||
|
|
||||||
url = indexer_result.download_url
|
url = indexer_result.download_url
|
||||||
torrent_filepath = BasicConfig().torrent_directory / f"{torrent.title}.torrent"
|
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
|
content = requests.get(url).content
|
||||||
file.write(content)
|
file.write(content)
|
||||||
|
|
||||||
with open(torrent_filepath, 'rb') as file:
|
with open(torrent_filepath, "rb") as file:
|
||||||
content = file.read()
|
content = file.read()
|
||||||
try:
|
try:
|
||||||
decoded_content = bencoder.decode(content)
|
decoded_content = bencoder.decode(content)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"Failed to decode torrent file: {e}")
|
log.error(f"Failed to decode torrent file: {e}")
|
||||||
raise e
|
raise e
|
||||||
torrent.hash = hashlib.sha1(bencoder.encode(decoded_content[b'info'])).hexdigest()
|
torrent.hash = hashlib.sha1(
|
||||||
answer = self.api_client.torrents_add(category="MediaManager", torrent_files=content,
|
bencoder.encode(decoded_content[b"info"])
|
||||||
save_path=torrent.title)
|
).hexdigest()
|
||||||
|
answer = self.api_client.torrents_add(
|
||||||
|
category="MediaManager", torrent_files=content, save_path=torrent.title
|
||||||
|
)
|
||||||
|
|
||||||
if answer == "Ok.":
|
if answer == "Ok.":
|
||||||
log.info(f"Successfully added torrent: {torrent.title}")
|
log.info(f"Successfully added torrent: {torrent.title}")
|
||||||
return self.get_torrent_status(torrent=torrent)
|
return self.get_torrent_status(torrent=torrent)
|
||||||
else:
|
else:
|
||||||
log.error(f"Failed to download torrent. API response: {answer}")
|
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:
|
def get_torrent_status(self, torrent: Torrent) -> Torrent:
|
||||||
log.info(f"Fetching status for torrent: {torrent.title}")
|
log.info(f"Fetching status for torrent: {torrent.title}")
|
||||||
@@ -161,14 +193,25 @@ class TorrentService:
|
|||||||
subtitle_files.append(file)
|
subtitle_files.append(file)
|
||||||
log.debug(f"File is a subtitle, it will be imported: {file}")
|
log.debug(f"File is a subtitle, it will be imported: {file}")
|
||||||
else:
|
else:
|
||||||
log.debug(f"File is neither a video nor a subtitle, will not be imported: {file}")
|
log.debug(
|
||||||
log.info(f"Importing these {len(video_files)} files:\n" + pprint.pformat(video_files))
|
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
|
# Fetch show and season information
|
||||||
show: Show = get_show_of_torrent(db=self.db, torrent_id=torrent.id)
|
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}]"
|
show_file_path = (
|
||||||
season_files: list[SeasonFile] = get_seasons_files_of_torrent(db=self.db, torrent_id=torrent.id)
|
BasicConfig().tv_directory
|
||||||
log.info(f"Found {len(season_files)} season files associated with torrent {torrent.title}")
|
/ 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
|
# creating directories and hard linking files
|
||||||
for season_file in season_files:
|
for season_file in season_files:
|
||||||
@@ -181,52 +224,82 @@ class TorrentService:
|
|||||||
log.warning(f"Path already exists: {season_path}")
|
log.warning(f"Path already exists: {season_path}")
|
||||||
|
|
||||||
for episode in season.episodes:
|
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 != "":
|
if season_file.file_path_suffix != "":
|
||||||
episode_file_name += f" - {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"
|
subtitle_pattern = pattern + r"[.]([A-Za-z]{2})[.]srt"
|
||||||
target_file_name = season_path / episode_file_name
|
target_file_name = season_path / episode_file_name
|
||||||
|
|
||||||
# import subtitles
|
# import subtitles
|
||||||
for subtitle_file in subtitle_files:
|
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)
|
regex_result = re.search(subtitle_pattern, subtitle_file.name)
|
||||||
if regex_result:
|
if regex_result:
|
||||||
language_code = regex_result.group(1)
|
language_code = regex_result.group(1)
|
||||||
log.debug(
|
log.debug(
|
||||||
f"Found matching pattern: {subtitle_pattern} in subtitle file: {subtitle_file.name}," +
|
f"Found matching pattern: {subtitle_pattern} in subtitle file: {subtitle_file.name},"
|
||||||
f" extracted language code: {language_code}")
|
+ 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)
|
target_subtitle_file = target_file_name.with_suffix(
|
||||||
|
f".{language_code}.srt"
|
||||||
|
)
|
||||||
|
import_file(
|
||||||
|
target_file=target_subtitle_file, source_file=subtitle_file
|
||||||
|
)
|
||||||
else:
|
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
|
# import episode videos
|
||||||
for file in video_files:
|
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):
|
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)
|
target_video_file = target_file_name.with_suffix(file.suffix)
|
||||||
import_file(target_file=target_video_file, source_file=file)
|
import_file(target_file=target_video_file, source_file=file)
|
||||||
break
|
break
|
||||||
else:
|
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
|
torrent.imported = True
|
||||||
|
|
||||||
return self.get_torrent_status(torrent=torrent)
|
return self.get_torrent_status(torrent=torrent)
|
||||||
|
|
||||||
def get_all_torrents(self) -> list[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:
|
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):
|
def delete_torrent(self, torrent_id: TorrentId):
|
||||||
t = torrent.repository.get_torrent_by_id(torrent_id=torrent_id, db=self.db)
|
t = torrent.repository.get_torrent_by_id(torrent_id=torrent_id, db=self.db)
|
||||||
if not t.imported:
|
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)
|
torrent.repository.delete_torrent(db=self.db, torrent_id=t.id)
|
||||||
|
|
||||||
@repeat_every(seconds=3600)
|
@repeat_every(seconds=3600)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from torrent.schemas import Torrent
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def list_files_recursively(path: Path = Path(".")) -> list[Path]:
|
def list_files_recursively(path: Path = Path(".")) -> list[Path]:
|
||||||
files = list(path.glob("**/*"))
|
files = list(path.glob("**/*"))
|
||||||
log.debug(f"Found {len(files)} entries via glob")
|
log.debug(f"Found {len(files)} entries via glob")
|
||||||
@@ -28,10 +29,13 @@ def extract_archives(files):
|
|||||||
for file in files:
|
for file in files:
|
||||||
file_type = mimetypes.guess_type(file)
|
file_type = mimetypes.guess_type(file)
|
||||||
log.debug(f"File: {file}, Size: {file.stat().st_size} bytes, Type: {file_type}")
|
log.debug(f"File: {file}, Size: {file.stat().st_size} bytes, Type: {file_type}")
|
||||||
if file_type[0] == 'application/x-compressed':
|
if file_type[0] == "application/x-compressed":
|
||||||
log.debug(f"File {file} is a compressed file, extracting it into directory {file.parent}")
|
log.debug(
|
||||||
|
f"File {file} is a compressed file, extracting it into directory {file.parent}"
|
||||||
|
)
|
||||||
patoolib.extract_archive(str(file), outdir=str(file.parent))
|
patoolib.extract_archive(str(file), outdir=str(file.parent))
|
||||||
|
|
||||||
|
|
||||||
def get_torrent_filepath(torrent: Torrent):
|
def get_torrent_filepath(torrent: Torrent):
|
||||||
return BasicConfig().torrent_directory / torrent.title
|
return BasicConfig().torrent_directory / torrent.title
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
class MediaAlreadyExists(ValueError):
|
class MediaAlreadyExists(ValueError):
|
||||||
'''Raised when a show already exists'''
|
"""Raised when a show already exists"""
|
||||||
|
|
||||||
|
|
||||||
class MediaDoesNotExist(ValueError):
|
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]
|
overview: Mapped[str]
|
||||||
year: Mapped[int | None]
|
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):
|
class Season(Base):
|
||||||
@@ -27,26 +29,34 @@ class Season(Base):
|
|||||||
__table_args__ = (UniqueConstraint("show_id", "number"),)
|
__table_args__ = (UniqueConstraint("show_id", "number"),)
|
||||||
|
|
||||||
id: Mapped[UUID] = mapped_column(primary_key=True)
|
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]
|
number: Mapped[int]
|
||||||
external_id: Mapped[int]
|
external_id: Mapped[int]
|
||||||
name: Mapped[str]
|
name: Mapped[str]
|
||||||
overview: Mapped[str]
|
overview: Mapped[str]
|
||||||
|
|
||||||
show: Mapped["Show"] = relationship(back_populates="seasons")
|
show: Mapped["Show"] = relationship(back_populates="seasons")
|
||||||
episodes: Mapped[list["Episode"]] = relationship(back_populates="season", cascade="all, delete")
|
episodes: Mapped[list["Episode"]] = relationship(
|
||||||
|
back_populates="season", cascade="all, delete"
|
||||||
|
)
|
||||||
|
|
||||||
season_files = relationship("SeasonFile", back_populates="season", cascade="all, delete")
|
season_files = relationship(
|
||||||
season_requests = relationship("SeasonRequest", back_populates="season", cascade="all, delete")
|
"SeasonFile", back_populates="season", cascade="all, delete"
|
||||||
|
)
|
||||||
|
season_requests = relationship(
|
||||||
|
"SeasonRequest", back_populates="season", cascade="all, delete"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Episode(Base):
|
class Episode(Base):
|
||||||
__tablename__ = "episode"
|
__tablename__ = "episode"
|
||||||
__table_args__ = (
|
__table_args__ = (UniqueConstraint("season_id", "number"),)
|
||||||
UniqueConstraint("season_id", "number"),
|
|
||||||
)
|
|
||||||
id: Mapped[UUID] = mapped_column(primary_key=True)
|
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]
|
number: Mapped[int]
|
||||||
external_id: Mapped[int]
|
external_id: Mapped[int]
|
||||||
title: Mapped[str]
|
title: Mapped[str]
|
||||||
@@ -56,30 +66,41 @@ class Episode(Base):
|
|||||||
|
|
||||||
class SeasonFile(Base):
|
class SeasonFile(Base):
|
||||||
__tablename__ = "season_file"
|
__tablename__ = "season_file"
|
||||||
__table_args__ = (
|
__table_args__ = (PrimaryKeyConstraint("season_id", "file_path_suffix"),)
|
||||||
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]
|
file_path_suffix: Mapped[str]
|
||||||
quality: Mapped[Quality]
|
quality: Mapped[Quality]
|
||||||
|
|
||||||
torrent = relationship("Torrent", back_populates="season_files", uselist=False)
|
torrent = relationship("Torrent", back_populates="season_files", uselist=False)
|
||||||
season = relationship("Season", back_populates="season_files", uselist=False)
|
season = relationship("Season", back_populates="season_files", uselist=False)
|
||||||
|
|
||||||
|
|
||||||
class SeasonRequest(Base):
|
class SeasonRequest(Base):
|
||||||
__tablename__ = "season_request"
|
__tablename__ = "season_request"
|
||||||
__table_args__ = (
|
__table_args__ = (UniqueConstraint("season_id", "wanted_quality"),)
|
||||||
UniqueConstraint("season_id", "wanted_quality"),
|
|
||||||
)
|
|
||||||
id: Mapped[UUID] = mapped_column(primary_key=True)
|
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]
|
wanted_quality: Mapped[Quality]
|
||||||
min_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: 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)
|
requested_by: Mapped["User|None"] = relationship(
|
||||||
authorized_by: Mapped["User|None"] = relationship(foreign_keys=[authorized_by_id], uselist=False)
|
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)
|
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 torrent.schemas import TorrentId, Torrent as TorrentSchema
|
||||||
from tv import log
|
from tv import log
|
||||||
from tv.models import Season, Show, Episode, SeasonRequest, SeasonFile
|
from tv.models import Season, Show, Episode, SeasonRequest, SeasonFile
|
||||||
from tv.schemas import Season as SeasonSchema, SeasonId, Show as ShowSchema, ShowId, \
|
from tv.schemas import (
|
||||||
SeasonRequest as SeasonRequestSchema, SeasonFile as SeasonFileSchema, SeasonNumber, SeasonRequestId, \
|
Season as SeasonSchema,
|
||||||
RichSeasonRequest as RichSeasonRequestSchema
|
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:
|
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.
|
:param db: The database session.
|
||||||
:return: A ShowSchema object if found, otherwise None.
|
: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()
|
result = db.execute(stmt).unique().scalar_one_or_none()
|
||||||
if not result:
|
if not result:
|
||||||
@@ -28,7 +40,9 @@ def get_show(show_id: ShowId, db: Session) -> ShowSchema | None:
|
|||||||
return ShowSchema.model_validate(result)
|
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.
|
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.
|
:return: A ShowSchema object if found, otherwise None.
|
||||||
"""
|
"""
|
||||||
stmt = (
|
stmt = (
|
||||||
select(Show).where(Show.external_id == external_id).where(Show.metadata_provider == metadata_provider).options(
|
select(Show)
|
||||||
joinedload(Show.seasons).joinedload(Season.episodes)))
|
.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()
|
result = db.execute(stmt).unique().scalar_one_or_none()
|
||||||
if not result:
|
if not result:
|
||||||
@@ -71,12 +88,35 @@ def save_show(show: ShowSchema, db: Session) -> ShowSchema:
|
|||||||
:return: The saved ShowSchema object.
|
:return: The saved ShowSchema object.
|
||||||
:raises ValueError: If a show with the same primary key already exists.
|
: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,
|
db_show = Show(
|
||||||
overview=show.overview, year=show.year, seasons=[
|
id=show.id,
|
||||||
Season(id=season.id, show_id=show.id, number=season.number, external_id=season.external_id,
|
external_id=show.external_id,
|
||||||
name=season.name, overview=season.overview, episodes=[
|
metadata_provider=show.metadata_provider,
|
||||||
Episode(id=episode.id, season_id=season.id, number=episode.number, external_id=episode.external_id,
|
name=show.name,
|
||||||
title=episode.title) for episode in season.episodes]) for season in show.seasons])
|
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)
|
db.add(db_show)
|
||||||
try:
|
try:
|
||||||
@@ -122,9 +162,13 @@ def add_season_request(season_request: SeasonRequestSchema, db: Session) -> None
|
|||||||
season_id=season_request.season_id,
|
season_id=season_request.season_id,
|
||||||
wanted_quality=season_request.wanted_quality,
|
wanted_quality=season_request.wanted_quality,
|
||||||
min_quality=season_request.min_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=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.add(db_model)
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -140,23 +184,40 @@ def delete_season_request(season_request_id: SeasonRequestId, db: Session) -> No
|
|||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
def get_season_by_number(db: Session, season_number: int, show_id: ShowId) -> SeasonSchema:
|
def get_season_by_number(
|
||||||
stmt = (select(Season).where(Season.show_id == show_id).where(Season.number == season_number).options(
|
db: Session, season_number: int, show_id: ShowId
|
||||||
joinedload(Season.episodes), joinedload(Season.show)))
|
) -> 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()
|
result = db.execute(stmt).unique().scalar_one_or_none()
|
||||||
return SeasonSchema.model_validate(result)
|
return SeasonSchema.model_validate(result)
|
||||||
|
|
||||||
|
|
||||||
def get_season_requests(db: Session) -> list[RichSeasonRequestSchema]:
|
def get_season_requests(db: Session) -> list[RichSeasonRequestSchema]:
|
||||||
stmt = select(SeasonRequest).options(joinedload(SeasonRequest.requested_by),
|
stmt = select(SeasonRequest).options(
|
||||||
joinedload(SeasonRequest.authorized_by),
|
joinedload(SeasonRequest.requested_by),
|
||||||
joinedload(SeasonRequest.season).joinedload(Season.show))
|
joinedload(SeasonRequest.authorized_by),
|
||||||
|
joinedload(SeasonRequest.season).joinedload(Season.show),
|
||||||
|
)
|
||||||
result = db.execute(stmt).scalars().unique().all()
|
result = db.execute(stmt).scalars().unique().all()
|
||||||
return [RichSeasonRequestSchema(min_quality=x.min_quality,
|
return [
|
||||||
wanted_quality=x.wanted_quality, show=x.season.show, season=x.season,
|
RichSeasonRequestSchema(
|
||||||
requested_by=x.requested_by, authorized_by=x.authorized_by, authorized=x.authorized,
|
min_quality=x.min_quality,
|
||||||
id=x.id, season_id=x.season.id)
|
wanted_quality=x.wanted_quality,
|
||||||
for x in result]
|
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:
|
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):
|
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)
|
db.execute(stmt)
|
||||||
|
|
||||||
|
|
||||||
def get_season_files_by_season_id(db: Session, season_id: SeasonId):
|
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()
|
result = db.execute(stmt).scalars().all()
|
||||||
return [SeasonFileSchema.model_validate(season_file) for season_file in result]
|
return [SeasonFileSchema.model_validate(season_file) for season_file in result]
|
||||||
|
|
||||||
|
|
||||||
def get_torrents_by_show_id(db: Session, show_id: ShowId) -> list[TorrentSchema]:
|
def get_torrents_by_show_id(db: Session, show_id: ShowId) -> list[TorrentSchema]:
|
||||||
stmt = (select(Torrent)
|
stmt = (
|
||||||
.distinct()
|
select(Torrent)
|
||||||
.join(SeasonFile, SeasonFile.torrent_id == Torrent.id)
|
.distinct()
|
||||||
.join(Season, Season.id == SeasonFile.season_id)
|
.join(SeasonFile, SeasonFile.torrent_id == Torrent.id)
|
||||||
.where(Season.show_id == show_id))
|
.join(Season, Season.id == SeasonFile.season_id)
|
||||||
|
.where(Season.show_id == show_id)
|
||||||
|
)
|
||||||
result = db.execute(stmt).scalars().unique().all()
|
result = db.execute(stmt).scalars().unique().all()
|
||||||
return [TorrentSchema.model_validate(torrent) for torrent in result]
|
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.
|
:param db: The database session.
|
||||||
:return: A list of ShowSchema objects.
|
:return: A list of ShowSchema objects.
|
||||||
"""
|
"""
|
||||||
stmt = (select(Show)
|
stmt = (
|
||||||
.distinct()
|
select(Show)
|
||||||
.join(Season, Show.id == Season.show_id)
|
.distinct()
|
||||||
.join(SeasonFile, Season.id == SeasonFile.season_id)
|
.join(Season, Show.id == Season.show_id)
|
||||||
.join(Torrent, SeasonFile.torrent_id == Torrent.id)
|
.join(SeasonFile, Season.id == SeasonFile.season_id)
|
||||||
.options(joinedload(Show.seasons).joinedload(Season.episodes))
|
.join(Torrent, SeasonFile.torrent_id == Torrent.id)
|
||||||
.order_by(Show.name))
|
.options(joinedload(Show.seasons).joinedload(Season.episodes))
|
||||||
|
.order_by(Show.name)
|
||||||
|
)
|
||||||
|
|
||||||
results = db.execute(stmt).scalars().unique().all()
|
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]:
|
def get_seasons_by_torrent_id(db: Session, torrent_id: TorrentId) -> list[SeasonNumber]:
|
||||||
stmt = (select(Season.number)
|
stmt = (
|
||||||
.distinct()
|
select(Season.number)
|
||||||
.join(SeasonFile, SeasonFile.torrent_id == Torrent.id)
|
.distinct()
|
||||||
.join(Season, Season.id == SeasonFile.season_id).where(
|
.join(SeasonFile, SeasonFile.torrent_id == Torrent.id)
|
||||||
Torrent.id == torrent_id).select_from(Torrent))
|
.join(Season, Season.id == SeasonFile.season_id)
|
||||||
|
.where(Torrent.id == torrent_id)
|
||||||
|
.select_from(Torrent)
|
||||||
|
)
|
||||||
result = db.execute(stmt).scalars().unique().all()
|
result = db.execute(stmt).scalars().unique().all()
|
||||||
|
|
||||||
return [SeasonNumber(x) for x in result]
|
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))
|
return SeasonRequestSchema.model_validate(db.get(SeasonRequest, season_request_id))
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import logging
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, status
|
from fastapi import APIRouter, Depends, status
|
||||||
@@ -15,8 +14,19 @@ from metadataProvider.schemas import MetaDataProviderShowSearchResult
|
|||||||
from torrent.schemas import Torrent
|
from torrent.schemas import Torrent
|
||||||
from tv import log
|
from tv import log
|
||||||
from tv.exceptions import MediaAlreadyExists
|
from tv.exceptions import MediaAlreadyExists
|
||||||
from tv.schemas import Show, SeasonRequest, ShowId, RichShowTorrent, PublicShow, PublicSeasonFile, SeasonNumber, \
|
from tv.schemas import (
|
||||||
CreateSeasonRequest, SeasonRequestId, UpdateSeasonRequest, RichSeasonRequest
|
Show,
|
||||||
|
SeasonRequest,
|
||||||
|
ShowId,
|
||||||
|
RichShowTorrent,
|
||||||
|
PublicShow,
|
||||||
|
PublicSeasonFile,
|
||||||
|
SeasonNumber,
|
||||||
|
CreateSeasonRequest,
|
||||||
|
SeasonRequestId,
|
||||||
|
UpdateSeasonRequest,
|
||||||
|
RichSeasonRequest,
|
||||||
|
)
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -25,18 +35,38 @@ router = APIRouter()
|
|||||||
# CREATE AND DELETE SHOWS
|
# 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"},
|
@router.post(
|
||||||
status.HTTP_409_CONFLICT: {"model": str, "description": "Show already exists"}, })
|
"/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"):
|
def add_a_show(db: DbSessionDependency, show_id: int, metadata_provider: str = "tmdb"):
|
||||||
try:
|
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:
|
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
|
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):
|
def delete_a_show(db: DbSessionDependency, show_id: ShowId):
|
||||||
db.delete(db.get(Show, show_id))
|
db.delete(db.get(Show, show_id))
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -46,15 +76,26 @@ def delete_a_show(db: DbSessionDependency, show_id: ShowId):
|
|||||||
# GET SHOW INFORMATION
|
# 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:
|
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:
|
else:
|
||||||
return tv.service.get_all_shows(db=db)
|
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):
|
def get_shows_with_torrents(db: DbSessionDependency):
|
||||||
"""
|
"""
|
||||||
get all shows that are associated with torrents
|
get all shows that are associated with torrents
|
||||||
@@ -64,30 +105,51 @@ def get_shows_with_torrents(db: DbSessionDependency):
|
|||||||
return result
|
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):
|
def get_a_show(db: DbSessionDependency, show_id: ShowId):
|
||||||
return tv.service.get_public_show_by_id(db=db, show_id=show_id)
|
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):
|
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
|
# 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,
|
@router.get(
|
||||||
dependencies=[Depends(current_active_user)])
|
"/shows/{show_id}/{season_number}/files",
|
||||||
def get_season_files(db: DbSessionDependency, season_number: SeasonNumber, show_id: ShowId) -> list[PublicSeasonFile]:
|
status_code=status.HTTP_200_OK,
|
||||||
return tv.service.get_public_season_files_by_season_number(db=db, season_number=season_number, show_id=show_id)
|
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
|
# MANAGE REQUESTS
|
||||||
# --------------------------------
|
# --------------------------------
|
||||||
|
|
||||||
|
|
||||||
@router.post("/seasons/requests", status_code=status.HTTP_204_NO_CONTENT)
|
@router.post("/seasons/requests", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
def request_a_season(db: DbSessionDependency, user: Annotated[User, Depends(current_active_user)],
|
def request_a_season(
|
||||||
season_request: CreateSeasonRequest):
|
db: DbSessionDependency,
|
||||||
|
user: Annotated[User, Depends(current_active_user)],
|
||||||
|
season_request: CreateSeasonRequest,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
adds request flag to a season
|
adds request flag to a season
|
||||||
"""
|
"""
|
||||||
@@ -100,33 +162,54 @@ def request_a_season(db: DbSessionDependency, user: Annotated[User, Depends(curr
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
@router.get("/seasons/requests", status_code=status.HTTP_200_OK, dependencies=[Depends(current_active_user)],
|
@router.get(
|
||||||
response_model=list[RichSeasonRequest])
|
"/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]:
|
def get_season_requests(db: DbSessionDependency) -> list[RichSeasonRequest]:
|
||||||
return tv.service.get_all_season_requests(db=db)
|
return tv.service.get_all_season_requests(db=db)
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/seasons/requests/{request_id}", status_code=status.HTTP_204_NO_CONTENT, )
|
@router.delete(
|
||||||
def delete_season_request(db: DbSessionDependency, user: Annotated[User, Depends(current_active_user)],
|
"/seasons/requests/{request_id}",
|
||||||
request_id: SeasonRequestId):
|
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)
|
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:
|
if user.is_superuser or request.requested_by.id == user.id:
|
||||||
tv.service.delete_season_request(db=db, season_request_id=request_id)
|
tv.service.delete_season_request(db=db, season_request_id=request_id)
|
||||||
log.info(f"User {user.id} deleted season request {request_id}.")
|
log.info(f"User {user.id} deleted season request {request_id}.")
|
||||||
else:
|
else:
|
||||||
log.warning(f"User {user.id} tried to delete season request {request_id} but is not authorized.")
|
log.warning(
|
||||||
return JSONResponse(status_code=status.HTTP_403_FORBIDDEN,
|
f"User {user.id} tried to delete season request {request_id} but is not authorized."
|
||||||
content={"message": "Not authorized to delete this request."})
|
)
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
content={"message": "Not authorized to delete this request."},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch(
|
||||||
@router.patch("/seasons/requests/{season_request_id}", status_code=status.HTTP_204_NO_CONTENT)
|
"/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):
|
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
|
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_by = UserRead.model_validate(user)
|
||||||
season_request.authorized = authorized_status
|
season_request.authorized = authorized_status
|
||||||
if not 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)
|
@router.put("/seasons/requests", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
def update_request(db: DbSessionDependency, user: Annotated[User, Depends(current_active_user)],
|
def update_request(
|
||||||
season_request: UpdateSeasonRequest):
|
db: DbSessionDependency,
|
||||||
|
user: Annotated[User, Depends(current_active_user)],
|
||||||
|
season_request: UpdateSeasonRequest,
|
||||||
|
):
|
||||||
season_request: SeasonRequest = SeasonRequest.model_validate(season_request)
|
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:
|
if request.requested_by.id == user.id or user.is_superuser:
|
||||||
season_request.requested_by = UserRead.model_validate(user)
|
season_request.requested_by = UserRead.model_validate(user)
|
||||||
tv.service.update_season_request(db=db, season_request=season_request)
|
tv.service.update_season_request(db=db, season_request=season_request)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------
|
# --------------------------------
|
||||||
# MANAGE TORRENTS
|
# MANAGE TORRENTS
|
||||||
# --------------------------------
|
# --------------------------------
|
||||||
|
|
||||||
|
|
||||||
# 1 is the default for season_number because it returns multi season 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)],
|
@router.get(
|
||||||
response_model=list[PublicIndexerQueryResult])
|
"/torrents",
|
||||||
def get_torrents_for_a_season(db: DbSessionDependency, show_id: ShowId, season_number: int = 1,
|
status_code=status.HTTP_200_OK,
|
||||||
search_query_override: str = None):
|
dependencies=[Depends(current_superuser)],
|
||||||
return tv.service.get_all_available_torrents_for_a_season(db=db, season_number=season_number, show_id=show_id,
|
response_model=list[PublicIndexerQueryResult],
|
||||||
search_query_override=search_query_override)
|
)
|
||||||
|
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
|
# download a torrent
|
||||||
@router.post("/torrents", status_code=status.HTTP_200_OK, response_model=Torrent,
|
@router.post(
|
||||||
dependencies=[Depends(current_superuser)])
|
"/torrents",
|
||||||
def download_a_torrent(db: DbSessionDependency, public_indexer_result_id: IndexerQueryResultId, show_id: ShowId,
|
status_code=status.HTTP_200_OK,
|
||||||
override_file_path_suffix: str = ""):
|
response_model=Torrent,
|
||||||
return tv.service.download_torrent(db=db, public_indexer_result_id=public_indexer_result_id, show_id=show_id,
|
dependencies=[Depends(current_superuser)],
|
||||||
override_show_file_path_suffix=override_file_path_suffix)
|
)
|
||||||
|
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
|
# SEARCH SHOWS ON METADATA PROVIDERS
|
||||||
# --------------------------------
|
# --------------------------------
|
||||||
|
|
||||||
@router.get("/search", dependencies=[Depends(current_active_user)],
|
|
||||||
response_model=list[MetaDataProviderShowSearchResult])
|
@router.get(
|
||||||
def search_metadata_providers_for_a_show(db: DbSessionDependency, query: str, metadata_provider: str = "tmdb"):
|
"/search",
|
||||||
return tv.service.search_for_show(query=query, metadata_provider=metadata_provider, db=db)
|
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)],
|
@router.get(
|
||||||
response_model=list[MetaDataProviderShowSearchResult])
|
"/recommended",
|
||||||
def search_metadata_providers_for_a_show(db: DbSessionDependency, metadata_provider: str = "tmdb"):
|
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)
|
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)
|
EpisodeNumber = typing.NewType("EpisodeNumber", int)
|
||||||
SeasonRequestId = typing.NewType("SeasonRequestId", UUID)
|
SeasonRequestId = typing.NewType("SeasonRequestId", UUID)
|
||||||
|
|
||||||
|
|
||||||
class Episode(BaseModel):
|
class Episode(BaseModel):
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|||||||
@@ -14,15 +14,35 @@ from torrent.service import TorrentService
|
|||||||
from tv import log
|
from tv import log
|
||||||
from tv.exceptions import MediaAlreadyExists
|
from tv.exceptions import MediaAlreadyExists
|
||||||
from tv.repository import add_season_file, get_season_files_by_season_id
|
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, \
|
from tv.schemas import (
|
||||||
PublicSeason, PublicShow, PublicSeasonFile, SeasonNumber, SeasonRequestId, RichSeasonRequest
|
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:
|
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):
|
if check_if_show_exists(
|
||||||
raise MediaAlreadyExists(f"Show with external ID {external_id} and" +
|
db=db, external_id=external_id, metadata_provider=metadata_provider
|
||||||
f" metadata provider {metadata_provider} already exists")
|
):
|
||||||
show_with_metadata = metadataProvider.get_show_metadata(id=external_id, 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)
|
saved_show = tv.repository.save_show(db=db, show=show_with_metadata)
|
||||||
return saved_show
|
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)
|
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)
|
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)
|
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)
|
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]
|
public_season_files = [PublicSeasonFile.model_validate(x) for x in season_files]
|
||||||
result = []
|
result = []
|
||||||
@@ -55,18 +79,25 @@ def get_public_season_files_by_season_id(db: Session, season_id: SeasonId) -> li
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def get_public_season_files_by_season_number(db: Session, season_number: SeasonNumber, show_id: ShowId) -> list[
|
def get_public_season_files_by_season_number(
|
||||||
PublicSeasonFile]:
|
db: Session, season_number: SeasonNumber, show_id: ShowId
|
||||||
season = tv.repository.get_season_by_number(db=db, season_number=season_number, show_id=show_id)
|
) -> 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)
|
return get_public_season_files_by_season_id(db=db, season_id=season.id)
|
||||||
|
|
||||||
|
|
||||||
def check_if_show_exists(db: Session,
|
def check_if_show_exists(
|
||||||
external_id: int = None,
|
db: Session,
|
||||||
metadata_provider: str = None,
|
external_id: int = None,
|
||||||
show_id: ShowId = None) -> bool:
|
metadata_provider: str = None,
|
||||||
|
show_id: ShowId = None,
|
||||||
|
) -> bool:
|
||||||
if external_id and metadata_provider:
|
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
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
@@ -76,22 +107,30 @@ def check_if_show_exists(db: Session,
|
|||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
else:
|
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,
|
def get_all_available_torrents_for_a_season(
|
||||||
search_query_override: str = None) -> list[
|
db: Session, season_number: int, show_id: ShowId, search_query_override: str = None
|
||||||
IndexerQueryResult]:
|
) -> list[IndexerQueryResult]:
|
||||||
log.debug(f"getting all available torrents for season {season_number} for show {show_id}")
|
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)
|
show = tv.repository.get_show(show_id=show_id, db=db)
|
||||||
if search_query_override:
|
if search_query_override:
|
||||||
search_query = search_query_override
|
search_query = search_query_override
|
||||||
else:
|
else:
|
||||||
# TODO: add more Search query strings and combine all the results, like "season 3", "s03", "s3"
|
# 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)
|
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:
|
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
|
return torrents
|
||||||
result: list[IndexerQueryResult] = []
|
result: list[IndexerQueryResult] = []
|
||||||
for torrent in torrents:
|
for torrent in torrents:
|
||||||
@@ -105,19 +144,27 @@ def get_all_shows(db: Session) -> list[Show]:
|
|||||||
return tv.repository.get_shows(db=db)
|
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)
|
results = metadataProvider.search_show(query, metadata_provider)
|
||||||
for result in results:
|
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
|
result.added = True
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
def get_popular_shows(metadata_provider: str, db: Session):
|
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:
|
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))
|
results.pop(results.index(result))
|
||||||
return results
|
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:
|
if season_file.torrent_id is None:
|
||||||
return True
|
return True
|
||||||
else:
|
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:
|
if torrent_file.imported:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_show_by_external_id(db: Session, external_id: int, metadata_provider: str) -> Show | None:
|
def get_show_by_external_id(
|
||||||
return tv.repository.get_show_by_external_id(external_id=external_id, metadata_provider=metadata_provider, db=db)
|
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:
|
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)
|
show_torrents = tv.repository.get_torrents_by_show_id(db=db, show_id=show.id)
|
||||||
rich_season_torrents = []
|
rich_season_torrents = []
|
||||||
for show_torrent in show_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)
|
season_files = get_seasons_files_of_torrent(db=db, torrent_id=show_torrent.id)
|
||||||
file_path_suffix = season_files[0].file_path_suffix
|
file_path_suffix = season_files[0].file_path_suffix
|
||||||
season_torrent = RichSeasonTorrent(torrent_id=show_torrent.id, torrent_title=show_torrent.title,
|
season_torrent = RichSeasonTorrent(
|
||||||
status=show_torrent.status, quality=show_torrent.quality,
|
torrent_id=show_torrent.id,
|
||||||
imported=show_torrent.imported, seasons=seasons,
|
torrent_title=show_torrent.title,
|
||||||
file_path_suffix=file_path_suffix
|
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)
|
rich_season_torrents.append(season_torrent)
|
||||||
return RichShowTorrent(show_id=show.id, name=show.name, year=show.year,
|
return RichShowTorrent(
|
||||||
metadata_provider=show.metadata_provider, torrents=rich_season_torrents)
|
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]:
|
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]
|
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,
|
def download_torrent(
|
||||||
override_show_file_path_suffix: str = "") -> Torrent:
|
db: Session,
|
||||||
indexer_result = indexer.service.get_indexer_query_result(db=db, result_id=public_indexer_result_id)
|
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)
|
show_torrent = TorrentService(db=db).download(indexer_result=indexer_result)
|
||||||
|
|
||||||
for season_number in indexer_result.season:
|
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 = tv.repository.get_season_by_number(
|
||||||
season_file = SeasonFile(season_id=season.id, quality=indexer_result.quality, torrent_id=show_torrent.id,
|
db=db, season_number=season_number, show_id=show_id
|
||||||
file_path_suffix=override_show_file_path_suffix)
|
)
|
||||||
|
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)
|
add_season_file(db=db, season_file=season_file)
|
||||||
return show_torrent
|
return show_torrent
|
||||||
|
|||||||
Reference in New Issue
Block a user