Ruff enable type annotations rule (#362)

This PR enables the ruff rule for return type annotations (ANN), and
adds the ty package for type checking.
This commit is contained in:
Maximilian Dorninger
2026-01-06 17:07:19 +01:00
committed by GitHub
parent dd0b439bbe
commit a39e0d204a
57 changed files with 259 additions and 179 deletions

View File

@@ -106,7 +106,13 @@ def run_migrations_online() -> None:
"""
def include_object(_object, name, type_, _reflected, _compare_to):
def include_object(
_object: object | None,
name: str | None,
type_: str | None,
_reflected: bool | None,
_compare_to: object | None,
) -> bool:
if type_ == "table" and name == "apscheduler_jobs":
return False
return True

View File

@@ -20,7 +20,3 @@ class AuthConfig(BaseSettings):
admin_emails: list[str] = []
email_password_resets: bool = False
openid_connect: OpenIdConfig = OpenIdConfig()
@property
def jwt_signing_key(self):
return self._jwt_signing_key

View File

@@ -39,5 +39,7 @@ async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
yield session
async def get_user_db(session: AsyncSession = Depends(get_async_session)):
async def get_user_db(
session: AsyncSession = Depends(get_async_session),
) -> AsyncGenerator[SQLAlchemyUserDatabase, None]:
yield SQLAlchemyUserDatabase(session, User, OAuthAccount)

View File

@@ -19,7 +19,7 @@ users_router = APIRouter()
auth_metadata_router = APIRouter()
def get_openid_router():
def get_openid_router() -> APIRouter:
if openid_client:
return get_oauth_router(
oauth_client=openid_client,

View File

@@ -1,7 +1,7 @@
import contextlib
import logging
import uuid
from typing import Any, Optional, override
from typing import Any, AsyncGenerator, Optional, override
from fastapi import Depends, Request
from fastapi.responses import RedirectResponse, Response
@@ -59,7 +59,9 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
await self.update(user=user, user_update=updated_user)
@override
async def on_after_register(self, user: User, request: Optional[Request] = None):
async def on_after_register(
self, user: User, request: Optional[Request] = None
) -> None:
log.info(f"User {user.id} has registered.")
if user.email in config.admin_emails:
updated_user = UserUpdate(is_superuser=True, is_verified=True)
@@ -68,7 +70,7 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
@override
async def on_after_forgot_password(
self, user: User, token: str, request: Optional[Request] = None
):
) -> None:
link = f"{MediaManagerConfig().misc.frontend_url}web/login/reset-password?token={token}"
log.info(f"User {user.id} has forgot their password. Reset Link: {link}")
@@ -83,7 +85,7 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
<p>Hi {user.email},
<br>
<br>
if you forgot your password, <a href="{link}">reset you password here</a>.<br>
if you forgot your password, <a href=\"{link}\">reset you password here</a>.<br>
If you did not request a password reset, you can ignore this email.</p>
<br>
<br>
@@ -99,23 +101,27 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
@override
async def on_after_reset_password(
self, user: User, request: Optional[Request] = None
):
) -> None:
log.info(f"User {user.id} has reset their password.")
@override
async def on_after_request_verify(
self, user: User, token: str, request: Optional[Request] = None
):
) -> None:
log.info(
f"Verification requested for user {user.id}. Verification token: {token}"
)
@override
async def on_after_verify(self, user: User, request: Optional[Request] = None):
async def on_after_verify(
self, user: User, request: Optional[Request] = None
) -> None:
log.info(f"User {user.id} has been verified")
async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db)):
async def get_user_manager(
user_db: SQLAlchemyUserDatabase = Depends(get_user_db),
) -> AsyncGenerator[UserManager, None]:
yield UserManager(user_db)
@@ -124,7 +130,7 @@ get_user_db_context = contextlib.asynccontextmanager(get_user_db)
get_user_manager_context = contextlib.asynccontextmanager(get_user_manager)
async def create_default_admin_user():
async def create_default_admin_user() -> None:
"""Create a default admin user if no users exist in the database"""
try:
async with get_async_session_context() as session:
@@ -177,10 +183,6 @@ async def create_default_admin_user():
)
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)

View File

@@ -41,7 +41,7 @@ class BasicConfig(BaseSettings):
movie_directory: Path = Path(__file__).parent.parent / "data" / "movies"
torrent_directory: Path = Path(__file__).parent.parent / "data" / "torrents"
frontend_url: AnyHttpUrl = "http://localhost:8000"
frontend_url: AnyHttpUrl = AnyHttpUrl("http://localhost:8000")
cors_urls: list[str] = []
development: bool = False

View File

@@ -9,6 +9,8 @@ from sqlalchemy.engine import Engine
from sqlalchemy.engine.url import URL
from sqlalchemy.orm import Session, declarative_base, sessionmaker
from media_manager.database.config import DbConfig
log = logging.getLogger(__name__)
Base = declarative_base()
@@ -35,7 +37,7 @@ def build_db_url(
def init_engine(
db_config: Any | None = None,
db_config: DbConfig | None = None,
url: str | URL | None = None,
) -> Engine:
"""

View File

@@ -7,7 +7,7 @@ from sqlalchemy.exc import IntegrityError
class MediaManagerError(Exception):
"""Base exception for MediaManager errors."""
def __init__(self, message: str = "An error occurred."):
def __init__(self, message: str = "An error occurred.") -> None:
super().__init__(message)
self.message = message
@@ -17,76 +17,76 @@ class MediaAlreadyExistsError(MediaManagerError):
def __init__(
self, message: str = "Entity with this ID or other identifier already exists"
):
) -> None:
super().__init__(message)
class NotFoundError(MediaManagerError):
"""Raised when an entity is not found (HTTP 404)."""
def __init__(self, message: str = "The requested entity was not found."):
def __init__(self, message: str = "The requested entity was not found.") -> None:
super().__init__(message)
class InvalidConfigError(MediaManagerError):
"""Raised when the server is improperly configured (HTTP 500)."""
def __init__(self, message: str = "The server is improperly configured."):
def __init__(self, message: str = "The server is improperly configured.") -> None:
super().__init__(message)
class BadRequestError(MediaManagerError):
"""Raised for invalid client requests (HTTP 400)."""
def __init__(self, message: str = "Bad request."):
def __init__(self, message: str = "Bad request.") -> None:
super().__init__(message)
class UnauthorizedError(MediaManagerError):
"""Raised for authentication failures (HTTP 401)."""
def __init__(self, message: str = "Unauthorized."):
def __init__(self, message: str = "Unauthorized.") -> None:
super().__init__(message)
class ForbiddenError(MediaManagerError):
"""Raised for forbidden actions (HTTP 403)."""
def __init__(self, message: str = "Forbidden."):
def __init__(self, message: str = "Forbidden.") -> None:
super().__init__(message)
class ConflictError(MediaManagerError):
"""Raised for resource conflicts (HTTP 409)."""
def __init__(self, message: str = "Conflict."):
def __init__(self, message: str = "Conflict.") -> None:
super().__init__(message)
class UnprocessableEntityError(MediaManagerError):
"""Raised for validation errors (HTTP 422)."""
def __init__(self, message: str = "Unprocessable entity."):
def __init__(self, message: str = "Unprocessable entity.") -> None:
super().__init__(message)
# Exception handlers
async def media_already_exists_exception_handler(
_request: Request, exc: MediaAlreadyExistsError
_request: Request, _exc: Exception
) -> JSONResponse:
return JSONResponse(status_code=409, content={"detail": exc.message})
return JSONResponse(status_code=409, content={"detail": str(_exc)})
async def not_found_error_exception_handler(
_request: Request, exc: NotFoundError
_request: Request, _exc: Exception
) -> JSONResponse:
return JSONResponse(status_code=404, content={"detail": exc.message})
return JSONResponse(status_code=404, content={"detail": str(_exc)})
async def invalid_config_error_exception_handler(
_request: Request, exc: InvalidConfigError
_request: Request, _exc: Exception
) -> JSONResponse:
return JSONResponse(status_code=500, content={"detail": exc.message})
return JSONResponse(status_code=500, content={"detail": str(_exc)})
async def bad_request_error_handler(
@@ -107,8 +107,8 @@ async def forbidden_error_handler(
return JSONResponse(status_code=403, content={"detail": exc.message})
async def conflict_error_handler(_request: Request, exc: ConflictError) -> JSONResponse:
return JSONResponse(status_code=409, content={"detail": exc.message})
async def conflict_error_handler(_request: Request, _exc: Exception) -> JSONResponse:
return JSONResponse(status_code=409, content={"detail": str(_exc)})
async def unprocessable_entity_error_handler(
@@ -128,7 +128,7 @@ async def sqlalchemy_integrity_error_handler(
)
def register_exception_handlers(app: FastAPI):
def register_exception_handlers(app: FastAPI) -> None:
app.add_exception_handler(NotFoundError, not_found_error_exception_handler)
app.add_exception_handler(
MediaAlreadyExistsError, media_already_exists_exception_handler

View File

@@ -1,8 +1,11 @@
import shutil
from logging import Logger
from pathlib import Path
from media_manager.config import MediaManagerConfig
def run_filesystem_checks(config, log):
def run_filesystem_checks(config: MediaManagerConfig, log: Logger) -> None:
log.info("Creating directories if they don't exist...")
config.misc.tv_directory.mkdir(parents=True, exist_ok=True)
config.misc.movie_directory.mkdir(parents=True, exist_ok=True)

View File

@@ -5,7 +5,6 @@ from fastapi import Depends
from media_manager.database import DbSessionDependency
from media_manager.indexer.repository import IndexerRepository
from media_manager.indexer.service import IndexerService
from media_manager.tv.service import TvService
def get_indexer_repository(db_session: DbSessionDependency) -> IndexerRepository:
@@ -21,4 +20,4 @@ def get_indexer_service(
return IndexerService(indexer_repository)
indexer_service_dep = Annotated[TvService, Depends(get_indexer_service)]
indexer_service_dep = Annotated[IndexerService, Depends(get_indexer_service)]

View File

@@ -8,7 +8,7 @@ from media_manager.tv.schemas import Show
class GenericIndexer(ABC):
name: str
def __init__(self, name: str):
def __init__(self, name: str) -> None:
self.name = name
@abstractmethod

View File

@@ -1,4 +1,5 @@
import concurrent
import concurrent.futures
import logging
from concurrent.futures.thread import ThreadPoolExecutor
@@ -15,7 +16,7 @@ log = logging.getLogger(__name__)
class Jackett(GenericIndexer, TorznabMixin):
def __init__(self):
def __init__(self) -> None:
"""
A subclass of GenericIndexer for interacting with the Jacket API.
@@ -73,7 +74,9 @@ class Jackett(GenericIndexer, TorznabMixin):
def search_season(
self, query: str, show: Show, season_number: int
) -> list[IndexerQueryResult]:
pass
log.debug(f"Searching for season {season_number} of show {show.title}")
return self.search(query=query, is_tv=True)
def search_movie(self, query: str, movie: Movie) -> list[IndexerQueryResult]:
pass
log.debug(f"Searching for movie {movie.title}")
return self.search(query=query, is_tv=False)

View File

@@ -1,7 +1,7 @@
import logging
from dataclasses import dataclass
from requests import Session
from requests import Response, Session
from media_manager.config import MediaManagerConfig
from media_manager.indexer.indexers.generic import GenericIndexer
@@ -31,14 +31,14 @@ class IndexerInfo:
class Prowlarr(GenericIndexer, TorznabMixin):
def __init__(self):
def __init__(self) -> None:
"""
A subclass of GenericIndexer for interacting with the Prowlarr API.
"""
super().__init__(name="prowlarr")
self.config = MediaManagerConfig().indexers.prowlarr
def _call_prowlarr_api(self, path: str, parameters: dict | None = None):
def _call_prowlarr_api(self, path: str, parameters: dict | None = None) -> Response:
url = f"{self.config.url}/api/v1{path}"
headers = {"X-Api-Key": self.config.api_key}
with Session() as session:

View File

@@ -61,12 +61,19 @@ class TorznabMixin:
if upload_volume_factor == 2:
flags.append("doubleupload")
if not item.find("size") or item.find("size").text is None:
log.warning(
f"Torznab item {item.find('title').text} has no size, skipping."
)
continue
size = int(item.find("size").text or "0")
result = IndexerQueryResult(
title=item.find("title").text,
title=item.find("title").text or "unknown",
download_url=str(item.find("enclosure").attrib["url"]),
seeders=seeders,
flags=flags,
size=int(item.find("size").text),
size=size,
usenet=is_usenet,
age=age,
indexer=indexer_name,

View File

@@ -14,7 +14,7 @@ log = logging.getLogger(__name__)
class IndexerRepository:
def __init__(self, db: Session):
def __init__(self, db: Session) -> None:
self.db = db
def get_result(self, result_id: IndexerQueryResultId) -> IndexerQueryResultSchema:

View File

@@ -13,7 +13,7 @@ IndexerQueryResultId = typing.NewType("IndexerQueryResultId", UUID)
class IndexerQueryResult(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: IndexerQueryResultId = pydantic.Field(default_factory=uuid4)
id: IndexerQueryResultId = pydantic.Field(default_factory=lambda: IndexerQueryResultId(uuid4()))
title: str
download_url: str = pydantic.Field(
exclude=True,
@@ -62,7 +62,7 @@ class IndexerQueryResult(BaseModel):
result = []
return result
def __gt__(self, other) -> bool:
def __gt__(self, other: "IndexerQueryResult") -> bool:
if self.quality.value != other.quality.value:
return self.quality.value < other.quality.value
if self.score != other.score:
@@ -76,7 +76,7 @@ class IndexerQueryResult(BaseModel):
return self.size < other.size
def __lt__(self, other) -> bool:
def __lt__(self, other: "IndexerQueryResult") -> bool:
if self.quality.value != other.quality.value:
return self.quality.value > other.quality.value
if self.score != other.score:

View File

@@ -14,7 +14,7 @@ log = logging.getLogger(__name__)
class IndexerService:
def __init__(self, indexer_repository: IndexerRepository):
def __init__(self, indexer_repository: IndexerRepository) -> None:
config = MediaManagerConfig()
self.repository = indexer_repository
self.indexers: list[GenericIndexer] = []
@@ -55,7 +55,7 @@ class IndexerService:
return results
def search_movie(self, movie: Movie):
def search_movie(self, movie: Movie) -> list[IndexerQueryResult]:
query = f"{movie.name} {movie.year}"
query = remove_special_chars_and_parentheses(query)
@@ -75,7 +75,7 @@ class IndexerService:
return results
def search_season(self, show: Show, season_number: int):
def search_season(self, show: Show, season_number: int) -> list[IndexerQueryResult]:
query = f"{show.name} {show.year} S{season_number:02d}"
query = remove_special_chars_and_parentheses(query)

View File

@@ -14,7 +14,7 @@ log = logging.getLogger(__name__)
def evaluate_indexer_query_result(
query_result: IndexerQueryResult, ruleset: ScoringRuleSet
) -> (IndexerQueryResult, bool):
) -> tuple[IndexerQueryResult, bool]:
title_rules = MediaManagerConfig().indexers.title_scoring_rules
indexer_flag_rules = MediaManagerConfig().indexers.indexer_flag_scoring_rules
for rule_name in ruleset.rule_names:

View File

@@ -11,7 +11,7 @@ from pythonjsonlogger.json import JsonFormatter
class ISOJsonFormatter(JsonFormatter):
@override
def formatTime(self, record, datefmt=None):
def formatTime(self, record: logging.LogRecord, datefmt: str | None = None) -> str:
dt = datetime.fromtimestamp(record.created, tz=timezone.utc)
return dt.isoformat(timespec="milliseconds").replace("+00:00", "Z")
@@ -62,7 +62,7 @@ LOGGING_CONFIG = {
}
def setup_logging():
def setup_logging() -> None:
dictConfig(LOGGING_CONFIG)
logging.basicConfig(
level=LOG_LEVEL,

View File

@@ -2,12 +2,12 @@ import logging
import os
import uvicorn
from fastapi import APIRouter, FastAPI
from fastapi import APIRouter, FastAPI, Request, Response
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from psycopg.errors import UniqueViolation
from sqlalchemy.exc import IntegrityError
from starlette.responses import FileResponse, RedirectResponse, Response
from starlette.responses import FileResponse, RedirectResponse
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
import media_manager.movies.router as movies_router
@@ -143,23 +143,23 @@ else:
@app.get("/")
async def root():
async def root() -> RedirectResponse:
return RedirectResponse(url="/web/")
@app.get("/dashboard")
async def dashboard():
async def dashboard() -> RedirectResponse:
return RedirectResponse(url="/web/")
@app.get("/login")
async def login():
async def login() -> RedirectResponse:
return RedirectResponse(url="/web/")
# this will serve the custom 404 page for frontend routes, so SvelteKit can handle routing
@app.exception_handler(404)
async def not_found_handler(request, _exc):
async def not_found_handler(request: Request, _exc: Exception) -> Response:
if not DISABLE_FRONTEND_MOUNT and any(
base_path in ["/web", "/dashboard", "/login"] for base_path in request.url.path
):

View File

@@ -19,13 +19,13 @@ class AbstractMetadataProvider(ABC):
@abstractmethod
def get_show_metadata(
self, show_id: int | None = None, language: str | None = None
self, show_id: int, language: str | None = None
) -> Show:
raise NotImplementedError()
@abstractmethod
def get_movie_metadata(
self, movie_id: int | None = None, language: str | None = None
self, movie_id: int, language: str | None = None
) -> Movie:
raise NotImplementedError()

View File

@@ -21,7 +21,7 @@ log = logging.getLogger(__name__)
class TmdbMetadataProvider(AbstractMetadataProvider):
name = "tmdb"
def __init__(self):
def __init__(self) -> None:
config = MediaManagerConfig().metadata.tmdb
self.url = config.tmdb_relay_url
self.primary_languages = config.primary_languages
@@ -244,12 +244,12 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
@override
def get_show_metadata(
self, show_id: int | None = None, language: str | None = None
self, show_id: int, language: str | None = None
) -> Show:
"""
:param id: the external id of the show
:type id: int
:param show_id: the external id of the show
:type show_id: int
:param language: optional language code (ISO 639-1) to fetch metadata in
:type language: str | None
:return: returns a Show object
@@ -374,13 +374,13 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
@override
def get_movie_metadata(
self, movie_id: int | None = None, language: str | None = None
self, movie_id: int, language: str | None = None
) -> Movie:
"""
Get movie metadata with language-aware fetching.
:param id: the external id of the movie
:type id: int
:param movie_id: the external id of the movie
:type movie_id: int
:param language: optional language code (ISO 639-1) to fetch metadata in
:type language: str | None
:return: returns a Movie object

View File

@@ -18,7 +18,7 @@ log = logging.getLogger(__name__)
class TvdbMetadataProvider(AbstractMetadataProvider):
name = "tvdb"
def __init__(self):
def __init__(self) -> None:
config = MediaManagerConfig().metadata.tvdb
self.url = config.tvdb_relay_url
@@ -64,11 +64,11 @@ class TvdbMetadataProvider(AbstractMetadataProvider):
@override
def get_show_metadata(
self, show_id: int | None = None, language: str | None = None
self, show_id: int, language: str | None = None
) -> Show:
"""
:param id: the external id of the show
:param show_id: The external id of the show
:param language: does nothing, TVDB does not support multiple languages
"""
series = self.__get_show(show_id)
@@ -230,9 +230,14 @@ class TvdbMetadataProvider(AbstractMetadataProvider):
except KeyError:
year = None
if result.get("image"):
poster_path = "https://artworks.thetvdb.com" + str(result.get("image"))
else:
poster_path = None
formatted_results.append(
MetaDataProviderSearchResult(
poster_path="https://artworks.thetvdb.com" + result.get("image")
poster_path= poster_path
if result.get("image")
else None,
overview=result.get("overview"),
@@ -265,7 +270,7 @@ class TvdbMetadataProvider(AbstractMetadataProvider):
@override
def get_movie_metadata(
self, movie_id: int | None = None, language: str | None = None
self, movie_id: int, language: str | None = None
) -> Movie:
"""
@@ -273,7 +278,7 @@ class TvdbMetadataProvider(AbstractMetadataProvider):
:param language: does nothing, TVDB does not support multiple languages
:return: returns a Movie object
"""
movie = self.__get_movie(movie_id)
movie = self.__get_movie(movie_id=movie_id)
# get imdb id from remote ids
imdb_id = None

View File

@@ -40,7 +40,7 @@ class MovieRepository:
Provides methods to retrieve, save, and delete movies.
"""
def __init__(self, db: Session):
def __init__(self, db: Session) -> None:
self.db = db
def get_movie_by_id(self, movie_id: MovieId) -> MovieSchema:

View File

@@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
from media_manager.auth.schemas import UserRead
from media_manager.auth.users import current_active_user, current_superuser
from media_manager.config import LibraryItem, MediaManagerConfig
from media_manager.exceptions import ConflictError
from media_manager.exceptions import ConflictError, NotFoundError
from media_manager.indexer.schemas import (
IndexerQueryResult,
IndexerQueryResultId,
@@ -97,7 +97,7 @@ def get_all_importable_movies(
)
def import_detected_movie(
movie_service: movie_service_dep, movie: movie_dep, directory: str
):
) -> None:
"""
Import a detected movie from the specified directory into the library.
"""
@@ -145,7 +145,7 @@ def add_a_movie(
metadata_provider: metadata_provider_dep,
movie_id: int,
language: str | None = None,
):
) -> Movie:
"""
Add a new movie to the library.
"""
@@ -159,6 +159,8 @@ def add_a_movie(
movie = movie_service.get_movie_by_external_id(
external_id=movie_id, metadata_provider=metadata_provider.name
)
if not movie:
raise NotFoundError from ConflictError
return movie
@@ -217,7 +219,7 @@ def create_movie_request(
log.info(
f"User {user.email} is creating a movie request for {movie_request.movie_id}"
)
movie_request = MovieRequest.model_validate(movie_request)
movie_request: MovieRequest = MovieRequest.model_validate(movie_request)
movie_request.requested_by = user
if user.is_superuser:
movie_request.authorized = True
@@ -254,7 +256,7 @@ def authorize_request(
movie_request_id: MovieRequestId,
user: Annotated[UserRead, Depends(current_superuser)],
authorized_status: bool = False,
):
) -> MovieRequest:
"""
Authorize or de-authorize a movie request.
"""
@@ -276,7 +278,7 @@ def authorize_request(
)
def delete_movie_request(
movie_service: movie_service_dep, movie_request_id: MovieRequestId
):
) -> None:
"""
Delete a movie request.
"""
@@ -309,7 +311,7 @@ def delete_a_movie(
movie: movie_dep,
delete_files_on_disk: bool = False,
delete_torrents: bool = False,
):
) -> None:
"""
Delete a movie from the library.
"""

View File

@@ -15,7 +15,7 @@ MovieRequestId = typing.NewType("MovieRequestId", UUID)
class Movie(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: MovieId = Field(default_factory=uuid.uuid4)
id: MovieId = Field(default_factory=lambda: MovieId(uuid.uuid4()))
name: str
overview: str
year: int | None
@@ -59,7 +59,7 @@ class CreateMovieRequest(MovieRequestBase):
class MovieRequest(MovieRequestBase):
model_config = ConfigDict(from_attributes=True)
id: MovieRequestId = Field(default_factory=uuid.uuid4)
id: MovieRequestId = Field(default_factory=lambda: MovieRequestId(uuid.uuid4()))
movie_id: MovieId

View File

@@ -32,6 +32,7 @@ from media_manager.movies.schemas import (
RichMovieRequest,
RichMovieTorrent,
)
from media_manager.notification.repository import NotificationRepository
from media_manager.notification.service import NotificationService
from media_manager.schemas import MediaImportSuggestion
from media_manager.torrent.repository import TorrentRepository
@@ -58,8 +59,8 @@ class MovieService:
movie_repository: MovieRepository,
torrent_service: TorrentService,
indexer_service: IndexerService,
notification_service: NotificationService = None,
):
notification_service: NotificationService,
) -> None:
self.movie_repository = movie_repository
self.torrent_service = torrent_service
self.indexer_service = indexer_service
@@ -70,7 +71,7 @@ class MovieService:
external_id: int,
metadata_provider: AbstractMetadataProvider,
language: str | None = None,
) -> Movie | None:
) -> Movie:
"""
Add a new movie to the database.
@@ -82,7 +83,7 @@ class MovieService:
movie_id=external_id, language=language
)
if not movie_with_metadata:
return None
raise NotFoundError
saved_movie = self.movie_repository.save_movie(movie=movie_with_metadata)
metadata_provider.download_movie_poster_image(movie=saved_movie)
@@ -99,7 +100,7 @@ class MovieService:
def get_movie_request_by_id(
self, movie_request_id: MovieRequestId
) -> MovieRequest | None:
) -> MovieRequest:
"""
Get a movie request by its ID.
@@ -781,14 +782,16 @@ def auto_download_all_approved_movie_requests() -> None:
Auto download all approved movie requests.
This is a standalone function as it creates its own DB session.
"""
db: Session = SessionLocal()
db: Session = SessionLocal() if SessionLocal else next(get_session())
movie_repository = MovieRepository(db=db)
torrent_service = TorrentService(torrent_repository=TorrentRepository(db=db))
indexer_service = IndexerService(indexer_repository=IndexerRepository(db=db))
notification_service = NotificationService(notification_repository=NotificationRepository(db=db))
movie_service = MovieService(
movie_repository=movie_repository,
torrent_service=torrent_service,
indexer_service=indexer_service,
notification_service=notification_service
)
log.info("Auto downloading all approved movie requests")
@@ -818,10 +821,12 @@ def import_all_movie_torrents() -> None:
movie_repository = MovieRepository(db=db)
torrent_service = TorrentService(torrent_repository=TorrentRepository(db=db))
indexer_service = IndexerService(indexer_repository=IndexerRepository(db=db))
notification_service = NotificationService(notification_repository=NotificationRepository(db=db))
movie_service = MovieService(
movie_repository=movie_repository,
torrent_service=torrent_service,
indexer_service=indexer_service,
notification_service=notification_service,
)
log.info("Importing all torrents")
torrents = torrent_service.get_all_torrents()
@@ -855,6 +860,7 @@ def update_all_movies_metadata() -> None:
movie_repository=movie_repository,
torrent_service=TorrentService(torrent_repository=TorrentRepository(db=db)),
indexer_service=IndexerService(indexer_repository=IndexerRepository(db=db)),
notification_service=NotificationService(notification_repository=NotificationRepository(db=db))
)
log.info("Updating metadata for all movies")

View File

@@ -31,7 +31,7 @@ class NotificationManager:
Manages and orchestrates notifications across all configured service providers.
"""
def __init__(self):
def __init__(self) -> None:
self.config = MediaManagerConfig().notifications
self.providers: List[AbstractNotificationServiceProvider] = []
self._initialize_providers()

