diff --git a/media_manager/auth/config.py b/media_manager/auth/config.py index c1445f9..1b028d1 100644 --- a/media_manager/auth/config.py +++ b/media_manager/auth/config.py @@ -3,21 +3,23 @@ from pydantic import Field import secrets +class OpenIdConfig(BaseSettings): + client_id: str = "" + client_secret: str = "" + configuration_endpoint: str = "" + name: str = "OpenID" + enabled: bool = False + + class AuthConfig(BaseSettings): # to get a signing key run: # openssl rand -hex 32 token_secret: str = Field(default_factory=secrets.token_hex) session_lifetime: int = 60 * 60 * 24 - admin_email: list[str] = [] + admin_emails: list[str] = [] email_password_resets: bool = False + openid_connect: OpenIdConfig = OpenIdConfig() @property def jwt_signing_key(self): return self._jwt_signing_key - - -class OpenIdConfig(BaseSettings): - client_id: str - client_secret: str - configuration_endpoint: str - name: str = "OpenID" diff --git a/media_manager/auth/router.py b/media_manager/auth/router.py index 667fa9c..68779c2 100644 --- a/media_manager/auth/router.py +++ b/media_manager/auth/router.py @@ -2,18 +2,15 @@ from fastapi import APIRouter, Depends from fastapi import status from sqlalchemy import select -from media_manager.auth.config import OpenIdConfig +from media_manager.config import AllEncompassingConfig from media_manager.auth.db import User from media_manager.auth.schemas import UserRead from media_manager.auth.users import current_superuser from media_manager.database import DbSessionDependency -from media_manager.auth.users import openid_client users_router = APIRouter() auth_metadata_router = APIRouter() -openid_enabled = openid_client is not None -if openid_enabled: - oauth_config = OpenIdConfig() +oauth_config = AllEncompassingConfig().auth.openid_connect @users_router.get( @@ -29,7 +26,7 @@ def get_all_users(db: DbSessionDependency) -> list[UserRead]: @auth_metadata_router.get("/auth/metadata", status_code=status.HTTP_200_OK) def get_auth_metadata() -> dict: - if openid_enabled: + if oauth_config.enabled: return { "oauth_name": oauth_config.name, } diff --git a/media_manager/auth/users.py b/media_manager/auth/users.py index e215522..4323728 100644 --- a/media_manager/auth/users.py +++ b/media_manager/auth/users.py @@ -1,5 +1,4 @@ import logging -import os import uuid from typing import Optional @@ -17,22 +16,18 @@ from fastapi.responses import RedirectResponse, Response from starlette import status import media_manager.notification.utils -from media_manager.auth.config import AuthConfig, OpenIdConfig from media_manager.auth.db import User, get_user_db from media_manager.auth.schemas import UserUpdate from media_manager.config import AllEncompassingConfig log = logging.getLogger(__name__) -config = AuthConfig() +config = AllEncompassingConfig().auth SECRET = config.token_secret LIFETIME = config.session_lifetime -if ( - os.getenv("OPENID_ENABLED") is not None - and os.getenv("OPENID_ENABLED").upper() == "TRUE" -): - openid_config = OpenIdConfig() +if config.openid_connect.enabled: + openid_config = AllEncompassingConfig().auth.openid_connect openid_client = OpenID( base_scopes=["openid", "email", "profile"], client_id=openid_config.client_id, @@ -51,7 +46,7 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]): async def on_after_register(self, user: User, request: Optional[Request] = None): log.info(f"User {user.id} has registered.") - if user.email in config.admin_email: + if user.email in config.admin_emails: updated_user = UserUpdate(is_superuser=True, is_verified=True) await self.update(user=user, user_update=updated_user) diff --git a/media_manager/config.py b/media_manager/config.py index 0211896..5d3d933 100644 --- a/media_manager/config.py +++ b/media_manager/config.py @@ -7,7 +7,7 @@ from pydantic_settings import ( ) from media_manager.auth.config import AuthConfig -from media_manager.database import DbConfig +from media_manager.database.config import DbConfig from media_manager.indexer.config import IndexerConfig from media_manager.metadataProvider.config import MetadataProviderConfig from media_manager.notification.config import NotificationConfig @@ -24,6 +24,8 @@ class BasicConfig(BaseSettings): FRONTEND_URL: AnyHttpUrl = "http://localhost:3000/" CORS_URLS: list[str] = [] DEVELOPMENT: bool = False + api_base_path: str = "/api/v1" + class AllEncompassingConfig(BaseSettings): model_config = SettingsConfigDict(toml_file="config.toml") @@ -38,5 +40,3 @@ class AllEncompassingConfig(BaseSettings): indexers: IndexerConfig = IndexerConfig() database: DbConfig = DbConfig() auth: AuthConfig = AuthConfig() - - diff --git a/media_manager/database/__init__.py b/media_manager/database/__init__.py index eb14e55..8b6fdf4 100644 --- a/media_manager/database/__init__.py +++ b/media_manager/database/__init__.py @@ -6,10 +6,10 @@ from fastapi import Depends from sqlalchemy import create_engine from sqlalchemy.orm import Session, declarative_base, sessionmaker -from media_manager.database.config import DbConfig +from media_manager.config import AllEncompassingConfig log = logging.getLogger(__name__) -config = DbConfig() +config = AllEncompassingConfig().database db_url = ( "postgresql+psycopg" diff --git a/media_manager/indexer/__init__.py b/media_manager/indexer/__init__.py index 9f59e42..44151f7 100644 --- a/media_manager/indexer/__init__.py +++ b/media_manager/indexer/__init__.py @@ -1,8 +1,7 @@ import logging -from media_manager.indexer.config import JackettConfig +from media_manager.config import AllEncompassingConfig from media_manager.indexer.indexers.jackett import Jackett -from media_manager.indexer.config import ProwlarrConfig from media_manager.indexer.indexers.generic import GenericIndexer from media_manager.indexer.indexers.prowlarr import Prowlarr @@ -10,7 +9,8 @@ log = logging.getLogger(__name__) indexers: list[GenericIndexer] = [] -if ProwlarrConfig().enabled: +config = AllEncompassingConfig() +if config.indexers.prowlarr.enabled: indexers.append(Prowlarr()) -if JackettConfig().enabled: +if config.indexers.jackett.enabled: indexers.append(Jackett()) diff --git a/media_manager/indexer/config.py b/media_manager/indexer/config.py index c82484c..6eb7748 100644 --- a/media_manager/indexer/config.py +++ b/media_manager/indexer/config.py @@ -13,6 +13,7 @@ class JackettConfig(BaseSettings): url: str = "http://localhost:9696" indexers: list[str] = ["all"] + class IndexerConfig(BaseSettings): prowlarr: ProwlarrConfig = ProwlarrConfig() jackett: JackettConfig = JackettConfig() diff --git a/media_manager/indexer/indexers/jackett.py b/media_manager/indexer/indexers/jackett.py index aae0f1e..504ddd9 100644 --- a/media_manager/indexer/indexers/jackett.py +++ b/media_manager/indexer/indexers/jackett.py @@ -6,8 +6,8 @@ import requests from pydantic import HttpUrl from media_manager.indexer.indexers.generic import GenericIndexer -from media_manager.indexer.config import JackettConfig from media_manager.indexer.schemas import IndexerQueryResult +from media_manager.config import AllEncompassingConfig log = logging.getLogger(__name__) @@ -19,7 +19,7 @@ class Jackett(GenericIndexer): """ super().__init__(name="jackett") - config = JackettConfig() + config = AllEncompassingConfig().indexers.jackett self.api_key = config.api_key self.url = config.url self.indexers = config.indexers diff --git a/media_manager/indexer/indexers/prowlarr.py b/media_manager/indexer/indexers/prowlarr.py index 9caf0b3..e57b140 100644 --- a/media_manager/indexer/indexers/prowlarr.py +++ b/media_manager/indexer/indexers/prowlarr.py @@ -3,7 +3,7 @@ import logging import requests from media_manager.indexer.indexers.generic import GenericIndexer -from media_manager.indexer.config import ProwlarrConfig +from media_manager.config import AllEncompassingConfig from media_manager.indexer.schemas import IndexerQueryResult log = logging.getLogger(__name__) @@ -18,7 +18,7 @@ class Prowlarr(GenericIndexer): :param kwargs: Additional keyword arguments to pass to the superclass constructor. """ super().__init__(name="prowlarr") - config = ProwlarrConfig() + config = AllEncompassingConfig().indexers.prowlarr self.api_key = config.api_key self.url = config.url log.debug("Registering Prowlarr as Indexer") diff --git a/media_manager/main.py b/media_manager/main.py index f5f84d4..2101d33 100644 --- a/media_manager/main.py +++ b/media_manager/main.py @@ -99,13 +99,12 @@ from apscheduler.triggers.cron import CronTrigger # noqa: E402 init_db() log.info("Database initialized") - -basic_config = AllEncompassingConfig().misc -if basic_config.DEVELOPMENT: - basic_config.torrent_directory.mkdir(parents=True, exist_ok=True) - basic_config.tv_directory.mkdir(parents=True, exist_ok=True) - basic_config.movie_directory.mkdir(parents=True, exist_ok=True) - basic_config.image_directory.mkdir(parents=True, exist_ok=True) +config = AllEncompassingConfig() +if config.misc.DEVELOPMENT: + config.misc.torrent_directory.mkdir(parents=True, exist_ok=True) + config.misc.tv_directory.mkdir(parents=True, exist_ok=True) + config.misc.movie_directory.mkdir(parents=True, exist_ok=True) + config.misc.image_directory.mkdir(parents=True, exist_ok=True) log.warning("Development Mode activated!") else: log.info("Development Mode not activated!") @@ -146,11 +145,11 @@ async def lifespan(app: FastAPI): scheduler.shutdown() -base_path = os.getenv("API_BASE_PATH") or "/api/v1" +base_path = config.misc.api_base_path log.info("Base Path for API: %s", base_path) app = FastAPI(root_path=base_path, lifespan=lifespan) -origins = basic_config.CORS_URLS +origins = config.misc.CORS_URLS log.info("CORS URLs activated for following origins:") for origin in origins: log.info(f" - {origin}") @@ -231,7 +230,7 @@ app.include_router(movies_router.router, prefix="/movies", tags=["movie"]) app.include_router(notification_router, prefix="/notification", tags=["notification"]) app.mount( "/static/image", - StaticFiles(directory=basic_config.image_directory), + StaticFiles(directory=config.misc.image_directory), name="static-images", ) @@ -248,26 +247,26 @@ log.info("Hello World!") # Startup filesystem checks # ---------------------------- try: - test_dir = basic_config.tv_directory / Path(".media_manager_test_dir") + test_dir = config.misc.tv_directory / Path(".media_manager_test_dir") test_dir.mkdir(parents=True, exist_ok=True) test_dir.rmdir() log.info(f"Successfully created test dir in TV directory at: {test_dir}") - test_dir = basic_config.movie_directory / Path(".media_manager_test_dir") + test_dir = config.misc.movie_directory / Path(".media_manager_test_dir") test_dir.mkdir(parents=True, exist_ok=True) test_dir.rmdir() log.info(f"Successfully created test dir in Movie directory at: {test_dir}") - test_dir = basic_config.image_directory / Path(".media_manager_test_dir") + test_dir = config.misc.image_directory / Path(".media_manager_test_dir") test_dir.touch() test_dir.unlink() log.info(f"Successfully created test file in Image directory at: {test_dir}") # check if hardlink creation works - test_dir = basic_config.tv_directory / Path(".media_manager_test_dir") + test_dir = config.misc.tv_directory / Path(".media_manager_test_dir") test_dir.mkdir(parents=True, exist_ok=True) - torrent_dir = basic_config.torrent_directory / Path(".media_manager_test_dir") + torrent_dir = config.misc.torrent_directory / Path(".media_manager_test_dir") torrent_dir.mkdir(parents=True, exist_ok=True) test_torrent_file = torrent_dir / Path(".media_manager.test.torrent") diff --git a/media_manager/metadataProvider/config.py b/media_manager/metadataProvider/config.py index a2c7e1f..7346733 100644 --- a/media_manager/metadataProvider/config.py +++ b/media_manager/metadataProvider/config.py @@ -6,4 +6,4 @@ from media_manager.metadataProvider.tvdb import TvdbConfig class MetadataProviderConfig(BaseSettings): tvdb: TvdbConfig = TvdbConfig() - tmdb: TmdbConfig = TmdbConfig() \ No newline at end of file + tmdb: TmdbConfig = TmdbConfig() diff --git a/media_manager/metadataProvider/tmdb.py b/media_manager/metadataProvider/tmdb.py index e8c5a73..f2054ba 100644 --- a/media_manager/metadataProvider/tmdb.py +++ b/media_manager/metadataProvider/tmdb.py @@ -4,6 +4,7 @@ import requests from pydantic_settings import BaseSettings import media_manager.metadataProvider.utils +from media_manager.config import AllEncompassingConfig from media_manager.metadataProvider.abstractMetaDataProvider import ( AbstractMetadataProvider, ) @@ -26,7 +27,7 @@ class TmdbMetadataProvider(AbstractMetadataProvider): name = "tmdb" def __init__(self): - config = TmdbConfig() + config = AllEncompassingConfig().metadata.tmdb self.url = config.TMDB_RELAY_URL def __get_show_metadata(self, id: int) -> dict: diff --git a/media_manager/metadataProvider/tvdb.py b/media_manager/metadataProvider/tvdb.py index f895b15..6d8e49f 100644 --- a/media_manager/metadataProvider/tvdb.py +++ b/media_manager/metadataProvider/tvdb.py @@ -4,6 +4,7 @@ import logging from pydantic_settings import BaseSettings import media_manager.metadataProvider.utils +from media_manager.config import AllEncompassingConfig from media_manager.metadataProvider.abstractMetaDataProvider import ( AbstractMetadataProvider, ) @@ -23,7 +24,7 @@ class TvdbMetadataProvider(AbstractMetadataProvider): name = "tvdb" def __init__(self): - config = TvdbConfig() + config = AllEncompassingConfig().metadata.tvdb self.url = config.TVDB_RELAY_URL def __get_show(self, id: int) -> dict: diff --git a/media_manager/notification/manager.py b/media_manager/notification/manager.py index c9badae..2ff441f 100644 --- a/media_manager/notification/manager.py +++ b/media_manager/notification/manager.py @@ -20,7 +20,7 @@ from media_manager.notification.service_providers.ntfy import ( from media_manager.notification.service_providers.pushover import ( PushoverNotificationServiceProvider, ) -from media_manager.notification.config import NotificationConfig +from media_manager.config import AllEncompassingConfig logger = logging.getLogger(__name__) @@ -31,7 +31,7 @@ class NotificationManager: """ def __init__(self): - self.config = NotificationConfig() + self.config = AllEncompassingConfig().notifications self.providers: List[AbstractNotificationServiceProvider] = [] self._initialize_providers() diff --git a/media_manager/notification/service_providers/email.py b/media_manager/notification/service_providers/email.py index 89f3c43..11040f3 100644 --- a/media_manager/notification/service_providers/email.py +++ b/media_manager/notification/service_providers/email.py @@ -3,12 +3,12 @@ from media_manager.notification.schemas import MessageNotification from media_manager.notification.service_providers.abstractNotificationServiceProvider import ( AbstractNotificationServiceProvider, ) -from media_manager.notification.config import NotificationConfig +from media_manager.config import AllEncompassingConfig class EmailNotificationServiceProvider(AbstractNotificationServiceProvider): def __init__(self): - self.config = NotificationConfig() + self.config = AllEncompassingConfig().notifications def send_notification(self, message: MessageNotification) -> bool: subject = "MediaManager - " + message.title diff --git a/media_manager/notification/service_providers/gotify.py b/media_manager/notification/service_providers/gotify.py index 427d9e5..3f00318 100644 --- a/media_manager/notification/service_providers/gotify.py +++ b/media_manager/notification/service_providers/gotify.py @@ -1,6 +1,5 @@ import requests - -from media_manager.notification.config import NotificationConfig +from media_manager.config import AllEncompassingConfig from media_manager.notification.schemas import MessageNotification from media_manager.notification.service_providers.abstractNotificationServiceProvider import ( AbstractNotificationServiceProvider, @@ -13,7 +12,7 @@ class GotifyNotificationServiceProvider(AbstractNotificationServiceProvider): """ def __init__(self): - self.config = NotificationConfig() + self.config = AllEncompassingConfig().notifications def send_notification(self, message: MessageNotification) -> bool: response = requests.post( diff --git a/media_manager/notification/service_providers/ntfy.py b/media_manager/notification/service_providers/ntfy.py index 3398783..443afa5 100644 --- a/media_manager/notification/service_providers/ntfy.py +++ b/media_manager/notification/service_providers/ntfy.py @@ -1,6 +1,5 @@ import requests - -from media_manager.notification.config import NotificationConfig +from media_manager.config import AllEncompassingConfig from media_manager.notification.schemas import MessageNotification from media_manager.notification.service_providers.abstractNotificationServiceProvider import ( AbstractNotificationServiceProvider, @@ -13,7 +12,7 @@ class NtfyNotificationServiceProvider(AbstractNotificationServiceProvider): """ def __init__(self): - self.config = NotificationConfig() + self.config = AllEncompassingConfig().notifications def send_notification(self, message: MessageNotification) -> bool: response = requests.post( diff --git a/media_manager/notification/service_providers/pushover.py b/media_manager/notification/service_providers/pushover.py index 3bbc224..a09846e 100644 --- a/media_manager/notification/service_providers/pushover.py +++ b/media_manager/notification/service_providers/pushover.py @@ -1,6 +1,5 @@ import requests - -from media_manager.notification.config import NotificationConfig +from media_manager.config import AllEncompassingConfig from media_manager.notification.schemas import MessageNotification from media_manager.notification.service_providers.abstractNotificationServiceProvider import ( AbstractNotificationServiceProvider, @@ -9,7 +8,7 @@ from media_manager.notification.service_providers.abstractNotificationServicePro class PushoverNotificationServiceProvider(AbstractNotificationServiceProvider): def __init__(self): - self.config = NotificationConfig() + self.config = AllEncompassingConfig().notifications def send_notification(self, message: MessageNotification) -> bool: response = requests.post( diff --git a/media_manager/notification/utils.py b/media_manager/notification/utils.py index 45b2b63..6f81a2b 100644 --- a/media_manager/notification/utils.py +++ b/media_manager/notification/utils.py @@ -3,13 +3,13 @@ import smtplib from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText -from media_manager.notification.config import EmailConfig +from media_manager.config import AllEncompassingConfig log = logging.getLogger(__name__) -def send_email(html: str, addressee: str, subject: str | list[str]) -> None: - email_conf = EmailConfig() +def send_email(subject: str, html: str, addressee: str) -> None: + email_conf = AllEncompassingConfig().notifications.smtp_config message = MIMEMultipart() message["From"] = email_conf.from_email message["To"] = addressee diff --git a/media_manager/torrent/config.py b/media_manager/torrent/config.py index d4ded4b..e2458aa 100644 --- a/media_manager/torrent/config.py +++ b/media_manager/torrent/config.py @@ -6,4 +6,4 @@ from media_manager.torrent.download_clients.sabnzbd import SabnzbdConfig class TorrentConfig(BaseSettings): qbittorrent: QbittorrentConfig = QbittorrentConfig() - sabnzbd: SabnzbdConfig = SabnzbdConfig() \ No newline at end of file + sabnzbd: SabnzbdConfig = SabnzbdConfig() diff --git a/media_manager/torrent/download_clients/qbittorrent.py b/media_manager/torrent/download_clients/qbittorrent.py index d1d1589..b3fe51d 100644 --- a/media_manager/torrent/download_clients/qbittorrent.py +++ b/media_manager/torrent/download_clients/qbittorrent.py @@ -22,6 +22,7 @@ class QbittorrentConfig(BaseSettings): port: int = 8080 username: str = "admin" password: str = "admin" + enabled: bool = False class QbittorrentDownloadClient(AbstractDownloadClient): @@ -48,7 +49,7 @@ class QbittorrentDownloadClient(AbstractDownloadClient): UNKNOWN_STATE = ("unknown",) def __init__(self): - self.config = QbittorrentConfig() + self.config = AllEncompassingConfig().torrents.qbittorrent self.api_client = qbittorrentapi.Client(**self.config.model_dump()) try: self.api_client.auth_log_in() @@ -69,7 +70,8 @@ class QbittorrentDownloadClient(AbstractDownloadClient): log.info(f"Attempting to download torrent: {indexer_result.title}") torrent_filepath = ( - AllEncompassingConfig().misc.torrent_directory / f"{indexer_result.title}.torrent" + AllEncompassingConfig().misc.torrent_directory + / f"{indexer_result.title}.torrent" ) if torrent_filepath.exists(): diff --git a/media_manager/torrent/download_clients/sabnzbd.py b/media_manager/torrent/download_clients/sabnzbd.py index 952f51a..222663f 100644 --- a/media_manager/torrent/download_clients/sabnzbd.py +++ b/media_manager/torrent/download_clients/sabnzbd.py @@ -1,6 +1,7 @@ import logging from pydantic_settings import BaseSettings, SettingsConfigDict +from media_manager.config import AllEncompassingConfig from media_manager.indexer.schemas import IndexerQueryResult from media_manager.torrent.download_clients.abstractDownloadClient import ( AbstractDownloadClient, @@ -16,6 +17,7 @@ class SabnzbdConfig(BaseSettings): host: str = "localhost" port: int = 8080 api_key: str = "" + enabled: bool = False class SabnzbdDownloadClient(AbstractDownloadClient): @@ -32,7 +34,7 @@ class SabnzbdDownloadClient(AbstractDownloadClient): UNKNOWN_STATE = ("Unknown",) def __init__(self): - self.config = SabnzbdConfig() + self.config = AllEncompassingConfig().torrents.sabnzbd self.client = sabnzbd_api.SabnzbdClient( host=self.config.host, port=str(self.config.port), @@ -59,9 +61,7 @@ class SabnzbdDownloadClient(AbstractDownloadClient): try: # Add NZB to SABnzbd queue - response = self.client.add_uri( - url=str(indexer_result.download_url), - ) + response = self.client.add_uri(url=str(indexer_result.download_url)) if not response["status"]: error_msg = response log.error(f"Failed to add NZB to SABnzbd: {error_msg}") diff --git a/media_manager/torrent/manager.py b/media_manager/torrent/manager.py index 597a60a..36df4a6 100644 --- a/media_manager/torrent/manager.py +++ b/media_manager/torrent/manager.py @@ -1,7 +1,7 @@ import logging -import os from enum import Enum +from media_manager.config import AllEncompassingConfig from media_manager.indexer.schemas import IndexerQueryResult from media_manager.torrent.download_clients.abstractDownloadClient import ( AbstractDownloadClient, @@ -30,13 +30,14 @@ class DownloadManager: def __init__(self): self._torrent_client: AbstractDownloadClient | None = None self._usenet_client: AbstractDownloadClient | None = None + self.config = AllEncompassingConfig().torrents self._initialize_clients() def _initialize_clients(self) -> None: """Initialize and register the default download clients""" # Initialize qBittorrent client for torrents - if os.getenv("QBITTORRENT_ENABLED", "false").lower() == "true": + if self.config.qbittorrent.enabled: try: self._torrent_client = QbittorrentDownloadClient() log.info( @@ -46,7 +47,7 @@ class DownloadManager: log.error(f"Failed to initialize qBittorrent client: {e}") # Initialize SABnzbd client for usenet - if os.getenv("SABNZBD_ENABLED", "false").lower() == "true": + if self.config.sabnzbd.enabled: try: self._usenet_client = SabnzbdDownloadClient() log.info("SABnzbd client initialized and set as active usenet client")