Merge pull request #8 from maxdorninger/add-automatic-downloads

Add automatic downloads
This commit is contained in:
Maximilian Dorninger
2025-05-31 18:30:03 +02:00
committed by GitHub
27 changed files with 397 additions and 136 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -8,12 +8,13 @@
<toc-element topic="introduction.md"/>
<toc-element topic="configuration-overview.md"/>
<toc-element topic="User-Guide.md"/>
<toc-element topic="Configuration.md">
<toc-element topic="authentication-setup.md"/>
<toc-element topic="configuration-frontend.md"/>
<toc-element topic="configuration-backend.md">
<toc-element topic="Indexer-Settings.md"/>
</toc-element>
<toc-element topic="Indexer-Settings.md"/>
<toc-element topic="configuration-frontend.md"/>
</toc-element>
<toc-element topic="troubleshooting.md"/>
<toc-element topic="developer-guide.md"/>

View File

@@ -22,4 +22,8 @@
<description>Created after removal of "Quick Start Guide" from MediaManager</description>
<accepts>Quick-Start-Guide.html</accepts>
</rule>
<rule id="570cb9d1">
<description>Created after removal of "user-guide" from MediaManager</description>
<accepts>user-guide.html</accepts>
</rule>
</rules>

View File

@@ -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.

View File

@@ -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:
__
<tabs>
<tab id="as-a-user" title="as a user">
<procedure title="Downloading/Requesting a show" id="request-show-user">
<step>Add a show on the "Add Show" page</step>
<step>After adding the show you will be redirected to the show's page.</step>
<step>There you can click the "Request Season" button.</step>
<step>Select one or more seasons that you want to download</step>
<step>Then select the "Min Quality", this will be the minimum resolution of the content to download.</step>
<step>Then select the "Wanted Quality", this will be the <strong>maximum</strong> resolution of the content to download.</step>
<step>Finally click Submit request, though this is not the last step!</step>
<step>An administrator first has to approve your request for download, only then will the requested content be downloaded.</step>
<p>Congratulation! You've downloaded a show.</p>
</procedure>
</tab>
<tab id="as-an-admin" title="as an admin">
<procedure title="Requesting a show" id="request-show-admin">
<step>Add a show on the "Add Show" page</step>
<step>After adding the show you will be redirected to the show's page.</step>
<step>There you can click the "Request Season" button.</step>
<step>Select one or more seasons that you want to download</step>
<step>Then select the "Min Quality", this will be the minimum resolution of the content to download.</step>
<step>Then select the "Wanted Quality", this will be the <strong>maximum</strong> resolution of the content to download.</step>
<step>Finally click Submit request, as you are an admin, your request will be automatically approved.</step>
<p>Congratulation! You've downloaded a show.</p>
</procedure>
<procedure title="Downloading a show" id="download-show-admin">
<p>You can only directly download a show if you are an admin!</p>
<step>Go to a show's page.</step>
<step>There you can click the "Download Season" button.</step>
<step>Enter the season's number that you want to download</step>
<step>Then optionally select the "File Path Suffix", <strong>it needs to be unique per season per show!</strong> </step>
<step>Then click "Download" on a torrent that you want to download.</step>
<p>Congratulation! You've downloaded a show.</p>
</procedure>
<procedure title="Managing requests" id="approving-request-admin">
<p>Users need their requests to be approved by an admin, to do this follow these steps:</p>
<step>Go to the "Requests" page.</step>
<step>There you can approve, delete or modify a user's request.</step>
</procedure>
</tab>
</tabs>

View File

@@ -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
<note>
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`.
</note>
| Variable | Description | Default |

View File

@@ -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 .` |

View File

@@ -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:
<note>
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`.
</note>
* [Backend Configuration](configuration-backend.md)
* [Frontend Configuration](configuration-frontend.md)
Build arguments are also used during the Docker image build process, primarily for versioning.

View File

@@ -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).

71
docker-compose.yaml Normal file
View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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 (

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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:

View File

@@ -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)

View File

@@ -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")

View File

@@ -26,4 +26,5 @@ dependencies = [
"typing-inspect>=0.9.0",
"uvicorn>=0.34.2",
"fastapi-utils>=0.8.0",
"apscheduler>=3.11.0",
]

26
uv.lock generated
View File

@@ -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"

View File

@@ -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

View File

@@ -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 } = {

View File

@@ -60,9 +60,9 @@
</Card.Root>
</div>
{:else}
<h3 class="scroll-m-20 text-2xl font-semibold tracking-tight">
You've not added any torrents yet.
</h3>
<div class="col-span-full text-center text-muted-foreground">
No Torrents added yet.
</div>
{/each}
</Accordion.Root>
{/await}

View File

@@ -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
}
}
};