View File

@@ -20,7 +20,7 @@ log = logging.getLogger(__name__)
class NotificationRepository:
def __init__(self, db: Session):
def __init__(self, db: Session) -> None:
self.db = db
def get_notification(self, nid: NotificationId) -> NotificationSchema:
@@ -60,7 +60,7 @@ class NotificationRepository:
log.error(f"Database error while retrieving notifications: {e}")
raise
def save_notification(self, notification: NotificationSchema):
def save_notification(self, notification: NotificationSchema) -> None:
try:
self.db.add(
Notification(

View File

@@ -69,7 +69,7 @@ def get_notification(
)
def mark_notification_as_read(
notification_id: NotificationId, notification_service: notification_service_dep
):
) -> None:
"""
Mark a notification as read.
"""
@@ -86,7 +86,7 @@ def mark_notification_as_read(
)
def mark_notification_as_unread(
notification_id: NotificationId, notification_service: notification_service_dep
):
) -> None:
"""
Mark a notification as unread.
"""
@@ -103,7 +103,7 @@ def mark_notification_as_unread(
)
def delete_notification(
notification_id: NotificationId, notification_service: notification_service_dep
):
) -> None:
"""
Delete a notification.
"""

View File

@@ -12,7 +12,7 @@ class Notification(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: NotificationId = Field(
default_factory=uuid.uuid4, description="Unique identifier for the notification"
default_factory=lambda: NotificationId(uuid.uuid4()), description="Unique identifier for the notification"
)
read: bool = Field(False, description="Whether the notification has been read")
message: str = Field(description="The content of the notification")

View File

@@ -7,7 +7,7 @@ class NotificationService:
def __init__(
self,
notification_repository: NotificationRepository,
):
) -> None:
self.notification_repository = notification_repository
self.notification_manager = notification_manager

