mirror of
https://github.com/maxdorninger/MediaManager.git
synced 2026-04-17 15:43:28 +02:00
Merge pull request #48 from maxdorninger/add-configuration-via-toml-file
Add configuration via toml file and enhance documentation
This commit is contained in:
@@ -31,7 +31,8 @@ other services.
|
||||
|
||||
```
|
||||
wget -O docker-compose.yaml https://raw.githubusercontent.com/maxdorninger/MediaManager/refs/heads/master/docker-compose.yaml
|
||||
# Edit docker-compose.yaml to set the environment variables!
|
||||
wget -O docker-compose.yaml https://raw.githubusercontent.com/maxdorninger/MediaManager/refs/heads/master/config.toml
|
||||
# you probably need to edit the config.toml file, for more help see the documentation
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
|
||||
@@ -10,11 +10,15 @@
|
||||
<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-backend.md">
|
||||
</toc-element>
|
||||
<toc-element topic="Indexer-Settings.md"/>
|
||||
<toc-element topic="configuration-backend.md"/>
|
||||
<toc-element topic="configuration-frontend.md"/>
|
||||
<toc-element topic="authentication-setup.md"/>
|
||||
<toc-element topic="database-configuration.md"/>
|
||||
<toc-element topic="download-client-configuration.md"/>
|
||||
<toc-element topic="Indexer-Settings.md"/>
|
||||
<toc-element topic="Notifications.md"/>
|
||||
<toc-element topic="Reverse-Proxy.md"/>
|
||||
<toc-element topic="metadata-provider-configuration.md"/>
|
||||
</toc-element>
|
||||
<toc-element topic="troubleshooting.md"/>
|
||||
<toc-element topic="developer-guide.md"/>
|
||||
|
||||
@@ -26,4 +26,8 @@
|
||||
<description>Created after removal of "user-guide" from MediaManager</description>
|
||||
<accepts>user-guide.html</accepts>
|
||||
</rule>
|
||||
<rule id="7cb7d801">
|
||||
<description>Created after removal of "MetadataRelay" from MediaManager</description>
|
||||
<accepts>MetadataRelay.html</accepts>
|
||||
</rule>
|
||||
</rules>
|
||||
@@ -1,6 +1,67 @@
|
||||
# Configuration
|
||||
|
||||
The configuration of MediaManager is divided into backend and frontend settings, which can be set in your
|
||||
`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.
|
||||
MediaManager uses a TOML configuration file (`config.toml`) for all backend settings.
|
||||
This centralized configuration approach makes it easier to manage, backup, and share your MediaManager setup.
|
||||
|
||||
Frontend settings are configured through environment variables in your `docker-compose.yaml` file.
|
||||
|
||||
## Configuration File Location
|
||||
|
||||
Your `config.toml` file should be mounted to `/data/config.toml` inside the container:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- ./config.toml:/data/config.toml
|
||||
```
|
||||
|
||||
Though you can change the location of the configuration file by setting the `CONFIG_FILE` env variable to another path,
|
||||
e.g. `/etc/mm/config.toml`.
|
||||
|
||||
## Configuration Sections
|
||||
|
||||
The configuration is organized into the following sections:
|
||||
|
||||
- `[misc]` - General settings
|
||||
- `[database]` - Database settings
|
||||
- `[auth]` - Authentication settings
|
||||
- `[notifications]` - Notification settings (Email, Gotify, Ntfy, Pushover)
|
||||
- `[torrents]` - Download client settings (qBittorrent, SABnzbd)
|
||||
- `[indexers]` - Indexer settings (Prowlarr and Jackett )
|
||||
- `[metadata]` - TMDB and TVDB settings
|
||||
|
||||
## Configuring Secrets
|
||||
|
||||
For sensitive information like API keys, passwords, and secrets, you _should_ use environment variables.
|
||||
You can actually set every configuration value through environment variables.
|
||||
For example, to set the `token_secret` value for authentication, with a .toml file you would use:
|
||||
|
||||
```toml
|
||||
[auth]
|
||||
token_secret = "your_super_secret_key_here"
|
||||
```
|
||||
|
||||
But you can also set it through an environment variable:
|
||||
|
||||
```
|
||||
AUTH_TOKEN_SECRET = "your_super_secret_key_here"
|
||||
```
|
||||
|
||||
or another example with the OIDC client secret:
|
||||
|
||||
```toml
|
||||
[auth]
|
||||
...
|
||||
[auth.openid_connect]
|
||||
client_secret = "your_client_secret_from_provider"
|
||||
```
|
||||
|
||||
env variable:
|
||||
|
||||
```
|
||||
AUTH_OPENID_CONNECT_CLIENT_SECRET = "your_client_secret_from_provider"
|
||||
```
|
||||
|
||||
So for every config "level", you basically have to take the name of the value and prepend it with the section names in
|
||||
uppercase with underscores as delimiters.
|
||||
|
||||
|
||||
|
||||
@@ -1,37 +1,53 @@
|
||||
# Indexer Settings
|
||||
# Indexers
|
||||
|
||||
## Prowlarr
|
||||
Indexer settings are configured in the `[indexers]` section of your `config.toml` file. MediaManager supports both Prowlarr and Jackett as indexer providers.
|
||||
|
||||
### `PROWLARR_ENABLED`
|
||||
## Prowlarr (`[indexers.prowlarr]`)
|
||||
|
||||
Set to `True` to enable Prowlarr. Default is `False`. Example: `true`.
|
||||
- `enabled`
|
||||
|
||||
### `PROWLARR_API_KEY`
|
||||
Set to `true` to enable Prowlarr. Default is `false`.
|
||||
|
||||
This is your Prowlarr API key. Example: `prowlarr_api_key`.
|
||||
- `url`
|
||||
|
||||
### `PROWLARR_URL`
|
||||
Base URL of your Prowlarr instance.
|
||||
|
||||
Base URL of your Prowlarr instance. Default is `http://localhost:9696`. Example: `http://prowlarr:9696`.
|
||||
- `api_key`
|
||||
|
||||
## Jackett
|
||||
API key for Prowlarr. You can find this in Prowlarr's settings under General.
|
||||
|
||||
### `JACKETT_ENABLED`
|
||||
## Jackett (`[indexers.jackett]`)
|
||||
|
||||
Set to `True` to enable Jackett. Default is `False`. Example: `true`.
|
||||
- `enabled`
|
||||
|
||||
### `JACKETT_API_KEY`
|
||||
Set to `true` to enable Jackett. Default is `false`.
|
||||
|
||||
This is your Prowlarr API key. Example: `jackett_api_key`.
|
||||
- `url`
|
||||
|
||||
### `JACKETT_URL`
|
||||
Base URL of your Jackett instance.
|
||||
|
||||
Base URL of your Prowlarr instance. Default is `http://localhost:9117`. Example: `http://prowlarr:9117`.
|
||||
- `api_key`
|
||||
|
||||
### `JACKETT_INDEXERS`
|
||||
API key for Jackett. You can find this in Jackett's dashboard.
|
||||
|
||||
List of all indexers for Jackett to search through. Default is `all`. Example: `["1337x","0magnet"]`.
|
||||
- `indexers`
|
||||
|
||||
<note>
|
||||
<include from="notes.topic" element-id="list-format"/>
|
||||
</note>
|
||||
List of indexer names to use with Jackett. You can specify which indexers Jackett should search through.
|
||||
|
||||
## Example Configuration
|
||||
|
||||
Here's a complete example of the indexers section in your `config.toml`:
|
||||
|
||||
```toml
|
||||
[indexers]
|
||||
[indexers.prowlarr]
|
||||
enabled = true
|
||||
url = "http://prowlarr:9696"
|
||||
api_key = "your_prowlarr_api_key"
|
||||
|
||||
[indexers.jackett]
|
||||
enabled = false
|
||||
url = "http://jackett:9117"
|
||||
api_key = "your_jackett_api_key"
|
||||
indexers = ["1337x", "rarbg"]
|
||||
```
|
||||
|
||||
121
Writerside/topics/Notifications.md
Normal file
121
Writerside/topics/Notifications.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Notifications
|
||||
|
||||
These settings are configured in the `[notifications]` section of your `config.toml` file.
|
||||
|
||||
### SMTP Configuration (`[notifications.smtp_config]`)
|
||||
|
||||
For sending emails, MediaManager uses the SMTP protocol. You can use any SMTP server, like Gmail or SMTP2GO.
|
||||
|
||||
- `smtp_host`
|
||||
|
||||
Hostname of the SMTP server.
|
||||
|
||||
- `smtp_port`
|
||||
|
||||
Port of the SMTP server.
|
||||
|
||||
- `smtp_user`
|
||||
|
||||
Username for the SMTP server.
|
||||
|
||||
- `smtp_password`
|
||||
|
||||
Password or app password for the SMTP server.
|
||||
|
||||
- `from_email`
|
||||
|
||||
Email address from which emails will be sent.
|
||||
|
||||
- `use_tls`
|
||||
|
||||
Set to `true` to use TLS for the SMTP connection. Default is `true`.
|
||||
|
||||
### Email Notifications (`[notifications.email_notifications]`)
|
||||
|
||||
- `enabled`
|
||||
|
||||
Set to `true` to enable email notifications. Default is `false`.
|
||||
|
||||
- `emails`
|
||||
|
||||
List of email addresses to send notifications to.
|
||||
|
||||
### Gotify Notifications (`[notifications.gotify]`)
|
||||
|
||||
- `enabled`
|
||||
|
||||
Set to `true` to enable Gotify notifications. Default is `false`.
|
||||
|
||||
- `api_key`
|
||||
|
||||
API key for Gotify.
|
||||
|
||||
- `url`
|
||||
|
||||
Base URL of your Gotify instance. Note the lack of a trailing slash.
|
||||
|
||||
### Ntfy Notifications (`[notifications.ntfy]`)
|
||||
|
||||
- `enabled`
|
||||
|
||||
Set to `true` to enable Ntfy notifications. Default is `false`.
|
||||
|
||||
- `url`
|
||||
|
||||
URL of your ntfy instance plus the topic.
|
||||
|
||||
### Pushover Notifications (`[notifications.pushover]`)
|
||||
|
||||
- `enabled`
|
||||
|
||||
Set to `true` to enable Pushover notifications. Default is `false`.
|
||||
|
||||
- `api_key`
|
||||
|
||||
API key for Pushover.
|
||||
|
||||
- `user`
|
||||
|
||||
User key for Pushover.
|
||||
|
||||
## Example Configuration
|
||||
|
||||
Here's a complete example of the notifications section in your `config.toml`:
|
||||
|
||||
```toml
|
||||
[notifications]
|
||||
# SMTP settings for email notifications and password resets
|
||||
[notifications.smtp_config]
|
||||
smtp_host = "smtp.gmail.com"
|
||||
smtp_port = 587
|
||||
smtp_user = "your-email@gmail.com"
|
||||
smtp_password = "your-app-password"
|
||||
from_email = "mediamanager@example.com"
|
||||
use_tls = true
|
||||
|
||||
# Email notification settings
|
||||
[notifications.email_notifications]
|
||||
enabled = true
|
||||
emails = ["admin@example.com", "notifications@example.com"]
|
||||
|
||||
# Gotify notification settings
|
||||
[notifications.gotify]
|
||||
enabled = true
|
||||
api_key = "your_gotify_api_key"
|
||||
url = "https://gotify.example.com"
|
||||
|
||||
# Ntfy notification settings
|
||||
[notifications.ntfy]
|
||||
enabled = false
|
||||
url = "https://ntfy.sh/your-private-topic"
|
||||
|
||||
# Pushover notification settings
|
||||
[notifications.pushover]
|
||||
enabled = false
|
||||
api_key = "your_pushover_api_key"
|
||||
user = "your_pushover_user_key"
|
||||
```
|
||||
|
||||
<note>
|
||||
You can enable multiple notification methods simultaneously. For example, you could have both email and Gotify notifications enabled at the same time.
|
||||
</note>
|
||||
55
Writerside/topics/Reverse-Proxy.md
Normal file
55
Writerside/topics/Reverse-Proxy.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Reverse Proxy
|
||||
|
||||
MediaManager is probably unlike any other service that you have deployed before, because it has a separate frontend and backend container.
|
||||
This means that setting up a reverse proxy is a bit different.
|
||||
|
||||
When deploying MediaManager, you have two choices:
|
||||
|
||||
- Use two different hostnames, e.g. `mediamanager.example.com` for the frontend and `api-mediamanager.example.com` for the backend.
|
||||
- Use a single hostname with a base path, e.g. `mediamanager.example.com` for the frontend and `mediamanager.example.com/api/v1` for the backend.
|
||||
|
||||
If you choose the first option, you can set up your reverse proxy as usual, forwarding requests to the appropriate containers based on the hostname.
|
||||
If you choose the second option, you need to ensure that your reverse proxy is configured to handle the base path correctly.
|
||||
|
||||
## Example Caddy Configuration
|
||||
|
||||
```
|
||||
mm.my-domain.com {
|
||||
@api path /api/*
|
||||
reverse_proxy @api 10.0.0.7:8000
|
||||
|
||||
reverse_proxy 10.0.0.7:3000
|
||||
}
|
||||
```
|
||||
|
||||
## Example Traefik Configuration
|
||||
This example assumes you use Traefik with Docker labels.
|
||||
|
||||
```yaml
|
||||
|
||||
services:
|
||||
backend:
|
||||
image: ghcr.io/maxdorninger/mediamanager/backend:latest
|
||||
ports:
|
||||
- "8000:8000"
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.mediamanager-backend.rule=Host(`media.example`)&&PathPrefix(`/api/v1`)"
|
||||
- "traefik.http.routers.mediamanager-backend.tls=true"
|
||||
- "traefik.http.routers.mediamanager-backend.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.routers.mediamanager-backend.entrypoints=websecure"
|
||||
environment:
|
||||
- MISC_FRONTEND_URL=https://media.example/
|
||||
frontend:
|
||||
image: ghcr.io/maxdorninger/mediamanager/frontend:latest
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.mediamanager-frontend.rule=Host(`media.example`)"
|
||||
- "traefik.http.routers.mediamanager-frontend.tls=true"
|
||||
- "traefik.http.routers.mediamanager-frontend.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.routers.mediamanager-frontend.entrypoints=websecure"
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- PUBLIC_API_URL=https://media.example/api/v1
|
||||
```
|
||||
@@ -1,80 +1,70 @@
|
||||
# Authentication
|
||||
|
||||
MediaManager supports multiple authentication methods. Email/password authentication is the default, but you can also
|
||||
enable OpenID Connect (OAuth 2.0) for integration with external identity providers.
|
||||
MediaManager supports multiple authentication methods. Email/password authentication is the default, but you can also enable OpenID Connect (OAuth 2.0) for integration with external identity providers.
|
||||
|
||||
All authentication settings are configured in the `[auth]` section of your `config.toml` file.
|
||||
|
||||
## General Authentication Settings (`[auth]`)
|
||||
|
||||
- `token_secret`
|
||||
|
||||
Strong secret key for signing JWTs (create with `openssl rand -hex 32`). This is a required field.
|
||||
|
||||
- `session_lifetime`
|
||||
|
||||
Lifetime of user sessions in seconds. Default is `86400` (1 day).
|
||||
|
||||
- `admin_emails`
|
||||
|
||||
A list of email addresses for administrator accounts. This is a required field.
|
||||
|
||||
- `email_password_resets`
|
||||
|
||||
Toggle for enabling password resets via email. If users request a password reset because they forgot their password, they will be sent an email with a link to reset it. Default is `false`.
|
||||
|
||||
<note>
|
||||
Note the lack of a trailing slash in some env vars like OPENID_CONFIGURATION_ENDPOINT. This is important.
|
||||
To use email password resets, you must also configure SMTP settings in the <code>[notifications.smtp_config]</code> section.
|
||||
</note>
|
||||
|
||||
## General Authentication Settings
|
||||
## OpenID Connect Settings (`[auth.openid_connect]`)
|
||||
|
||||
### `AUTH_TOKEN_SECRET`
|
||||
OpenID Connect allows you to integrate with external identity providers like Google, Microsoft Azure AD, Keycloak, or any other OIDC-compliant provider.
|
||||
|
||||
Strong secret key for signing JWTs (create with `openssl rand -hex 32`). This is a required field. Example:
|
||||
`AUTH_TOKEN_SECRET=your_super_secret_key`.
|
||||
- `enabled`
|
||||
|
||||
### `AUTH_SESSION_LIFETIME`
|
||||
Set to `true` to enable OpenID Connect authentication. Default is `false`.
|
||||
|
||||
Lifetime of user sessions in seconds. Default is `86400` (1 day). Example: `AUTH_SESSION_LIFETIME=604800` (1 week).
|
||||
- `client_id`
|
||||
|
||||
### `AUTH_ADMIN_EMAIL`
|
||||
Client ID provided by your OpenID Connect provider.
|
||||
|
||||
A list of email addresses for administrator accounts. This is a required field. Example:
|
||||
`AUTH_ADMIN_EMAIL=admin@example.com`.
|
||||
- `client_secret`
|
||||
|
||||
### `FRONTEND_URL`
|
||||
Client secret provided by your OpenID Connect provider.
|
||||
|
||||
The URL the frontend will be accessed from. This is a required field. Example: `https://mediamanager.example/`.
|
||||
- `configuration_endpoint`
|
||||
|
||||
### `AUTH_EMAIL_PASSWORD_RESETS`
|
||||
OpenID Connect configuration endpoint URL. Note the lack of a trailing slash - this is important.
|
||||
|
||||
Toggle for enabling password resets via email. If users request a password reset in case they forgot their password,
|
||||
they will be sent an email with a link to reset it. Default is `FALSE`.
|
||||
- `name`
|
||||
|
||||
<note>
|
||||
On login/registration, every user whose email is in <code>AUTH_ADMIN_EMAIL</code> will be granted admin privileges.
|
||||
Users whose email is not in <code>AUTH_ADMIN_EMAIL</code> will be regular users and will need to be verified by an administrator,
|
||||
this can be done in the settings page.
|
||||
</note>
|
||||
<tip>
|
||||
<include from="notes.topic" element-id="list-format"/>
|
||||
</tip>
|
||||
Display name for the OpenID Connect provider that will be shown on the login page.
|
||||
|
||||
## OpenID Connect (OAuth 2.0)
|
||||
## Example Configuration
|
||||
|
||||
### `OPENID_ENABLED`
|
||||
Here's a complete example of the authentication section in your `config.toml`:
|
||||
|
||||
Enables OpenID authentication. Default is `FALSE`. Example: `TRUE`.
|
||||
|
||||
### `OPENID_CLIENT_ID`
|
||||
|
||||
Client ID from your OpenID provider.
|
||||
|
||||
### `OPENID_CLIENT_SECRET`
|
||||
|
||||
Client Secret from your OpenID provider.
|
||||
|
||||
### `OPENID_CONFIGURATION_ENDPOINT`
|
||||
|
||||
URL of your OpenID provider's discovery document (e.g., `.../.well-known/openid-configuration`). Example:
|
||||
`https://authentik.example.com/application/o/mediamanager/.well-known/openid-configuration`.
|
||||
|
||||
### `OPENID_NAME`
|
||||
|
||||
Display name for this OpenID provider. Default is `OpenID`. Example: `Authentik`.
|
||||
|
||||
### Configuring OpenID Connect
|
||||
|
||||
1. Set `OPENID_ENABLED=TRUE`
|
||||
2. Configure the following environment variables:
|
||||
* `OPENID_CLIENT_ID`
|
||||
* `OPENID_CLIENT_SECRET`
|
||||
* `OPENID_CONFIGURATION_ENDPOINT`
|
||||
* `OPENID_NAME` (optional)
|
||||
* `FRONTEND_URL` (it is important that this is set correctly, as it is used for the redirect URIs)
|
||||
3. Your OpenID server will likely want a redirect URI. This URL will be like:
|
||||
`{FRONTEND_URL}/api/v1/auth/cookie/{OPENID_NAME}/callback`. The exact path depends on the `OPENID_NAME`.
|
||||
|
||||
4. Example URL: `https://mediamanager.example/api/v1/auth/cookie/Authentik/callback`
|
||||
```toml
|
||||
[auth]
|
||||
token_secret = "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6"
|
||||
session_lifetime = 604800 # 1 week
|
||||
admin_emails = ["admin@example.com", "manager@example.com"]
|
||||
email_password_resets = true
|
||||
|
||||
[auth.openid_connect]
|
||||
enabled = true
|
||||
client_id = "mediamanager-client"
|
||||
client_secret = "your-secret-key-here"
|
||||
configuration_endpoint = "https://auth.example.com/.well-known/openid-configuration"
|
||||
name = "Authentik"
|
||||
```
|
||||
|
||||
@@ -1,220 +1,43 @@
|
||||
# Backend
|
||||
These settings configure the core backend application through the `config.toml` file. All backend configuration is now centralized in this TOML file instead of environment variables.
|
||||
|
||||
These variables configure the core backend application, database connections, authentication, and integrations.
|
||||
## General Settings (`[misc]`)
|
||||
|
||||
- `frontend_url`
|
||||
|
||||
The URL the frontend will be accessed from. This is a required field and must include the trailing slash.
|
||||
|
||||
- `cors_urls`
|
||||
|
||||
A list of origins you are going to access the API from. Note the lack of trailing slashes.
|
||||
|
||||
- `api_base_path`
|
||||
|
||||
The URL base path of the backend API. Default is `/api/v1`. Note the lack of a trailing slash.
|
||||
|
||||
- `development`
|
||||
|
||||
Set to `true` to enable development mode. Default is `false`.
|
||||
|
||||
## Example Configuration
|
||||
|
||||
Here's a complete example of the general settings section in your `config.toml`:
|
||||
|
||||
```toml
|
||||
[misc]
|
||||
# REQUIRED: Change this to match your actual frontend URL
|
||||
frontend_url = "http://localhost:3000/"
|
||||
|
||||
# REQUIRED: List all origins that will access the API
|
||||
cors_urls = ["http://localhost:3000", "http://localhost:8000"]
|
||||
|
||||
# Optional: API base path (rarely needs to be changed)
|
||||
api_base_path = "/api/v1"
|
||||
|
||||
# Optional: Development mode (set to true for debugging)
|
||||
development = false
|
||||
```
|
||||
|
||||
<note>
|
||||
<include from="notes.topic" element-id="list-format"/>
|
||||
The <code>frontend_url</code> and <code>cors_urls</code> are the most important settings to configure correctly. Make sure they match your actual deployment URLs.
|
||||
</note>
|
||||
|
||||
## General Settings
|
||||
|
||||
### `API_BASE_PATH`
|
||||
|
||||
The url base of the backend. Default is `/api/v1`.
|
||||
|
||||
### `CORS_URLS`
|
||||
|
||||
Enter a list of origins you are going to access the api from. Example: `["https://mm.example"]`.
|
||||
|
||||
## Database Settings
|
||||
|
||||
### `DB_HOST`
|
||||
|
||||
Hostname or IP of the PostgreSQL server. Default is `localhost`.
|
||||
|
||||
### `DB_PORT`
|
||||
|
||||
Port number of the PostgreSQL server. Default is `5432`.
|
||||
|
||||
### `DB_USER`
|
||||
|
||||
Username for PostgreSQL connection. Default is `MediaManager`.
|
||||
|
||||
### `DB_PASSWORD`
|
||||
|
||||
Password for the PostgreSQL user. Default is `MediaManager`.
|
||||
|
||||
### `DB_DBNAME`
|
||||
|
||||
Name of the PostgreSQL database. Default is `MediaManager`.
|
||||
|
||||
## Download Client Settings
|
||||
|
||||
Currently, only qBittorrent is supported as a download client. But support for other clients isn't unlikely in the
|
||||
future.
|
||||
|
||||
### `QBITTORRENT_HOST`
|
||||
|
||||
Host of the QBittorrent API. Default is `localhost`. Example: `qbit.example.com`.
|
||||
|
||||
### `QBITTORRENT_PORT`
|
||||
|
||||
Port of the QBittorrent API. Default is `8080`. Example: `443`.
|
||||
|
||||
### `QBITTORRENT_USER`
|
||||
|
||||
Username for QBittorrent. Default is `admin`.
|
||||
|
||||
### `QBITTORRENT_PASSWORD`
|
||||
|
||||
Password for QBittorrent. Default is `admin`.
|
||||
|
||||
## Metadata Provider Settings
|
||||
|
||||
<note>
|
||||
Note the lack of a trailing slash in some env vars like <code>TMDB_RELAY_URL</code>. This is important.
|
||||
</note>
|
||||
|
||||
|
||||
|
||||
These settings configure the integrations with external metadata providers like The Movie Database (TMDB) and The TVDB.
|
||||
|
||||
### TMDB (The Movie Database)
|
||||
|
||||
TMDB is the primary metadata provider for MediaManager. It provides detailed information about movies and TV shows.
|
||||
|
||||
|
||||
<tip>
|
||||
Other software like Jellyfin use TMDB as well, so there won't be any metadata discrepancies.
|
||||
</tip>
|
||||
|
||||
#### `TMDB_RELAY_URL`
|
||||
|
||||
If you want use your own TMDB relay service, set this to the URL of your own MetadataRelay. Otherwise, don't set it to
|
||||
use the default relay.
|
||||
|
||||
Default: `https://metadata-relay.maxid.me/tmdb`.
|
||||
|
||||
### TVDB (The TVDB)
|
||||
|
||||
<warning>
|
||||
The TVDB might provide false metadata, also it doesn't support some features of MediaManager like to show overviews, therfore TMDB is the preferred metadata provider.
|
||||
</warning>
|
||||
|
||||
#### `TVDB_RELAY_URL`
|
||||
|
||||
If you want use your own TVDB relay service, set this to the URL of your own MetadataRelay. Otherwise, don't set it to
|
||||
use the default relay.
|
||||
|
||||
Default: `https://metadata-relay.maxid.me/tvdb`.
|
||||
|
||||
### MetadataRelay
|
||||
|
||||
<note>
|
||||
To use MediaManager <strong>you don't need to set up your own MetadataRelay</strong>, as the default relay which is hosted by me, the dev of MediaManager, should be sufficient for most purposes.
|
||||
</note>
|
||||
|
||||
The MetadataRelay is a service that provides metadata for MediaManager. It acts as a proxy for TMDB and TVDB, allowing
|
||||
you to use your own API keys, but not strictly needing your own because only me, the developer, needs to create accounts
|
||||
for API keys.
|
||||
You might want to use it if you want to avoid rate limits, to protect your privacy, or other reasons.
|
||||
If you know Sonarr's Skyhook, this is similar to that.
|
||||
|
||||
#### Where to get API keys
|
||||
|
||||
Get an API key from [The Movie Database](https://www.themoviedb.org/settings/api). You can create
|
||||
an account and generate a free API key in your account settings.
|
||||
|
||||
Get an API key from [The TVDB](https://thetvdb.com/auth/register). You can create an account and
|
||||
generate a free API key in your account settings.
|
||||
|
||||
<tip>
|
||||
If you want to use your own MetadataRelay, you can set the <code>TMDB_RELAY_URL</code> and/or <code>TVDB_RELAY_URL</code> to your own relay service.
|
||||
</tip>
|
||||
|
||||
## Directory Settings
|
||||
|
||||
<note>
|
||||
Normally you don't need to change these, as the default mountpoints are usually sufficient. In your <code>docker-compose.yaml</code>, you can just mount <code>/any/directory</code> to <code>/data/torrents</code>.
|
||||
</note>
|
||||
|
||||
### `IMAGE_DIRECTORY`
|
||||
|
||||
Media images (posters, backdrops) will be stored here. Default is `/data/images`.
|
||||
|
||||
### `TV_DIRECTORY`
|
||||
|
||||
Location of TV show files. Default is `/data/tv`.
|
||||
|
||||
### `MOVIE_DIRECTORY`
|
||||
|
||||
Location of movie files. Default is `/data/movies`.
|
||||
|
||||
### `TORRENT_DIRECTORY`
|
||||
|
||||
Location of torrent files and downloads. Default is `/data/torrents`.
|
||||
|
||||
## Email Settings
|
||||
|
||||
For sending emails, MediaManager uses the SMTP protocol. You can use any SMTP server, like Gmail or SMTP2GO.
|
||||
|
||||
### `EMAIL_SMTP_HOST`
|
||||
|
||||
Hostname of the SMTP server.
|
||||
|
||||
### `EMAIL_SMTP_PORT`
|
||||
|
||||
Port of the SMTP server.
|
||||
|
||||
### `EMAIL_SMTP_USER`
|
||||
|
||||
Username for the SMTP server.
|
||||
|
||||
### `EMAIL_SMTP_PASSWORD`
|
||||
|
||||
Password for the SMTP server.
|
||||
|
||||
### `EMAIL_FROM_EMAIL`
|
||||
|
||||
Email address from which emails will be sent.
|
||||
|
||||
### `EMAIL_USE_TLS`
|
||||
|
||||
Set to `True` to use TLS for the SMTP connection. Default is `False`.
|
||||
For secure connections, use TLS.
|
||||
|
||||
## Notification Settings
|
||||
|
||||
MediaManager can send Notifications via email, ntfy.sh, Pushover and Gotify. You can enable any of these.
|
||||
To enable a notification method, set the corresponding environment variables.
|
||||
|
||||
### Email
|
||||
|
||||
#### `NOTIFICATION_EMAIL`
|
||||
|
||||
If set notifications will be sent via email to this email address.
|
||||
Example: `notifications@example.com`.
|
||||
|
||||
### Gotify
|
||||
|
||||
#### `NOTIFICATION_GOTIFY_API_KEY`
|
||||
|
||||
API key for Gotify.
|
||||
|
||||
#### `NOTIFICATION_GOTIFY_URL`
|
||||
|
||||
Base URL of your Gotify instance. Example: `https://gotify.example.com`.
|
||||
Note the lack of a trailing slash.
|
||||
|
||||
### Ntfy
|
||||
|
||||
#### `NOTIFICATION_NTFY_URL`
|
||||
|
||||
URL of your ntfy instance + the topic. Example `https://ntfy.sh/your-topic`.
|
||||
|
||||
### Pushover
|
||||
|
||||
#### `NOTIFICATION_PUSHOVER_API_KEY`
|
||||
|
||||
API key for Pushover.
|
||||
|
||||
#### `NOTIFICATION_PUSHOVER_USER`
|
||||
|
||||
Username for Pushover.
|
||||
|
||||
## Build Arguments (Dockerfile)
|
||||
|
||||
### `VERSION`
|
||||
|
||||
Labels the Docker image with a version. Passed during build (e.g., by GitHub Actions). Frontend uses this as
|
||||
`PUBLIC_VERSION`. Example (in build command): `docker build --build-arg VERSION=1.2.3 .`
|
||||
|
||||
@@ -11,13 +11,15 @@ You (the browser) must reach the backend from this url. Default is `http://local
|
||||
|
||||
**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.
|
||||
Unfortunately you need to build the frontend docker container, to configure a url base path for the frontend. This is because
|
||||
SvelteKit needs to know the base path at build time.
|
||||
|
||||
### `BASE_URL`
|
||||
|
||||
Sets the base url path, it must begin with a slash and not end with one. Example (in build command):
|
||||
`docker build --build-arg BASE_URL=/media -f web/Dockerfile .`
|
||||
- clone the repo
|
||||
- cd into the repo's root directory
|
||||
- `docker build --build-arg BASE_URL=/media -f web/Dockerfile .`
|
||||
|
||||
### `VERSION`
|
||||
|
||||
|
||||
42
Writerside/topics/database-configuration.md
Normal file
42
Writerside/topics/database-configuration.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Database
|
||||
|
||||
Database settings are configured in the `[database]` section of your `config.toml` file. MediaManager uses PostgreSQL as its database backend.
|
||||
|
||||
## Database Settings (`[database]`)
|
||||
|
||||
- `host`
|
||||
|
||||
Hostname or IP of the PostgreSQL server. Default is `localhost`.
|
||||
|
||||
- `port`
|
||||
|
||||
Port number of the PostgreSQL server. Default is `5432`.
|
||||
|
||||
- `user`
|
||||
|
||||
Username for PostgreSQL connection. Default is `MediaManager`.
|
||||
|
||||
- `password`
|
||||
|
||||
Password for the PostgreSQL user. Default is `MediaManager`.
|
||||
|
||||
- `dbname`
|
||||
|
||||
Name of the PostgreSQL database. Default is `MediaManager`.
|
||||
|
||||
## Example Configuration
|
||||
|
||||
Here's a complete example of the database section in your `config.toml`:
|
||||
|
||||
```toml
|
||||
[database]
|
||||
host = "db"
|
||||
port = 5432
|
||||
user = "MediaManager"
|
||||
password = "your_secure_password"
|
||||
dbname = "MediaManager"
|
||||
```
|
||||
|
||||
<tip>
|
||||
In docker-compose deployments the containers name is simultaneously its hostname, so you can use "db" or "postgres" as host.
|
||||
</tip>
|
||||
109
Writerside/topics/download-client-configuration.md
Normal file
109
Writerside/topics/download-client-configuration.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Download Clients
|
||||
|
||||
Download client settings are configured in the `[torrents]` section of your `config.toml` file. MediaManager supports both qBittorrent and SABnzbd as download clients.
|
||||
|
||||
## qBittorrent Settings (`[torrents.qbittorrent]`)
|
||||
|
||||
qBittorrent is a popular BitTorrent client that MediaManager can integrate with for downloading torrents.
|
||||
|
||||
- `enabled`
|
||||
|
||||
Set to `true` to enable qBittorrent integration. Default is `false`.
|
||||
|
||||
- `host`
|
||||
|
||||
Hostname or IP of the qBittorrent server. Include the protocol (http/https).
|
||||
|
||||
- `port`
|
||||
|
||||
Port of the qBittorrent Web UI/API. Default is `8080`.
|
||||
|
||||
- `username`
|
||||
|
||||
Username for qBittorrent Web UI authentication. Default is `admin`.
|
||||
|
||||
- `password`
|
||||
|
||||
Password for qBittorrent Web UI authentication. Default is `admin`.
|
||||
|
||||
## SABnzbd Settings (`[torrents.sabnzbd]`)
|
||||
|
||||
SABnzbd is a Usenet newsreader that MediaManager can integrate with for downloading NZB files.
|
||||
|
||||
- `enabled`
|
||||
|
||||
Set to `true` to enable SABnzbd integration. Default is `false`.
|
||||
|
||||
- `host`
|
||||
|
||||
Hostname or IP of the SABnzbd server.
|
||||
|
||||
- `port`
|
||||
|
||||
Port of the SABnzbd API. Default is `8080`.
|
||||
|
||||
- `api_key`
|
||||
|
||||
API key for SABnzbd. You can find this in SABnzbd's configuration under "General" → "API Key".
|
||||
|
||||
## Example Configuration
|
||||
|
||||
Here's a complete example of the download clients section in your `config.toml`:
|
||||
|
||||
```toml
|
||||
[torrents]
|
||||
# qBittorrent configuration
|
||||
[torrents.qbittorrent]
|
||||
enabled = true
|
||||
host = "http://qbittorrent"
|
||||
port = 8080
|
||||
username = "admin"
|
||||
password = "your_secure_password"
|
||||
|
||||
# SABnzbd configuration
|
||||
[torrents.sabnzbd]
|
||||
enabled = false
|
||||
host = "sabnzbd"
|
||||
port = 8080
|
||||
api_key = "your_sabnzbd_api_key"
|
||||
```
|
||||
|
||||
## Docker Compose Integration
|
||||
|
||||
When using Docker Compose, make sure your download clients are accessible from the MediaManager backend:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
# MediaManager backend
|
||||
backend:
|
||||
image: ghcr.io/maxdorninger/mediamanager/backend:latest
|
||||
# ... other configuration ...
|
||||
|
||||
# qBittorrent service
|
||||
qbittorrent:
|
||||
image: lscr.io/linuxserver/qbittorrent:latest
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
- WEBUI_PORT=8080
|
||||
volumes:
|
||||
- ./data/torrents:/downloads
|
||||
# ... other configuration ...
|
||||
|
||||
# SABnzbd service (optional)
|
||||
sabnzbd:
|
||||
image: lscr.io/linuxserver/sabnzbd:latest
|
||||
ports:
|
||||
- "8081:8080"
|
||||
volumes:
|
||||
- ./data/usenet:/downloads
|
||||
# ... other configuration ...
|
||||
```
|
||||
|
||||
<note>
|
||||
You can enable both qBittorrent and SABnzbd simultaneously if you want to support both BitTorrent and Usenet downloads.
|
||||
</note>
|
||||
|
||||
<tip>
|
||||
Make sure the download directories in your download clients are accessible to MediaManager for proper file management and organization.
|
||||
</tip>
|
||||
69
Writerside/topics/metadata-provider-configuration.md
Normal file
69
Writerside/topics/metadata-provider-configuration.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Metadata Provider Configuration
|
||||
|
||||
Metadata provider settings are configured in the `[metadata]` section of your `config.toml` file. These settings control how MediaManager retrieves information about movies and TV shows.
|
||||
|
||||
## TMDB Settings (`[metadata.tmdb]`)
|
||||
|
||||
TMDB (The Movie Database) is the primary metadata provider for MediaManager. It provides detailed information about movies and TV shows.
|
||||
|
||||
<tip>
|
||||
Other software like Jellyfin use TMDB as well, so there won't be any metadata discrepancies.
|
||||
</tip>
|
||||
|
||||
### `tmdb_relay_url`
|
||||
|
||||
If you want to use your own TMDB relay service, set this to the URL of your own MetadataRelay. Otherwise, use the default relay.
|
||||
|
||||
- **Default:** `https://metadata-relay.maxid.me/tmdb`
|
||||
- **Example:** `https://your-own-relay.example.com/tmdb`
|
||||
|
||||
## TVDB Settings (`[metadata.tvdb]`)
|
||||
|
||||
<warning>
|
||||
The TVDB might provide false metadata and doesn't support some features of MediaManager like showing overviews. Therefore, TMDB is the preferred metadata provider.
|
||||
</warning>
|
||||
|
||||
### `tvdb_relay_url`
|
||||
|
||||
If you want to use your own TVDB relay service, set this to the URL of your own MetadataRelay. Otherwise, use the default relay.
|
||||
|
||||
- **Default:** `https://metadata-relay.maxid.me/tvdb`
|
||||
- **Example:** `https://your-own-relay.example.com/tvdb`
|
||||
|
||||
## MetadataRelay
|
||||
|
||||
<note>
|
||||
To use MediaManager <strong>you don't need to set up your own MetadataRelay</strong>, as the default relay hosted by the developer should be sufficient for most purposes.
|
||||
</note>
|
||||
|
||||
The MetadataRelay is a service that provides metadata for MediaManager. It acts as a proxy for TMDB and TVDB, allowing you to use your own API keys if needed, but the default relay means you don't need to create accounts for API keys yourself.
|
||||
|
||||
You might want to use your own relay if you want to avoid rate limits, protect your privacy, or for other reasons. If you know Sonarr's Skyhook, this is similar to that.
|
||||
|
||||
### Where to get API keys
|
||||
|
||||
- Get a TMDB API key from [The Movie Database](https://www.themoviedb.org/settings/api)
|
||||
- Get a TVDB API key from [The TVDB](https://thetvdb.com/auth/register)
|
||||
|
||||
<tip>
|
||||
If you want to use your own MetadataRelay, you can set the <code>tmdb_relay_url</code> and/or <code>tvdb_relay_url</code> to your own relay service.
|
||||
</tip>
|
||||
|
||||
## Example Configuration
|
||||
|
||||
Here's a complete example of the metadata section in your `config.toml`:
|
||||
|
||||
```toml
|
||||
[metadata]
|
||||
# TMDB configuration
|
||||
[metadata.tmdb]
|
||||
tmdb_relay_url = "https://metadata-relay.maxid.me/tmdb"
|
||||
|
||||
# TVDB configuration
|
||||
[metadata.tvdb]
|
||||
tvdb_relay_url = "https://metadata-relay.maxid.me/tvdb"
|
||||
```
|
||||
|
||||
<note>
|
||||
In most cases, you can simply use the default values and don't need to specify these settings in your config file at all.
|
||||
</note>
|
||||
@@ -13,12 +13,12 @@
|
||||
* 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`.
|
||||
* Check if your reverse proxy is correctly configured, see [Reverse Proxy Configuration](Reverse-Proxy.md) for examples.
|
||||
|
||||
## Data Not Appearing / File Issues
|
||||
|
||||
@@ -26,3 +26,4 @@
|
||||
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).
|
||||
* For hardlinks to work, you must not use different docker volumes for TV, Torrents, etc.
|
||||
104
config.toml
Normal file
104
config.toml
Normal file
@@ -0,0 +1,104 @@
|
||||
# MediaManager Complete Configuration File
|
||||
# This file contains all available configuration options for MediaManager
|
||||
# Documentation: https://maxdorninger.github.io/MediaManager/introduction.html
|
||||
|
||||
[misc]
|
||||
# it's very likely that you need to change this for MediaManager to work
|
||||
frontend_url = "http://localhost:3000/" # note the trailing slash
|
||||
cors_urls = ["http://localhost:3000", "http://localhost:8000"] # note the lack of a trailing slash
|
||||
|
||||
# you probaly don't need to change this
|
||||
api_base_path = "/api/v1"
|
||||
development = false
|
||||
|
||||
[database]
|
||||
host = "db"
|
||||
port = 5432
|
||||
user = "MediaManager"
|
||||
password = "MediaManager"
|
||||
dbname = "MediaManager"
|
||||
|
||||
[auth]
|
||||
email_password_resets = false # if true, you also need to set up SMTP (notifications.smtp_config)
|
||||
|
||||
token_secret = "" # generate a random string with "openssl rand -hex 32", e.g. here https://www.cryptool.org/en/cto/openssl/
|
||||
session_lifetime = 86400 # this is how long you will be logged in after loggin in, in seconds
|
||||
admin_emails = ["admin@example.com", "admin2@example.com"]
|
||||
|
||||
# OpenID Connect settings
|
||||
[auth.openid_connect]
|
||||
enabled = false
|
||||
client_id = ""
|
||||
client_secret = ""
|
||||
configuration_endpoint = "https://openid.example.com/.well-known/openid-configuration"
|
||||
name = "OpenID"
|
||||
|
||||
[notifications]
|
||||
# SMTP settings for email notifications and email password resets
|
||||
[notifications.smtp_config]
|
||||
smtp_host = "smtp.example.com"
|
||||
smtp_port = 587
|
||||
smtp_user = "admin"
|
||||
smtp_password = "admin"
|
||||
from_email = "mediamanager@example.com"
|
||||
use_tls = true
|
||||
|
||||
# Email notification settings
|
||||
[notifications.email_notifications]
|
||||
enabled = false
|
||||
emails = ["admin@example.com", "admin2@example.com"] # List of email addresses to send notifications to
|
||||
|
||||
# Gotify notification settings
|
||||
[notifications.gotify]
|
||||
enabled = false
|
||||
api_key = ""
|
||||
url = "https://gotify.example.com"
|
||||
|
||||
# Ntfy notification settings
|
||||
[notifications.ntfy]
|
||||
enabled = false
|
||||
url = "https://ntfy.sh/your-topic"
|
||||
|
||||
# Pushover notification settings
|
||||
[notifications.pushover]
|
||||
enabled = false
|
||||
api_key = ""
|
||||
user = ""
|
||||
|
||||
[torrents]
|
||||
# qBittorrent settings
|
||||
[torrents.qbittorrent]
|
||||
enabled = false
|
||||
host = "http://localhost"
|
||||
port = 8080
|
||||
username = "admin"
|
||||
password = "admin"
|
||||
|
||||
# SABnzbd settings
|
||||
[torrents.sabnzbd]
|
||||
enabled = false
|
||||
host = "localhost"
|
||||
port = 8080
|
||||
api_key = ""
|
||||
|
||||
[indexers]
|
||||
# Prowlarr settings
|
||||
[indexers.prowlarr]
|
||||
enabled = false
|
||||
url = "http://localhost:9696"
|
||||
api_key = ""
|
||||
|
||||
# Jackett settings
|
||||
[indexers.jackett]
|
||||
enabled = false
|
||||
url = "http://localhost:9117"
|
||||
api_key = ""
|
||||
indexers = ["1337x"] # List of indexer names to use
|
||||
|
||||
# its very unlikely that you need to change this
|
||||
[metadata]
|
||||
[metadata.tmdb]
|
||||
tmdb_relay_url = "https://metadata-relay.maxid.me/tmdb"
|
||||
|
||||
[metadata.tvdb]
|
||||
tvdb_relay_url = "https://metadata-relay.maxid.me/tvdb"
|
||||
@@ -4,71 +4,10 @@ services:
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
- QBITTORRENT_PASSWORD=
|
||||
- QBITTORRENT_HOST=
|
||||
- QBITTORRENT_USERNAME=
|
||||
- QBITTORRENT_PORT=
|
||||
|
||||
- CORS_URLS=["http://localhost:3000","https://mm.example.com"]
|
||||
|
||||
- DB_HOST=db
|
||||
#- DB_NAME=
|
||||
#- DB_PORT=
|
||||
#- DB_PASSWORD=
|
||||
#- DB_DBNAME=
|
||||
|
||||
#- PROWLARR_ENABLED=
|
||||
#- PROWLARR_URL=http://prowlarr:9696
|
||||
#- PROWLARR_API_KEY=
|
||||
|
||||
#- JACKETT_ENABLED=
|
||||
#- JACKETT_URL=http://jackett:9117
|
||||
#- JACKETT_API_KEY=
|
||||
|
||||
# example indexers, you can also just use ["all"] to use all indexers configured in Jackett
|
||||
#- JACKETT_INDEXERS=["1337x","Rarbg"]
|
||||
|
||||
# generate a random string with "openssl rand -hex 32"
|
||||
- AUTH_TOKEN_SECRET=
|
||||
|
||||
# this should be you email address
|
||||
- AUTH_ADMIN_EMAIL=["admin1@example.com","admin2@example.com"]
|
||||
|
||||
# if you forget your password you can request a link and get it via email, you must have the email settings configured for this to work obviously
|
||||
#- AUTH_EMAIL_PASSWORD_RESETS=TRUE
|
||||
|
||||
# this is the URL of your frontend, e.g. https://mediamanager.example.com
|
||||
- FRONTEND_URL=
|
||||
|
||||
#- EMAIL_SMTP_HOST=
|
||||
#- EMAIL_SMTP_PORT=
|
||||
#- EMAIL_SMTP_USER=
|
||||
#- EMAIL_SMTP_PASSWORD=
|
||||
#- EMAIL_FROM_EMAIL=
|
||||
#- EMAIL_USE_TLS=TRUE
|
||||
|
||||
#- NOTIFICATION_EMAIL=notify@example.com
|
||||
|
||||
#- NOTIFICATION_GOTIFY_API_KEY=
|
||||
#- NOTIFICATION_GOTIFY_URL=https://gotify.example.com
|
||||
|
||||
#- NOTIFICATION_PUSHOVER_API_KEY=
|
||||
#- NOTIFICATION_PUSHOVER_USER=
|
||||
|
||||
#- NOTIFICATION_NTFY_URL=https://ntfy.sh/your-topic
|
||||
|
||||
#- OPENID_ENABLED=FALSE
|
||||
#- OPENID_CLIENT_ID=
|
||||
#- OPENID_CLIENT_SECRET=
|
||||
#- OPENID_CONFIGURATION_ENDPOINT=
|
||||
#- OPENID_NAME=
|
||||
|
||||
#- AUTH_SESSION_LIFETIME=
|
||||
#- API_BASE_PATH=/api/v1
|
||||
#- DEVELOPMENT=
|
||||
|
||||
- CONFIG_FILE=/data/config.toml
|
||||
volumes:
|
||||
- ./data/:/data/
|
||||
- ./config.toml:/data/config.toml
|
||||
frontend:
|
||||
image: ghcr.io/maxdorninger/mediamanager/frontend:latest
|
||||
ports:
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from pydantic_settings import BaseSettings
|
||||
from pydantic import Field
|
||||
import secrets
|
||||
|
||||
|
||||
class OpenIdConfig(BaseSettings):
|
||||
client_id: str = ""
|
||||
client_secret: str = ""
|
||||
configuration_endpoint: str = ""
|
||||
name: str = "OpenID"
|
||||
enabled: bool = False
|
||||
|
||||
|
||||
class AuthConfig(BaseSettings):
|
||||
# to get a signing key run:
|
||||
# openssl rand -hex 32
|
||||
model_config = SettingsConfigDict(env_prefix="AUTH_")
|
||||
token_secret: str = Field(default_factory=secrets.token_hex)
|
||||
session_lifetime: int = 60 * 60 * 24
|
||||
admin_email: list[str] = []
|
||||
admin_emails: list[str] = []
|
||||
email_password_resets: bool = False
|
||||
openid_connect: OpenIdConfig
|
||||
|
||||
@property
|
||||
def jwt_signing_key(self):
|
||||
return self._jwt_signing_key
|
||||
|
||||
|
||||
class OpenIdConfig(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="OPENID_")
|
||||
client_id: str
|
||||
client_secret: str
|
||||
configuration_endpoint: str
|
||||
name: str = "OpenID"
|
||||
|
||||
@@ -2,18 +2,15 @@ from fastapi import APIRouter, Depends
|
||||
from fastapi import status
|
||||
from sqlalchemy import select
|
||||
|
||||
from media_manager.auth.config import OpenIdConfig
|
||||
from media_manager.config import AllEncompassingConfig
|
||||
from media_manager.auth.db import User
|
||||
from media_manager.auth.schemas import UserRead
|
||||
from media_manager.auth.users import current_superuser
|
||||
from media_manager.database import DbSessionDependency
|
||||
from media_manager.auth.users import openid_client
|
||||
|
||||
users_router = APIRouter()
|
||||
auth_metadata_router = APIRouter()
|
||||
openid_enabled = openid_client is not None
|
||||
if openid_enabled:
|
||||
oauth_config = OpenIdConfig()
|
||||
oauth_config = AllEncompassingConfig().auth.openid_connect
|
||||
|
||||
|
||||
@users_router.get(
|
||||
@@ -29,7 +26,7 @@ def get_all_users(db: DbSessionDependency) -> list[UserRead]:
|
||||
|
||||
@auth_metadata_router.get("/auth/metadata", status_code=status.HTTP_200_OK)
|
||||
def get_auth_metadata() -> dict:
|
||||
if openid_enabled:
|
||||
if oauth_config.enabled:
|
||||
return {
|
||||
"oauth_name": oauth_config.name,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
@@ -17,23 +16,18 @@ from fastapi.responses import RedirectResponse, Response
|
||||
from starlette import status
|
||||
|
||||
import media_manager.notification.utils
|
||||
from media_manager.auth.config import AuthConfig, OpenIdConfig
|
||||
from media_manager.auth.db import User, get_user_db
|
||||
from media_manager.auth.schemas import UserUpdate
|
||||
from media_manager.config import BasicConfig
|
||||
|
||||
from media_manager.config import AllEncompassingConfig
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
config = AuthConfig()
|
||||
config = AllEncompassingConfig().auth
|
||||
SECRET = config.token_secret
|
||||
LIFETIME = config.session_lifetime
|
||||
|
||||
if (
|
||||
os.getenv("OPENID_ENABLED") is not None
|
||||
and os.getenv("OPENID_ENABLED").upper() == "TRUE"
|
||||
):
|
||||
openid_config = OpenIdConfig()
|
||||
if config.openid_connect.enabled:
|
||||
openid_config = AllEncompassingConfig().auth.openid_connect
|
||||
openid_client = OpenID(
|
||||
base_scopes=["openid", "email", "profile"],
|
||||
client_id=openid_config.client_id,
|
||||
@@ -52,14 +46,14 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
||||
|
||||
async def on_after_register(self, user: User, request: Optional[Request] = None):
|
||||
log.info(f"User {user.id} has registered.")
|
||||
if user.email in config.admin_email:
|
||||
if user.email in config.admin_emails:
|
||||
updated_user = UserUpdate(is_superuser=True, is_verified=True)
|
||||
await self.update(user=user, user_update=updated_user)
|
||||
|
||||
async def on_after_forgot_password(
|
||||
self, user: User, token: str, request: Optional[Request] = None
|
||||
):
|
||||
link = f"{BasicConfig().FRONTEND_URL}login/reset-password?token={token}"
|
||||
link = f"{AllEncompassingConfig().misc.frontend_url}login/reset-password?token={token}"
|
||||
log.info(f"User {user.id} has forgot their password. Reset Link: {link}")
|
||||
|
||||
if not config.email_password_resets:
|
||||
@@ -115,7 +109,7 @@ def get_jwt_strategy() -> JWTStrategy[models.UP, models.ID]:
|
||||
class RedirectingCookieTransport(CookieTransport):
|
||||
async def get_login_response(self, token: str) -> Response:
|
||||
response = RedirectResponse(
|
||||
str(BasicConfig().FRONTEND_URL) + "dashboard",
|
||||
str(AllEncompassingConfig().misc.frontend_url) + "dashboard",
|
||||
status_code=status.HTTP_302_FOUND,
|
||||
)
|
||||
return self._set_login_cookie(response, token)
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Type, Tuple
|
||||
|
||||
from pydantic import AnyHttpUrl
|
||||
from pydantic_settings import BaseSettings
|
||||
from pydantic_settings import (
|
||||
BaseSettings,
|
||||
SettingsConfigDict,
|
||||
PydanticBaseSettingsSource,
|
||||
TomlConfigSettingsSource,
|
||||
)
|
||||
|
||||
from media_manager.auth.config import AuthConfig
|
||||
from media_manager.database.config import DbConfig
|
||||
from media_manager.indexer.config import IndexerConfig
|
||||
from media_manager.metadataProvider.config import MetadataProviderConfig
|
||||
from media_manager.notification.config import NotificationConfig
|
||||
from media_manager.torrent.config import TorrentConfig
|
||||
|
||||
config_path = os.getenv("CONFIG_FILE")
|
||||
|
||||
if config_path is None:
|
||||
config_path = Path(__file__).parent.parent / "config.toml"
|
||||
else:
|
||||
config_path = Path(config_path)
|
||||
|
||||
|
||||
class BasicConfig(BaseSettings):
|
||||
@@ -9,8 +30,42 @@ class BasicConfig(BaseSettings):
|
||||
tv_directory: Path = Path(__file__).parent.parent / "data" / "tv"
|
||||
movie_directory: Path = Path(__file__).parent.parent / "data" / "movies"
|
||||
torrent_directory: Path = Path(__file__).parent.parent / "data" / "torrents"
|
||||
usenet_directory: Path = Path(__file__).parent.parent / "data" / "usenet"
|
||||
|
||||
FRONTEND_URL: AnyHttpUrl = "http://localhost:3000/"
|
||||
CORS_URLS: list[str] = []
|
||||
DEVELOPMENT: bool = False
|
||||
frontend_url: AnyHttpUrl = "http://localhost:3000/"
|
||||
cors_urls: list[str] = []
|
||||
development: bool = False
|
||||
api_base_path: str = "/api/v1"
|
||||
|
||||
|
||||
class AllEncompassingConfig(BaseSettings):
|
||||
model_config = SettingsConfigDict(
|
||||
toml_file=config_path, case_sensitive=False, env_nested_delimiter="_"
|
||||
)
|
||||
"""
|
||||
This class is used to load all configurations from the environment variables.
|
||||
It combines the BasicConfig with any additional configurations needed.
|
||||
"""
|
||||
misc: BasicConfig
|
||||
torrents: TorrentConfig
|
||||
notifications: NotificationConfig
|
||||
metadata: MetadataProviderConfig
|
||||
indexers: IndexerConfig
|
||||
database: DbConfig
|
||||
auth: AuthConfig
|
||||
|
||||
@classmethod
|
||||
def settings_customise_sources(
|
||||
cls,
|
||||
settings_cls: Type[BaseSettings],
|
||||
init_settings: PydanticBaseSettingsSource,
|
||||
env_settings: PydanticBaseSettingsSource,
|
||||
dotenv_settings: PydanticBaseSettingsSource,
|
||||
file_secret_settings: PydanticBaseSettingsSource,
|
||||
) -> Tuple[PydanticBaseSettingsSource, ...]:
|
||||
return (
|
||||
init_settings,
|
||||
env_settings,
|
||||
dotenv_settings,
|
||||
TomlConfigSettingsSource(settings_cls),
|
||||
file_secret_settings,
|
||||
)
|
||||
|
||||
@@ -6,23 +6,23 @@ from fastapi import Depends
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session, declarative_base, sessionmaker
|
||||
|
||||
from media_manager.database.config import DbConfig
|
||||
from media_manager.config import AllEncompassingConfig
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
config = DbConfig()
|
||||
config = AllEncompassingConfig().database
|
||||
|
||||
db_url = (
|
||||
"postgresql+psycopg"
|
||||
+ "://"
|
||||
+ config.USER
|
||||
+ config.user
|
||||
+ ":"
|
||||
+ config.PASSWORD
|
||||
+ config.password
|
||||
+ "@"
|
||||
+ config.HOST
|
||||
+ config.host
|
||||
+ ":"
|
||||
+ str(config.PORT)
|
||||
+ str(config.port)
|
||||
+ "/"
|
||||
+ config.DBNAME
|
||||
+ config.dbname
|
||||
)
|
||||
|
||||
engine = create_engine(db_url, echo=False)
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class DbConfig(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="DB_")
|
||||
HOST: str = "localhost"
|
||||
PORT: int = 5432
|
||||
USER: str = "MediaManager"
|
||||
PASSWORD: str = "MediaManager"
|
||||
DBNAME: str = "MediaManager"
|
||||
host: str = "localhost"
|
||||
port: int = 5432
|
||||
user: str = "MediaManager"
|
||||
password: str = "MediaManager"
|
||||
dbname: str = "MediaManager"
|
||||
|
||||
@@ -1,16 +1,3 @@
|
||||
import logging
|
||||
|
||||
from media_manager.indexer.config import JackettConfig
|
||||
from media_manager.indexer.indexers.jackett import Jackett
|
||||
from media_manager.indexer.config import ProwlarrConfig
|
||||
from media_manager.indexer.indexers.generic import GenericIndexer
|
||||
from media_manager.indexer.indexers.prowlarr import Prowlarr
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
indexers: list[GenericIndexer] = []
|
||||
|
||||
if ProwlarrConfig().enabled:
|
||||
indexers.append(Prowlarr())
|
||||
if JackettConfig().enabled:
|
||||
indexers.append(Jackett())
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class ProwlarrConfig(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="PROWLARR_")
|
||||
enabled: bool | None = False
|
||||
api_key: str | None = None
|
||||
enabled: bool = False
|
||||
api_key: str = ""
|
||||
url: str = "http://localhost:9696"
|
||||
|
||||
|
||||
class JackettConfig(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="JACKETT_")
|
||||
enabled: bool | None = False
|
||||
api_key: str | None = None
|
||||
enabled: bool = False
|
||||
api_key: str = ""
|
||||
url: str = "http://localhost:9696"
|
||||
indexers: list[str] = ["all"]
|
||||
|
||||
|
||||
class IndexerConfig(BaseSettings):
|
||||
prowlarr: ProwlarrConfig
|
||||
jackett: JackettConfig
|
||||
|
||||
@@ -6,8 +6,8 @@ import requests
|
||||
from pydantic import HttpUrl
|
||||
|
||||
from media_manager.indexer.indexers.generic import GenericIndexer
|
||||
from media_manager.indexer.config import JackettConfig
|
||||
from media_manager.indexer.schemas import IndexerQueryResult
|
||||
from media_manager.config import AllEncompassingConfig
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -19,7 +19,7 @@ class Jackett(GenericIndexer):
|
||||
|
||||
"""
|
||||
super().__init__(name="jackett")
|
||||
config = JackettConfig()
|
||||
config = AllEncompassingConfig().indexers.jackett
|
||||
self.api_key = config.api_key
|
||||
self.url = config.url
|
||||
self.indexers = config.indexers
|
||||
|
||||
@@ -3,7 +3,7 @@ import logging
|
||||
import requests
|
||||
|
||||
from media_manager.indexer.indexers.generic import GenericIndexer
|
||||
from media_manager.indexer.config import ProwlarrConfig
|
||||
from media_manager.config import AllEncompassingConfig
|
||||
from media_manager.indexer.schemas import IndexerQueryResult
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -18,7 +18,7 @@ class Prowlarr(GenericIndexer):
|
||||
:param kwargs: Additional keyword arguments to pass to the superclass constructor.
|
||||
"""
|
||||
super().__init__(name="prowlarr")
|
||||
config = ProwlarrConfig()
|
||||
config = AllEncompassingConfig().indexers.prowlarr
|
||||
self.api_key = config.api_key
|
||||
self.url = config.url
|
||||
log.debug("Registering Prowlarr as Indexer")
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
from media_manager.indexer import log, indexers
|
||||
import logging
|
||||
|
||||
from media_manager.config import AllEncompassingConfig
|
||||
from media_manager.indexer.indexers.generic import GenericIndexer
|
||||
from media_manager.indexer.indexers.jackett import Jackett
|
||||
from media_manager.indexer.indexers.prowlarr import Prowlarr
|
||||
from media_manager.indexer.schemas import IndexerQueryResultId, IndexerQueryResult
|
||||
from media_manager.indexer.repository import IndexerRepository
|
||||
from media_manager.notification.manager import notification_manager
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IndexerService:
|
||||
def __init__(self, indexer_repository: IndexerRepository):
|
||||
config = AllEncompassingConfig()
|
||||
self.repository = indexer_repository
|
||||
self.indexers: list[GenericIndexer] = []
|
||||
|
||||
if config.indexers.prowlarr.enabled:
|
||||
self.indexers.append(Prowlarr())
|
||||
if config.indexers.jackett.enabled:
|
||||
self.indexers.append(Jackett())
|
||||
|
||||
def get_result(self, result_id: IndexerQueryResultId) -> IndexerQueryResult:
|
||||
return self.repository.get_result(result_id=result_id)
|
||||
@@ -24,7 +38,7 @@ class IndexerService:
|
||||
results = []
|
||||
failed_indexers = []
|
||||
|
||||
for indexer in indexers:
|
||||
for indexer in self.indexers:
|
||||
try:
|
||||
indexer_results = indexer.search(query, is_tv=is_tv)
|
||||
results.extend(indexer_results)
|
||||
|
||||
@@ -50,7 +50,7 @@ logging.basicConfig(
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
from media_manager.database import init_db # noqa: E402
|
||||
from media_manager.config import BasicConfig # noqa: E402
|
||||
from media_manager.config import AllEncompassingConfig # noqa: E402
|
||||
import media_manager.torrent.router as torrent_router # noqa: E402
|
||||
import media_manager.movies.router as movies_router # noqa: E402
|
||||
import media_manager.tv.router as tv_router # noqa: E402
|
||||
@@ -99,13 +99,12 @@ from apscheduler.triggers.cron import CronTrigger # noqa: E402
|
||||
|
||||
init_db()
|
||||
log.info("Database initialized")
|
||||
|
||||
basic_config = BasicConfig()
|
||||
if basic_config.DEVELOPMENT:
|
||||
basic_config.torrent_directory.mkdir(parents=True, exist_ok=True)
|
||||
basic_config.tv_directory.mkdir(parents=True, exist_ok=True)
|
||||
basic_config.movie_directory.mkdir(parents=True, exist_ok=True)
|
||||
basic_config.image_directory.mkdir(parents=True, exist_ok=True)
|
||||
config = AllEncompassingConfig()
|
||||
if config.misc.development:
|
||||
config.misc.torrent_directory.mkdir(parents=True, exist_ok=True)
|
||||
config.misc.tv_directory.mkdir(parents=True, exist_ok=True)
|
||||
config.misc.movie_directory.mkdir(parents=True, exist_ok=True)
|
||||
config.misc.image_directory.mkdir(parents=True, exist_ok=True)
|
||||
log.warning("Development Mode activated!")
|
||||
else:
|
||||
log.info("Development Mode not activated!")
|
||||
@@ -146,11 +145,11 @@ async def lifespan(app: FastAPI):
|
||||
scheduler.shutdown()
|
||||
|
||||
|
||||
base_path = os.getenv("API_BASE_PATH") or "/api/v1"
|
||||
base_path = config.misc.api_base_path
|
||||
log.info("Base Path for API: %s", base_path)
|
||||
app = FastAPI(root_path=base_path, lifespan=lifespan)
|
||||
|
||||
origins = basic_config.CORS_URLS
|
||||
origins = config.misc.cors_urls
|
||||
log.info("CORS URLs activated for following origins:")
|
||||
for origin in origins:
|
||||
log.info(f" - {origin}")
|
||||
@@ -231,7 +230,7 @@ app.include_router(movies_router.router, prefix="/movies", tags=["movie"])
|
||||
app.include_router(notification_router, prefix="/notification", tags=["notification"])
|
||||
app.mount(
|
||||
"/static/image",
|
||||
StaticFiles(directory=basic_config.image_directory),
|
||||
StaticFiles(directory=config.misc.image_directory),
|
||||
name="static-images",
|
||||
)
|
||||
|
||||
@@ -248,26 +247,26 @@ log.info("Hello World!")
|
||||
# Startup filesystem checks
|
||||
# ----------------------------
|
||||
try:
|
||||
test_dir = basic_config.tv_directory / Path(".media_manager_test_dir")
|
||||
test_dir = config.misc.tv_directory / Path(".media_manager_test_dir")
|
||||
test_dir.mkdir(parents=True, exist_ok=True)
|
||||
test_dir.rmdir()
|
||||
log.info(f"Successfully created test dir in TV directory at: {test_dir}")
|
||||
|
||||
test_dir = basic_config.movie_directory / Path(".media_manager_test_dir")
|
||||
test_dir = config.misc.movie_directory / Path(".media_manager_test_dir")
|
||||
test_dir.mkdir(parents=True, exist_ok=True)
|
||||
test_dir.rmdir()
|
||||
log.info(f"Successfully created test dir in Movie directory at: {test_dir}")
|
||||
|
||||
test_dir = basic_config.image_directory / Path(".media_manager_test_dir")
|
||||
test_dir = config.misc.image_directory / Path(".media_manager_test_dir")
|
||||
test_dir.touch()
|
||||
test_dir.unlink()
|
||||
log.info(f"Successfully created test file in Image directory at: {test_dir}")
|
||||
|
||||
# check if hardlink creation works
|
||||
test_dir = basic_config.tv_directory / Path(".media_manager_test_dir")
|
||||
test_dir = config.misc.tv_directory / Path(".media_manager_test_dir")
|
||||
test_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
torrent_dir = basic_config.torrent_directory / Path(".media_manager_test_dir")
|
||||
torrent_dir = config.misc.torrent_directory / Path(".media_manager_test_dir")
|
||||
torrent_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
test_torrent_file = torrent_dir / Path(".media_manager.test.torrent")
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
import media_manager.config
|
||||
from media_manager.metadataProvider.schemas import MetaDataProviderSearchResult
|
||||
from media_manager.tv.schemas import Show
|
||||
from media_manager.movies.schemas import Movie
|
||||
from media_manager.config import AllEncompassingConfig
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AbstractMetadataProvider(ABC):
|
||||
storage_path = media_manager.config.BasicConfig().image_directory
|
||||
storage_path = AllEncompassingConfig().misc.image_directory
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
|
||||
14
media_manager/metadataProvider/config.py
Normal file
14
media_manager/metadataProvider/config.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class TmdbConfig(BaseSettings):
|
||||
tmdb_relay_url: str = "https://metadata-relay.maxid.me/tmdb"
|
||||
|
||||
|
||||
class TvdbConfig(BaseSettings):
|
||||
tvdb_relay_url: str = "https://metadata-relay.maxid.me/tvdb"
|
||||
|
||||
|
||||
class MetadataProviderConfig(BaseSettings):
|
||||
tvdb: TvdbConfig
|
||||
tmdb: TmdbConfig
|
||||
@@ -1,9 +1,9 @@
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
import media_manager.metadataProvider.utils
|
||||
from media_manager.config import AllEncompassingConfig
|
||||
from media_manager.metadataProvider.abstractMetaDataProvider import (
|
||||
AbstractMetadataProvider,
|
||||
)
|
||||
@@ -13,10 +13,6 @@ from media_manager.movies.schemas import Movie
|
||||
from media_manager.notification.manager import notification_manager
|
||||
|
||||
|
||||
class TmdbConfig(BaseSettings):
|
||||
TMDB_RELAY_URL: str = "https://metadata-relay.maxid.me/tmdb"
|
||||
|
||||
|
||||
ENDED_STATUS = {"Ended", "Canceled"}
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -26,8 +22,8 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
|
||||
name = "tmdb"
|
||||
|
||||
def __init__(self):
|
||||
config = TmdbConfig()
|
||||
self.url = config.TMDB_RELAY_URL
|
||||
config = AllEncompassingConfig().metadata.tmdb
|
||||
self.url = config.tmdb_relay_url
|
||||
|
||||
def __get_show_metadata(self, id: int) -> dict:
|
||||
try:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import requests
|
||||
import logging
|
||||
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
import media_manager.metadataProvider.utils
|
||||
from media_manager.config import AllEncompassingConfig
|
||||
from media_manager.metadataProvider.abstractMetaDataProvider import (
|
||||
AbstractMetadataProvider,
|
||||
)
|
||||
@@ -12,10 +12,6 @@ from media_manager.tv.schemas import Episode, Season, Show, SeasonNumber
|
||||
from media_manager.movies.schemas import Movie
|
||||
|
||||
|
||||
class TvdbConfig(BaseSettings):
|
||||
TVDB_RELAY_URL: str = "https://metadata-relay.maxid.me/tvdb"
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -23,8 +19,8 @@ class TvdbMetadataProvider(AbstractMetadataProvider):
|
||||
name = "tvdb"
|
||||
|
||||
def __init__(self):
|
||||
config = TvdbConfig()
|
||||
self.url = config.TVDB_RELAY_URL
|
||||
config = AllEncompassingConfig().metadata.tvdb
|
||||
self.url = config.tvdb_relay_url
|
||||
|
||||
def __get_show(self, id: int) -> dict:
|
||||
return requests.get(f"{self.url}/tv/shows/{id}").json()
|
||||
|
||||
@@ -3,6 +3,7 @@ import re
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from media_manager.config import AllEncompassingConfig
|
||||
from media_manager.exceptions import InvalidConfigError
|
||||
from media_manager.indexer.repository import IndexerRepository
|
||||
from media_manager.database import SessionLocal
|
||||
@@ -28,7 +29,6 @@ from media_manager.torrent.schemas import QualityStrings
|
||||
from media_manager.movies.repository import MovieRepository
|
||||
from media_manager.exceptions import NotFoundError
|
||||
import pprint
|
||||
from media_manager.config import BasicConfig
|
||||
from media_manager.torrent.repository import TorrentRepository
|
||||
from media_manager.torrent.utils import import_file, import_torrent
|
||||
from media_manager.indexer.service import IndexerService
|
||||
@@ -464,7 +464,7 @@ class MovieService:
|
||||
)
|
||||
|
||||
movie_file_path = (
|
||||
BasicConfig().movie_directory
|
||||
AllEncompassingConfig().misc.movie_directory
|
||||
/ f"{movie.name} ({movie.year}) [{movie.metadata_provider}id-{movie.external_id}]"
|
||||
)
|
||||
movie_files: list[MovieFile] = self.torrent_service.get_movie_files_of_torrent(
|
||||
|
||||
@@ -1,29 +1,44 @@
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class EmailConfig(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="EMAIL_")
|
||||
smtp_host: str
|
||||
smtp_port: int
|
||||
smtp_user: str
|
||||
smtp_password: str
|
||||
from_email: str
|
||||
smtp_host: str = ""
|
||||
smtp_port: int = 587
|
||||
smtp_user: str = ""
|
||||
smtp_password: str = ""
|
||||
from_email: str = ""
|
||||
use_tls: bool = False
|
||||
|
||||
|
||||
class NotificationConfig(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="NOTIFICATION_")
|
||||
class EmailNotificationsConfig(BaseSettings):
|
||||
enabled: bool = False
|
||||
emails: list[str] = [] # the email addresses to send notifications to
|
||||
|
||||
email: str | None = None # the email address to send notifications to
|
||||
|
||||
ntfy_url: str | None = (
|
||||
class GotifyConfig(BaseSettings):
|
||||
enabled: bool = False
|
||||
api_key: str | None = None
|
||||
url: str | None = (
|
||||
None # e.g. https://gotify.example.com (note lack of trailing slash)
|
||||
)
|
||||
|
||||
|
||||
class NtfyConfig(BaseSettings):
|
||||
enabled: bool = False
|
||||
url: str | None = (
|
||||
None # e.g. https://ntfy.sh/your-topic (note lack of trailing slash)
|
||||
)
|
||||
|
||||
pushover_api_key: str | None = None
|
||||
pushover_user: str | None = None
|
||||
|
||||
gotify_api_key: str | None = None
|
||||
gotify_url: str | None = (
|
||||
None # e.g. https://gotify.example.com (note lack of trailing slash)
|
||||
)
|
||||
class PushoverConfig(BaseSettings):
|
||||
enabled: bool = False
|
||||
api_key: str | None = None
|
||||
user: str | None = None
|
||||
|
||||
|
||||
class NotificationConfig(BaseSettings):
|
||||
smtp_config: EmailConfig
|
||||
email_notifications: EmailNotificationsConfig
|
||||
gotify: GotifyConfig
|
||||
ntfy: NtfyConfig
|
||||
pushover: PushoverConfig
|
||||
|
||||
@@ -20,7 +20,7 @@ from media_manager.notification.service_providers.ntfy import (
|
||||
from media_manager.notification.service_providers.pushover import (
|
||||
PushoverNotificationServiceProvider,
|
||||
)
|
||||
from media_manager.notification.config import NotificationConfig
|
||||
from media_manager.config import AllEncompassingConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -31,13 +31,13 @@ class NotificationManager:
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.config = NotificationConfig()
|
||||
self.config = AllEncompassingConfig().notifications
|
||||
self.providers: List[AbstractNotificationServiceProvider] = []
|
||||
self._initialize_providers()
|
||||
|
||||
def _initialize_providers(self) -> None:
|
||||
# Email provider
|
||||
if self.config.email:
|
||||
if self.config.email_notifications.enabled:
|
||||
try:
|
||||
self.providers.append(EmailNotificationServiceProvider())
|
||||
logger.info("Email notification provider initialized")
|
||||
@@ -45,7 +45,7 @@ class NotificationManager:
|
||||
logger.error(f"Failed to initialize Email provider: {e}")
|
||||
|
||||
# Gotify provider
|
||||
if self.config.gotify_api_key and self.config.gotify_url:
|
||||
if self.config.gotify.enabled:
|
||||
try:
|
||||
self.providers.append(GotifyNotificationServiceProvider())
|
||||
logger.info("Gotify notification provider initialized")
|
||||
@@ -53,7 +53,7 @@ class NotificationManager:
|
||||
logger.error(f"Failed to initialize Gotify provider: {e}")
|
||||
|
||||
# Ntfy provider
|
||||
if self.config.ntfy_url:
|
||||
if self.config.ntfy.enabled:
|
||||
try:
|
||||
self.providers.append(NtfyNotificationServiceProvider())
|
||||
logger.info("Ntfy notification provider initialized")
|
||||
@@ -61,7 +61,7 @@ class NotificationManager:
|
||||
logger.error(f"Failed to initialize Ntfy provider: {e}")
|
||||
|
||||
# Pushover provider
|
||||
if self.config.pushover_api_key and self.config.pushover_user:
|
||||
if self.config.pushover.enabled:
|
||||
try:
|
||||
self.providers.append(PushoverNotificationServiceProvider())
|
||||
logger.info("Pushover notification provider initialized")
|
||||
|
||||
@@ -3,12 +3,12 @@ from media_manager.notification.schemas import MessageNotification
|
||||
from media_manager.notification.service_providers.abstractNotificationServiceProvider import (
|
||||
AbstractNotificationServiceProvider,
|
||||
)
|
||||
from media_manager.notification.config import NotificationConfig
|
||||
from media_manager.config import AllEncompassingConfig
|
||||
|
||||
|
||||
class EmailNotificationServiceProvider(AbstractNotificationServiceProvider):
|
||||
def __init__(self):
|
||||
self.config = NotificationConfig()
|
||||
self.config = AllEncompassingConfig().notifications.email_notifications
|
||||
|
||||
def send_notification(self, message: MessageNotification) -> bool:
|
||||
subject = "MediaManager - " + message.title
|
||||
@@ -23,7 +23,10 @@ class EmailNotificationServiceProvider(AbstractNotificationServiceProvider):
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
media_manager.notification.utils.send_email(
|
||||
subject=subject, html=html, addressee=self.config.email
|
||||
)
|
||||
|
||||
for email in self.config.emails:
|
||||
media_manager.notification.utils.send_email(
|
||||
subject=subject, html=html, addressee=email
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import requests
|
||||
|
||||
from media_manager.notification.config import NotificationConfig
|
||||
from media_manager.config import AllEncompassingConfig
|
||||
from media_manager.notification.schemas import MessageNotification
|
||||
from media_manager.notification.service_providers.abstractNotificationServiceProvider import (
|
||||
AbstractNotificationServiceProvider,
|
||||
@@ -13,11 +13,11 @@ class GotifyNotificationServiceProvider(AbstractNotificationServiceProvider):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.config = NotificationConfig()
|
||||
self.config = AllEncompassingConfig().notifications.gotify
|
||||
|
||||
def send_notification(self, message: MessageNotification) -> bool:
|
||||
response = requests.post(
|
||||
url=f"{self.config.gotify_url}/message?token={self.config.gotify_api_key}",
|
||||
url=f"{self.config.url}/message?token={self.config.api_key}",
|
||||
json={
|
||||
"message": message.message,
|
||||
"title": message.title,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import requests
|
||||
|
||||
from media_manager.notification.config import NotificationConfig
|
||||
from media_manager.config import AllEncompassingConfig
|
||||
from media_manager.notification.schemas import MessageNotification
|
||||
from media_manager.notification.service_providers.abstractNotificationServiceProvider import (
|
||||
AbstractNotificationServiceProvider,
|
||||
@@ -13,11 +13,11 @@ class NtfyNotificationServiceProvider(AbstractNotificationServiceProvider):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.config = NotificationConfig()
|
||||
self.config = AllEncompassingConfig().notifications.ntfy
|
||||
|
||||
def send_notification(self, message: MessageNotification) -> bool:
|
||||
response = requests.post(
|
||||
url=self.config.ntfy_url,
|
||||
url=self.config.url,
|
||||
data=message.message.encode(encoding="utf-8"),
|
||||
headers={
|
||||
"Title": "MediaManager - " + message.title,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import requests
|
||||
|
||||
from media_manager.notification.config import NotificationConfig
|
||||
from media_manager.config import AllEncompassingConfig
|
||||
from media_manager.notification.schemas import MessageNotification
|
||||
from media_manager.notification.service_providers.abstractNotificationServiceProvider import (
|
||||
AbstractNotificationServiceProvider,
|
||||
@@ -9,14 +9,14 @@ from media_manager.notification.service_providers.abstractNotificationServicePro
|
||||
|
||||
class PushoverNotificationServiceProvider(AbstractNotificationServiceProvider):
|
||||
def __init__(self):
|
||||
self.config = NotificationConfig()
|
||||
self.config = AllEncompassingConfig().notifications.pushover
|
||||
|
||||
def send_notification(self, message: MessageNotification) -> bool:
|
||||
response = requests.post(
|
||||
url="https://api.pushover.net/1/messages.json",
|
||||
params={
|
||||
"token": self.config.pushover_api_key,
|
||||
"user": self.config.pushover_user,
|
||||
"token": self.config.api_key,
|
||||
"user": self.config.user,
|
||||
"message": message.message,
|
||||
"title": "MediaManager - " + message.title,
|
||||
},
|
||||
|
||||
@@ -3,13 +3,13 @@ import smtplib
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
from media_manager.notification.config import EmailConfig
|
||||
from media_manager.config import AllEncompassingConfig
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def send_email(html: str, addressee: str, subject: str | list[str]) -> None:
|
||||
email_conf = EmailConfig()
|
||||
def send_email(subject: str, html: str, addressee: str) -> None:
|
||||
email_conf = AllEncompassingConfig().notifications.smtp_config
|
||||
message = MIMEMultipart()
|
||||
message["From"] = email_conf.from_email
|
||||
message["To"] = addressee
|
||||
|
||||
23
media_manager/torrent/config.py
Normal file
23
media_manager/torrent/config.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class QbittorrentConfig(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="QBITTORRENT_")
|
||||
host: str = "localhost"
|
||||
port: int = 8080
|
||||
username: str = "admin"
|
||||
password: str = "admin"
|
||||
enabled: bool = False
|
||||
|
||||
|
||||
class SabnzbdConfig(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="SABNZBD_")
|
||||
host: str = "localhost"
|
||||
port: int = 8080
|
||||
api_key: str = ""
|
||||
enabled: bool = False
|
||||
|
||||
|
||||
class TorrentConfig(BaseSettings):
|
||||
qbittorrent: QbittorrentConfig
|
||||
sabnzbd: SabnzbdConfig
|
||||
@@ -4,9 +4,8 @@ import logging
|
||||
import bencoder
|
||||
import qbittorrentapi
|
||||
import requests
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
from media_manager.config import BasicConfig
|
||||
from media_manager.config import AllEncompassingConfig
|
||||
from media_manager.indexer.schemas import IndexerQueryResult
|
||||
from media_manager.torrent.download_clients.abstractDownloadClient import (
|
||||
AbstractDownloadClient,
|
||||
@@ -16,14 +15,6 @@ from media_manager.torrent.schemas import TorrentStatus, Torrent
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class QbittorrentConfig(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="QBITTORRENT_")
|
||||
host: str = "localhost"
|
||||
port: int = 8080
|
||||
username: str = "admin"
|
||||
password: str = "admin"
|
||||
|
||||
|
||||
class QbittorrentDownloadClient(AbstractDownloadClient):
|
||||
DOWNLOADING_STATE = (
|
||||
"allocating",
|
||||
@@ -48,7 +39,7 @@ class QbittorrentDownloadClient(AbstractDownloadClient):
|
||||
UNKNOWN_STATE = ("unknown",)
|
||||
|
||||
def __init__(self):
|
||||
self.config = QbittorrentConfig()
|
||||
self.config = AllEncompassingConfig().torrents.qbittorrent
|
||||
self.api_client = qbittorrentapi.Client(**self.config.model_dump())
|
||||
try:
|
||||
self.api_client.auth_log_in()
|
||||
@@ -69,7 +60,8 @@ class QbittorrentDownloadClient(AbstractDownloadClient):
|
||||
log.info(f"Attempting to download torrent: {indexer_result.title}")
|
||||
|
||||
torrent_filepath = (
|
||||
BasicConfig().torrent_directory / f"{indexer_result.title}.torrent"
|
||||
AllEncompassingConfig().misc.torrent_directory
|
||||
/ f"{indexer_result.title}.torrent"
|
||||
)
|
||||
|
||||
if torrent_filepath.exists():
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
from media_manager.config import AllEncompassingConfig
|
||||
from media_manager.indexer.schemas import IndexerQueryResult
|
||||
from media_manager.torrent.download_clients.abstractDownloadClient import (
|
||||
AbstractDownloadClient,
|
||||
@@ -11,13 +11,6 @@ import sabnzbd_api
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SabnzbdConfig(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="SABNZBD_")
|
||||
host: str = "localhost"
|
||||
port: int = 8080
|
||||
api_key: str = ""
|
||||
|
||||
|
||||
class SabnzbdDownloadClient(AbstractDownloadClient):
|
||||
DOWNLOADING_STATE = (
|
||||
"Downloading",
|
||||
@@ -32,7 +25,7 @@ class SabnzbdDownloadClient(AbstractDownloadClient):
|
||||
UNKNOWN_STATE = ("Unknown",)
|
||||
|
||||
def __init__(self):
|
||||
self.config = SabnzbdConfig()
|
||||
self.config = AllEncompassingConfig().torrents.sabnzbd
|
||||
self.client = sabnzbd_api.SabnzbdClient(
|
||||
host=self.config.host,
|
||||
port=str(self.config.port),
|
||||
@@ -59,9 +52,7 @@ class SabnzbdDownloadClient(AbstractDownloadClient):
|
||||
|
||||
try:
|
||||
# Add NZB to SABnzbd queue
|
||||
response = self.client.add_uri(
|
||||
url=str(indexer_result.download_url),
|
||||
)
|
||||
response = self.client.add_uri(url=str(indexer_result.download_url))
|
||||
if not response["status"]:
|
||||
error_msg = response
|
||||
log.error(f"Failed to add NZB to SABnzbd: {error_msg}")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
import os
|
||||
from enum import Enum
|
||||
|
||||
from media_manager.config import AllEncompassingConfig
|
||||
from media_manager.indexer.schemas import IndexerQueryResult
|
||||
from media_manager.torrent.download_clients.abstractDownloadClient import (
|
||||
AbstractDownloadClient,
|
||||
@@ -30,13 +30,14 @@ class DownloadManager:
|
||||
def __init__(self):
|
||||
self._torrent_client: AbstractDownloadClient | None = None
|
||||
self._usenet_client: AbstractDownloadClient | None = None
|
||||
self.config = AllEncompassingConfig().torrents
|
||||
self._initialize_clients()
|
||||
|
||||
def _initialize_clients(self) -> None:
|
||||
"""Initialize and register the default download clients"""
|
||||
|
||||
# Initialize qBittorrent client for torrents
|
||||
if os.getenv("QBITTORRENT_ENABLED", "false").lower() == "true":
|
||||
if self.config.qbittorrent.enabled:
|
||||
try:
|
||||
self._torrent_client = QbittorrentDownloadClient()
|
||||
log.info(
|
||||
@@ -46,7 +47,7 @@ class DownloadManager:
|
||||
log.error(f"Failed to initialize qBittorrent client: {e}")
|
||||
|
||||
# Initialize SABnzbd client for usenet
|
||||
if os.getenv("SABNZBD_ENABLED", "false").lower() == "true":
|
||||
if self.config.sabnzbd.enabled:
|
||||
try:
|
||||
self._usenet_client = SabnzbdDownloadClient()
|
||||
log.info("SABnzbd client initialized and set as active usenet client")
|
||||
|
||||
@@ -4,7 +4,7 @@ from pathlib import Path
|
||||
import shutil
|
||||
import patoolib
|
||||
|
||||
from media_manager.config import BasicConfig
|
||||
from media_manager.config import AllEncompassingConfig
|
||||
from media_manager.torrent.schemas import Torrent
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -53,7 +53,7 @@ def extract_archives(files):
|
||||
|
||||
|
||||
def get_torrent_filepath(torrent: Torrent):
|
||||
return BasicConfig().torrent_directory / torrent.title
|
||||
return AllEncompassingConfig().misc.torrent_directory / torrent.title
|
||||
|
||||
|
||||
def import_file(target_file: Path, source_file: Path):
|
||||
|
||||
@@ -3,6 +3,7 @@ import re
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from media_manager.config import AllEncompassingConfig
|
||||
from media_manager.exceptions import InvalidConfigError
|
||||
from media_manager.indexer.repository import IndexerRepository
|
||||
from media_manager.database import SessionLocal
|
||||
@@ -35,7 +36,6 @@ from media_manager.tv.repository import TvRepository
|
||||
from media_manager.exceptions import NotFoundError
|
||||
import pprint
|
||||
from pathlib import Path
|
||||
from media_manager.config import BasicConfig
|
||||
from media_manager.torrent.repository import TorrentRepository
|
||||
from media_manager.torrent.utils import import_file, import_torrent
|
||||
from media_manager.indexer.service import IndexerService
|
||||
@@ -502,7 +502,7 @@ class TvService:
|
||||
)
|
||||
|
||||
show_file_path = (
|
||||
BasicConfig().tv_directory
|
||||
AllEncompassingConfig().misc.tv_directory
|
||||
/ f"{show.name} ({show.year}) [{show.metadata_provider}id-{show.external_id}]"
|
||||
)
|
||||
season_files = self.torrent_service.get_season_files_of_torrent(torrent=torrent)
|
||||
|
||||
@@ -15,7 +15,7 @@ dependencies = [
|
||||
"patool>=4.0.1",
|
||||
"psycopg[binary]>=3.2.9",
|
||||
"pydantic>=2.11.5",
|
||||
"pydantic-settings>=2.9.1",
|
||||
"pydantic-settings[toml]>=2.9.1",
|
||||
"python-json-logger>=3.3.0",
|
||||
"qbittorrent-api>=2025.5.0",
|
||||
"requests>=2.32.3",
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import uuid
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
from pydantic import HttpUrl
|
||||
|
||||
from media_manager.indexer.schemas import IndexerQueryResult, IndexerQueryResultId
|
||||
from media_manager.indexer.repository import IndexerRepository
|
||||
from media_manager.indexer.service import IndexerService
|
||||
from media_manager.indexer.indexers.generic import GenericIndexer
|
||||
|
||||
|
||||
class DummyIndexer:
|
||||
class DummyIndexer(GenericIndexer):
|
||||
def __init__(self):
|
||||
super().__init__(name="DummyIndexer")
|
||||
|
||||
def search(self, query, is_tv=True):
|
||||
return [
|
||||
IndexerQueryResult(
|
||||
id=IndexerQueryResultId(uuid.uuid4()),
|
||||
title=f"{query} S01 1080p",
|
||||
download_url="https://example.com/torrent1",
|
||||
download_url=HttpUrl("https://example.com/torrent1"),
|
||||
seeders=10,
|
||||
flags=["test"],
|
||||
size=123456,
|
||||
@@ -31,10 +36,17 @@ def mock_indexer_repository():
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def indexer_service(monkeypatch, mock_indexer_repository):
|
||||
# Patch the global indexers list
|
||||
monkeypatch.setattr("media_manager.indexer.service.indexers", [DummyIndexer()])
|
||||
return IndexerService(indexer_repository=mock_indexer_repository)
|
||||
def indexer_service(mock_indexer_repository):
|
||||
# Mock the config to disable real indexers
|
||||
with patch("media_manager.indexer.service.AllEncompassingConfig") as mock_config:
|
||||
# Configure the mock to disable all real indexers
|
||||
mock_config.return_value.indexers.prowlarr.enabled = False
|
||||
mock_config.return_value.indexers.jackett.enabled = False
|
||||
|
||||
service = IndexerService(indexer_repository=mock_indexer_repository)
|
||||
# Manually set the dummy indexer
|
||||
service.indexers = [DummyIndexer()]
|
||||
return service
|
||||
|
||||
|
||||
def test_search_returns_results(indexer_service, mock_indexer_repository):
|
||||
@@ -50,7 +62,7 @@ def test_get_result_returns_result(mock_indexer_repository):
|
||||
expected_result = IndexerQueryResult(
|
||||
id=result_id,
|
||||
title="Test S01 1080p",
|
||||
download_url="https://example.com/torrent2",
|
||||
download_url=HttpUrl("https://example.com/torrent2"),
|
||||
seeders=10,
|
||||
flags=["test"],
|
||||
size=123456,
|
||||
|
||||
28
uv.lock
generated
28
uv.lock
generated
@@ -637,7 +637,7 @@ dependencies = [
|
||||
{ name = "pillow-avif-plugin" },
|
||||
{ name = "psycopg", extra = ["binary"] },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "pydantic-settings", extra = ["toml"] },
|
||||
{ name = "pytest" },
|
||||
{ name = "python-json-logger" },
|
||||
{ name = "qbittorrent-api" },
|
||||
@@ -669,7 +669,7 @@ requires-dist = [
|
||||
{ name = "pillow-avif-plugin", specifier = ">=1.5.2" },
|
||||
{ name = "psycopg", extras = ["binary"], specifier = ">=3.2.9" },
|
||||
{ name = "pydantic", specifier = ">=2.11.5" },
|
||||
{ name = "pydantic-settings", specifier = ">=2.9.1" },
|
||||
{ name = "pydantic-settings", extras = ["toml"], specifier = ">=2.9.1" },
|
||||
{ name = "pytest", specifier = ">=8.4.0" },
|
||||
{ name = "python-json-logger", specifier = ">=3.3.0" },
|
||||
{ name = "qbittorrent-api", specifier = ">=2025.5.0" },
|
||||
@@ -907,6 +907,11 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
toml = [
|
||||
{ name = "tomli" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
@@ -1175,6 +1180,25 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/dd/ade05d202db728b23e54aa0959622d090776023917e7308c1b2469a07b76/tmdbsimple-2.9.1-py3-none-any.whl", hash = "sha256:b52387c667bad1dccf5f776a576a971622a111fc08b7f731e360fabcc9860616", size = 38954, upload-time = "2022-01-25T21:48:34.653Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tvdb-v4-official"
|
||||
version = "1.1.0"
|
||||
|
||||
Reference in New Issue
Block a user