mirror of
https://github.com/maxdorninger/MediaManager.git
synced 2026-04-27 19:25:40 +02:00
fix typo
This commit is contained in:
@@ -1,73 +0,0 @@
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import jwt
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from jwt.exceptions import InvalidTokenError
|
||||
from pydantic import BaseModel
|
||||
|
||||
from auth.config import AuthConfig
|
||||
from database import DbSessionDependency
|
||||
from database.users import User
|
||||
|
||||
|
||||
# TODO: evaluate FASTAPI-Users package
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
|
||||
|
||||
class TokenData(BaseModel):
|
||||
uid: str | None = None
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/v1/token")
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
async def get_current_user(db: DbSessionDependency, token: str = Depends(oauth2_scheme)) -> User:
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
auth_config = AuthConfig
|
||||
log.debug("token: " + token)
|
||||
|
||||
try:
|
||||
payload = jwt.decode(token, auth_config.jwt_signing_key, algorithms=[auth_config.jwt_signing_algorithm])
|
||||
log.debug("jwt payload: " + payload.__str__())
|
||||
user_uid: str = payload.get("sub")
|
||||
log.debug("jwt payload sub (USER uid): " + user_uid)
|
||||
if user_uid is None:
|
||||
raise credentials_exception
|
||||
token_data = TokenData(uid=user_uid)
|
||||
except InvalidTokenError:
|
||||
log.warning("received invalid token: " + token)
|
||||
raise credentials_exception
|
||||
|
||||
user: User | None = db.get(User, token_data.uid)
|
||||
|
||||
if user is None:
|
||||
log.debug("USER not found")
|
||||
raise credentials_exception
|
||||
|
||||
log.debug("received USER: " + user.__str__())
|
||||
return user
|
||||
|
||||
|
||||
def create_access_token(data: dict, expires_delta: timedelta | None = None):
|
||||
to_encode = data.copy()
|
||||
auth_config = AuthConfig
|
||||
if expires_delta:
|
||||
expire = datetime.now(timezone.utc) + expires_delta
|
||||
else:
|
||||
expire = datetime.now(timezone.utc) + timedelta(minutes=auth_config.jwt_access_token_lifetime)
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, auth_config.jwt_signing_key, algorithm=auth_config.jwt_signing_algorithm)
|
||||
return encoded_jwt
|
||||
@@ -1,13 +1,12 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class AuthConfig(BaseSettings):
|
||||
# to get a signing key run:
|
||||
# openssl rand -hex 32
|
||||
jwt_signing_key: str
|
||||
jwt_signing_algorithm: str = "HS256"
|
||||
jwt_access_token_lifetime: int = 60 * 24 * 30
|
||||
|
||||
model_config = SettingsConfigDict(env_prefix='AUTH_')
|
||||
token_secret: str
|
||||
session_lifetime: int = 60 * 60 * 24
|
||||
@property
|
||||
def jwt_signing_key(self):
|
||||
return self._jwt_signing_key
|
||||
|
||||
34
backend/src/auth/db.py
Normal file
34
backend/src/auth/db.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
from fastapi import Depends
|
||||
from fastapi_users.db import SQLAlchemyBaseUserTableUUID, SQLAlchemyUserDatabase
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
import database
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class User(SQLAlchemyBaseUserTableUUID, Base):
|
||||
pass
|
||||
|
||||
|
||||
engine = create_async_engine(database.db_url, echo=False)
|
||||
async_session_maker = async_sessionmaker(engine, expire_on_commit=False)
|
||||
|
||||
|
||||
async def create_db_and_tables():
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
|
||||
async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
|
||||
async with async_session_maker() as session:
|
||||
yield session
|
||||
|
||||
|
||||
async def get_user_db(session: AsyncSession = Depends(get_async_session)):
|
||||
yield SQLAlchemyUserDatabase(session, User)
|
||||
@@ -1,4 +0,0 @@
|
||||
#TODO: Implement OAuth2/Open ID Connect
|
||||
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
from typing import Annotated
|
||||
|
||||
import bcrypt
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlmodel import select
|
||||
|
||||
from auth import Token, create_access_token, router
|
||||
from database import DbSessionDependency
|
||||
from database.users import User
|
||||
|
||||
|
||||
def verify_password(plain_password, hashed_password):
|
||||
return bcrypt.checkpw(
|
||||
bytes(plain_password, encoding="utf-8"),
|
||||
bytes(hashed_password, encoding="utf-8"),
|
||||
)
|
||||
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
|
||||
|
||||
|
||||
def authenticate_user(db: DbSessionDependency, email: str, password: str) -> bool | User:
|
||||
"""
|
||||
|
||||
:param email: email of the USER
|
||||
:param password: PASSWORD of the USER
|
||||
:return: if authentication succeeds, returns the USER object with added name and lastname, otherwise or if the USER doesn't exist returns False
|
||||
"""
|
||||
user: User | None = db.exec(select(User).where(User.email == email)).first()
|
||||
if not user:
|
||||
return False
|
||||
if not verify_password(password, user.hashed_password):
|
||||
return False
|
||||
return user
|
||||
|
||||
|
||||
@router.post("/token")
|
||||
async def login_for_access_token(
|
||||
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
|
||||
db: DbSessionDependency,
|
||||
) -> Token:
|
||||
user = authenticate_user(db,form_data.username, form_data.password)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect email or PASSWORD",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
# id needs to be converted because a UUID object isn't json serializable
|
||||
access_token = create_access_token(data={"sub": user.id.__str__()})
|
||||
return Token(access_token=access_token, token_type="bearer")
|
||||
15
backend/src/auth/schemas.py
Normal file
15
backend/src/auth/schemas.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import uuid
|
||||
|
||||
from fastapi_users import schemas
|
||||
|
||||
|
||||
class UserRead(schemas.BaseUser[uuid.UUID]):
|
||||
pass
|
||||
|
||||
|
||||
class UserCreate(schemas.BaseUserCreate):
|
||||
pass
|
||||
|
||||
|
||||
class UserUpdate(schemas.BaseUserUpdate):
|
||||
pass
|
||||
64
backend/src/auth/users.py
Normal file
64
backend/src/auth/users.py
Normal file
@@ -0,0 +1,64 @@
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import Depends, Request
|
||||
from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin, models
|
||||
from fastapi_users.authentication import (
|
||||
AuthenticationBackend,
|
||||
BearerTransport,
|
||||
CookieTransport, JWTStrategy,
|
||||
)
|
||||
from fastapi_users.db import SQLAlchemyUserDatabase
|
||||
|
||||
import auth.config
|
||||
from auth.db import User, get_user_db
|
||||
|
||||
config = auth.config.AuthConfig()
|
||||
SECRET = config.token_secret
|
||||
LIFETIME = config.session_lifetime
|
||||
|
||||
|
||||
# TODO: implement on_xxx methods
|
||||
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
||||
reset_password_token_secret = SECRET
|
||||
verification_token_secret = SECRET
|
||||
|
||||
async def on_after_register(self, user: User, request: Optional[Request] = None):
|
||||
print(f"User {user.id} has registered.")
|
||||
|
||||
async def on_after_forgot_password(
|
||||
self, user: User, token: str, request: Optional[Request] = None
|
||||
):
|
||||
print(f"User {user.id} has forgot their password. Reset token: {token}")
|
||||
|
||||
async def on_after_request_verify(
|
||||
self, user: User, token: str, request: Optional[Request] = None
|
||||
):
|
||||
print(f"Verification requested for user {user.id}. Verification token: {token}")
|
||||
|
||||
|
||||
async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db)):
|
||||
yield UserManager(user_db)
|
||||
|
||||
|
||||
def get_jwt_strategy() -> JWTStrategy[models.UP, models.ID]:
|
||||
return JWTStrategy(secret=SECRET, lifetime_seconds=LIFETIME)
|
||||
|
||||
|
||||
bearer_transport = BearerTransport(tokenUrl="auth/jwt/login")
|
||||
cookie_transport = CookieTransport(cookie_max_age=LIFETIME)
|
||||
|
||||
bearer_auth_backend = AuthenticationBackend(
|
||||
name="jwt",
|
||||
transport=bearer_transport,
|
||||
get_strategy=get_jwt_strategy,
|
||||
)
|
||||
cookie_auth_backend = AuthenticationBackend(
|
||||
name="cookie",
|
||||
transport=cookie_transport,
|
||||
get_strategy=get_jwt_strategy,
|
||||
)
|
||||
|
||||
fastapi_users = FastAPIUsers[User, uuid.UUID](get_user_manager, [bearer_auth_backend, cookie_auth_backend])
|
||||
|
||||
current_active_user = fastapi_users.current_user(active=True)
|
||||
@@ -2,7 +2,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class DbConfig(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_prefix='DB')
|
||||
model_config = SettingsConfigDict(env_prefix='DB_')
|
||||
HOST: str = "localhost"
|
||||
PORT: int = 5432
|
||||
USER: str = "MediaManager"
|
||||
|
||||
@@ -1,49 +1,54 @@
|
||||
import uuid
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import ForeignKeyConstraint, UniqueConstraint
|
||||
from sqlmodel import Field, Relationship, SQLModel
|
||||
|
||||
from database.torrents import TorrentMixin
|
||||
from sqlalchemy import ForeignKey, ForeignKeyConstraint, Integer, String, UniqueConstraint
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||
|
||||
|
||||
class Show(SQLModel, table=True):
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class Show(Base):
|
||||
__tablename__ = "show"
|
||||
__table_args__ = (UniqueConstraint("external_id", "metadata_provider", "version"),)
|
||||
id: UUID = Field(primary_key=True, default_factory=uuid.uuid4)
|
||||
external_id: int
|
||||
metadata_provider: str
|
||||
name: str
|
||||
overview: str
|
||||
# For some shows the first_air_date isn't known, therefore it needs to be nullable
|
||||
year: int | None
|
||||
version: str = Field(default="")
|
||||
|
||||
seasons: list["Season"] = Relationship(back_populates="show", cascade_delete=True)
|
||||
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
||||
external_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
metadata_provider: Mapped[str] = mapped_column(String, nullable=False)
|
||||
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||
overview: Mapped[str] = mapped_column(String, nullable=False)
|
||||
year: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
version: Mapped[str] = mapped_column(String, default="")
|
||||
|
||||
seasons: Mapped[list["Season"]] = relationship(back_populates="show", cascade="all, delete")
|
||||
|
||||
|
||||
class Season(SQLModel, TorrentMixin, table=True):
|
||||
class Season(Base):
|
||||
__tablename__ = "season"
|
||||
__table_args__ = (UniqueConstraint("show_id", "number"),)
|
||||
id: UUID = Field(primary_key=True, default_factory=uuid.uuid4)
|
||||
|
||||
show_id: UUID = Field(foreign_key="show.id", ondelete="CASCADE")
|
||||
number: int = Field()
|
||||
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
||||
show_id: Mapped[UUID] = mapped_column(ForeignKey("show.id", ondelete="CASCADE"), nullable=False)
|
||||
number: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
external_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||
overview: Mapped[str] = mapped_column(String, nullable=False)
|
||||
|
||||
external_id: int
|
||||
name: str
|
||||
overview: str
|
||||
|
||||
show: Show = Relationship(back_populates="seasons")
|
||||
episodes: list["Episode"] = Relationship(back_populates="season", cascade_delete=True)
|
||||
show: Mapped[Show] = relationship(back_populates="seasons")
|
||||
episodes: Mapped[list["Episode"]] = relationship(back_populates="season", cascade="all, delete")
|
||||
|
||||
|
||||
class Episode(SQLModel, table=True):
|
||||
class Episode(Base):
|
||||
__tablename__ = "episode"
|
||||
__table_args__ = (
|
||||
ForeignKeyConstraint(['show_id', 'season_number'], ['season.show_id', 'season.number'], ondelete="CASCADE"),
|
||||
ForeignKeyConstraint(["show_id", "season_number"], ["season.show_id", "season.number"], ondelete="CASCADE"),
|
||||
)
|
||||
show_id: UUID = Field(primary_key=True)
|
||||
season_number: int = Field(primary_key=True)
|
||||
number: int = Field(primary_key=True)
|
||||
external_id: int
|
||||
title: str
|
||||
|
||||
season: Season = Relationship(back_populates="episodes")
|
||||
show_id: Mapped[UUID] = mapped_column(ForeignKey("show.id"), primary_key=True)
|
||||
season_number: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
number: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
external_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
title: Mapped[str] = mapped_column(String, nullable=False)
|
||||
|
||||
season: Mapped[Season] = relationship(back_populates="episodes")
|
||||
|
||||
@@ -1,19 +1,3 @@
|
||||
import uuid
|
||||
from uuid import UUID
|
||||
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
|
||||
class UserBase(SQLModel):
|
||||
name: str = Field()
|
||||
lastname: str
|
||||
email: str = Field(unique=True)
|
||||
|
||||
class UserPublic(UserBase):
|
||||
id: UUID = Field(primary_key=True, default_factory=uuid.uuid4)
|
||||
|
||||
class User(UserPublic, table=True):
|
||||
hashed_password: str
|
||||
|
||||
class UserCreate(UserBase):
|
||||
password: str
|
||||
@@ -14,6 +14,7 @@ class QbittorrentConfig(BaseSettings):
|
||||
host: str = "localhost"
|
||||
port: int = 8080
|
||||
username: str = "admin"
|
||||
password: str = "admin"
|
||||
|
||||
|
||||
class QbittorrentClient(GenericDownloadClient):
|
||||
|
||||
@@ -23,5 +23,5 @@ def search(query: str | Season) -> list[IndexerQueryResult]:
|
||||
|
||||
indexers: list[GenericIndexer] = []
|
||||
|
||||
if ProwlarrConfig.enabled:
|
||||
if ProwlarrConfig().enabled:
|
||||
indexers.append(Prowlarr())
|
||||
|
||||
@@ -5,4 +5,4 @@ class ProwlarrConfig(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="PROWLARR_")
|
||||
enabled: bool = True
|
||||
api_key: str
|
||||
url: str
|
||||
url: str = "http://localhost:96969"
|
||||
|
||||
@@ -17,7 +17,7 @@ class Prowlarr(GenericIndexer):
|
||||
:param kwargs: Additional keyword arguments to pass to the superclass constructor.
|
||||
"""
|
||||
super().__init__(name='prowlarr')
|
||||
config = ProwlarrConfig
|
||||
config = ProwlarrConfig()
|
||||
self.api_key = config.api_key
|
||||
self.url = config.url
|
||||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import logging
|
||||
import sys
|
||||
from contextlib import asynccontextmanager
|
||||
from logging.config import dictConfig
|
||||
|
||||
import database
|
||||
from auth.db import create_db_and_tables
|
||||
from auth.schemas import UserCreate, UserRead, UserUpdate
|
||||
from auth.users import bearer_auth_backend, fastapi_users
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG,
|
||||
format="%(asctime)s - %(levelname)s - %(name)s - %(funcName)s(): %(message)s",
|
||||
stream=sys.stdout,
|
||||
@@ -11,10 +17,7 @@ log = logging.getLogger(__name__)
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
|
||||
import database.users
|
||||
import tv.router
|
||||
from auth import password
|
||||
from users import routers
|
||||
|
||||
LOGGING_CONFIG = {
|
||||
"version": 1,
|
||||
@@ -42,11 +45,50 @@ LOGGING_CONFIG = {
|
||||
# Apply logging config
|
||||
dictConfig(LOGGING_CONFIG)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Not needed if you setup a migration system like Alembic
|
||||
await create_db_and_tables()
|
||||
yield
|
||||
|
||||
|
||||
database.init_db()
|
||||
app = FastAPI(root_path="/api/v1")
|
||||
app.include_router(routers.router, tags=["users"])
|
||||
app.include_router(password.router, tags=["authentication"])
|
||||
app.include_router(tv.router.router, tags=["tv"])
|
||||
app = FastAPI(root_path="/api/v1", lifespan=lifespan)
|
||||
app.include_router(
|
||||
fastapi_users.get_auth_router(bearer_auth_backend),
|
||||
prefix="/auth/jwt",
|
||||
tags=["auth"]
|
||||
)
|
||||
app.include_router(
|
||||
fastapi_users.get_auth_router(bearer_auth_backend),
|
||||
prefix="/auth/cookie",
|
||||
tags=["auth"]
|
||||
)
|
||||
app.include_router(
|
||||
fastapi_users.get_register_router(UserRead, UserCreate),
|
||||
prefix="/auth",
|
||||
tags=["auth"],
|
||||
)
|
||||
app.include_router(
|
||||
fastapi_users.get_reset_password_router(),
|
||||
prefix="/auth",
|
||||
tags=["auth"],
|
||||
)
|
||||
app.include_router(
|
||||
fastapi_users.get_verify_router(UserRead),
|
||||
prefix="/auth",
|
||||
tags=["auth"],
|
||||
)
|
||||
app.include_router(
|
||||
fastapi_users.get_users_router(UserRead, UserUpdate),
|
||||
prefix="/users",
|
||||
tags=["users"],
|
||||
)
|
||||
|
||||
app.include_router(
|
||||
tv.router.router,
|
||||
tags=["tv"])
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(app, host="127.0.0.1", port=5049, log_config=LOGGING_CONFIG)
|
||||
|
||||
@@ -14,7 +14,7 @@ class TmdbConfig(BaseSettings):
|
||||
TMDB_API_KEY: str | None = None
|
||||
|
||||
|
||||
config = TmdbConfig
|
||||
config = TmdbConfig()
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
fastapi==0.115.11
|
||||
fastapi==0.115.12
|
||||
ollama==0.4.7
|
||||
psycopg==3.2.4
|
||||
pydantic==2.10.6
|
||||
pydantic==2.11.0
|
||||
pydantic_settings==2.8.1
|
||||
PyJWT==2.10.1
|
||||
PyJWT==2.10.1
|
||||
python_bcrypt==0.3.2
|
||||
|
||||
57
backend/src/tv/models.py
Normal file
57
backend/src/tv/models.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import uuid
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import ForeignKey, ForeignKeyConstraint, Integer, String, UniqueConstraint
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class Show(Base):
|
||||
__tablename__ = "show"
|
||||
__table_args__ = (UniqueConstraint("external_id", "metadata_provider", "version"),)
|
||||
|
||||
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
||||
external_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
metadata_provider: Mapped[str] = mapped_column(String, nullable=False)
|
||||
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||
overview: Mapped[str] = mapped_column(String, nullable=False)
|
||||
year: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
version: Mapped[str] = mapped_column(String, default="")
|
||||
|
||||
seasons: Mapped[list["Season"]] = relationship(back_populates="show", cascade="all, delete")
|
||||
|
||||
|
||||
class Season(Base):
|
||||
__tablename__ = "season"
|
||||
__table_args__ = (UniqueConstraint("show_id", "number"),)
|
||||
|
||||
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
||||
show_id: Mapped[UUID] = mapped_column(ForeignKey(column="show.id", ondelete="CASCADE"), nullable=False)
|
||||
number: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
external_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||
overview: Mapped[str] = mapped_column(String, nullable=False)
|
||||
torrent_id: Mapped[UUID] = mapped_column(ForeignKey(column="torrent.id"), nullable=False)
|
||||
|
||||
show: Mapped[Show] = relationship(back_populates="seasons")
|
||||
episodes: Mapped[list["Episode"]] = relationship(back_populates="season", cascade="all, delete")
|
||||
|
||||
|
||||
class Episode(Base):
|
||||
__tablename__ = "episode"
|
||||
__table_args__ = (
|
||||
ForeignKeyConstraint(columns=["show_id", "season_number"],
|
||||
refcolumns=["season.show_id", "season.number"],
|
||||
ondelete="CASCADE"),
|
||||
)
|
||||
|
||||
show_id: Mapped[UUID] = mapped_column(ForeignKey("show.id"), primary_key=True)
|
||||
season_number: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
number: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
external_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
title: Mapped[str] = mapped_column(String, nullable=False)
|
||||
|
||||
season: Mapped[Season] = relationship(back_populates="episodes")
|
||||
@@ -8,16 +8,15 @@ from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
from sqlmodel import select
|
||||
|
||||
import auth
|
||||
import dowloadClients
|
||||
import indexer
|
||||
import metadataProvider
|
||||
from auth.users import current_active_user
|
||||
from database import DbSessionDependency
|
||||
from database.torrents import Torrent
|
||||
from database.tv import Season, Show
|
||||
from indexer import IndexerQueryResult
|
||||
from tv import log
|
||||
from users.routers import Message
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/tv",
|
||||
@@ -29,10 +28,10 @@ class ShowDetails(BaseModel):
|
||||
seasons: list[Season]
|
||||
|
||||
|
||||
@router.post("/show", status_code=status.HTTP_201_CREATED, dependencies=[Depends(auth.get_current_user)],
|
||||
@router.post("/show", 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": Message, "description": "Show already exists"},
|
||||
status.HTTP_409_CONFLICT: {"model": str, "description": "Show already exists"},
|
||||
})
|
||||
def add_show(db: DbSessionDependency, show_id: int, metadata_provider: str = "tmdb", version: str = ""):
|
||||
res = db.exec(select(Show).
|
||||
@@ -58,13 +57,13 @@ def add_show(db: DbSessionDependency, show_id: int, metadata_provider: str = "tm
|
||||
return show
|
||||
|
||||
|
||||
@router.delete("/{show_id}", status_code=status.HTTP_200_OK)
|
||||
@router.delete("/{show_id}", status_code=status.HTTP_200_OK, dependencies=[Depends(current_active_user)])
|
||||
def delete_show(db: DbSessionDependency, show_id: UUID):
|
||||
db.delete(db.get(Show, show_id))
|
||||
db.commit()
|
||||
|
||||
|
||||
@router.patch("/{show_id}/{season_id}", status_code=status.HTTP_200_OK, dependencies=[Depends(auth.get_current_user)],
|
||||
@router.patch("/{show_id}/{season_id}", status_code=status.HTTP_200_OK, dependencies=[Depends(current_active_user)],
|
||||
response_model=Season)
|
||||
def add_season(db: DbSessionDependency, season_id: UUID):
|
||||
"""
|
||||
@@ -79,7 +78,7 @@ def add_season(db: DbSessionDependency, season_id: UUID):
|
||||
return season
|
||||
|
||||
|
||||
@router.delete("/{show_id}/{season_id}", status_code=status.HTTP_200_OK, dependencies=[Depends(auth.get_current_user)],
|
||||
@router.delete("/{show_id}/{season_id}", status_code=status.HTTP_200_OK, dependencies=[Depends(current_active_user)],
|
||||
response_model=Show)
|
||||
def delete_season(db: DbSessionDependency, show_id: UUID, season: int):
|
||||
"""
|
||||
@@ -94,7 +93,7 @@ def delete_season(db: DbSessionDependency, show_id: UUID, season: int):
|
||||
|
||||
|
||||
@router.get("/{show_id}/{season_id}/torrent", status_code=status.HTTP_200_OK, dependencies=[Depends(
|
||||
auth.get_current_user)],
|
||||
current_active_user)],
|
||||
response_model=list[IndexerQueryResult])
|
||||
def get_season_torrents(db: DbSessionDependency, show_id: UUID, season_id: UUID):
|
||||
season = db.get(Season, season_id)
|
||||
@@ -120,7 +119,7 @@ def get_season_torrents(db: DbSessionDependency, show_id: UUID, season_id: UUID)
|
||||
|
||||
|
||||
@router.post("/{show_id}/torrent", status_code=status.HTTP_200_OK, dependencies=[Depends(
|
||||
auth.get_current_user)], response_model=list[Season])
|
||||
current_active_user)], response_model=list[Season])
|
||||
def download_seasons_torrent(db: DbSessionDependency, show_id: UUID, torrent_id: UUID):
|
||||
"""
|
||||
downloads torrents for a show season, links the torrent for all seasons the torrent contains
|
||||
@@ -152,7 +151,7 @@ def download_seasons_torrent(db: DbSessionDependency, show_id: UUID, torrent_id:
|
||||
|
||||
|
||||
@router.post("/{show_id}/{season_id}/torrent", status_code=status.HTTP_200_OK, dependencies=[Depends(
|
||||
auth.get_current_user)], response_model=list[Season])
|
||||
current_active_user)], response_model=list[Season])
|
||||
def delete_seasons_torrent(db: DbSessionDependency, show_id: UUID, season_id: UUID, torrent_id: UUID):
|
||||
"""
|
||||
downloads torrents for a season, links the torrent only to the specified season
|
||||
@@ -185,13 +184,13 @@ def delete_seasons_torrent(db: DbSessionDependency, show_id: UUID, season_id: UU
|
||||
return seasons
|
||||
|
||||
|
||||
@router.get("/", dependencies=[Depends(auth.get_current_user)], response_model=list[Show])
|
||||
@router.get("/", dependencies=[Depends(current_active_user)], response_model=list[Show])
|
||||
def get_shows(db: DbSessionDependency):
|
||||
""""""
|
||||
return db.exec(select(Show)).unique().fetchall()
|
||||
|
||||
|
||||
@router.get("/{show_id}", dependencies=[Depends(auth.get_current_user)], response_model=ShowDetails)
|
||||
@router.get("/{show_id}", dependencies=[Depends(current_active_user)], response_model=ShowDetails)
|
||||
def get_show(db: DbSessionDependency, show_id: UUID):
|
||||
"""
|
||||
|
||||
@@ -210,6 +209,6 @@ def get_show(db: DbSessionDependency, show_id: UUID):
|
||||
return ShowDetails(show=shows.first()[0], seasons=seasons)
|
||||
|
||||
|
||||
@router.get("/search")
|
||||
@router.get("/search", dependencies=[Depends(current_active_user)])
|
||||
def search_show(query: str, metadata_provider: str = "tmdb"):
|
||||
return metadataProvider.search_show(query, metadata_provider)
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -1,54 +0,0 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from auth import get_current_user
|
||||
from auth.password import get_password_hash
|
||||
from database import DbSessionDependency
|
||||
from database.users import User, UserCreate, UserPublic
|
||||
from users import log
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/users",
|
||||
)
|
||||
|
||||
# TODO: remove from users.py
|
||||
class Message(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
|
||||
@router.post("/", status_code=201, responses={
|
||||
409: {"model": Message, "description": "User with provided email already exists"},
|
||||
201: {"model": UserPublic, "description": "User created successfully"}
|
||||
})
|
||||
async def create_user(
|
||||
db: DbSessionDependency,
|
||||
user: UserCreate = Depends(UserCreate),
|
||||
):
|
||||
internal_user = User(name=user.name, lastname=user.lastname, email=user.email,
|
||||
hashed_password=get_password_hash(user.password))
|
||||
db.add(internal_user)
|
||||
try:
|
||||
db.commit()
|
||||
except IntegrityError as e:
|
||||
log.debug(e)
|
||||
log.warning("Failed to create new USER, User with this email already exists " + internal_user.model_dump().__str__())
|
||||
return JSONResponse(status_code=409, content={"message": "User with this email already exists"})
|
||||
log.info("Created new USER " + internal_user.email)
|
||||
return UserPublic(**internal_user.model_dump())
|
||||
|
||||
|
||||
@router.get("/me")
|
||||
async def read_users_me(
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
return JSONResponse(status_code=200, content=current_user.model_dump())
|
||||
|
||||
|
||||
@router.get("/me/items")
|
||||
async def read_own_items(
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
return [{"item_id": "Foo", "owner": current_user.name}]
|
||||
Reference in New Issue
Block a user