View File

@@ -7,7 +7,7 @@ from media_manager.notification.service_providers.abstract_notification_service_
class EmailNotificationServiceProvider(AbstractNotificationServiceProvider):
def __init__(self):
def __init__(self) -> None:
self.config = MediaManagerConfig().notifications.email_notifications
def send_notification(self, message: MessageNotification) -> bool:

View File

@@ -12,7 +12,7 @@ class GotifyNotificationServiceProvider(AbstractNotificationServiceProvider):
Gotify Notification Service Provider
"""
def __init__(self):
def __init__(self) -> None:
self.config = MediaManagerConfig().notifications.gotify
def send_notification(self, message: MessageNotification) -> bool:

View File

@@ -12,7 +12,7 @@ class NtfyNotificationServiceProvider(AbstractNotificationServiceProvider):
Ntfy Notification Service Provider
"""
def __init__(self):
def __init__(self) -> None:
self.config = MediaManagerConfig().notifications.ntfy
def send_notification(self, message: MessageNotification) -> bool:

View File

@@ -8,7 +8,7 @@ from media_manager.notification.service_providers.abstract_notification_service_
class PushoverNotificationServiceProvider(AbstractNotificationServiceProvider):
def __init__(self):
def __init__(self) -> None:
self.config = MediaManagerConfig().notifications.pushover
def send_notification(self, message: MessageNotification) -> bool:

