diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index 9340893..0000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM mcr.microsoft.com/devcontainers/python:1-3.11-bullseye - -ENV PYTHONUNBUFFERED 1 - -# [Optional] If your requirements rarely change, uncomment this section to add them to the image. -# COPY requirements.txt /tmp/pip-tmp/ -# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ -# && rm -rf /tmp/pip-tmp - -# [Optional] Uncomment this section to install additional OS packages. -# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ -# && apt-get -y install --no-install-recommends - - - diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 516f8b4..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,31 +0,0 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/postgres -{ - "name": "Python 3 & PostgreSQL", - "dockerComposeFile": "./docker-compose.yml", - "service": "app", - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", - - // Features to add to the dev container. More info: https://containers.dev/features. - // "features": {}, - - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // This can be used to network with other containers or the host. - // "forwardPorts": [5000, 5432], - - // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "pip install --user -r ./MediaManager/src/requirements.txt", - - // Configure tool-specific properties. - "customizations" : { - "jetbrains" : { - "settings": { - "com.intellij:app:HttpConfigurable.use_proxy_pac": true - }, - "backend" : "PyCharm" - } - }, - - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" -} diff --git a/.gitignore b/.gitignore index bfd1cf9..71a0421 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ .idea venv MediaManager.iml -MediaManager/res -MediaManager/res/.env +backend/res +backend/res/.env docker-compose.yml \ No newline at end of file diff --git a/MediaManager/src/config/__init__.py b/MediaManager/src/config/__init__.py deleted file mode 100644 index 026be66..0000000 --- a/MediaManager/src/config/__init__.py +++ /dev/null @@ -1,60 +0,0 @@ -import os -from typing import Literal - -from pydantic import BaseModel - - -class DbConfig(BaseModel): - host: str = os.getenv("DB_HOST") or "localhost" - port: int = int(os.getenv("DB_PORT") or 5432) - user: str = os.getenv("DB_USERNAME") or "MediaManager" - _password: str = os.getenv("DB_PASSWORD") or "MediaManager" - dbname: str = os.getenv("DB_NAME") or "MediaManager" - - @property - def password(self): - return self._password - - -class TmdbConfig(BaseModel): - api_key: str = os.getenv("TMDB_API_KEY") or None - - -class BasicConfig(BaseModel): - storage_directory: str = os.getenv("STORAGE_FILE_PATH") or "." - -class ProwlarrConfig(BaseModel): - enabled: bool = bool(os.getenv("PROWLARR_ENABLED") or True) - api_key: str = os.getenv("PROWLARR_API_KEY") - url: str = os.getenv("PROWLARR_URL") - - -class AuthConfig(BaseModel): - # to get a signing key run: - # openssl rand -hex 32 - _jwt_signing_key: str = os.getenv("JWT_SIGNING_KEY") - jwt_signing_algorithm: str = "HS256" - jwt_access_token_lifetime: int = int(os.getenv("JWT_ACCESS_TOKEN_LIFETIME") or 60 * 24 * 30) - - @property - def jwt_signing_key(self): - return self._jwt_signing_key - - -class QbittorrentConfig(BaseModel): - host: str = os.getenv("QBITTORRENT_HOST") or "localhost" - port: int = os.getenv("QBITTORRENT_PORT") or 8080 - username: str = os.getenv("QBITTORRENT_USERNAME") or "admin" - password: str = os.getenv("QBITTORRENT_PASSWORD") or "adminadmin" - - -class DownloadClientConfig(BaseModel): - client: Literal['qbit'] = os.getenv("DOWNLOAD_CLIENT") or "qbit" - - -class MachineLearningConfig(BaseModel): - model_name: str = os.getenv("OLLAMA_MODEL_NAME") or "qwen2.5:0.5b" - - -def get_db_config() -> DbConfig: - return DbConfig() diff --git a/MediaManager/src/dowloadClients/__init__.py b/MediaManager/src/dowloadClients/__init__.py deleted file mode 100644 index e31891a..0000000 --- a/MediaManager/src/dowloadClients/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from config import DownloadClientConfig -from dowloadClients.qbittorrent import QbittorrentClient - -config = DownloadClientConfig() - -# TODO: add more elif when implementing more download clients -if config.client == "qbit": - client = QbittorrentClient() -else: - client = QbittorrentClient() diff --git a/MediaManager/src/auth/__init__.py b/backend/src/auth/__init__.py similarity index 66% rename from MediaManager/src/auth/__init__.py rename to backend/src/auth/__init__.py index dd2ec28..8b5520d 100644 --- a/MediaManager/src/auth/__init__.py +++ b/backend/src/auth/__init__.py @@ -2,16 +2,17 @@ import logging from datetime import datetime, timedelta, timezone import jwt -from fastapi import Depends, HTTPException, status, APIRouter +from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from jwt.exceptions import InvalidTokenError from pydantic import BaseModel -from config import AuthConfig -from database import SessionDependency +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 @@ -29,20 +30,20 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/v1/token") router = APIRouter() -async def get_current_user(db: SessionDependency, token: str = Depends(oauth2_scheme)) -> User: +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"}, ) - config = AuthConfig() + auth_config = AuthConfig log.debug("token: " + token) try: - payload = jwt.decode(token, config.jwt_signing_key, algorithms=[config.jwt_signing_algorithm]) + 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) + log.debug("jwt payload sub (USER uid): " + user_uid) if user_uid is None: raise credentials_exception token_data = TokenData(uid=user_uid) @@ -53,20 +54,20 @@ async def get_current_user(db: SessionDependency, token: str = Depends(oauth2_sc user: User | None = db.get(User, token_data.uid) if user is None: - log.debug("user not found") + log.debug("USER not found") raise credentials_exception - log.debug("received user: " + user.__str__()) + log.debug("received USER: " + user.__str__()) return user def create_access_token(data: dict, expires_delta: timedelta | None = None): to_encode = data.copy() - config = AuthConfig() + auth_config = AuthConfig if expires_delta: expire = datetime.now(timezone.utc) + expires_delta else: - expire = datetime.now(timezone.utc) + timedelta(minutes=config.jwt_access_token_lifetime) + expire = datetime.now(timezone.utc) + timedelta(minutes=auth_config.jwt_access_token_lifetime) to_encode.update({"exp": expire}) - encoded_jwt = jwt.encode(to_encode, config.jwt_signing_key, algorithm=config.jwt_signing_algorithm) + encoded_jwt = jwt.encode(to_encode, auth_config.jwt_signing_key, algorithm=auth_config.jwt_signing_algorithm) return encoded_jwt diff --git a/backend/src/auth/config.py b/backend/src/auth/config.py new file mode 100644 index 0000000..553f998 --- /dev/null +++ b/backend/src/auth/config.py @@ -0,0 +1,13 @@ +from pydantic_settings import BaseSettings + + +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 + + @property + def jwt_signing_key(self): + return self._jwt_signing_key diff --git a/MediaManager/src/auth/oidc.py b/backend/src/auth/oidc.py similarity index 100% rename from MediaManager/src/auth/oidc.py rename to backend/src/auth/oidc.py diff --git a/MediaManager/src/auth/password.py b/backend/src/auth/password.py similarity index 71% rename from MediaManager/src/auth/password.py rename to backend/src/auth/password.py index 3aea110..7e7537f 100644 --- a/MediaManager/src/auth/password.py +++ b/backend/src/auth/password.py @@ -1,15 +1,12 @@ from typing import Annotated -import hashlib - import bcrypt from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm -from sqlmodel import Session, select +from sqlmodel import select -import database -from auth import create_access_token, Token, router -from database import users, SessionDependency +from auth import Token, create_access_token, router +from database import DbSessionDependency from database.users import User @@ -24,12 +21,12 @@ def get_password_hash(password: str) -> str: return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8") -def authenticate_user(db: SessionDependency, email: str, password: str) -> bool | User: +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 + :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: @@ -42,13 +39,13 @@ def authenticate_user(db: SessionDependency, email: str, password: str) -> bool @router.post("/token") async def login_for_access_token( form_data: Annotated[OAuth2PasswordRequestForm, Depends()], - db: SessionDependency, + 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", + detail="Incorrect email or PASSWORD", headers={"WWW-Authenticate": "Bearer"}, ) # id needs to be converted because a UUID object isn't json serializable diff --git a/backend/src/config.py b/backend/src/config.py new file mode 100644 index 0000000..405ed01 --- /dev/null +++ b/backend/src/config.py @@ -0,0 +1,5 @@ +from pydantic_settings import BaseSettings + + +class BasicConfig(BaseSettings): + storage_directory: str = "." diff --git a/MediaManager/src/database/__init__.py b/backend/src/database/__init__.py similarity index 58% rename from MediaManager/src/database/__init__.py rename to backend/src/database/__init__.py index e591513..7ad61e9 100644 --- a/MediaManager/src/database/__init__.py +++ b/backend/src/database/__init__.py @@ -4,14 +4,13 @@ from typing import Annotated, Any, Generator from fastapi import Depends from sqlmodel import SQLModel, Session, create_engine -import config -from config import DbConfig +from database.config import DbConfig log = logging.getLogger(__name__) -config: DbConfig = config.get_db_config() +config = DbConfig() -db_url = "postgresql+psycopg" + "://" + config.user + ":" + config.password + "@" + config.host + ":" + str( - config.port) + "/" + config.dbname +db_url = "postgresql+psycopg" + "://" + config.USER + ":" + config.PASSWORD + "@" + config.HOST + ":" + str( + config.PORT) + "/" + config.DBNAME engine = create_engine(db_url, echo=False) @@ -24,5 +23,5 @@ def get_session() -> Generator[Session, Any, None]: with Session(engine) as session: yield session -SessionDependency = Annotated[Session, Depends(get_session)] +DbSessionDependency = Annotated[Session, Depends(get_session)] diff --git a/backend/src/database/config.py b/backend/src/database/config.py new file mode 100644 index 0000000..7e00808 --- /dev/null +++ b/backend/src/database/config.py @@ -0,0 +1,10 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class DbConfig(BaseSettings): + model_config = SettingsConfigDict(env_prefix='DB') + HOST: str = "localhost" + PORT: int = 5432 + USER: str = "MediaManager" + PASSWORD: str = "MediaManager" + DBNAME: str = "MediaManager" diff --git a/MediaManager/src/database/torrents.py b/backend/src/database/torrents.py similarity index 100% rename from MediaManager/src/database/torrents.py rename to backend/src/database/torrents.py diff --git a/MediaManager/src/database/tv.py b/backend/src/database/tv.py similarity index 100% rename from MediaManager/src/database/tv.py rename to backend/src/database/tv.py diff --git a/MediaManager/src/database/users.py b/backend/src/database/users.py similarity index 100% rename from MediaManager/src/database/users.py rename to backend/src/database/users.py diff --git a/backend/src/dowloadClients/__init__.py b/backend/src/dowloadClients/__init__.py new file mode 100644 index 0000000..16b1e0c --- /dev/null +++ b/backend/src/dowloadClients/__init__.py @@ -0,0 +1,9 @@ +from dowloadClients.config import DownloadClientConfig +from dowloadClients.qbittorrent import QbittorrentClient + +config = DownloadClientConfig() + +if config.download_client == "qbit": + client = QbittorrentClient() +else: + raise ValueError("Unknown download client") diff --git a/backend/src/dowloadClients/config.py b/backend/src/dowloadClients/config.py new file mode 100644 index 0000000..b3f3a51 --- /dev/null +++ b/backend/src/dowloadClients/config.py @@ -0,0 +1,5 @@ +from pydantic_settings import BaseSettings + + +class DownloadClientConfig(BaseSettings): + download_client: str = "qbit" diff --git a/MediaManager/src/dowloadClients/genericDownloadClient.py b/backend/src/dowloadClients/genericDownloadClient.py similarity index 91% rename from MediaManager/src/dowloadClients/genericDownloadClient.py rename to backend/src/dowloadClients/genericDownloadClient.py index e9580b7..a1211ac 100644 --- a/MediaManager/src/dowloadClients/genericDownloadClient.py +++ b/backend/src/dowloadClients/genericDownloadClient.py @@ -9,7 +9,6 @@ class GenericDownloadClient(object): raise ValueError('name cannot be None') self.name = name - # TODO: change Torrents type to SeasonTorrents|MovieTorrents def download(self, torrent: TorrentMixin) -> TorrentMixin: """ downloads a torrent diff --git a/MediaManager/src/dowloadClients/qbittorrent.py b/backend/src/dowloadClients/qbittorrent.py similarity index 91% rename from MediaManager/src/dowloadClients/qbittorrent.py rename to backend/src/dowloadClients/qbittorrent.py index e730264..5ad90ee 100644 --- a/MediaManager/src/dowloadClients/qbittorrent.py +++ b/backend/src/dowloadClients/qbittorrent.py @@ -1,13 +1,21 @@ import logging import qbittorrentapi +from pydantic_settings import BaseSettings, SettingsConfigDict -from config import QbittorrentConfig from database.torrents import Torrent from dowloadClients.genericDownloadClient import GenericDownloadClient log = logging.getLogger(__name__) + +class QbittorrentConfig(BaseSettings): + model_config = SettingsConfigDict(env_prefix='QBITTORRENT_') + host: str = "localhost" + port: int = 8080 + username: str = "admin" + + class QbittorrentClient(GenericDownloadClient): DOWNLOADING_STATE = ("allocating", "downloading", "metaDL", "pausedDL", "queuedDL", "stalledDL", "checkingDL", "forcedDL", "moving") diff --git a/MediaManager/src/indexer/__init__.py b/backend/src/indexer/__init__.py similarity index 89% rename from MediaManager/src/indexer/__init__.py rename to backend/src/indexer/__init__.py index f1c6d9d..b4c4a11 100644 --- a/MediaManager/src/indexer/__init__.py +++ b/backend/src/indexer/__init__.py @@ -1,7 +1,7 @@ import logging -import config from database.tv import Season +from indexer.config import ProwlarrConfig from indexer.generic import GenericIndexer, IndexerQueryResult from indexer.prowlarr import Prowlarr @@ -23,5 +23,5 @@ def search(query: str | Season) -> list[IndexerQueryResult]: indexers: list[GenericIndexer] = [] -if config.ProwlarrConfig().enabled: +if ProwlarrConfig.enabled: indexers.append(Prowlarr()) diff --git a/backend/src/indexer/config.py b/backend/src/indexer/config.py new file mode 100644 index 0000000..783d88a --- /dev/null +++ b/backend/src/indexer/config.py @@ -0,0 +1,8 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class ProwlarrConfig(BaseSettings): + model_config = SettingsConfigDict(env_prefix="PROWLARR_") + enabled: bool = True + api_key: str + url: str diff --git a/MediaManager/src/indexer/generic.py b/backend/src/indexer/generic.py similarity index 96% rename from MediaManager/src/indexer/generic.py rename to backend/src/indexer/generic.py index 8eef87f..086fd93 100644 --- a/MediaManager/src/indexer/generic.py +++ b/backend/src/indexer/generic.py @@ -7,6 +7,7 @@ from pydantic import BaseModel, computed_field from database.torrents import QualityMixin, Torrent +# TODO: use something like strategy pattern to make sorting more user customizable class IndexerQueryResult(BaseModel, QualityMixin): id: UUID = pydantic.Field(default_factory=uuid4) title: str diff --git a/MediaManager/src/indexer/prowlarr.py b/backend/src/indexer/prowlarr.py similarity index 95% rename from MediaManager/src/indexer/prowlarr.py rename to backend/src/indexer/prowlarr.py index 30f57bf..fb4b298 100644 --- a/MediaManager/src/indexer/prowlarr.py +++ b/backend/src/indexer/prowlarr.py @@ -2,8 +2,8 @@ import logging import requests -from config import ProwlarrConfig from indexer import GenericIndexer, IndexerQueryResult +from indexer.config import ProwlarrConfig log = logging.getLogger(__name__) @@ -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 diff --git a/MediaManager/src/main.py b/backend/src/main.py similarity index 94% rename from MediaManager/src/main.py rename to backend/src/main.py index 7fa9454..44e2111 100644 --- a/MediaManager/src/main.py +++ b/backend/src/main.py @@ -14,7 +14,7 @@ from fastapi import FastAPI import database.users import tv.router from auth import password -from routers import users +from users import routers LOGGING_CONFIG = { "version": 1, @@ -44,7 +44,7 @@ dictConfig(LOGGING_CONFIG) database.init_db() app = FastAPI(root_path="/api/v1") -app.include_router(users.router, tags=["users"]) +app.include_router(routers.router, tags=["users"]) app.include_router(password.router, tags=["authentication"]) app.include_router(tv.router.router, tags=["tv"]) diff --git a/MediaManager/src/metadataProvider/__init__.py b/backend/src/metadataProvider/__init__.py similarity index 100% rename from MediaManager/src/metadataProvider/__init__.py rename to backend/src/metadataProvider/__init__.py diff --git a/MediaManager/src/metadataProvider/abstractMetaDataProvider.py b/backend/src/metadataProvider/abstractMetaDataProvider.py similarity index 83% rename from MediaManager/src/metadataProvider/abstractMetaDataProvider.py rename to backend/src/metadataProvider/abstractMetaDataProvider.py index 7a47c0a..862d89e 100644 --- a/MediaManager/src/metadataProvider/abstractMetaDataProvider.py +++ b/backend/src/metadataProvider/abstractMetaDataProvider.py @@ -7,7 +7,7 @@ from database.tv import Show log = logging.getLogger(__name__) -class MetadataProvider(ABC): +class AbstractMetadataProvider(ABC): storage_path = config.BasicConfig().storage_directory @property @abstractmethod @@ -26,6 +26,6 @@ class MetadataProvider(ABC): metadata_providers = {} -def register_metadata_provider(metadata_provider: MetadataProvider): +def register_metadata_provider(metadata_provider: AbstractMetadataProvider): log.info("Registering metadata provider:" + metadata_provider.name) metadata_providers[metadata_provider.name] = metadata_provider diff --git a/MediaManager/src/metadataProvider/tmdb.py b/backend/src/metadataProvider/tmdb.py similarity index 88% rename from MediaManager/src/metadataProvider/tmdb.py rename to backend/src/metadataProvider/tmdb.py index 8a94e97..f3f4195 100644 --- a/MediaManager/src/metadataProvider/tmdb.py +++ b/backend/src/metadataProvider/tmdb.py @@ -3,18 +3,24 @@ import mimetypes import requests import tmdbsimple +from pydantic_settings import BaseSettings from tmdbsimple import TV, TV_Seasons -import config from database.tv import Episode, Season, Show -from metadataProvider.abstractMetaDataProvider import MetadataProvider, register_metadata_provider +from metadataProvider.abstractMetaDataProvider import AbstractMetadataProvider, register_metadata_provider -config = config.TmdbConfig() + +class TmdbConfig(BaseSettings): + TMDB_API_KEY: str | None = None + + +config = TmdbConfig log = logging.getLogger(__name__) -class TmdbMetadataProvider(MetadataProvider): +class TmdbMetadataProvider(AbstractMetadataProvider): name = "tmdb" + def get_show_metadata(self, id: int = None) -> Show: """ @@ -86,6 +92,6 @@ class TmdbMetadataProvider(MetadataProvider): tmdbsimple.API_KEY = api_key -if config.api_key is not None: +if config.TMDB_API_KEY is not None: log.info("Registering TMDB as metadata provider") - register_metadata_provider(metadata_provider=TmdbMetadataProvider(config.api_key)) + register_metadata_provider(metadata_provider=TmdbMetadataProvider(config.TMDB_API_KEY)) diff --git a/MediaManager/src/ml/__init__.py b/backend/src/ml/__init__.py similarity index 90% rename from MediaManager/src/ml/__init__.py rename to backend/src/ml/__init__.py index 3d6aa68..5cc938c 100644 --- a/MediaManager/src/ml/__init__.py +++ b/backend/src/ml/__init__.py @@ -6,7 +6,7 @@ from typing import List from ollama import ChatResponse, chat from pydantic import BaseModel -import config +from ml.config import MachineLearningConfig class NFO(BaseModel): @@ -22,11 +22,11 @@ def get_season(nfo: str) -> int | None: for i in range(0, 5): responses.append(chat( - model=config.model_name, + model=config.ollama_model_name, format=NFO.model_json_schema(), messages=[ { - 'role': 'user', + 'role': 'USER', 'content': "Tell me which season the torrent with this description contains?" + " output a season number in json format, the season number is an integer" + @@ -54,11 +54,11 @@ def contains_season(season_number: int, string_to_analyze: str) -> bool: for i in range(0, 3): responses.append(chat( - model=config.model_name, + model=config.ollama_model_name, format=Contains.model_json_schema(), messages=[ { - 'role': 'user', + 'role': 'USER', 'content': "Does this torrent contain the season " + season_number.__str__() + " ?" + " output a boolean json format" + @@ -79,5 +79,6 @@ def contains_season(season_number: int, string_to_analyze: str) -> bool: log.debug(f"according to AI {string_to_analyze} contains season {season_number} {most_common[0][0]}") return most_common[0][0] -config = config.MachineLearningConfig() + +config = MachineLearningConfig log = logging.getLogger(__name__) diff --git a/MediaManager/src/ml/_testing.py b/backend/src/ml/_testing.py similarity index 94% rename from MediaManager/src/ml/_testing.py rename to backend/src/ml/_testing.py index d120c35..b1e012d 100644 --- a/MediaManager/src/ml/_testing.py +++ b/backend/src/ml/_testing.py @@ -1,8 +1,7 @@ import json from datetime import datetime, timedelta -from ollama import ChatResponse -from ollama import chat +from ollama import ChatResponse, chat from pydantic import BaseModel @@ -19,7 +18,7 @@ while start_time > datetime.now(): format=NFO.model_json_schema() , messages=[ { - 'role': 'user', + 'role': 'USER', 'content': "which season does a torrent with the following NFO contain? output the season number, which is an integer in json please\n" + "The.Big.Bang.Theory.(2007).Season.9.S09.(1080p.BluRay.x265.HEVC.10bit.AAC.5.1.Vyndros)" diff --git a/backend/src/ml/config.py b/backend/src/ml/config.py new file mode 100644 index 0000000..804cfea --- /dev/null +++ b/backend/src/ml/config.py @@ -0,0 +1,5 @@ +from pydantic_settings import BaseSettings + + +class MachineLearningConfig(BaseSettings): + ollama_model_name: str = "qwen2.5:0.5b" diff --git a/MediaManager/src/ml/test__init__.py b/backend/src/ml/test__init__.py similarity index 100% rename from MediaManager/src/ml/test__init__.py rename to backend/src/ml/test__init__.py diff --git a/MediaManager/src/requirements.txt b/backend/src/requirements.txt similarity index 100% rename from MediaManager/src/requirements.txt rename to backend/src/requirements.txt diff --git a/MediaManager/src/routers/__init__.py b/backend/src/tv/__init__.py similarity index 100% rename from MediaManager/src/routers/__init__.py rename to backend/src/tv/__init__.py diff --git a/MediaManager/src/tv/router.py b/backend/src/tv/router.py similarity index 89% rename from MediaManager/src/tv/router.py rename to backend/src/tv/router.py index f566e94..dbd4396 100644 --- a/MediaManager/src/tv/router.py +++ b/backend/src/tv/router.py @@ -12,12 +12,12 @@ import auth import dowloadClients import indexer import metadataProvider -from database import SessionDependency +from database import DbSessionDependency from database.torrents import Torrent from database.tv import Season, Show from indexer import IndexerQueryResult -from routers.users import Message from tv import log +from users.routers import Message router = APIRouter( prefix="/tv", @@ -34,7 +34,7 @@ class ShowDetails(BaseModel): status.HTTP_201_CREATED: {"model": Show, "description": "Successfully created show"}, status.HTTP_409_CONFLICT: {"model": Message, "description": "Show already exists"}, }) -def add_show(db: SessionDependency, show_id: int, metadata_provider: str = "tmdb", version: str = ""): +def add_show(db: DbSessionDependency, show_id: int, metadata_provider: str = "tmdb", version: str = ""): res = db.exec(select(Show). where(Show.external_id == show_id). where(Show.metadata_provider == metadata_provider). @@ -59,14 +59,14 @@ def add_show(db: SessionDependency, show_id: int, metadata_provider: str = "tmdb @router.delete("/{show_id}", status_code=status.HTTP_200_OK) -def delete_show(db: SessionDependency, show_id: UUID): +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)], response_model=Season) -def add_season(db: SessionDependency, season_id: UUID): +def add_season(db: DbSessionDependency, season_id: UUID): """ adds requested flag to a season """ @@ -81,7 +81,7 @@ def add_season(db: SessionDependency, season_id: UUID): @router.delete("/{show_id}/{season_id}", status_code=status.HTTP_200_OK, dependencies=[Depends(auth.get_current_user)], response_model=Show) -def delete_season(db: SessionDependency, show_id: UUID, season: int): +def delete_season(db: DbSessionDependency, show_id: UUID, season: int): """ removes requested flag from a season """ @@ -96,7 +96,7 @@ def delete_season(db: SessionDependency, show_id: UUID, season: int): @router.get("/{show_id}/{season_id}/torrent", status_code=status.HTTP_200_OK, dependencies=[Depends( auth.get_current_user)], response_model=list[IndexerQueryResult]) -def get_season_torrents(db: SessionDependency, show_id: UUID, season_id: UUID): +def get_season_torrents(db: DbSessionDependency, show_id: UUID, season_id: UUID): season = db.get(Season, season_id) if season is None: @@ -121,7 +121,7 @@ def get_season_torrents(db: SessionDependency, 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]) -def download_seasons_torrent(db: SessionDependency, show_id: UUID, torrent_id: UUID): +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 @@ -153,7 +153,7 @@ def download_seasons_torrent(db: SessionDependency, show_id: UUID, torrent_id: U @router.post("/{show_id}/{season_id}/torrent", status_code=status.HTTP_200_OK, dependencies=[Depends( auth.get_current_user)], response_model=list[Season]) -def delete_seasons_torrent(db: SessionDependency, show_id: UUID, season_id: UUID, torrent_id: UUID): +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 this means that multiple torrents can contain a season but you can choose from one which the content should be @@ -186,13 +186,13 @@ def delete_seasons_torrent(db: SessionDependency, show_id: UUID, season_id: UUID @router.get("/", dependencies=[Depends(auth.get_current_user)], response_model=list[Show]) -def get_shows(db: SessionDependency): +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) -def get_show(db: SessionDependency, show_id: UUID): +def get_show(db: DbSessionDependency, show_id: UUID): """ :param show_id: diff --git a/MediaManager/src/tv/__init__.py b/backend/src/users/__init__.py similarity index 100% rename from MediaManager/src/tv/__init__.py rename to backend/src/users/__init__.py diff --git a/MediaManager/src/routers/users.py b/backend/src/users/routers.py similarity index 80% rename from MediaManager/src/routers/users.py rename to backend/src/users/routers.py index c276fe0..8046f3b 100644 --- a/MediaManager/src/routers/users.py +++ b/backend/src/users/routers.py @@ -1,14 +1,13 @@ -from fastapi import APIRouter -from fastapi import Depends -from sqlalchemy.exc import IntegrityError +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 SessionDependency, get_session +from database import DbSessionDependency from database.users import User, UserCreate, UserPublic -from routers import log +from users import log router = APIRouter( prefix="/users", @@ -25,7 +24,7 @@ class Message(BaseModel): 201: {"model": UserPublic, "description": "User created successfully"} }) async def create_user( - db: SessionDependency, + db: DbSessionDependency, user: UserCreate = Depends(UserCreate), ): internal_user = User(name=user.name, lastname=user.lastname, email=user.email, @@ -35,9 +34,9 @@ async def create_user( 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__()) + 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) + log.info("Created new USER " + internal_user.email) return UserPublic(**internal_user.model_dump())