diff --git a/.github/workflows/build-push-backend.yml b/.github/workflows/build-push-backend.yml
index 04622f3..cec9d99 100644
--- a/.github/workflows/build-push-backend.yml
+++ b/.github/workflows/build-push-backend.yml
@@ -32,13 +32,9 @@ jobs:
with:
images: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}/backend
tags: |
- # set latest tag for default branch
- type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
- # tag event 'refs/tags/v1.0.0' will generate 'v1.0.0'
+ type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
type=ref,event=tag
- # branch event 'refs/heads/main' will generate 'main'
type=ref,event=branch
- # sha
type=sha
- name: Build and push Docker image
diff --git a/.github/workflows/build-push-frontend.yml b/.github/workflows/build-push-frontend.yml
index 6057f99..4f482a4 100644
--- a/.github/workflows/build-push-frontend.yml
+++ b/.github/workflows/build-push-frontend.yml
@@ -35,7 +35,7 @@ jobs:
with:
images: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}/frontend
tags: |
- type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
+ type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
type=ref,event=tag
type=ref,event=branch
type=sha
diff --git a/Dockerfile b/Dockerfile
index 7e0d612..a5e5126 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -9,6 +9,9 @@ ENV MOVIE_DIRECTORY=/data/movies
ENV TORRENT_DIRECTORY=/data/torrents
ENV OPENID_ENABLED=FALSE
+RUN apt-get update && apt-get install -y ca-certificates
+
+
WORKDIR /app
COPY media_manager ./media_manager
COPY alembic ./alembic
diff --git a/Writerside/mm.tree b/Writerside/mm.tree
index 6a06360..076f527 100644
--- a/Writerside/mm.tree
+++ b/Writerside/mm.tree
@@ -8,12 +8,13 @@
+
-
-
+
+
diff --git a/Writerside/redirection-rules.xml b/Writerside/redirection-rules.xml
index ce11b76..11a9a84 100644
--- a/Writerside/redirection-rules.xml
+++ b/Writerside/redirection-rules.xml
@@ -22,4 +22,8 @@
Created after removal of "Quick Start Guide" from MediaManager
Quick-Start-Guide.html
+
+ Created after removal of "user-guide" from MediaManager
+ user-guide.html
+
\ No newline at end of file
diff --git a/Writerside/topics/Configuration.md b/Writerside/topics/Configuration.md
index 21b1192..5d3f8cc 100644
--- a/Writerside/topics/Configuration.md
+++ b/Writerside/topics/Configuration.md
@@ -1,6 +1,6 @@
# Configuration
The configuration of MediaManager is divided into backend and frontend settings, which can be set in your
-`docker-compose.yml` file or in separate `.env` files.
+`docker-compose.yaml` file or in separate `.env` files.
All settings are set as environment variables, because this makes backing up the configuration easier and allows for
easier sharing/transferring of the configuration.
\ No newline at end of file
diff --git a/Writerside/topics/User-Guide.md b/Writerside/topics/User-Guide.md
new file mode 100644
index 0000000..b29d75c
--- /dev/null
+++ b/Writerside/topics/User-Guide.md
@@ -0,0 +1,51 @@
+# Usage
+
+If you are coming from Radarr or Sonarr you will find that MediaManager does things a bit differently.
+Instead of completely automatically downloading and managing your media, MediaManager focuses on providing an
+easy-to-use interface to guide you through the process of finding and downloading media. Advanced features like multiple
+qualities of a show/movie necessitate such a paradigm shift. __So here is a quick step-by-step guide to get you started:
+__
+
+
+
+
+ Add a show on the "Add Show" page
+ After adding the show you will be redirected to the show's page.
+ There you can click the "Request Season" button.
+ Select one or more seasons that you want to download
+ Then select the "Min Quality", this will be the minimum resolution of the content to download.
+ Then select the "Wanted Quality", this will be the maximum resolution of the content to download.
+ Finally click Submit request, though this is not the last step!
+ An administrator first has to approve your request for download, only then will the requested content be downloaded.
+ Congratulation! You've downloaded a show.
+
+
+
+
+ Add a show on the "Add Show" page
+ After adding the show you will be redirected to the show's page.
+ There you can click the "Request Season" button.
+ Select one or more seasons that you want to download
+ Then select the "Min Quality", this will be the minimum resolution of the content to download.
+ Then select the "Wanted Quality", this will be the maximum resolution of the content to download.
+ Finally click Submit request, as you are an admin, your request will be automatically approved.
+ Congratulation! You've downloaded a show.
+
+
+ You can only directly download a show if you are an admin!
+ Go to a show's page.
+ There you can click the "Download Season" button.
+ Enter the season's number that you want to download
+ Then optionally select the "File Path Suffix", it needs to be unique per season per show!
+ Then click "Download" on a torrent that you want to download.
+ Congratulation! You've downloaded a show.
+
+
+ Users need their requests to be approved by an admin, to do this follow these steps:
+ Go to the "Requests" page.
+ There you can approve, delete or modify a user's request.
+
+
+
+
+
diff --git a/Writerside/topics/configuration-backend.md b/Writerside/topics/configuration-backend.md
index c925ebd..a315ce1 100644
--- a/Writerside/topics/configuration-backend.md
+++ b/Writerside/topics/configuration-backend.md
@@ -1,7 +1,12 @@
# Backend
-These variables configure the core backend application, database connections, authentication, and integrations. They are
-typically set as environment variables for the backend Docker container.
+These variables configure the core backend application, database connections, authentication, and integrations.
+
+## General Settings
+
+| Variable | Description | Default |
+|-----------------|-----------------------------|-----------|
+| `API_BASE_PATH` | The url base of the backend | `/api/v1` |
## Database Settings
@@ -13,6 +18,18 @@ typically set as environment variables for the backend Docker container.
| `DB_PASSWORD` | Password for the PostgreSQL user. | `MediaManager` | `mypassword` |
| `DB_DBNAME` | Name of the PostgreSQL database. | `MediaManager` | `mydatabase` |
+## Download Client Settings
+
+Currently, only qBittorrent is supported as a download client. But support for other clients isn't unlikely in the
+future.
+
+| Variable | Description | Default | Example |
+|--------------------|-----------------------------|-------------|--------------------|
+| `QBITTORRENT_HOST` | Host of the QBittorrent API | `localhost` | `qbit.example.com` |
+| `QBITTORRENT_PORT` | Port of the QBittorrent API | `8080` | `443` |
+| `QBITTORRENT_USER` | Username for QBittorrent | `admin` | - |
+| `QBITTORRENT_PASS` | Password for QBittorrent | `admin` | - |
+
## Metadata Provider Settings
These settings configure the integrations with external metadata providers like The Movie Database (TMDB) and The TVDB.
@@ -47,7 +64,7 @@ generate a free API key in your account settings.
## Directory Settings
- Normally you don't need to change these, as the default mountpoints are usually sufficient. In your `docker-compose.yml`, you can just mount `/any/directory` to `/data/torrents`.
+ Normally you don't need to change these, as the default mountpoints are usually sufficient. In your `docker-compose.yaml`, you can just mount `/any/directory` to `/data/torrents`.
| Variable | Description | Default |
diff --git a/Writerside/topics/configuration-frontend.md b/Writerside/topics/configuration-frontend.md
index 872cc1d..69cf205 100644
--- a/Writerside/topics/configuration-frontend.md
+++ b/Writerside/topics/configuration-frontend.md
@@ -8,7 +8,13 @@
## Build Arguments (web/Dockerfile)
-| Argument | Description | Example (in build command) |
-|-----------|-----------------------------------------------------------------------------------------------------------|--------------------------------------------------------------|
-| `VERSION` | Sets the `PUBLIC_VERSION` environment variable at runtime in the frontend container. Passed during build. | `docker build --build-arg VERSION=1.2.3 -f web/Dockerfile .` |
+**TODO: expand on this section**
+
+To configure a url base path for the frontend, you need to build the frontend docker container, this is because
+unfortunately SvelteKit needs to know the base path at build time.
+
+| Argument | Description | Example (in build command) |
+|------------|-----------------------------------------------------------------------------------------------------------|----------------------------------------------------------------|
+| `VERSION` | Sets the `PUBLIC_VERSION` environment variable at runtime in the frontend container. Passed during build. | `docker build --build-arg VERSION=1.2.3 -f web/Dockerfile .` |
+| `BASE_URL` | Sets the base url path, it must begin with a slash and not end | `docker build --build-arg BASE_URL=/media -f web/Dockerfile .` |
diff --git a/Writerside/topics/configuration-overview.md b/Writerside/topics/configuration-overview.md
index 305b390..81cd87a 100644
--- a/Writerside/topics/configuration-overview.md
+++ b/Writerside/topics/configuration-overview.md
@@ -2,33 +2,26 @@
The recommended way to install and run Media Manager is using Docker and Docker Compose.
-1. **Prerequisites:**
- * Ensure Docker and Docker Compose are installed on your system.
- * If you plan to use OAuth 2.0 / OpenID Connect for authentication, you will need an account and client credentials
- from an OpenID provider (e.g., Authentik, Pocket ID).
+## Prerequisites
-2. **Setup:**
- * Copy the docker-compose.yml from the MediaManager repo.
- * Configure the necessary environment variables in your `docker-compose.yml` file.
- * (Optional) Create a `.env` file in the root directory for backend environment variables and/or a `web/.env` for
- frontend environment variables if you prefer to manage them separately from `docker-compose.yml`.
+* Ensure Docker and Docker Compose are installed on your system.
+* If you plan to use OAuth 2.0 / OpenID Connect for authentication, you will need an account and client credentials
+ from an OpenID provider (e.g., Authentik, Pocket ID).
-3. **Running the Application:**
- * Execute the command `docker-compose up -d` from the root directory. This will build the Docker images (if not
- already built) and start all the services (backend, frontend, and potentially a database if configured in your
- compose file).
- * The backend will typically be available at `http://localhost:8000` and the frontend at `http://localhost:3000` (or
- as configured).
+## Setup
-# Configuration Overview
+* Download the docker-compose.yaml from the MediaManager repo with the following command:
+ ```
+ wget -o docker-compose.yaml https://raw.githubusercontent.com/maxdorninger/MediaManager/refs/heads/master/docker-compose.yaml
+ ```
-Media Manager is configured primarily through environment variables. These can be set in your `docker-compose.yml` file,
-a `.env` file.
+* Configure the necessary environment variables in your `docker-compose.yaml` file.
+* For more information on the available configuration options, see the [Configuration section](Configuration.md) of the
+ documentation.
-Detailed configuration options are split into backend and frontend sections:
+
+ It is good practice to put API keys and other sensitive information in a separate `.env` file and reference them in your
+ `docker-compose.yaml`.
+
-* [Backend Configuration](configuration-backend.md)
-* [Frontend Configuration](configuration-frontend.md)
-
-Build arguments are also used during the Docker image build process, primarily for versioning.
diff --git a/Writerside/topics/troubleshooting.md b/Writerside/topics/troubleshooting.md
index 5e46b75..9193f17 100644
--- a/Writerside/topics/troubleshooting.md
+++ b/Writerside/topics/troubleshooting.md
@@ -10,21 +10,21 @@
## Authentication Issues
- * Double-check `AUTH_TOKEN_SECRET`. If it changes, existing sessions/tokens will be invalidated.
- * For OpenID:
- * Verify `OPENID_CLIENT_ID`, `OPENID_CLIENT_SECRET`, and `OPENID_CONFIGURATION_ENDPOINT` are correct.
- * Ensure the `FRONTEND_URL` is accurate and that your OpenID provider has the correct redirect URI whitelisted (
- e.g., `http://your-frontend-url/api/v1/auth/cookie/Authentik/callback`).
- * Check backend logs for errors from `httpx_oauth` or `fastapi-users`.
+* Double-check `AUTH_TOKEN_SECRET`. If it changes, existing sessions/tokens will be invalidated.
+* For OpenID:
+ * Verify `OPENID_CLIENT_ID`, `OPENID_CLIENT_SECRET`, and `OPENID_CONFIGURATION_ENDPOINT` are correct.
+ * Ensure the `FRONTEND_URL` is accurate and that your OpenID provider has the correct redirect URI whitelisted (
+ e.g., `http://your-frontend-url/api/v1/auth/cookie/Authentik/callback`).
+ * Check backend logs for errors from `httpx_oauth` or `fastapi-users`.
## CORS Errors
- * Ensure `FRONTEND_URL` is correctly set.
- * Ensure your frontend's url is listed in `CORS_URLS`.
+* Ensure `FRONTEND_URL` is correctly set.
+* Ensure your frontend's url is listed in `CORS_URLS`.
## Data Not Appearing / File Issues
- * Verify that the volume mounts for `IMAGE_DIRECTORY`, `TV_DIRECTORY`, `MOVIE_DIRECTORY`, and `TORRENT_DIRECTORY` in
- your `docker-compose.yml` are correctly pointing to your media folders on the host machine.
- * Check file and directory permissions for the user running the Docker container (or the `node` user inside the
- containers).
+* Verify that the volume mounts for `IMAGE_DIRECTORY`, `TV_DIRECTORY`, `MOVIE_DIRECTORY`, and `TORRENT_DIRECTORY` in
+ your `docker-compose.yaml` are correctly pointing to your media folders on the host machine.
+* Check file and directory permissions for the user running the Docker container (or the `node` user inside the
+ containers).
diff --git a/docker-compose.yaml b/docker-compose.yaml
new file mode 100644
index 0000000..0414354
--- /dev/null
+++ b/docker-compose.yaml
@@ -0,0 +1,71 @@
+services:
+ backend:
+ image: ghcr.io/maxdorninger/mediamanager/backend:latest
+ container_name: backend
+ ports:
+ - "8000:8000"
+
+ environment:
+ - QBITTORRENT_PASSWORD=
+ - QBITTORRENT_HOST=
+ - QBITTORRENT_USERNAME=
+ - QBITTORRENT_PORT=
+
+ - PROWLARR_URL=http://prowlarr:9696
+ - PROWLARR_ENABLED=TRUE
+ - PROWLARR_API_KEY=
+
+ - TMDB_API_KEY=
+ - CORS_URLS=
+
+ - DB_HOST=db
+ #- DB_NAME=
+ #- DB_PORT=
+ #- DB_PASSOWORD=
+ #- DB_DBNAME=
+
+ - AUTH_TOKEN_SECRET=
+ - AUTH_ADMIN_EMAIL=
+ - FRONTEND_URL=
+ #- AUTH_SESSION_LIFETIME=
+
+ #- OPENID_ENABLED=TRUE
+ #- OPENID_CLIENT_ID=
+ #- OPENID_CLIENT_SECRET=
+ #- OPENID_CONFIGURATION_ENDPOINT=
+ #- OPENID_NAME=
+
+ #- API_BASE_PATH=/api/v1
+ #- TVDB_API_KEY=
+ #- DEVELOPMENT=
+
+ volumes:
+ - ./data:/data/images
+ - ./tv:/data/tv
+ - ./movie:/data/movies
+ - ./torrent:/data/torrents
+ frontend:
+ image: ghcr.io/maxdorninger/mediamanager/frontend:latest
+ container_name: frontend
+ ports:
+ - "3000:3000"
+ volumes:
+ - ./cache:/app/cache
+ environment:
+ - PUBLIC_API_URL=http://localhost:8000/api/v1
+ - PUBLIC_SSR_API_URL=http://backend:8000/api/v1
+ # - PUBLIC_WEB_SSR=false
+ db:
+ image: postgres:latest
+ restart: unless-stopped
+ container_name: postgres
+ volumes:
+ - ./postgres:/var/lib/postgresql/data
+ environment:
+ POSTGRES_USER: MediaManager
+ POSTGRES_DB: MediaManager
+ POSTGRES_PASSWORD: MediaManager
+ ports:
+ - "5432:5432"
+
+
diff --git a/docker-compose.yml b/docker-compose.yml
deleted file mode 100644
index ce5c6d8..0000000
--- a/docker-compose.yml
+++ /dev/null
@@ -1,55 +0,0 @@
-services:
- db:
- image: postgres:latest
- restart: unless-stopped
- container_name: postgres
- volumes:
- - .\res\postgres:/var/lib/postgresql/data
- environment:
- POSTGRES_USER: MediaManager
- POSTGRES_DB: MediaManager
- POSTGRES_PASSWORD: MediaManager
- ports:
- - "5432:5432"
- prowlarr:
- image: lscr.io/linuxserver/prowlarr:latest
- container_name: prowlarr
- environment:
- - PUID=1000
- - PGID=1000
- - TZ=Etc/UTC
- volumes:
- - .\res\prowlarr:/config
- restart: unless-stopped
- ports:
- - "9696:9696"
- qbittorrent:
- image: lscr.io/linuxserver/qbittorrent:latest
- container_name: qbittorrent
- environment:
- - PUID=1000
- - PGID=1000
- - TZ=Etc/UTC
- - WEBUI_PORT=8080
- - TORRENTING_PORT=6881
- ports:
- - 8080:8080
- - 6881:6881
- - 6881:6881/udp
- restart: unless-stopped
- volumes:
- - ./torrent/:/download/:rw
- pocket-id:
- image: ghcr.io/pocket-id/pocket-id
- restart: unless-stopped
- env_file: .env
- ports:
- - 1411:1411
- volumes:
- - "./res/pocket-id:/app/data"
- healthcheck:
- test: "curl -f http://localhost:1411/healthz"
- interval: 1m30s
- timeout: 5s
- retries: 2
- start_period: 10s
\ No newline at end of file
diff --git a/media_manager/auth/oauth.py b/media_manager/auth/oauth.py
index a161039..db66358 100644
--- a/media_manager/auth/oauth.py
+++ b/media_manager/auth/oauth.py
@@ -1,13 +1,13 @@
from typing import Optional
import jwt
-from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
+from fastapi import APIRouter, Depends, HTTPException, Request, status
from httpx_oauth.integrations.fastapi import OAuth2AuthorizeCallback
from httpx_oauth.oauth2 import BaseOAuth2, OAuth2Token
from pydantic import BaseModel
-from fastapi_users import models, schemas
-from fastapi_users.authentication import AuthenticationBackend, Authenticator, Strategy
+from fastapi_users import models
+from fastapi_users.authentication import AuthenticationBackend, Strategy
from fastapi_users.exceptions import UserAlreadyExists
from fastapi_users.jwt import SecretType, decode_jwt, generate_jwt
from fastapi_users.manager import BaseUserManager, UserManagerDependency
diff --git a/media_manager/auth/users.py b/media_manager/auth/users.py
index c925f91..29fdb50 100644
--- a/media_manager/auth/users.py
+++ b/media_manager/auth/users.py
@@ -2,7 +2,6 @@ import os
import uuid
from typing import Optional
-import httpx
from fastapi import Depends, Request
from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin, models
from fastapi_users.authentication import (
diff --git a/media_manager/indexer/schemas.py b/media_manager/indexer/schemas.py
index 6091998..d9b5ed0 100644
--- a/media_manager/indexer/schemas.py
+++ b/media_manager/indexer/schemas.py
@@ -30,13 +30,13 @@ class IndexerQueryResult(BaseModel):
very_low_quality_pattern = r"\b(480p|480P|360p|360P)\b"
if re.search(high_quality_pattern, self.title):
- return Quality.high
+ return Quality.uhd
elif re.search(medium_quality_pattern, self.title):
- return Quality.medium
+ return Quality.fullhd
elif re.search(low_quality_pattern, self.title):
- return Quality.low
+ return Quality.hd
elif re.search(very_low_quality_pattern, self.title):
- return Quality.very_low
+ return Quality.sd
return Quality.unknown
diff --git a/media_manager/main.py b/media_manager/main.py
index 7c790a4..a6e7a02 100644
--- a/media_manager/main.py
+++ b/media_manager/main.py
@@ -1,9 +1,13 @@
import logging
+import os
import sys
from logging.config import dictConfig
from pythonjsonlogger.json import JsonFormatter
+import torrent.service
+from database import SessionLocal
+
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
@@ -46,13 +50,18 @@ log = logging.getLogger(__name__)
from media_manager.database import init_db
import media_manager.tv.router as tv_router
+from media_manager.tv.service import auto_download_all_approved_season_requests
import media_manager.torrent.router as torrent_router
-init_db()
-log.info("Database initialized")
-
from media_manager.config import BasicConfig
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
+from datetime import datetime
+from contextlib import asynccontextmanager
+from apscheduler.schedulers.background import BackgroundScheduler
+from apscheduler.triggers.cron import CronTrigger
+
+init_db()
+log.info("Database initialized")
basic_config = BasicConfig()
if basic_config.DEVELOPMENT:
@@ -64,7 +73,31 @@ if basic_config.DEVELOPMENT:
else:
log.info("Development Mode not activated!")
-app = FastAPI(root_path="/api/v1")
+
+def hourly_tasks():
+ log.info(f"Tasks are running at {datetime.now()}")
+ auto_download_all_approved_season_requests()
+ torrent.service.TorrentService(db=SessionLocal()).import_all_torrents()
+
+
+scheduler = BackgroundScheduler()
+trigger = CronTrigger(minute=0, hour="*")
+scheduler.add_job(hourly_tasks, trigger)
+
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+ scheduler.start()
+ yield
+ scheduler.shutdown()
+
+
+app = FastAPI(lifespan=lifespan)
+
+
+base_path = os.getenv("API_BASE_PATH") or "/api/v1"
+log.info("Base Path for API: %s", base_path)
+app = FastAPI(root_path=base_path)
if basic_config.DEVELOPMENT:
origins = [
@@ -162,5 +195,7 @@ app.mount(
name="static-images",
)
+log.info("Hello World!")
+
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=5049, log_config=LOGGING_CONFIG)
diff --git a/media_manager/torrent/schemas.py b/media_manager/torrent/schemas.py
index db5a861..73ad3f4 100644
--- a/media_manager/torrent/schemas.py
+++ b/media_manager/torrent/schemas.py
@@ -8,13 +8,21 @@ TorrentId = typing.NewType("TorrentId", uuid.UUID)
class Quality(Enum):
- high = 1
- medium = 2
- low = 3
- very_low = 4
+ uhd = 1
+ fullhd = 2
+ hd = 3
+ sd = 4
unknown = 5
+class QualityStrings(Enum):
+ uhd = "4K"
+ fullhd = "1080p"
+ hd = "720p"
+ sd = "400p"
+ unknown = "unknown"
+
+
class TorrentStatus(Enum):
finished = 1
downloading = 2
diff --git a/media_manager/torrent/service.py b/media_manager/torrent/service.py
index 522c8b3..327816f 100644
--- a/media_manager/torrent/service.py
+++ b/media_manager/torrent/service.py
@@ -8,7 +8,6 @@ from pathlib import Path
import bencoder
import qbittorrentapi
import requests
-from fastapi_utils.tasks import repeat_every
from pydantic_settings import BaseSettings, SettingsConfigDict
from sqlalchemy.orm import Session
@@ -291,10 +290,10 @@ class TorrentService:
for x in media_manager.torrent.repository.get_all_torrents(db=self.db)
]
- def get_torrent_by_id(self, id: TorrentId) -> Torrent:
+ def get_torrent_by_id(self, torrent_id: TorrentId) -> Torrent:
return self.get_torrent_status(
media_manager.torrent.repository.get_torrent_by_id(
- torrent_id=id, db=self.db
+ torrent_id=torrent_id, db=self.db
)
)
@@ -308,10 +307,10 @@ class TorrentService:
)
media_manager.torrent.repository.delete_torrent(db=self.db, torrent_id=t.id)
- @repeat_every(seconds=3600)
def import_all_torrents(self) -> list[Torrent]:
log.info("Importing all torrents")
torrents = self.get_all_torrents()
+ log.info("Found %d torrents to import", len(torrents))
imported_torrents = []
for t in torrents:
if t.imported == False and t.status == TorrentStatus.finished:
diff --git a/media_manager/tv/repository.py b/media_manager/tv/repository.py
index 44029c8..245a37d 100644
--- a/media_manager/tv/repository.py
+++ b/media_manager/tv/repository.py
@@ -289,3 +289,24 @@ def get_season_request(
db: Session, season_request_id: SeasonRequestId
) -> SeasonRequestSchema:
return SeasonRequestSchema.model_validate(db.get(SeasonRequest, season_request_id))
+
+
+def get_show_by_season_id(db: Session, season_id: SeasonId) -> ShowSchema | None:
+ """
+ Retrieve a show by one of its season's ID.
+
+ :param db: The database session.
+ :param season_id: The ID of the season to retrieve the show for.
+ :return: A ShowSchema object if found, otherwise None.
+ """
+ stmt = (
+ select(Show)
+ .join(Season, Show.id == Season.show_id)
+ .where(Season.id == season_id)
+ )
+
+ result = db.execute(stmt).unique().scalar_one_or_none()
+ if not result:
+ return None
+
+ return ShowSchema.model_validate(result)
diff --git a/media_manager/tv/service.py b/media_manager/tv/service.py
index 07e3b4c..9b58287 100644
--- a/media_manager/tv/service.py
+++ b/media_manager/tv/service.py
@@ -4,6 +4,7 @@ import media_manager.indexer.service
import media_manager.metadataProvider
import media_manager.torrent.repository
import media_manager.tv.repository
+from media_manager.database import SessionLocal
from media_manager.indexer import IndexerQueryResult
from media_manager.indexer.schemas import IndexerQueryResultId
from media_manager.metadataProvider.schemas import MetaDataProviderShowSearchResult
@@ -29,6 +30,7 @@ from media_manager.tv.schemas import (
SeasonRequestId,
RichSeasonRequest,
)
+from media_manager.torrent.schemas import QualityStrings
def add_show(db: Session, external_id: int, metadata_provider: str) -> Show | None:
@@ -284,3 +286,82 @@ def download_torrent(
)
add_season_file(db=db, season_file=season_file)
return show_torrent
+
+
+def download_approved_season_request(
+ db: Session,
+ season_request: SeasonRequest,
+ show_id: ShowId,
+) -> bool:
+ if not season_request.authorized:
+ log.error(f"Season request {season_request.id} is not authorized for download")
+ raise ValueError(
+ f"Season request {season_request.id} is not authorized for download"
+ )
+ log.info(f"Downloading approved season request {season_request.id}")
+
+ season = get_season(db=db, season_id=season_request.season_id)
+ torrents = get_all_available_torrents_for_a_season(
+ db=db, season_number=season.number, show_id=show_id
+ )
+ available_torrents: list[IndexerQueryResult] = []
+ for torrent in torrents:
+ if (
+ torrent.quality > season_request.wanted_quality
+ or torrent.quality < season_request.min_quality
+ or torrent.seeders < 3
+ ):
+ log.info(
+ f"Skipping torrent {torrent.title} with quality {torrent.quality} for season {season.id}, because it does not match the requested quality {season_request.wanted_quality}"
+ )
+ elif torrent.season == [
+ season.number,
+ ]:
+ log.info(
+ f"Skipping torrent {torrent.title} with quality {torrent.quality} for season {season.id}, because it contains to many/wrong seasons {torrent.season} (wanted: {season.number})"
+ )
+ else:
+ available_torrents.append(torrent)
+ log.info(
+ f"Taking torrent {torrent.title} with quality {torrent.quality} for season {season.id} into consideration"
+ )
+ if len(available_torrents) == 0:
+ log.warning(
+ f"No torrents matching criteria were found (wanted quality: {season_request.wanted_quality}, min_quality: {season_request.min_quality} for season {season.id})"
+ )
+ return False
+
+ available_torrents.sort()
+
+ torrent = TorrentService(db=db).download(indexer_result=available_torrents[0])
+ season_file = SeasonFile(
+ season_id=season.id,
+ quality=torrent.quality,
+ torrent_id=torrent.id,
+ file_path_suffix=QualityStrings[torrent.quality.name].value.upper(),
+ )
+ add_season_file(db=db, season_file=season_file)
+ return True
+
+
+def auto_download_all_approved_season_requests() -> None:
+ db: Session = SessionLocal()
+ log.info("Auto downloading all approved season requests")
+ season_requests = media_manager.tv.repository.get_season_requests(db=db)
+ log.info(f"Found {len(season_requests)} season requests to process")
+ count = 0
+ for season_request in season_requests:
+ if season_request.authorized:
+ log.info(f"Processing season request {season_request.id} for download")
+ show = media_manager.tv.repository.get_show_by_season_id(
+ db=db, season_id=season_request.season_id
+ )
+ if download_approved_season_request(
+ db=db, season_request=season_request, show_id=show.id
+ ):
+ count += 1
+ else:
+ log.warning(
+ f"Failed to download season request {season_request.id} for show {show.name}"
+ )
+ log.info(f"Auto downloaded {count} approved season requests")
diff --git a/pyproject.toml b/pyproject.toml
index 226cadc..ec1830d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -26,4 +26,5 @@ dependencies = [
"typing-inspect>=0.9.0",
"uvicorn>=0.34.2",
"fastapi-utils>=0.8.0",
+ "apscheduler>=3.11.0",
]
diff --git a/uv.lock b/uv.lock
index 2a02129..b6ef704 100644
--- a/uv.lock
+++ b/uv.lock
@@ -24,6 +24,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
]
+[[package]]
+name = "apscheduler"
+version = "3.11.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "tzlocal" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4e/00/6d6814ddc19be2df62c8c898c4df6b5b1914f3bd024b780028caa392d186/apscheduler-3.11.0.tar.gz", hash = "sha256:4c622d250b0955a65d5d0eb91c33e6d43fd879834bf541e0a18661ae60460133", size = 107347, upload-time = "2024-11-24T19:39:26.463Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d0/ae/9a053dd9229c0fde6b1f1f33f609ccff1ee79ddda364c756a924c6d8563b/APScheduler-3.11.0-py3-none-any.whl", hash = "sha256:fc134ca32e50f5eadcc4938e3a4545ab19131435e851abb40b34d63d5141c6da", size = 64004, upload-time = "2024-11-24T19:39:24.442Z" },
+]
+
[[package]]
name = "argon2-cffi"
version = "23.1.0"
@@ -575,6 +587,7 @@ name = "mediamanager"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
+ { name = "apscheduler" },
{ name = "bencoder" },
{ name = "cachetools" },
{ name = "fastapi", extra = ["standard"] },
@@ -601,6 +614,7 @@ dependencies = [
[package.metadata]
requires-dist = [
+ { name = "apscheduler", specifier = ">=3.11.0" },
{ name = "bencoder", specifier = ">=0.2.0" },
{ name = "cachetools", specifier = ">=6.0.0" },
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.12" },
@@ -1090,6 +1104,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
]
+[[package]]
+name = "tzlocal"
+version = "5.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "tzdata", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" },
+]
+
[[package]]
name = "urllib3"
version = "2.4.0"
diff --git a/web/Dockerfile b/web/Dockerfile
index 5e7a2ff..994f598 100644
--- a/web/Dockerfile
+++ b/web/Dockerfile
@@ -1,14 +1,13 @@
ARG VERSION
-
+ARG BASE_URL=""
FROM node:24-alpine AS build
-ARG VERSION
USER node:node
WORKDIR /app
COPY --chown=node:node . .
RUN npm ci
-RUN env PUBLIC_VERSION=${VERSION} npm run build
+RUN env PUBLIC_VERSION=${VERSION} BASE_URL=${BASE_URL} npm run build
FROM node:24-alpine AS frontend
ARG VERSION
diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts
index fe0810a..71d218f 100644
--- a/web/src/lib/utils.ts
+++ b/web/src/lib/utils.ts
@@ -9,10 +9,10 @@ import {browser} from "$app/environment";
const apiUrl = browser ? env.PUBLIC_API_URL : env.PUBLIC_SSR_API_URL;
export const qualityMap: { [key: number]: string } = {
- 1: 'high',
- 2: 'medium',
- 3: 'low',
- 4: 'very low',
+ 1: '4K/UHD',
+ 2: '1080p/FullHD',
+ 3: '720p/HD',
+ 4: '480p/SD',
5: 'unknown'
};
export const torrentStatusMap: { [key: number]: string } = {
diff --git a/web/src/routes/dashboard/tv/torrents/+page.svelte b/web/src/routes/dashboard/tv/torrents/+page.svelte
index 2a45f15..598f715 100644
--- a/web/src/routes/dashboard/tv/torrents/+page.svelte
+++ b/web/src/routes/dashboard/tv/torrents/+page.svelte
@@ -60,9 +60,9 @@
{:else}
-
- You've not added any torrents yet.
-
+
+ No Torrents added yet.
+
{/each}
{/await}
diff --git a/web/svelte.config.js b/web/svelte.config.js
index a730c79..c8c96ba 100644
--- a/web/svelte.config.js
+++ b/web/svelte.config.js
@@ -1,6 +1,8 @@
import adapter from '@sveltejs/adapter-node';
import {vitePreprocess} from '@sveltejs/vite-plugin-svelte';
+const base = process.env.BASE_URL || '';
+
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://svelte.dev/docs/kit/integrations
@@ -11,7 +13,10 @@ const config = {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
- adapter: adapter()
+ adapter: adapter(),
+ paths: {
+ base: base
+ }
}
};