View File

@@ -3,6 +3,7 @@ from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
import media_manager.database
from media_manager.config import MediaManagerConfig
from media_manager.movies.service import (
auto_download_all_approved_movie_requests,
import_all_movie_torrents,
@@ -15,7 +16,7 @@ from media_manager.tv.service import (
)
def setup_scheduler(config):
def setup_scheduler(config: MediaManagerConfig) -> BackgroundScheduler:
from media_manager.database import init_engine
init_engine(config.database)

View File

@@ -16,11 +16,11 @@ class AbstractDownloadClient(ABC):
pass
@abstractmethod
def download_torrent(self, torrent: IndexerQueryResult) -> Torrent:
def download_torrent(self, indexer_result: IndexerQueryResult) -> Torrent:
"""
Add a torrent to the download client and return the torrent object.
:param torrent: The indexer query result of the torrent file to download.
:param indexer_result: The indexer query result of the torrent file to download.
:return: The torrent object with calculated hash and initial status.
"""

View File

@@ -43,7 +43,7 @@ class QbittorrentDownloadClient(AbstractDownloadClient):
ERROR_STATE = ("missingFiles", "error", "checkingResumeData")
UNKNOWN_STATE = ("unknown",)
def __init__(self):
def __init__(self) -> None:
self.config = MediaManagerConfig().torrents.qbittorrent
self.api_client = qbittorrentapi.Client(
host=self.config.host,

View File

@@ -27,7 +27,7 @@ class SabnzbdDownloadClient(AbstractDownloadClient):
ERROR_STATE = ("Failed",)
UNKNOWN_STATE = ("Unknown",)
def __init__(self):
def __init__(self) -> None:
self.config = MediaManagerConfig().torrents.sabnzbd
self.client = sabnzbd_api.SabnzbdClient(
host=self.config.host,

View File

@@ -30,7 +30,7 @@ class TransmissionDownloadClient(AbstractDownloadClient):
}
)
def __init__(self):
def __init__(self) -> None:
self.config = MediaManagerConfig().torrents.transmission
try:
self._client = transmission_rpc.Client(

View File

@@ -30,7 +30,7 @@ class DownloadManager:
Only one torrent client and one usenet client are active at a time.
"""
def __init__(self):
def __init__(self) -> None:
self._torrent_client: AbstractDownloadClient | None = None
self._usenet_client: AbstractDownloadClient | None = None
self.config = MediaManagerConfig().torrents

View File

@@ -18,7 +18,7 @@ from media_manager.tv.schemas import Show as ShowSchema
class TorrentRepository:
def __init__(self, db: DbSessionDependency):
def __init__(self, db: DbSessionDependency) -> None:
self.db = db
def get_seasons_files_of_torrent(
@@ -62,7 +62,7 @@ class TorrentRepository:
def delete_torrent(
self, torrent_id: TorrentId, delete_associated_media_files: bool = False
):
) -> None:
if delete_associated_media_files:
movie_files_stmt = delete(MovieFile).where(
MovieFile.torrent_id == torrent_id
@@ -76,7 +76,7 @@ class TorrentRepository:
self.db.delete(self.db.get(Torrent, torrent_id))
def get_movie_of_torrent(self, torrent_id: TorrentId):
def get_movie_of_torrent(self, torrent_id: TorrentId) -> MovieSchema | None:
stmt = (
select(Movie)
.join(MovieFile, Movie.id == MovieFile.movie_id)
@@ -87,7 +87,7 @@ class TorrentRepository:
return None
return MovieSchema.model_validate(result)
def get_movie_files_of_torrent(self, torrent_id: TorrentId):
def get_movie_files_of_torrent(self, torrent_id: TorrentId) -> list[MovieFileSchema]:
stmt = select(MovieFile).where(MovieFile.torrent_id == torrent_id)
result = self.db.execute(stmt).scalars().all()
return [MovieFileSchema.model_validate(movie_file) for movie_file in result]

View File

@@ -36,7 +36,7 @@ def delete_torrent(
service: torrent_service_dep,
torrent: torrent_dep,
delete_files: bool = False,
):
) -> None:
if delete_files:
try:
service.cancel_download(torrent=torrent, delete_files=False)
@@ -54,7 +54,7 @@ def delete_torrent(
def retry_torrent_download(
service: torrent_service_dep,
torrent: torrent_dep,
):
) -> None:
service.pause_download(torrent=torrent)
service.resume_download(torrent=torrent)

View File

@@ -33,7 +33,7 @@ class TorrentStatus(Enum):
class Torrent(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: TorrentId = Field(default_factory=uuid.uuid4)
id: TorrentId = Field(default_factory=lambda: TorrentId(uuid.uuid4()))
status: TorrentStatus
title: str
quality: Quality

View File

@@ -1,7 +1,7 @@
import logging
from media_manager.indexer.schemas import IndexerQueryResult
from media_manager.movies.schemas import Movie
from media_manager.movies.schemas import Movie, MovieFile
from media_manager.torrent.manager import DownloadManager
from media_manager.torrent.repository import TorrentRepository
from media_manager.torrent.schemas import Torrent, TorrentId
@@ -14,8 +14,8 @@ class TorrentService:
def __init__(
self,
torrent_repository: TorrentRepository,
download_manager: DownloadManager = None,
):
download_manager: DownloadManager | None = None,
) -> None:
self.torrent_repository = torrent_repository
self.download_manager = download_manager or DownloadManager()
@@ -101,7 +101,7 @@ class TorrentService:
self.torrent_repository.get_torrent_by_id(torrent_id=torrent_id)
)
def delete_torrent(self, torrent_id: TorrentId):
def delete_torrent(self, torrent_id: TorrentId) -> None:
log.info(f"Deleting torrent with ID: {torrent_id}")
t = self.torrent_repository.get_torrent_by_id(torrent_id=torrent_id)
delete_media_files = not t.imported
@@ -109,5 +109,5 @@ class TorrentService:
torrent_id=torrent_id, delete_associated_media_files=delete_media_files
)
def get_movie_files_of_torrent(self, torrent: Torrent):
def get_movie_files_of_torrent(self, torrent: Torrent) -> list[MovieFile]:
return self.torrent_repository.get_movie_files_of_torrent(torrent_id=torrent.id)

View File

@@ -34,7 +34,7 @@ def list_files_recursively(path: Path = Path()) -> list[Path]:
return valid_files
def extract_archives(files):
def extract_archives(files: list) -> None:
archive_types = {
"application/zip",
"application/x-zip-compressedapplication/x-compressed",
@@ -61,11 +61,11 @@ def extract_archives(files):
log.error(f"Failed to extract archive {file}. Error: {e}")
def get_torrent_filepath(torrent: Torrent):
def get_torrent_filepath(torrent: Torrent) -> Path:
return MediaManagerConfig().misc.torrent_directory / torrent.title
def import_file(target_file: Path, source_file: Path):
def import_file(target_file: Path, source_file: Path) -> None:
if target_file.exists():
target_file.unlink()
@@ -87,11 +87,15 @@ def get_files_for_import(
Extracts all files from the torrent download directory, including extracting archives.
Returns a tuple containing: seperated video files, subtitle files, and all files found in the torrent directory.
"""
search_directory = directory if directory else get_torrent_filepath(torrent=torrent)
if torrent:
log.info(f"Importing torrent {torrent}")
else:
search_directory = get_torrent_filepath(torrent=torrent)
elif directory:
log.info(f"Importing files from directory {directory}")
search_directory = directory
else:
msg = "Either torrent or directory must be provided."
raise ValueError(msg)
all_files: list[Path] = list_files_recursively(path=search_directory)
log.debug(f"Found {len(all_files)} files downloaded by the torrent")

View File

@@ -44,7 +44,7 @@ class TvRepository:
Provides methods to retrieve, save, and delete shows and seasons.
"""
def __init__(self, db: Session):
def __init__(self, db: Session) -> None:
self.db = db
def get_show_by_id(self, show_id: ShowId) -> ShowSchema:
@@ -341,10 +341,10 @@ class TvRepository:
results = self.db.execute(stmt).scalars().unique().all()
return [
RichSeasonRequestSchema(
id=x.id,
id=SeasonRequestId(x.id),
min_quality=x.min_quality,
wanted_quality=x.wanted_quality,
season_id=x.season_id,
season_id=SeasonId(x.season_id),
show=x.season.show,
season=x.season,
requested_by=x.requested_by,

View File

@@ -7,7 +7,7 @@ from media_manager.auth.db import User
from media_manager.auth.schemas import UserRead
from media_manager.auth.users import current_active_user, current_superuser
from media_manager.config import LibraryItem, MediaManagerConfig
from media_manager.exceptions import MediaAlreadyExistsError
from media_manager.exceptions import MediaAlreadyExistsError, NotFoundError
from media_manager.indexer.schemas import (
IndexerQueryResult,
IndexerQueryResultId,
@@ -94,7 +94,7 @@ def get_all_importable_shows(
dependencies=[Depends(current_superuser)],
status_code=status.HTTP_204_NO_CONTENT,
)
def import_detected_show(tv_service: tv_service_dep, tv_show: show_dep, directory: str):
def import_detected_show(tv_service: tv_service_dep, tv_show: show_dep, directory: str) -> None:
"""
Import a detected show from the specified directory into the library.
"""
@@ -140,12 +140,12 @@ def add_a_show(
metadata_provider: metadata_provider_dep,
show_id: int,
language: str | None = None,
):
) -> Show:
"""
Add a new show to the library.
"""
try:
show = tv_service.add_show(
show = tv_service.add_show(
external_id=show_id,
metadata_provider=metadata_provider,
language=language,
@@ -154,6 +154,8 @@ def add_a_show(
show = tv_service.get_show_by_external_id(
show_id, metadata_provider=metadata_provider.name
)
if not show:
raise NotFoundError from MediaAlreadyExistsError
return show
@@ -205,7 +207,7 @@ def delete_a_show(
show: show_dep,
delete_files_on_disk: bool = False,
delete_torrents: bool = False,
):
) -> None:
"""
Delete a show from the library.
"""
@@ -296,7 +298,7 @@ def request_a_season(
user: Annotated[User, Depends(current_active_user)],
season_request: CreateSeasonRequest,
tv_service: tv_service_dep,
):
) -> None:
"""
Create a new season request.
"""
@@ -314,7 +316,7 @@ def update_request(
tv_service: tv_service_dep,
user: Annotated[User, Depends(current_active_user)],
season_request: UpdateSeasonRequest,
):
) -> None:
"""
Update an existing season request.
"""
@@ -336,13 +338,15 @@ def authorize_request(
user: Annotated[User, Depends(current_superuser)],
season_request_id: SeasonRequestId,
authorized_status: bool = False,
):
) -> None:
"""
Authorize or de-authorize a season request.
"""
season_request = tv_service.get_season_request_by_id(
season_request_id=season_request_id
)
if not season_request:
raise NotFoundError
season_request.authorized_by = UserRead.model_validate(user)
season_request.authorized = authorized_status
if not authorized_status:
@@ -359,7 +363,7 @@ def delete_season_request(
tv_service: tv_service_dep,
user: Annotated[User, Depends(current_active_user)],
request_id: SeasonRequestId,
):
) -> None:
"""
Delete a season request.
"""
@@ -367,11 +371,11 @@ def delete_season_request(
if user.is_superuser or request.requested_by.id == user.id:
tv_service.delete_season_request(season_request_id=request_id)
log.info(f"User {user.id} deleted season request {request_id}.")
return None
return
log.warning(
f"User {user.id} tried to delete season request {request_id} but is not authorized."
)
return HTTPException(
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to delete this request",
)

View File

@@ -20,7 +20,7 @@ SeasonRequestId = typing.NewType("SeasonRequestId", UUID)
class Episode(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: EpisodeId = Field(default_factory=uuid.uuid4)
id: EpisodeId = Field(default_factory=lambda: EpisodeId(uuid.uuid4()))
number: EpisodeNumber
external_id: int
title: str
@@ -29,7 +29,7 @@ class Episode(BaseModel):
class Season(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: SeasonId = Field(default_factory=uuid.uuid4)
id: SeasonId = Field(default_factory=lambda: SeasonId(uuid.uuid4()))
number: SeasonNumber
name: str
@@ -43,7 +43,7 @@ class Season(BaseModel):
class Show(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: ShowId = Field(default_factory=uuid.uuid4)
id: ShowId = Field(default_factory=lambda: ShowId(uuid.uuid4()))
name: str
overview: str
@@ -85,7 +85,7 @@ class UpdateSeasonRequest(SeasonRequestBase):
class SeasonRequest(SeasonRequestBase):
model_config = ConfigDict(from_attributes=True)
id: SeasonRequestId = Field(default_factory=uuid.uuid4)
id: SeasonRequestId = Field(default_factory=lambda: SeasonRequestId(uuid.uuid4()))
season_id: SeasonId
requested_by: UserRead | None = None

View File

@@ -19,6 +19,7 @@ from media_manager.metadataProvider.abstract_metadata_provider import (
from media_manager.metadataProvider.schemas import MetaDataProviderSearchResult
from media_manager.metadataProvider.tmdb import TmdbMetadataProvider
from media_manager.metadataProvider.tvdb import TvdbMetadataProvider
from media_manager.notification.repository import NotificationRepository
from media_manager.notification.service import NotificationService
from media_manager.schemas import MediaImportSuggestion
from media_manager.torrent.repository import TorrentRepository
@@ -66,8 +67,8 @@ class TvService:
tv_repository: TvRepository,
torrent_service: TorrentService,
indexer_service: IndexerService,
notification_service: NotificationService = None,
):
notification_service: NotificationService,
) -> None:
self.tv_repository = tv_repository
self.torrent_service = torrent_service
self.indexer_service = indexer_service
@@ -78,7 +79,7 @@ class TvService:
external_id: int,
metadata_provider: AbstractMetadataProvider,
language: str | None = None,
) -> Show | None:
) -> Show:
"""
Add a new show to the database.
@@ -572,7 +573,7 @@ class TvService:
self.delete_season_request(season_request.id)
return True
def get_root_show_directory(self, show: Show):
def get_root_show_directory(self, show: Show) -> Path:
misc_config = MediaManagerConfig().misc
show_directory_name = f"{remove_special_characters(show.name)} ({show.year}) [{show.metadata_provider}id-{show.external_id}]"
log.debug(
@@ -966,10 +967,12 @@ def auto_download_all_approved_season_requests() -> None:
tv_repository = TvRepository(db=db)
torrent_service = TorrentService(torrent_repository=TorrentRepository(db=db))
indexer_service = IndexerService(indexer_repository=IndexerRepository(db=db))
notification_service = NotificationService(notification_repository=NotificationRepository(db=db))
tv_service = TvService(
tv_repository=tv_repository,
torrent_service=torrent_service,
indexer_service=indexer_service,
notification_service=notification_service
)
log.info("Auto downloading all approved season requests")
@@ -1001,10 +1004,12 @@ def import_all_show_torrents() -> None:
tv_repository = TvRepository(db=db)
torrent_service = TorrentService(torrent_repository=TorrentRepository(db=db))
indexer_service = IndexerService(indexer_repository=IndexerRepository(db=db))
notification_service = NotificationService(notification_repository=NotificationRepository(db=db))
tv_service = TvService(
tv_repository=tv_repository,
torrent_service=torrent_service,
indexer_service=indexer_service,
notification_service=notification_service
)
log.info("Importing all torrents")
torrents = torrent_service.get_all_torrents()
@@ -1037,6 +1042,7 @@ def update_all_non_ended_shows_metadata() -> None:
tv_repository=tv_repository,
torrent_service=TorrentService(torrent_repository=TorrentRepository(db=db)),
indexer_service=IndexerService(indexer_repository=IndexerRepository(db=db)),
notification_service=NotificationService(notification_repository=NotificationRepository(db=db))
)
log.info("Updating metadata for all non-ended shows")

View File

@@ -16,39 +16,39 @@ else:
tmdbsimple.API_KEY = tmdb_api_key
@router.get("/tv/trending")
async def get_tmdb_trending_tv(language: str = "en"):
async def get_tmdb_trending_tv(language: str = "en") -> dict:
return Trending(media_type="tv").info(language=language)
@router.get("/tv/search")
async def search_tmdb_tv(query: str, page: int = 1, language: str = "en"):
async def search_tmdb_tv(query: str, page: int = 1, language: str = "en") -> dict:
return Search().tv(page=page, query=query, language=language)
@router.get("/tv/shows/{show_id}")
async def get_tmdb_show(show_id: int, language: str = "en"):
async def get_tmdb_show(show_id: int, language: str = "en") -> dict:
return TV(show_id).info(language=language)
@router.get("/tv/shows/{show_id}/external_ids")
async def get_tmdb_show_external_ids(show_id: int):
async def get_tmdb_show_external_ids(show_id: int) -> dict:
return TV(show_id).external_ids()
@router.get("/tv/shows/{show_id}/{season_number}")
async def get_tmdb_season(season_number: int, show_id: int, language: str = "en"):
async def get_tmdb_season(season_number: int, show_id: int, language: str = "en") -> dict:
return TV_Seasons(season_number=season_number, tv_id=show_id).info(
language=language
)
@router.get("/movies/trending")
async def get_tmdb_trending_movies(language: str = "en"):
async def get_tmdb_trending_movies(language: str = "en") -> dict:
return Trending(media_type="movie").info(language=language)
@router.get("/movies/search")
async def search_tmdb_movies(query: str, page: int = 1, language: str = "en"):
async def search_tmdb_movies(query: str, page: int = 1, language: str = "en") -> dict:
return Search().movie(page=page, query=query, language=language)
@router.get("/movies/{movie_id}")
async def get_tmdb_movie(movie_id: int, language: str = "en"):
async def get_tmdb_movie(movie_id: int, language: str = "en") -> dict:
return Movies(movie_id).info(language=language)
@router.get("/movies/{movie_id}/external_ids")
async def get_tmdb_movie_external_ids(movie_id: int):
async def get_tmdb_movie_external_ids(movie_id: int) -> dict:
return Movies(movie_id).external_ids()

View File

@@ -16,29 +16,29 @@ else:
tvdb_client = tvdb_v4_official.TVDB(tvdb_api_key)
@router.get("/tv/trending")
async def get_tvdb_trending_tv():
async def get_tvdb_trending_tv() -> list:
return tvdb_client.get_all_series()
@router.get("/tv/search")
async def search_tvdb_tv(query: str):
async def search_tvdb_tv(query: str) -> list:
return tvdb_client.search(query)
@router.get("/tv/shows/{show_id}")
async def get_tvdb_show(show_id: int):
async def get_tvdb_show(show_id: int) -> dict:
return tvdb_client.get_series_extended(show_id)
@router.get("/tv/seasons/{season_id}")
async def get_tvdb_season(season_id: int):
async def get_tvdb_season(season_id: int) -> dict:
return tvdb_client.get_season_extended(season_id)
@router.get("/movies/trending")
async def get_tvdb_trending_movies():
async def get_tvdb_trending_movies() -> list:
return tvdb_client.get_all_movies()
@router.get("/movies/search")
async def search_tvdb_movies(query: str):
async def search_tvdb_movies(query: str) -> list:
return tvdb_client.search(query)
@router.get("/movies/{movie_id}")
async def get_tvdb_movie(movie_id: int):
async def get_tvdb_movie(movie_id: int) -> dict:
return tvdb_client.get_movie_extended(movie_id)

View File

@@ -5,7 +5,7 @@ from app.tvdb import router as tvdb_router
from fastapi import FastAPI
from starlette_exporter import PrometheusMiddleware, handle_metrics
app = FastAPI(root_path=os.getenv("BASE_PATH"))
app = FastAPI(root_path=os.getenv("BASE_PATH", ""))
app.add_middleware(PrometheusMiddleware)
app.add_route("/metrics", handle_metrics)
@@ -15,5 +15,5 @@ app.include_router(tvdb_router)
@app.get("/")
async def root():
async def root() -> dict:
return {"message": "Hello World"}

View File

@@ -36,7 +36,10 @@ dependencies = [
]
[dependency-groups]
dev = ["ruff"]
dev = [
"ruff",
"ty>=0.0.9",
]
[tool.setuptools.packages.find]
include = ["media_manager*"]

View File

@@ -5,9 +5,9 @@ line-ending = "lf"
quote-style = "double"
[lint]
# to be enabled: ANN, BLE, C90, CPY, D, DOC, DTZ, FBT, G, PL, RSE, SLF, SIM, TC, TRY, UP
# to be enabled: BLE, C90, CPY, D, DOC, DTZ, FBT, G, PL, RSE, SLF, SIM, TC, TRY, UP
extend-select = [
"A", "ARG", "ASYNC",
"A", "ARG", "ASYNC", "ANN",
"B",
"C4", "COM",
"DTZ",

31
uv.lock generated
View File

@@ -886,6 +886,7 @@ dependencies = [
[package.dev-dependencies]
dev = [
{ name = "ruff" },
{ name = "ty" },
]
[package.metadata]
@@ -922,7 +923,10 @@ requires-dist = [
]
[package.metadata.requires-dev]
dev = [{ name = "ruff" }]
dev = [
{ name = "ruff" },
{ name = "ty", specifier = ">=0.0.9" },
]
[[package]]
name = "mypy-extensions"
@@ -1673,6 +1677,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/50/d2/3f3e03fe96c23701fa24890dcd393034f4d37fb1e4649f573b1a6f3cf994/tvdb_v4_official-1.1.0-py3-none-any.whl", hash = "sha256:1d66f87f7d3d36feb4923b37aefd5a048dd208096bc640d1898acb1956fc0ba1", size = 3801, upload-time = "2022-09-22T19:11:49.819Z" },
]
[[package]]
name = "ty"
version = "0.0.9"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/19/7b/4f677c622d58563c593c32081f8a8572afd90e43dc15b0dedd27b4305038/ty-0.0.9.tar.gz", hash = "sha256:83f980c46df17586953ab3060542915827b43c4748a59eea04190c59162957fe", size = 4858642, upload-time = "2026-01-05T12:24:56.528Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/3f/c1ee119738b401a8081ff84341781122296b66982e5982e6f162d946a1ff/ty-0.0.9-py3-none-linux_armv6l.whl", hash = "sha256:dd270d4dd6ebeb0abb37aee96cbf9618610723677f500fec1ba58f35bfa8337d", size = 9763596, upload-time = "2026-01-05T12:24:37.43Z" },
{ url = "https://files.pythonhosted.org/packages/63/41/6b0669ef4cd806d4bd5c30263e6b732a362278abac1bc3a363a316cde896/ty-0.0.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:debfb2ba418b00e86ffd5403cb666b3f04e16853f070439517dd1eaaeeff9255", size = 9591514, upload-time = "2026-01-05T12:24:26.891Z" },
{ url = "https://files.pythonhosted.org/packages/02/a1/874aa756aee5118e690340a771fb9ded0d0c2168c0b7cc7d9561c2a750b0/ty-0.0.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:107c76ebb05a13cdb669172956421f7ffd289ad98f36d42a44a465588d434d58", size = 9097773, upload-time = "2026-01-05T12:24:14.442Z" },
{ url = "https://files.pythonhosted.org/packages/32/62/cb9a460cf03baab77b3361d13106b93b40c98e274d07c55f333ce3c716f6/ty-0.0.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6868ca5c87ca0caa1b3cb84603c767356242b0659b88307eda69b2fb0bfa416b", size = 9581824, upload-time = "2026-01-05T12:24:35.074Z" },
{ url = "https://files.pythonhosted.org/packages/5a/97/633ecb348c75c954f09f8913669de8c440b13b43ea7d214503f3f1c4bb60/ty-0.0.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d14a4aa0eb5c1d3591c2adbdda4e44429a6bb5d2e298a704398bb2a7ccdafdfe", size = 9591050, upload-time = "2026-01-05T12:24:08.804Z" },
{ url = "https://files.pythonhosted.org/packages/6f/e6/4b0c6a7a8a234e2113f88c80cc7aaa9af5868de7a693859f3c49da981934/ty-0.0.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01bd4466504cefa36b465c6608e9af4504415fa67f6affc01c7d6ce36663c7f4", size = 10018262, upload-time = "2026-01-05T12:24:53.791Z" },
{ url = "https://files.pythonhosted.org/packages/cb/97/076d72a028f6b31e0b87287aa27c5b71a2f9927ee525260ea9f2f56828b8/ty-0.0.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:76c8253d1b30bc2c3eaa1b1411a1c34423decde0f4de0277aa6a5ceacfea93d9", size = 10911642, upload-time = "2026-01-05T12:24:48.264Z" },
{ url = "https://files.pythonhosted.org/packages/3f/5a/705d6a5ed07ea36b1f23592c3f0dbc8fc7649267bfbb3bf06464cdc9a98a/ty-0.0.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8992fa4a9c6a5434eae4159fdd4842ec8726259bfd860e143ab95d078de6f8e3", size = 10632468, upload-time = "2026-01-05T12:24:24.118Z" },
{ url = "https://files.pythonhosted.org/packages/44/78/4339a254537488d62bf392a936b3ec047702c0cc33d6ce3a5d613f275cd0/ty-0.0.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c79d503d151acb4a145a3d98702d07cb641c47292f63e5ffa0151e4020a5d33", size = 10273422, upload-time = "2026-01-05T12:24:45.8Z" },
{ url = "https://files.pythonhosted.org/packages/90/40/e7f386e87c9abd3670dcee8311674d7e551baa23b2e4754e2405976e6c92/ty-0.0.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a7ebf89ed276b564baa1f0dd9cd708e7b5aa89f19ce1b2f7d7132075abf93e", size = 10120289, upload-time = "2026-01-05T12:24:17.424Z" },
{ url = "https://files.pythonhosted.org/packages/f7/46/1027442596e725c50d0d1ab5179e9fa78a398ab412994b3006d0ee0899c7/ty-0.0.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ae3866e50109d2400a886bb11d9ef607f23afc020b226af773615cf82ae61141", size = 9566657, upload-time = "2026-01-05T12:24:51.048Z" },
{ url = "https://files.pythonhosted.org/packages/56/be/df921cf1967226aa01690152002b370a7135c6cced81e86c12b86552cdc4/ty-0.0.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:185244a5eacfcd8f5e2d85b95e4276316772f1e586520a6cb24aa072ec1bac26", size = 9610334, upload-time = "2026-01-05T12:24:20.334Z" },
{ url = "https://files.pythonhosted.org/packages/ac/e8/f085268860232cc92ebe95415e5c8640f7f1797ac3a49ddd137c6222924d/ty-0.0.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f834ff27d940edb24b2e86bbb3fb45ab9e07cf59ca8c5ac615095b2542786408", size = 9726701, upload-time = "2026-01-05T12:24:29.785Z" },
{ url = "https://files.pythonhosted.org/packages/42/b4/9394210c66041cd221442e38f68a596945103d9446ece505889ffa9b3da9/ty-0.0.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:773f4b3ba046de952d7c1ad3a2c09b24f3ed4bc8342ae3cbff62ebc14aa6d48c", size = 10227082, upload-time = "2026-01-05T12:24:40.132Z" },
{ url = "https://files.pythonhosted.org/packages/dc/9f/75951eb573b473d35dd9570546fc1319f7ca2d5b5c50a5825ba6ea6cb33a/ty-0.0.9-py3-none-win32.whl", hash = "sha256:1f20f67e373038ff20f36d5449e787c0430a072b92d5933c5b6e6fc79d3de4c8", size = 9176458, upload-time = "2026-01-05T12:24:32.559Z" },
{ url = "https://files.pythonhosted.org/packages/9b/80/b1cdf71ac874e72678161e25e2326a7d30bc3489cd3699561355a168e54f/ty-0.0.9-py3-none-win_amd64.whl", hash = "sha256:2c415f3bbb730f8de2e6e0b3c42eb3a91f1b5fbbcaaead2e113056c3b361c53c", size = 10040479, upload-time = "2026-01-05T12:24:42.697Z" },
{ url = "https://files.pythonhosted.org/packages/b5/8f/abc75c4bb774b12698629f02d0d12501b0a7dff9c31dc3bd6b6c6467e90a/ty-0.0.9-py3-none-win_arm64.whl", hash = "sha256:48e339d794542afeed710ea4f846ead865cc38cecc335a9c781804d02eaa2722", size = 9543127, upload-time = "2026-01-05T12:24:11.731Z" },
]
[[package]]
name = "typer"
version = "0.21.0"