Compare commits

...

22 Commits

Author SHA1 Message Date
dependabot[bot]
cd70ab8711 Bump actions/setup-python from 5 to 6
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-13 17:08:28 +00:00
Maximilian Dorninger
51b8794e4d Merge pull request #411 from maxdorninger/Dependabot-auto-bump-deps
Configure Dependabot for multiple package ecosystems
2026-02-13 18:07:54 +01:00
Mark Riabov
0cfd1fa724 Fix suffix formatting for with_suffix call (#408)
Fixes issue ValueError: Invalid suffix 'jpg'

Completely prevents downloading posters from metadata provider
2026-02-10 20:29:05 +01:00
Maximilian Dorninger
b5b297e99a add new sponsor syn (#405)
this PR adds the new sponsor syn
2026-02-08 20:10:06 +01:00
maxid
58414cadae update all links to docs 2026-02-08 19:47:17 +01:00
maxid
462794520e update docs workflow 2026-02-08 19:43:13 +01:00
maxid
59afba007d update docs workflow 2026-02-08 19:36:07 +01:00
Maximilian Dorninger
cfa303e4f3 Merge pull request #404 from maxdorninger/mkdocs
This PR replaces Gitbook with Mkdocs to provide documentation
2026-02-08 19:27:15 +01:00
maxid
d3dde9c7eb add docs workflow 2026-02-08 19:22:34 +01:00
maxid
9c94ef6de0 convert gitbook files to mkdocs 2026-02-08 19:16:38 +01:00
Maximilian Dorninger
2665106847 Merge pull request #401 from maxdorninger/fix-env-variables
Fix download clients config being read from env variables
2026-02-08 16:37:15 +01:00
maxid
d029177fc0 hot fix: fix search tag name for episode in jackett 2026-02-04 23:52:07 +01:00
Maximilian Dorninger
1698c404cd Merge pull request #400 from maxdorninger/add-search-by-id-support-to-jackett
Add search by id support to jackett
2026-02-04 23:00:00 +01:00
maxid
abac894a95 fix download clients config being read from env variables without the mediamanager prefix 2026-02-04 22:49:24 +01:00
maxid
12854ff661 format files 2026-02-04 21:34:37 +01:00
maxid
3d52a87302 add id search capabilities to jackett 2026-02-04 21:34:31 +01:00
Maximilian Dorninger
9ee5cc6895 make the container user configurable (#399)
This PR makes the user the container runs as configurable. Before, the
container always tried stepping down (from root) to the mediamanager
user. Now it detects if it's already running as a non-root user and
starts the server directly. Fixes #397
2026-02-04 19:01:18 +01:00
Maximilian Dorninger
c45c9e5873 add correlation id to logging (#398)
This PR adds Correlation IDs to logs and request responses.

```
2026-02-04 12:40:32,793 - [afd825081d874d6e835b5c59a6ddb371] DEBUG - media_manager.movies - get_importable_movies(): Found 5 importable movies.
2026-02-04 12:40:32,794 - [afd825081d874d6e835b5c59a6ddb371] INFO - uvicorn.access - send(): 172.19.0.1:64094 - "GET /api/v1/movies/importable HTTP/1.1" 200
2026-02-04 12:40:47,322 - [41d30b7003fd45288c6a4bb1cfba5e7a] INFO - uvicorn.access - send(): 127.0.0.1:52964 - "GET /api/v1/health HTTP/1.1" 200
2026-02-04 12:41:17,408 - [157027ea5dde472a9e620f53739ccd53] INFO - uvicorn.access - send(): 127.0.0.1:39850 - "GET /api/v1/health HTTP/1.1" 200
```
2026-02-04 13:55:05 +01:00
Sergey Khruschak
24fcba6bee Torrent file name sanitizing (#390)
Hi, I've added file names sanitization when saving the torrent file, as
previously the import was failing on torrents with special characters in
names. This fixes #367
2026-02-03 17:09:36 +01:00
Maximilian Dorninger
d5994a9037 Fix docker permission issues (#395)
This PR fixes docker permission issues by first starting as root and
then chown-ing all the volumes. This should fix #388 #389
2026-02-03 13:06:18 +01:00
just_Bri
9e0d0c03c0 feat: add links to media detail pages in requests and torrent tables (#352)
Feature Request: https://github.com/maxdorninger/MediaManager/issues/351

[feat: add links to media detail pages in requests and torrent
tables](ac376c0d6d)
2026-02-02 22:48:14 +01:00
Maximilian Dorninger
70ff8f6ace Fix the broken link to the disable ascii art page (#396)
Fix the broken link to the disable ascii art page
2026-02-02 22:22:11 +01:00
81 changed files with 846 additions and 557 deletions

View File

@@ -53,5 +53,5 @@ YOUR CONFIG HERE
``` ```
- [ ] I understand, that without logs and/or screenshots and a detailed description of the problem, it is very hard to fix bugs. - [ ] I understand, that without logs and/or screenshots and a detailed description of the problem, it is very hard to fix bugs.
- [ ] I have checked the [documentation](https://maximilian-dorninger.gitbook.io/mediamanager) for help. - [ ] I have checked the [documentation](https://maxdorninger.github.io/MediaManager/) for help.
- [ ] I have searched the [issues](https://github.com/maxdorninger/MediaManager/issues) for similar issues and found none. - [ ] I have searched the [issues](https://github.com/maxdorninger/MediaManager/issues) for similar issues and found none.

25
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
- package-ecosystem: "npm"
directory: "/web"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
- package-ecosystem: "uv"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5

62
.github/workflows/docs.yml vendored Normal file
View File

@@ -0,0 +1,62 @@
name: Publish docs via GitHub Pages
on:
push:
branches:
- master
tags:
- v*
workflow_dispatch:
inputs:
set_default_alias:
description: 'Alias to set as default (e.g. latest, master)'
required: false
default: 'latest'
permissions:
contents: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Configure Git Credentials
run: |
git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
- uses: actions/setup-python@v6
with:
python-version: 3.x
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
- uses: actions/cache@v4
with:
key: mkdocs-material-${{ env.cache_id }}
path: .cache
restore-keys: |
mkdocs-material-
- name: Install dependencies
run: pip install mkdocs-material mike
- name: Deploy (master)
if: github.ref == 'refs/heads/master'
run: |
mike deploy --push --update-aliases master
- name: Deploy (tag)
if: startsWith(github.ref, 'refs/tags/v')
run: |
version=${GITHUB_REF#refs/tags/}
mike deploy --push --update-aliases $version latest --title "$version"
mike set-default --push latest
- name: Set Default (Manual)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.set_default_alias != ''
run: |
mike set-default --push ${{ github.event.inputs.set_default_alias }}

4
.gitignore vendored
View File

@@ -49,5 +49,5 @@ __pycache__
# Postgres # Postgres
/postgres /postgres
# Node modules # MkDocs
/node_modules/* site/

View File

@@ -18,7 +18,7 @@ Generally, if you have any questions or need help on the implementation side of
just ask in the issue, or in a draft PR. just ask in the issue, or in a draft PR.
Also, see the contribution guide in the docs for information on how to setup the dev environment: Also, see the contribution guide in the docs for information on how to setup the dev environment:
https://maximilian-dorninger.gitbook.io/mediamanager https://maxdorninger.github.io/MediaManager/
### For something that is a one or two line fix: ### For something that is a one or two line fix:

View File

@@ -13,7 +13,7 @@ RUN env PUBLIC_VERSION=${VERSION} PUBLIC_API_URL=${BASE_PATH} BASE_PATH=${BASE_P
FROM ghcr.io/astral-sh/uv:python3.13-trixie-slim AS base FROM ghcr.io/astral-sh/uv:python3.13-trixie-slim AS base
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y ca-certificates bash libtorrent21 gcc bc locales postgresql media-types mailcap curl gzip unzip tar 7zip bzip2 unar && \ apt-get install -y ca-certificates bash libtorrent21 gcc bc locales postgresql media-types mailcap curl gzip unzip tar 7zip bzip2 unar gosu && \
apt-get clean && \ apt-get clean && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
@@ -33,7 +33,6 @@ RUN chown -R mediamanager:mediamanager /app
USER mediamanager USER mediamanager
# Set uv cache to a writable home directory and use copy mode for volume compatibility
ENV UV_CACHE_DIR=/home/mediamanager/.cache/uv \ ENV UV_CACHE_DIR=/home/mediamanager/.cache/uv \
UV_LINK_MODE=copy UV_LINK_MODE=copy
@@ -47,6 +46,7 @@ ARG BASE_PATH=""
LABEL author="github.com/maxdorninger" LABEL author="github.com/maxdorninger"
LABEL version=${VERSION} LABEL version=${VERSION}
LABEL description="Docker image for MediaManager" LABEL description="Docker image for MediaManager"
USER root
ENV PUBLIC_VERSION=${VERSION} \ ENV PUBLIC_VERSION=${VERSION} \
CONFIG_DIR="/app/config" \ CONFIG_DIR="/app/config" \

View File

@@ -1,7 +1,7 @@
<br /> <br />
<div align="center"> <div align="center">
<a href="https://maximilian-dorninger.gitbook.io/mediamanager"> <a href="https://maxdorninger.github.io/MediaManager/">
<img src="https://github.com/maxdorninger/MediaManager/blob/master/web/static/logo.svg" alt="Logo" width="260" height="260"> <img src="https://raw.githubusercontent.com/maxdorninger/MediaManager/refs/heads/master/web/static/logo.svg" alt="Logo" width="260" height="260">
</a> </a>
<h3 align="center">MediaManager</h3> <h3 align="center">MediaManager</h3>
@@ -9,7 +9,7 @@
<p align="center"> <p align="center">
Modern management system for your media library Modern management system for your media library
<br /> <br />
<a href="https://maximilian-dorninger.gitbook.io/mediamanager"><strong>Explore the docs »</strong></a> <a href="https://maxdorninger.github.io/MediaManager/"><strong>Explore the docs »</strong></a>
<br /> <br />
<a href="https://github.com/maxdorninger/MediaManager/issues/new?labels=bug&template=bug_report.md">Report Bug</a> <a href="https://github.com/maxdorninger/MediaManager/issues/new?labels=bug&template=bug_report.md">Report Bug</a>
&middot; &middot;
@@ -35,7 +35,7 @@ wget -O ./config/config.toml https://github.com/maxdorninger/MediaManager/releas
docker compose up -d docker compose up -d
``` ```
### [View the docs for installation instructions and more](https://maximilian-dorninger.gitbook.io/mediamanager) ### [View the docs for installation instructions and more](https://maxdorninger.github.io/MediaManager/)
## Support MediaManager ## Support MediaManager
@@ -60,6 +60,7 @@ docker compose up -d
<a href="https://buymeacoffee.com/maxdorninger"><img src="https://cdn.buymeacoffee.com/uploads/profile_pictures/default/v2/DEBBB9/JO.png" width="80px" alt="Josh" /></a>&nbsp;&nbsp; <a href="https://buymeacoffee.com/maxdorninger"><img src="https://cdn.buymeacoffee.com/uploads/profile_pictures/default/v2/DEBBB9/JO.png" width="80px" alt="Josh" /></a>&nbsp;&nbsp;
<a href="https://buymeacoffee.com/maxdorninger"><img src="https://cdn.buymeacoffee.com/uploads/profile_pictures/2025/11/2VeQ8sTGPhj4tiLy.jpg" width="80px" alt="PuppiestDoggo" /></a>&nbsp;&nbsp; <a href="https://buymeacoffee.com/maxdorninger"><img src="https://cdn.buymeacoffee.com/uploads/profile_pictures/2025/11/2VeQ8sTGPhj4tiLy.jpg" width="80px" alt="PuppiestDoggo" /></a>&nbsp;&nbsp;
<a href="https://github.com/seferino-fernandez"><img src="https://avatars.githubusercontent.com/u/5546622" width="80px" alt="Seferino" /></a>&nbsp;&nbsp; <a href="https://github.com/seferino-fernandez"><img src="https://avatars.githubusercontent.com/u/5546622" width="80px" alt="Seferino" /></a>&nbsp;&nbsp;
<a href="https://buymeacoffee.com/maxdorninger"><img src="https://cdn.buymeacoffee.com/uploads/profile_pictures/default/v2/EC9689/SY.png" width="80px" alt="syn" /></a>&nbsp;&nbsp;
## Star History ## Star History
@@ -80,7 +81,7 @@ docker compose up -d
## Developer Quick Start ## Developer Quick Start
For the developer guide see the [Developer Guide](https://maximilian-dorninger.gitbook.io/mediamanager). For the developer guide see the [Developer Guide](https://maxdorninger.github.io/MediaManager/).
<!-- LICENSE --> <!-- LICENSE -->

View File

@@ -1,6 +1,6 @@
# MediaManager Dev Configuration File # MediaManager Dev Configuration File
# This file contains all available configuration options for MediaManager # This file contains all available configuration options for MediaManager
# Documentation: https://maximilian-dorninger.gitbook.io/mediamanager # Documentation: https://maxdorninger.github.io/MediaManager/
# #
# This is an example configuration file that gets copied to your config folder # This is an example configuration file that gets copied to your config folder
# on first boot. You should modify the values below to match your setup. # on first boot. You should modify the values below to match your setup.

View File

@@ -1,6 +1,6 @@
# MediaManager Example Configuration File # MediaManager Example Configuration File
# This file contains all available configuration options for MediaManager # This file contains all available configuration options for MediaManager
# Documentation: https://maximilian-dorninger.gitbook.io/mediamanager # Documentation: https://maxdorninger.github.io/MediaManager/
# #
# This is an example configuration file that gets copied to your config folder # This is an example configuration file that gets copied to your config folder
# on first boot. You should modify the values below to match your setup. # on first boot. You should modify the values below to match your setup.

View File

@@ -56,6 +56,15 @@ services:
- ./web:/app - ./web:/app
depends_on: depends_on:
- mediamanager - mediamanager
docs:
image: squidfunk/mkdocs-material:9
container_name: mediamanager-docs
volumes:
- .:/docs
ports:
- "9000:9000"
command: serve -w /docs -a 0.0.0.0:9000
# ---------------------------- # ----------------------------
# Additional services can be uncommented and configured as needed # Additional services can be uncommented and configured as needed
@@ -130,17 +139,17 @@ services:
# ports: # ports:
# - 8081:8080 # - 8081:8080
# restart: unless-stopped # restart: unless-stopped
# jackett: jackett:
# image: lscr.io/linuxserver/jackett:latest image: lscr.io/linuxserver/jackett:latest
# container_name: jackett container_name: jackett
# environment: environment:
# - PUID=1000 - PUID=1000
# - PGID=1000 - PGID=1000
# - TZ=Etc/UTC - TZ=Etc/UTC
# - AUTO_UPDATE=true - AUTO_UPDATE=true
# volumes: volumes:
# - ./res/jackett/data:/config - ./res/jackett/data:/config
# - ./res/jackett/torrents:/downloads - ./res/jackett/torrents:/downloads
# ports: ports:
# - 9117:9117 - 9117:9117
# restart: unless-stopped restart: unless-stopped

View File

@@ -1,34 +0,0 @@
---
layout:
width: default
title:
visible: true
description:
visible: true
tableOfContents:
visible: true
outline:
visible: false
pagination:
visible: true
metadata:
visible: true
---
# MediaManager
MediaManager is the modern, easy-to-use successor to the fragmented "Arr" stack. Manage, discover, and automate your TV and movie collection in a single, simple interface.
_Replaces Sonarr, Radarr, Seerr, and more._
### Quick Links
<table data-view="cards" data-full-width="false"><thead><tr><th align="center"></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td align="center">Installation Guide</td><td><a href="installation/">installation</a></td></tr><tr><td align="center">Configuration</td><td><a href="configuration/">configuration</a></td></tr><tr><td align="center">Developer Guide</td><td><a href="contributing-to-mediamanager/developer-guide.md">developer-guide.md</a></td></tr><tr><td align="center">Troubleshooting</td><td><a href="troubleshooting.md">troubleshooting.md</a></td></tr><tr><td align="center">Advanced Features</td><td><a href="advanced-features/">advanced-features</a></td></tr><tr><td align="center">Import Existing Media</td><td><a href="importing-existing-media.md">importing-existing-media.md</a></td></tr></tbody></table>
## Support MediaManager & Maximilian Dorninger
<table data-card-size="large" data-view="cards" data-full-width="false"><thead><tr><th></th><th data-hidden data-card-target data-type="content-ref"></th><th data-hidden data-card-cover data-type="image">Cover image</th></tr></thead><tbody><tr><td>Sponsor me on GitHub Sponsors :)</td><td><a href="https://github.com/sponsors/maxdorninger">https://github.com/sponsors/maxdorninger</a></td><td></td></tr><tr><td>Buy me a coffee :)</td><td><a href="https://buymeacoffee.com/maxdorninger">https://buymeacoffee.com/maxdorninger</a></td><td></td></tr></tbody></table>
### MediaManager Sponsors
<table data-view="cards" data-full-width="false"><thead><tr><th>Sponsor</th><th data-hidden data-card-target data-type="content-ref"></th><th data-hidden data-card-cover data-type="image">Cover image</th></tr></thead><tbody><tr><td>Aljaž Mur Eržen</td><td><a href="https://fosstodon.org/@aljazmerzen">https://fosstodon.org/@aljazmerzen</a></td><td><a href="https://github.com/aljazerzen.png">https://github.com/aljazerzen.png</a></td></tr><tr><td>Luis Rodriguez</td><td><a href="https://github.com/ldrrp">https://github.com/ldrrp</a></td><td><a href="https://github.com/ldrrp.png">https://github.com/ldrrp.png</a></td></tr><tr><td>Brandon P.</td><td><a href="https://github.com/brandon-dacrib">https://github.com/brandon-dacrib</a></td><td><a href="https://github.com/brandon-dacrib.png">https://github.com/brandon-dacrib.png</a></td></tr><tr><td>SeimusS</td><td><a href="https://github.com/SeimusS">https://github.com/SeimusS</a></td><td><a href="https://github.com/SeimusS.png">https://github.com/SeimusS.png</a></td></tr><tr><td>HadrienKerlero</td><td><a href="https://github.com/HadrienKerlero">https://github.com/HadrienKerlero</a></td><td><a href="https://github.com/HadrienKerlero.png">https://github.com/HadrienKerlero.png</a></td></tr><tr><td>keyxmakerx</td><td><a href="https://github.com/keyxmakerx">https://github.com/keyxmakerx</a></td><td><a href="https://github.com/keyxmakerx.png">https://github.com/keyxmakerx.png</a></td></tr><tr><td>LITUATUI</td><td><a href="https://github.com/LITUATUI">https://github.com/LITUATUI</a></td><td><a href="https://github.com/LITUATUI.png">https://github.com/LITUATUI.png</a></td></tr><tr><td>Nicolas</td><td><a href="https://buymeacoffee.com/maxdorninger">https://buymeacoffee.com/maxdorninger</a></td><td><a href="https://cdn.buymeacoffee.com/uploads/profile_pictures/default/v2/B6CDBD/NI.png">https://cdn.buymeacoffee.com/uploads/profile_pictures/default/v2/B6CDBD/NI.png</a></td></tr><tr><td>Josh</td><td><a href="https://buymeacoffee.com/maxdorninger">https://buymeacoffee.com/maxdorninger</a></td><td><a href="https://cdn.buymeacoffee.com/uploads/profile_pictures/default/v2/DEBBB9/JO.png">https://cdn.buymeacoffee.com/uploads/profile_pictures/default/v2/DEBBB9/JO.png</a></td></tr><tr><td>PuppiestDoggo</td><td><a href="https://buymeacoffee.com/maxdorninger">https://buymeacoffee.com/maxdorninger</a></td><td><a href="https://cdn.buymeacoffee.com/uploads/profile_pictures/2025/11/2VeQ8sTGPhj4tiLy.jpg">https://cdn.buymeacoffee.com/uploads/profile_pictures/2025/11/2VeQ8sTGPhj4tiLy.jpg</a></td></tr><tr><td>Seferino</td><td><a href="https://github.com/seferino-fernandez">https://github.com/seferino-fernandez</a></td><td><a href="https://avatars.githubusercontent.com/u/5546622">https://avatars.githubusercontent.com/u/5546622</a></td></tr><tr><td>Powered by DigitalOcean</td><td><a href="https://m.do.co/c/4edf05429dca">https://m.do.co/c/4edf05429dca</a></td><td data-object-fit="contain"><a href="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_vertical_blue.svg">https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_vertical_blue.svg</a></td></tr></tbody></table>

View File

@@ -1,33 +0,0 @@
# Table of contents
* [MediaManager](README.md)
* [Installation Guide](installation/README.md)
* [Docker Compose](installation/docker.md)
* [Nix Flakes \[Community\]](installation/flakes.md)
* [Importing existing media](importing-existing-media.md)
* [Usage](usage.md)
* [Configuration](configuration/README.md)
* [Backend](configuration/backend.md)
* [Authentication](configuration/authentication.md)
* [Database](configuration/database.md)
* [Download Clients](configuration/download-clients.md)
* [Indexers](configuration/indexers.md)
* [Scoring Rulesets](configuration/scoring-rulesets.md)
* [Notifications](configuration/notifications.md)
* [Custom Libraries](configuration/custom-libraries.md)
* [Logging](configuration/logging.md)
* [Advanced Features](advanced-features/README.md)
* [qBittorrent Category](advanced-features/qbittorrent-category.md)
* [URL Prefix](advanced-features/url-prefix.md)
* [Metadata Provider Configuration](advanced-features/metadata-provider-configuration.md)
* [Custom port](advanced-features/custom-port.md)
* [Follow symlinks in frontend files](advanced-features/follow-symlinks-in-frontend-files.md)
* [Disable startup ascii art](advanced-featured/disable-startup-ascii-art.md)
* [Troubleshooting](troubleshooting.md)
* [API Reference](api-reference.md)
* [Screenshots](screenshots.md)
## Contributing to MediaManager
* [Developer Guide](contributing-to-mediamanager/developer-guide.md)
* [Documentation](contributing-to-mediamanager/documentation.md)

View File

@@ -1,9 +0,0 @@
---
description: >-
The features in this section are not required to run MediaManager and serve
their purpose in very specific environments, but they can enhance your
experience and provide additional functionality.
---
# Advanced Features

View File

@@ -7,8 +7,6 @@ MediaManager can be configured to follow symlinks when serving frontend files. T
* `FRONTEND_FOLLOW_SYMLINKS`\ * `FRONTEND_FOLLOW_SYMLINKS`\
Set this environment variable to `true` to follow symlinks when serving frontend files. Default is `false`. Set this environment variable to `true` to follow symlinks when serving frontend files. Default is `false`.
{% code title=".env" %} ```bash title=".env"
```bash
FRONTEND_FOLLOW_SYMLINKS=true FRONTEND_FOLLOW_SYMLINKS=true
``` ```
{% endcode %}

View File

@@ -8,9 +8,8 @@ Metadata provider settings are configured in the `[metadata]` section of your `c
TMDB (The Movie Database) is the primary metadata provider for MediaManager. It provides detailed information about movies and TV shows. TMDB (The Movie Database) is the primary metadata provider for MediaManager. It provides detailed information about movies and TV shows.
{% hint style="info" %} !!! info
Other software like Jellyfin use TMDB as well, so there won't be any metadata discrepancies. Other software like Jellyfin use TMDB as well, so there won't be any metadata discrepancies.
{% endhint %}
* `tmdb_relay_url`\ * `tmdb_relay_url`\
URL of the TMDB relay (MetadataRelay). Default is `https://metadata-relay.dorninger.co/tmdb`. Example: `https://your-own-relay.example.com/tmdb`. URL of the TMDB relay (MetadataRelay). Default is `https://metadata-relay.dorninger.co/tmdb`. Example: `https://your-own-relay.example.com/tmdb`.
@@ -19,24 +18,21 @@ Other software like Jellyfin use TMDB as well, so there won't be any metadata di
* `default_language`\ * `default_language`\
TMDB language parameter used when searching and adding. Default is `en`. Format: ISO 639-1 (2 letters). TMDB language parameter used when searching and adding. Default is `en`. Format: ISO 639-1 (2 letters).
{% hint style="warning" %} !!! warning
`default_language` sets the TMDB `language` parameter when searching and adding TV shows and movies. If TMDB does not find a matching translation, metadata in the original language will be fetched with no option for a fallback language. It is therefore highly advised to only use "broad" languages. For most use cases, the default setting is safest. `default_language` sets the TMDB `language` parameter when searching and adding TV shows and movies. If TMDB does not find a matching translation, metadata in the original language will be fetched with no option for a fallback language. It is therefore highly advised to only use "broad" languages. For most use cases, the default setting is safest.
{% endhint %}
### TVDB Settings (`[metadata.tvdb]`) ### TVDB Settings (`[metadata.tvdb]`)
{% hint style="warning" %} !!! 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. The TVDB might provide false metadata and doesn't support some features of MediaManager like showing overviews. Therefore, TMDB is the preferred metadata provider.
{% endhint %}
* `tvdb_relay_url`\ * `tvdb_relay_url`\
URL of the TVDB relay (MetadataRelay). Default is `https://metadata-relay.dorninger.co/tvdb`. Example: `https://your-own-relay.example.com/tvdb`. URL of the TVDB relay (MetadataRelay). Default is `https://metadata-relay.dorninger.co/tvdb`. Example: `https://your-own-relay.example.com/tvdb`.
### MetadataRelay ### MetadataRelay
{% hint style="info" %} !!! info
To use MediaManager you don't need to set up your own MetadataRelay, as the default relay hosted by the developer should be sufficient for most purposes. To use MediaManager you don't need to set up your own MetadataRelay, as the default relay hosted by the developer should be sufficient for most purposes.
{% endhint %}
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. 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.
@@ -47,16 +43,14 @@ You might want to use your own relay if you want to avoid rate limits, protect y
* Get a TMDB API key from [The Movie Database](https://www.themoviedb.org/settings/api) * 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) * Get a TVDB API key from [The TVDB](https://thetvdb.com/auth/register)
{% hint style="info" %} !!! info
If you want to use your own MetadataRelay, you can set the `tmdb_relay_url` and/or `tvdb_relay_url` to your own relay service. If you want to use your own MetadataRelay, you can set the `tmdb_relay_url` and/or `tvdb_relay_url` to your own relay service.
{% endhint %}
### Example Configuration ### Example Configuration
Here's a complete example of the metadata section in your `config.toml`: Here's a complete example of the metadata section in your `config.toml`:
{% code title="config.toml" %} ```toml title="config.toml"
```toml
[metadata] [metadata]
# TMDB configuration # TMDB configuration
[metadata.tmdb] [metadata.tmdb]
@@ -66,8 +60,6 @@ Here's a complete example of the metadata section in your `config.toml`:
[metadata.tvdb] [metadata.tvdb]
tvdb_relay_url = "https://metadata-relay.dorninger.co/tvdb" tvdb_relay_url = "https://metadata-relay.dorninger.co/tvdb"
``` ```
{% endcode %}
{% hint style="info" %} !!! info
In most cases, you can simply use the default values and don't need to specify these settings in your config file at all. In most cases, you can simply use the default values and don't need to specify these settings in your config file at all.
{% endhint %}

View File

@@ -9,10 +9,8 @@ Use the following variables to customize behavior:
* `torrents.qbittorrent.category_save_path`\ * `torrents.qbittorrent.category_save_path`\
Save path for the category in qBittorrent. By default, no subdirectory is used. Example: `/data/torrents/MediaManager`. Save path for the category in qBittorrent. By default, no subdirectory is used. Example: `/data/torrents/MediaManager`.
{% hint style="info" %} !!! info
qBittorrent saves torrents to the path specified by `torrents.qbittorrent.category_save_path`, so it must be a valid path that qBittorrent can write to. qBittorrent saves torrents to the path specified by `torrents.qbittorrent.category_save_path`, so it must be a valid path that qBittorrent can write to.
{% endhint %}
{% hint style="warning" %} !!! warning
For MediaManager to successfully import torrents, you must add the subdirectory to the `misc.torrent_directory` variable. For MediaManager to successfully import torrents, you must add the subdirectory to the `misc.torrent_directory` variable.
{% endhint %}

View File

@@ -6,23 +6,20 @@ In order to run it on a prefixed path, like `maxdorninger.github.io/media`, the
In short, clone the repository, then run: In short, clone the repository, then run:
{% code title="Build Docker image" %} ```none title="Build Docker image"
```none
docker build \ docker build \
--build-arg BASE_PATH=/media \ --build-arg BASE_PATH=/media \
--build-arg VERSION=my-custom-version \ --build-arg VERSION=my-custom-version \
-t MediaManager:my-custom-version \ -t MediaManager:my-custom-version \
-f Dockerfile . -f Dockerfile .
``` ```
{% endcode %}
You also need to set the `BASE_PATH` environment variable at runtime in `docker-compose.yaml`: You also need to set the `BASE_PATH` environment variable at runtime in `docker-compose.yaml`:
* `BASE_PATH`\ * `BASE_PATH`\
Base path prefix MediaManager is served under. Example: `/media`. This must match the `BASE_PATH` build arg. Base path prefix MediaManager is served under. Example: `/media`. This must match the `BASE_PATH` build arg.
{% code title="docker-compose.yaml (excerpt)" %} ```yaml title="docker-compose.yaml (excerpt)"
```yaml
services: services:
mediamanager: mediamanager:
image: MediaManager:my-custom-version image: MediaManager:my-custom-version
@@ -32,10 +29,8 @@ services:
BASE_PATH: /media BASE_PATH: /media
... ...
``` ```
{% endcode %}
{% hint style="info" %} !!! info
Make sure to include the base path in the `frontend_url` field in the config file. See [Backend](../configuration/backend.md). Make sure to include the base path in the `frontend_url` field in the config file. See [Backend](../configuration/backend.md).
{% endhint %}
Finally, ensure that whatever reverse proxy you're using leaves the incoming path unchanged; that is, you should not strip the `/media` from `/media/web/`. Finally, ensure that whatever reverse proxy you're using leaves the incoming path unchanged; that is, you should not strip the `/media` from `/media/web/`.

View File

@@ -1,8 +1,7 @@
# API Reference # API Reference
{% hint style="info" %} !!! info
Media Manager's backend is built with FastAPI, which automatically generates interactive API documentation. Media Manager's backend is built with FastAPI, which automatically generates interactive API documentation.
{% endhint %}
* Swagger UI (typically available at `http://localhost:8000/docs`) * Swagger UI (typically available at `http://localhost:8000/docs`)
* ReDoc (typically available at `http://localhost:8000/redoc`) * ReDoc (typically available at `http://localhost:8000/redoc`)

View File

Before

Width:  |  Height:  |  Size: 3.1 MiB

After

Width:  |  Height:  |  Size: 3.1 MiB

View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 244 KiB

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 8.9 MiB

After

Width:  |  Height:  |  Size: 8.9 MiB

View File

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

Before

Width:  |  Height:  |  Size: 5.5 MiB

After

Width:  |  Height:  |  Size: 5.5 MiB

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

Before

Width:  |  Height:  |  Size: 7.6 MiB

After

Width:  |  Height:  |  Size: 7.6 MiB

View File

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 123 KiB

158
docs/assets/logo.svg Normal file
View File

@@ -0,0 +1,158 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:svg="http://www.w3.org/2000/svg"
version="1.1"
id="svg1"
width="2000"
height="2000"
viewBox="0 0 2000 2000"
sodipodi:docname="logo2.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xmlns="http://www.w3.org/2000/svg">
<defs
id="defs1">
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath1">
<path
d="M 0,1500 H 1500 V 0 H 0 Z"
id="path1"/>
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath3">
<path
d="M 0,0 H 1500 V 1500 H 0 Z"
transform="matrix(1.3333333,0,0,-1.3333333,0,2000)"
id="path3"/>
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath4">
<path
d="M -17.6886,1032.99 H 1106.27 V 238.53 H -17.6886 Z"
transform="translate(-319.61281,-1032.9941)"
id="path4"/>
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath6">
<path
d="M 0,0 H 1500 V 1500 H 0 Z"
transform="matrix(1.3333333,0,0,-1.3333333,0,2000)"
id="path6"/>
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath7">
<path
d="M 223.314,1226.85 H 1182.49 V 548.867 H 223.314 Z"
transform="translate(-894.64255,-548.86681)"
id="path7"/>
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath9">
<path
d="M 0,0 H 1500 V 1500 H 0 Z"
transform="matrix(1.3333333,0,0,-1.3333333,0,2000)"
id="path9"/>
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath10">
<path
d="M 301.561,1098.17 H 1517.73 V 238.53 H 301.561 Z"
transform="translate(-666.53282,-1098.1678)"
id="path10"/>
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath12">
<path
d="M 0,0 H 1500 V 1500 H 0 Z"
transform="matrix(1.3333333,0,0,-1.3333333,0,2000)"
id="path12"/>
</clipPath>
</defs>
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.9075"
inkscape:cx="999.44904"
inkscape:cy="1000"
inkscape:window-width="3840"
inkscape:window-height="2054"
inkscape:window-x="3373"
inkscape:window-y="199"
inkscape:window-maximized="1"
inkscape:current-layer="g1">
<inkscape:page
x="0"
y="0"
inkscape:label="1"
id="page1"
width="2000"
height="2000"
margin="0"
bleed="0"/>
</sodipodi:namedview>
<g
id="g1"
inkscape:groupmode="layer"
inkscape:label="1">
<g
id="g2"
clip-path="url(#clipPath3)">
<path
d="M 0,0 H 1500 V 1500 H 0 Z"
style="fill:#9ed8f7;fill-opacity:0;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,0,2000)"
clip-path="url(#clipPath1)"
id="path2"/>
</g>
<g
opacity="0.720001"
id="g5"
clip-path="url(#clipPath6)">
<path
d="m 0,0 h 669.787 c 68.994,0 116.873,-68.746 92.95,-133.46 L 542.309,-729.728 c -14.382,-38.904 -51.472,-64.736 -92.95,-64.736 h -669.787 c -68.994,0 -116.873,68.746 -92.949,133.46 L -92.949,-64.736 C -78.567,-25.832 -41.478,0 0,0"
style="fill:#2842fc;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,426.1504,622.67453)"
clip-path="url(#clipPath4)"
id="path5"/>
</g>
<g
opacity="0.720001"
id="g8"
clip-path="url(#clipPath9)">
<path
d="m 0,0 h -571.59 c -58.879,0 -99.738,58.667 -79.322,113.893 l 188.111,508.849 c 12.274,33.201 43.925,55.246 79.322,55.246 h 571.59 c 58.879,0 99.739,-58.667 79.322,-113.894 L 79.322,55.245 C 67.049,22.045 35.397,0 0,0"
style="fill:#ff5e00;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,1192.8567,1268.1776)"
clip-path="url(#clipPath7)"
id="path8"/>
</g>
<g
opacity="0.75"
id="g11"
clip-path="url(#clipPath12)">
<path
d="m 0,0 h 724.733 c 74.654,0 126.46,-74.386 100.575,-144.408 L 586.797,-789.591 c -15.562,-42.096 -55.694,-70.047 -100.575,-70.047 h -724.733 c -74.654,0 -126.461,74.386 -100.574,144.409 l 238.511,645.182 C -85.013,-27.952 -44.88,0 0,0"
style="fill:#f20a4c;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,888.7104,535.77627)"
clip-path="url(#clipPath10)"
id="path11"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -6,9 +6,8 @@ Frontend settings are configured through environment variables in your `docker-c
## Configuration File Location ## Configuration File Location
{% hint style="warning" %} !!! warning
Note that MediaManager may need to be restarted for changes in the config file to take effect. Note that MediaManager may need to be restarted for changes in the config file to take effect.
{% endhint %}
Your `config.toml` file should be in the directory that's mounted to `/app/config/config.toml` inside the container: Your `config.toml` file should be in the directory that's mounted to `/app/config/config.toml` inside the container:
@@ -66,6 +65,5 @@ MEDIAMANAGER_AUTH__OPENID_CONNECT__CLIENT_SECRET = "your_client_secret_from_prov
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 2 underscores as delimiters and `MEDIAMANAGER_` as the prefix. 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 2 underscores as delimiters and `MEDIAMANAGER_` as the prefix.
{% hint style="warning" %} !!! warning
Note that not every env variable starts with `MEDIAMANAGER_`; this prefix only applies to env variables which replace/overwrite values in the config file. Variables like the `CONFIG_DIR` env variable must not be prefixed. Note that not every env variable starts with `MEDIAMANAGER_`; this prefix only applies to env variables which replace/overwrite values in the config file. Variables like the `CONFIG_DIR` env variable must not be prefixed.
{% endhint %}

View File

@@ -20,13 +20,11 @@ All authentication settings are configured in the `[auth]` section of your `conf
* `email_password_resets`\ * `email_password_resets`\
Enables password resets via email. Default is `false`. Enables password resets via email. Default is `false`.
{% hint style="info" %} !!! info
To use email password resets, you must also configure SMTP settings in the `[notifications.smtp_config]` section. To use email password resets, you must also configure SMTP settings in the `[notifications.smtp_config]` section.
{% endhint %}
{% hint style="info" %} !!! info
When setting up MediaManager for the first time, you should add your email to `admin_emails` in the `[auth]` config section. MediaManager will then use this email instead of the default admin email. Your account will automatically be created as an admin account, allowing you to manage other users, media and settings. When setting up MediaManager for the first time, you should add your email to `admin_emails` in the `[auth]` config section. MediaManager will then use this email instead of the default admin email. Your account will automatically be created as an admin account, allowing you to manage other users, media and settings.
{% endhint %}
## OpenID Connect Settings (`[auth.openid_connect]`) ## OpenID Connect Settings (`[auth.openid_connect]`)
@@ -53,22 +51,20 @@ The OpenID server will likely require a redirect URI. This URL will usually look
{MEDIAMANAGER_URL}/api/v1/auth/oauth/callback {MEDIAMANAGER_URL}/api/v1/auth/oauth/callback
``` ```
{% hint style="warning" %} !!! warning
It is very important that you set the correct callback URI, otherwise it won't work! It is very important that you set the correct callback URI, otherwise it won't work!
{% endhint %}
#### Authentik Example #### Authentik Example
Here is an example configuration for the OpenID Connect provider for Authentik. Here is an example configuration for the OpenID Connect provider for Authentik.
![authentik-redirect-url-example](<../.gitbook/assets/authentik redirect url example.png>) ![authentik-redirect-url-example](<../assets/assets/authentik redirect url example.png>)
## Example Configuration ## Example Configuration
Here's a complete example of the authentication section in your `config.toml`: Here's a complete example of the authentication section in your `config.toml`:
{% code title="config.toml" %} ```toml title="config.toml"
```toml
[auth] [auth]
token_secret = "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6" token_secret = "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6"
session_lifetime = 604800 # 1 week session_lifetime = 604800 # 1 week
@@ -82,4 +78,4 @@ client_secret = "your-secret-key-here"
configuration_endpoint = "https://auth.example.com/.well-known/openid-configuration" configuration_endpoint = "https://auth.example.com/.well-known/openid-configuration"
name = "Authentik" name = "Authentik"
``` ```
{% endcode %}

View File

@@ -26,8 +26,7 @@ description: >-
Here's a complete example of the general settings section in your `config.toml`: Here's a complete example of the general settings section in your `config.toml`:
{% code title="config.toml" %} ```toml title="config.toml"
```toml
[misc] [misc]
# REQUIRED: Change this to match your actual frontend domain. # REQUIRED: Change this to match your actual frontend domain.
@@ -38,8 +37,6 @@ cors_urls = ["http://localhost:8000"]
# Optional: Development mode (set to true for debugging) # Optional: Development mode (set to true for debugging)
development = false development = false
``` ```
{% endcode %}
{% hint style="info" %} !!! info
The `frontend_url` is the most important setting to configure correctly. Make sure it matches your actual deployment URLs. The `frontend_url` is the most important setting to configure correctly. Make sure it matches your actual deployment URLs.
{% endhint %}

View File

@@ -6,9 +6,8 @@ MediaManager supports custom libraries, allowing you to add multiple folders for
Custom libraries are configured in the `misc` section in the `config.toml` file. You can add as many libraries as you need. Custom libraries are configured in the `misc` section in the `config.toml` file. You can add as many libraries as you need.
{% hint style="info" %} !!! info
You are not limited to `/data/tv` or `/data/movies`, you can choose the entire path freely! You are not limited to `/data/tv` or `/data/movies`, you can choose the entire path freely!
{% endhint %}
### Movie Libraries ### Movie Libraries

View File

@@ -19,8 +19,7 @@ Database settings are configured in the `[database]` section of your `config.tom
Here's a complete example of the database section in your `config.toml`: Here's a complete example of the database section in your `config.toml`:
{% code title="config.toml" %} ```toml title="config.toml"
```toml
[database] [database]
host = "db" host = "db"
port = 5432 port = 5432
@@ -28,8 +27,6 @@ user = "MediaManager"
password = "your_secure_password" password = "your_secure_password"
dbname = "MediaManager" dbname = "MediaManager"
``` ```
{% endcode %}
{% hint style="info" %} !!! info
In docker-compose deployments the container name is simultaneously its hostname, so you can use "db" or "postgres" as host. In docker-compose deployments the container name is simultaneously its hostname, so you can use "db" or "postgres" as host.
{% endhint %}

View File

@@ -19,9 +19,8 @@ qBittorrent is a popular BitTorrent client that MediaManager can integrate with
## Transmission Settings (`[torrents.transmission]`) ## Transmission Settings (`[torrents.transmission]`)
{% hint style="info" %} !!! info
The downloads path in Transmission and MediaManager must be the same, i.e. the path `/data/torrents` must link to the same volume for both containers. The downloads path in Transmission and MediaManager must be the same, i.e. the path `/data/torrents` must link to the same volume for both containers.
{% endhint %}
Transmission is a BitTorrent client that MediaManager can integrate with for downloading torrents. Transmission is a BitTorrent client that MediaManager can integrate with for downloading torrents.
@@ -59,8 +58,7 @@ SABnzbd is a Usenet newsreader that MediaManager can integrate with for download
Here's a complete example of the download clients section in your `config.toml`: Here's a complete example of the download clients section in your `config.toml`:
{% code title="config.toml" %} ```toml title="config.toml"
```toml
[torrents] [torrents]
# qBittorrent configuration # qBittorrent configuration
[torrents.qbittorrent] [torrents.qbittorrent]
@@ -87,14 +85,12 @@ Here's a complete example of the download clients section in your `config.toml`:
port = 8080 port = 8080
api_key = "your_sabnzbd_api_key" api_key = "your_sabnzbd_api_key"
``` ```
{% endcode %}
## Docker Compose Integration ## Docker Compose Integration
When using Docker Compose, make sure your download clients are accessible from the MediaManager backend: When using Docker Compose, make sure your download clients are accessible from the MediaManager backend:
{% code title="docker-compose.yml" %} ```yaml title="docker-compose.yml"
```yaml
services: services:
# MediaManager backend # MediaManager backend
backend: backend:
@@ -121,12 +117,9 @@ services:
- ./data/usenet:/downloads - ./data/usenet:/downloads
# ... other configuration ... # ... other configuration ...
``` ```
{% endcode %}
{% hint style="warning" %} !!! warning
You should enable only one BitTorrent and only one Usenet Download Client at any time. You should enable only one BitTorrent and only one Usenet Download Client at any time.
{% endhint %}
{% hint style="info" %} !!! info
Make sure the download directories in your download clients are accessible to MediaManager for proper file management and organization. Make sure the download directories in your download clients are accessible to MediaManager for proper file management and organization.
{% endhint %}

View File

@@ -13,9 +13,8 @@ Indexer settings are configured in the `[indexers]` section of your `config.toml
* `timeout_seconds`\ * `timeout_seconds`\
Timeout in seconds for requests to Prowlarr. Default is `60`. Timeout in seconds for requests to Prowlarr. Default is `60`.
{% hint style="warning" %} !!! warning
Symptoms of timeouts are typically no search results ("No torrents found!") in conjunction with logs showing read timeouts. Symptoms of timeouts are typically no search results ("No torrents found!") in conjunction with logs showing read timeouts.
{% endhint %}
<details> <details>
@@ -50,8 +49,7 @@ DEBUG - media_manager.indexer.utils -
## Example Configuration ## Example Configuration
{% code title="config.toml" %} ```toml title="config.toml"
```toml
[indexers] [indexers]
[indexers.prowlarr] [indexers.prowlarr]
enabled = true enabled = true
@@ -66,4 +64,4 @@ api_key = "your_jackett_api_key"
indexers = ["1337x", "rarbg"] indexers = ["1337x", "rarbg"]
timeout_seconds = 60 timeout_seconds = 60
``` ```
{% endcode %}

View File

@@ -57,8 +57,7 @@ Controls which emails receive notifications.
Here's a complete example of the notifications section in your `config.toml`: Here's a complete example of the notifications section in your `config.toml`:
{% code title="config.toml" %} ```toml title="config.toml"
```toml
[notifications] [notifications]
# SMTP settings for email notifications and password resets # SMTP settings for email notifications and password resets
[notifications.smtp_config] [notifications.smtp_config]
@@ -91,8 +90,7 @@ Here's a complete example of the notifications section in your `config.toml`:
api_key = "your_pushover_api_key" api_key = "your_pushover_api_key"
user = "your_pushover_user_key" user = "your_pushover_user_key"
``` ```
{% endcode %}
{% hint style="info" %}
You can enable multiple notification methods simultaneously. For example, you could have both email and Gotify notifications enabled at the same time. !!! info
{% endhint %} You can enable multiple notification methods simultaneously. For example, you could have both email and Gotify notifications enabled at the same time.

View File

@@ -17,9 +17,8 @@ Rules define how MediaManager scores releases based on their titles or indexer f
* Reject releases that do not meet certain criteria (e.g., non-freeleech releases). * Reject releases that do not meet certain criteria (e.g., non-freeleech releases).
* and more. * and more.
{% hint style="info" %} !!! info
The keywords and flags are compared case-insensitively. The keywords and flags are compared case-insensitively.
{% endhint %}
### Title Rules ### Title Rules
@@ -38,8 +37,7 @@ Each title rule consists of:
Examples for Title Rules Examples for Title Rules
{% code title="config.toml" %} ```toml title="config.toml"
```toml
[[indexers.title_scoring_rules]] [[indexers.title_scoring_rules]]
name = "prefer_h265" name = "prefer_h265"
keywords = ["h265", "hevc", "x265"] keywords = ["h265", "hevc", "x265"]
@@ -52,7 +50,6 @@ keywords = ["cam", "ts"]
score_modifier = -10000 score_modifier = -10000
negate = false negate = false
``` ```
{% endcode %}
* The first rule increases the score for releases containing "h265", "hevc", or "x265". * The first rule increases the score for releases containing "h265", "hevc", or "x265".
* The second rule heavily penalizes releases containing "cam" or "ts". * The second rule heavily penalizes releases containing "cam" or "ts".
@@ -76,8 +73,7 @@ Each indexer flag rule consists of:
Examples for Indexer Flag Rules Examples for Indexer Flag Rules
{% code title="config.toml" %} ```toml title="config.toml"
```toml
[[indexers.indexer_flag_scoring_rules]] [[indexers.indexer_flag_scoring_rules]]
name = "reject_non_freeleech" name = "reject_non_freeleech"
flags = ["freeleech", "freeleech75"] flags = ["freeleech", "freeleech75"]
@@ -90,7 +86,6 @@ flags = ["nuked"]
score_modifier = -10000 score_modifier = -10000
negate = false negate = false
``` ```
{% endcode %}
* The first rule penalizes releases that do not have the "freeleech" or "freeleech75" flag. * The first rule penalizes releases that do not have the "freeleech" or "freeleech75" flag.
* The second rule penalizes releases that are marked as "nuked". * The second rule penalizes releases that are marked as "nuked".
@@ -99,8 +94,7 @@ If `negate` is set to `true`, the `score_modifier` is applied only if none of th
## Example ## Example
{% code title="config.toml" %} ```toml title="config.toml"
```toml
[[indexers.scoring_rule_sets]] [[indexers.scoring_rule_sets]]
name = "default" name = "default"
libraries = ["ALL_TV", "ALL_MOVIES"] libraries = ["ALL_TV", "ALL_MOVIES"]
@@ -111,7 +105,6 @@ name = "strict_quality"
libraries = ["ALL_MOVIES"] libraries = ["ALL_MOVIES"]
rule_names = ["prefer_h265", "avoid_cam", "reject_non_freeleech"] rule_names = ["prefer_h265", "avoid_cam", "reject_non_freeleech"]
``` ```
{% endcode %}
## Libraries ## Libraries
@@ -127,9 +120,8 @@ You can use special library names in your rulesets:
This allows you to set global rules for all TV or movie content, or provide fallback rules for uncategorized media. This allows you to set global rules for all TV or movie content, or provide fallback rules for uncategorized media.
{% hint style="info" %} !!! info
You don't need to create lots of libraries with different directories, multiple libraries can share the same directory. You can set multiple (unlimited) libraries to the default directory `/data/movies` or `/data/tv` and use different rulesets with them. You don't need to create lots of libraries with different directories, multiple libraries can share the same directory. You can set multiple (unlimited) libraries to the default directory `/data/movies` or `/data/tv` and use different rulesets with them.
{% endhint %}
## Relation to Sonarr/Radarr Profiles ## Relation to Sonarr/Radarr Profiles

View File

@@ -10,7 +10,7 @@ description: >-
* `media_manager/`: Backend FastAPI application * `media_manager/`: Backend FastAPI application
* `web/`: Frontend SvelteKit application * `web/`: Frontend SvelteKit application
* `docs/`: Documentation (GitBook) * `docs/`: Documentation (MkDocs)
* `metadata_relay/`: Metadata relay service, also FastAPI * `metadata_relay/`: Metadata relay service, also FastAPI
## Special Dev Configuration ## Special Dev Configuration
@@ -44,9 +44,8 @@ MediaManager uses various environment variables for configuration. In the Docker
* `DISABLE_FRONTEND_MOUNT`\ * `DISABLE_FRONTEND_MOUNT`\
When `TRUE`, disables mounting built frontend files (allows separate frontend container). When `TRUE`, disables mounting built frontend files (allows separate frontend container).
{% hint style="info" %} !!! info
This is automatically set in `docker-compose.dev.yaml` to enable the separate frontend development container This is automatically set in `docker-compose.dev.yaml` to enable the separate frontend development container
{% endhint %}
#### Configuration Files #### Configuration Files
@@ -105,10 +104,9 @@ This means when your browser makes a request to `http://localhost:5173/api/v1/tv
### Setting up the full development environment with Docker (Recommended) ### Setting up the full development environment with Docker (Recommended)
This is the easiest and recommended way to get started. Everything runs in Docker with hot-reloading enabled.
{% stepper %}
{% step %}
### Prepare config files ### Prepare config files
Create config directory (only needed on first run) and copy example config files: Create config directory (only needed on first run) and copy example config files:
@@ -118,9 +116,9 @@ mkdir -p res/config # Only needed on first run
cp config.dev.toml res/config/config.toml cp config.dev.toml res/config/config.toml
cp web/.env.example web/.env cp web/.env.example web/.env
``` ```
{% endstep %}
{% step %}
### Start all services ### Start all services
Recommended: Use make commands for easy development Recommended: Use make commands for easy development
@@ -135,9 +133,9 @@ Alternative: Use docker compose directly (if make is not available)
```bash ```bash
docker compose -f docker-compose.dev.yaml up docker compose -f docker-compose.dev.yaml up
``` ```
{% endstep %}
{% step %}
### Access the application ### Access the application
* Frontend (with HMR): http://localhost:5173 * Frontend (with HMR): http://localhost:5173
@@ -151,12 +149,10 @@ Now you can edit code and see changes instantly:
* Edit Python files → Backend auto-reloads * Edit Python files → Backend auto-reloads
* Edit Svelte/TypeScript files → Frontend HMR updates in browser * Edit Svelte/TypeScript files → Frontend HMR updates in browser
* Edit config.toml → Changes apply immediately * Edit config.toml → Changes apply immediately
{% endstep %}
{% endstepper %}
{% hint style="info" %}
Run `make help` to see all available development commands including `make down`, `make logs`, `make app` (shell into backend), and more. !!! info
{% endhint %} Run `make help` to see all available development commands including `make down`, `make logs`, `make app` (shell into backend), and more.
## Setting up the backend development environment (Local) ## Setting up the backend development environment (Local)
@@ -217,18 +213,17 @@ ruff check .
## Setting up the frontend development environment (Local, Optional) ## Setting up the frontend development environment (Local, Optional)
Using the Docker setup above is recommended. This section is for those who prefer to run the frontend locally outside of Docker.
{% stepper %}
{% step %}
### Clone & change dir ### Clone & change dir
1. Clone the repository 1. Clone the repository
2. cd into repo root 2. cd into repo root
3. cd into `web` directory 3. cd into `web` directory
{% endstep %}
{% step %}
### Install Node.js (example using nvm-windows) ### Install Node.js (example using nvm-windows)
I used nvm-windows: I used nvm-windows:
@@ -243,9 +238,9 @@ If using PowerShell you may need:
```powershell ```powershell
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
``` ```
{% endstep %}
{% step %}
### Create .env for frontend ### Create .env for frontend
```bash ```bash
@@ -253,18 +248,18 @@ cp .env.example .env
``` ```
Update `PUBLIC_API_URL` if your backend is not at `http://localhost:8000` Update `PUBLIC_API_URL` if your backend is not at `http://localhost:8000`
{% endstep %}
{% step %}
### Install dependencies and run dev server ### Install dependencies and run dev server
```bash ```bash
npm install npm install
npm run dev npm run dev
``` ```
{% endstep %}
{% step %}
### Format & lint ### Format & lint
* Format: * Format:
@@ -278,12 +273,10 @@ npm run format
```bash ```bash
npm run lint npm run lint
``` ```
{% endstep %}
{% endstepper %}
{% hint style="info" %}
If running frontend locally, make sure to add `http://localhost:5173` to the `cors_urls` in your backend config file. !!! info
{% endhint %} If running frontend locally, make sure to add `http://localhost:5173` to the `cors_urls` in your backend config file.
## Troubleshooting ## Troubleshooting

View File

@@ -1,11 +1,14 @@
# Documentation # Documentation
MediaManager currently uses GitBook for documentation. MediaManager uses [MkDocs](https://www.mkdocs.org/) with
the [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) theme for documentation.
The files for the documentation are in the \`/docs\` directory. They are \_mostly\_ standard markdown. The files for the documentation are in the `/docs` directory.
Unfortunately GitBook doesn't provide a way to locally preview the documentation. Instead you can submit a PR with your proposed changes and a GitBook workflow will run which will provide a link to the preview. To preview the documentation locally, you need to have mkdocs or Docker installed.
To access the preview just open the \`Details\` link. ## How to preview the documentation locally with docker
<figure><img src="../.gitbook/assets/image.png" alt=""><figcaption></figcaption></figure> 1. Run the mkdocs container in `docker-compose.dev.yaml`
2. Open `http://127.0.0.1:9000/` in your browser.

View File

@@ -23,9 +23,8 @@ Here is an example, using these rules:
If your folder structure is in the correct format, you can start importing. To do this, log in as an administrator and go to the TV/movie dashboard. If your folder structure is in the correct format, you can start importing. To do this, log in as an administrator and go to the TV/movie dashboard.
{% hint style="info" %} !!! info
After importing, MediaManager will automatically prefix the old root TV show/movie folders with a dot to mark them as "imported". After importing, MediaManager will automatically prefix the old root TV show/movie folders with a dot to mark them as "imported".
{% endhint %}
So after importing, the directory would look like this (using the above directory structure): So after importing, the directory would look like this (using the above directory structure):

2
docs/index.md Normal file
View File

@@ -0,0 +1,2 @@
--8<-- "README.md"

View File

@@ -2,4 +2,5 @@
The recommended way to install and run Media Manager is using Docker and Docker Compose. Other installation methods are not officially supported, but listed here for convenience. The recommended way to install and run Media Manager is using Docker and Docker Compose. Other installation methods are not officially supported, but listed here for convenience.
<table data-view="cards" data-full-width="false"><thead><tr><th align="center"></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td align="center">Docker Compose (recommended)</td><td><a href="docker.md">docker.md</a></td></tr><tr><td align="center">Nix Flakes [Community]</td><td><a href="flakes.md">flakes.md</a></td></tr></tbody></table> [Docker Compose (recommended)](docker.md){ .md-button .md-button--primary }
[Nix Flakes [Community]](flakes.md){ .md-button }

View File

@@ -9,8 +9,8 @@
Follow these steps to get MediaManager running with Docker Compose: Follow these steps to get MediaManager running with Docker Compose:
{% stepper %}
{% step %}
#### Get the docker-compose file #### Get the docker-compose file
Download the `docker-compose.yaml` from the MediaManager repo: Download the `docker-compose.yaml` from the MediaManager repo:
@@ -18,9 +18,9 @@ Download the `docker-compose.yaml` from the MediaManager repo:
```bash ```bash
wget -O docker-compose.yaml https://github.com/maxdorninger/MediaManager/releases/latest/download/docker-compose.yaml wget -O docker-compose.yaml https://github.com/maxdorninger/MediaManager/releases/latest/download/docker-compose.yaml
``` ```
{% endstep %}
{% step %}
#### Prepare configuration directory and example config #### Prepare configuration directory and example config
Create a config directory and download the example configuration: Create a config directory and download the example configuration:
@@ -29,15 +29,15 @@ Create a config directory and download the example configuration:
mkdir config mkdir config
wget -O ./config/config.toml https://github.com/maxdorninger/MediaManager/releases/latest/download/config.example.toml wget -O ./config/config.toml https://github.com/maxdorninger/MediaManager/releases/latest/download/config.example.toml
``` ```
{% endstep %}
{% step %}
#### Edit configuration #### Edit configuration
You probably need to edit the `config.toml` file in the `./config` directory to suit your environment and preferences. [How to configure MediaManager.](configuration/) You probably need to edit the `config.toml` file in the `./config` directory to suit your environment and preferences. [How to configure MediaManager.](../configuration/README.md)
{% endstep %}
{% step %}
#### Start MediaManager #### Start MediaManager
Bring up the stack: Bring up the stack:
@@ -45,16 +45,15 @@ Bring up the stack:
```bash ```bash
docker compose up -d docker compose up -d
``` ```
{% endstep %}
{% endstepper %}
* Upon first run, MediaManager will create a default `config.toml` file in the `./config` directory (if not already present). * Upon first run, MediaManager will create a default `config.toml` file in the `./config` directory (if not already present).
* Upon first run, MediaManager will also create a default admin user. The credentials of the default admin user will be printed in the logs of the container — it's recommended to change the password of this user after the first login. * Upon first run, MediaManager will also create a default admin user. The credentials of the default admin user will be printed in the logs of the container — it's recommended to change the password of this user after the first login.
* [For more information on the available configuration options, see the Configuration section of the documentation.](configuration/) * [For more information on the available configuration options, see the Configuration section of the documentation.](../configuration/README.md)
{% hint style="info" %} !!! info
When setting up MediaManager for the first time, you should add your email to `admin_emails` in the `[auth]` config section. MediaManager will then use this email instead of the default admin email. Your account will automatically be created as an admin account, allowing you to manage other users, media, and settings. When setting up MediaManager for the first time, you should add your email to `admin_emails` in the `[auth]` config section. MediaManager will then use this email instead of the default admin email. Your account will automatically be created as an admin account, allowing you to manage other users, media, and settings.
{% endhint %}
## Docker Images ## Docker Images
@@ -70,9 +69,8 @@ MetadataRelay images are also available on both registries:
From v1.12.1 onwards, both MediaManager and MetadataRelay images are available on both Quay.io and GHCR. The reason for the switch to Quay.io as the primary image registry is due to [GHCR's continued slow performance.](https://github.com/orgs/community/discussions/173607) From v1.12.1 onwards, both MediaManager and MetadataRelay images are available on both Quay.io and GHCR. The reason for the switch to Quay.io as the primary image registry is due to [GHCR's continued slow performance.](https://github.com/orgs/community/discussions/173607)
{% hint style="info" %} !!! info
You can use either the Quay.io or GHCR images interchangeably, as they are built from the same source and the tags are the same across both registries. You can use either the Quay.io or GHCR images interchangeably, as they are built from the same source and the tags are the same across both registries.
{% endhint %}
### Tags ### Tags

View File

@@ -1,11 +1,9 @@
# Nix Flakes # Nix Flakes
{% hint style="note" %} !!! note
This is a community contribution and not officially supported by the MediaManager team, but included here for convenience. This is a community contribution and not officially supported by the MediaManager team, but included here for convenience.
{% endhint %}
*Please report issues with this method at the [corresponding GitHub repository](https://github.com/strangeglyph/mediamanager-nix).* *Please report issues with this method at the [corresponding GitHub repository](https://github.com/strangeglyph/mediamanager-nix).*
</note>
## Prerequisites ## Prerequisites
@@ -64,12 +62,11 @@ The host and port that MediaManager listens on can be set using `services.media-
To configure MediaManager, use `services.media-manager.settings`, which follows the same structure as the MediaManager To configure MediaManager, use `services.media-manager.settings`, which follows the same structure as the MediaManager
`config.toml`. To provision secrets, set `services.media-manager.environmentFile` to a protected file, for example one `config.toml`. To provision secrets, set `services.media-manager.environmentFile` to a protected file, for example one
provided by [agenix](https://github.com/ryantm/agenix) or [sops-nix](https://github.com/Mic92/sops-nix). provided by [agenix](https://github.com/ryantm/agenix) or [sops-nix](https://github.com/Mic92/sops-nix).
See [Configuration](Configuration.md#configuring-secrets) for guidance on using environment variables. See [Configuration](../configuration/README.md#configuring-secrets) for guidance on using environment variables.
{% hint style="warning" %} !!! warning
Do not place secrets in the nix store, as it is world-readable. Do not place secrets in the nix store, as it is world-readable.
{% endhint %}
## Automatic Postgres Setup ## Automatic Postgres Setup

View File

@@ -1,7 +1,6 @@
# Screenshots # Screenshots
{% hint style="info" %} !!! info
MediaManager also supports darkmode! MediaManager also supports darkmode!
{% endhint %}
![screenshot-dashboard.png](<.gitbook/assets/screenshot dashboard.png>) ![screenshot-tv-dashboard.png](<.gitbook/assets/screenshot tv dashboard.png>) ![screenshot-download-season.png](<.gitbook/assets/screenshot download season.png>) ![screenshot-request-season.png](<.gitbook/assets/screenshot request season.png>) ![screenshot-tv-torrents.png](<.gitbook/assets/screenshot tv torrents.png>) ![screenshot-settings.png](<.gitbook/assets/screenshot settings.png>) ![screenshot-login.png](<.gitbook/assets/screenshot login.png>) ![screenshot-dashboard.png](<assets/assets/screenshot dashboard.png>) ![screenshot-tv-dashboard.png](<assets/assets/screenshot tv dashboard.png>) ![screenshot-download-season.png](<assets/assets/screenshot download season.png>) ![screenshot-request-season.png](<assets/assets/screenshot request season.png>) ![screenshot-tv-torrents.png](<assets/assets/screenshot tv torrents.png>) ![screenshot-settings.png](<assets/assets/screenshot settings.png>) ![screenshot-login.png](<assets/assets/screenshot login.png>)

View File

@@ -1,8 +1,7 @@
# Troubleshooting # Troubleshooting
{% hint style="info" %} !!! info
Always check the container and browser logs for more specific error messages Always check the container and browser logs for more specific error messages
{% endhint %}
<details> <details>
@@ -60,10 +59,9 @@ Switch to advanced tabTry switching to the advanced tab when searching for torre
#### Possible Fixes: #### Possible Fixes:
* [Unable to pull image from GitHub Container Registry (Stack Overflow)](https://stackoverflow.com/questions/74656167/unable-to-pull-image-from-github-container-registry-ghcr) * [Unable to pull image from GitHub Container Registry (Stack Overflow)](https://stackoverflow.com/questions/74656167/unable-to-pull-image-from-github-container-registry-ghcr)
* [Try pulling the image from Quay.io](/broken/pages/09241b2fcda5d337e8878e4052f4634fe2902d10#mediamanager-and-metadatarelay-docker-images) * [Try pulling the image from Quay.io](installation/docker.md#docker-images)
</details> </details>
{% hint style="info" %} !!! info
If it still doesn't work, [please open an Issue.](https://github.com/maxdorninger/MediaManager/issues) It is possible that a bug is causing the issue. If it still doesn't work, [please open an Issue.](https://github.com/maxdorninger/MediaManager/issues) It is possible that a bug is causing the issue.
{% endhint %}

View File

@@ -1,133 +0,0 @@
# 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:
#### Downloading/Requesting a show
{% stepper %}
{% step %}
### Add the show
Add a show on the "Add Show" page. After adding the show you will be redirected to the show's page.
{% endstep %}
{% step %}
### Request season(s)
Click the "Request Season" button on the show's page. Select one or more seasons that you want to download.
{% endstep %}
{% step %}
### Select qualities
Select the "Min Quality" — the minimum resolution of the content to download.\
Select the "Wanted Quality" — the **maximum** resolution of the content to download.
{% endstep %}
{% step %}
### Submit request
Click "Submit request". This is not the last step: an administrator must first approve your request for download. Only after approval will the requested content be downloaded.
{% endstep %}
{% step %}
### Finished
Congratulation! You've downloaded a show (after admin approval).
{% endstep %}
{% endstepper %}
#### Requesting a show (as an admin)
{% stepper %}
{% step %}
### Add the show
Add a show on the "Add Show" page. After adding the show you will be redirected to the show's page.
{% endstep %}
{% step %}
### Request season(s)
Click the "Request Season" button on the show's page. Select one or more seasons that you want to download.
{% endstep %}
{% step %}
### Select qualities
Select the "Min Quality" — the minimum resolution of the content to download.\
Select the "Wanted Quality" — the **maximum** resolution of the content to download.
{% endstep %}
{% step %}
### Submit request (auto-approved)
Click "Submit request". As an admin, your request will be automatically approved.
{% endstep %}
{% step %}
### Finished
Congratulation! You've downloaded a show.
{% endstep %}
{% endstepper %}
#### Downloading a show (admin-only)
You can only directly download a show if you are an admin!
{% stepper %}
{% step %}
### Go to the show's page
Open the show's page that contains the season you wish to download.
{% endstep %}
{% step %}
### Start download
Click the "Download Season" button.
{% endstep %}
{% step %}
### Enter season number
Enter the season number that you want to download.
{% endstep %}
{% step %}
### Optional file path suffix
Optionally select the "File Path Suffix". Note: **it needs to be unique per season per show!**
{% endstep %}
{% step %}
### Choose torrent and download
Click "Download" on the torrent that you want to download.
{% endstep %}
{% step %}
### Finished
Congratulation! You've downloaded a show.
{% endstep %}
{% endstepper %}
#### Managing requests
Users need their requests to be approved by an admin. To manage requests:
{% stepper %}
{% step %}
### Open Requests page
Go to the "Requests" page.
{% endstep %}
{% step %}
### Approve, delete or modify
From the Requests page you can approve, delete, or modify a user's request.
{% endstep %}
{% endstepper %}

View File

@@ -1,7 +1,9 @@
import concurrent import concurrent
import concurrent.futures import concurrent.futures
import logging import logging
import xml.etree.ElementTree as ET
from concurrent.futures.thread import ThreadPoolExecutor from concurrent.futures.thread import ThreadPoolExecutor
from dataclasses import dataclass
import requests import requests
@@ -15,6 +17,21 @@ from media_manager.tv.schemas import Show
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@dataclass
class IndexerInfo:
supports_tv_search: bool
supports_tv_search_tmdb: bool
supports_tv_search_imdb: bool
supports_tv_search_tvdb: bool
supports_tv_search_season: bool
supports_tv_search_episode: bool
supports_movie_search: bool
supports_movie_search_tmdb: bool
supports_movie_search_imdb: bool
supports_movie_search_tvdb: bool
class Jackett(GenericIndexer, TorznabMixin): class Jackett(GenericIndexer, TorznabMixin):
def __init__(self) -> None: def __init__(self) -> None:
""" """
@@ -31,11 +48,16 @@ class Jackett(GenericIndexer, TorznabMixin):
def search(self, query: str, is_tv: bool) -> list[IndexerQueryResult]: def search(self, query: str, is_tv: bool) -> list[IndexerQueryResult]:
log.debug("Searching for " + query) log.debug("Searching for " + query)
params = {"q": query, "t": "tvsearch" if is_tv else "movie"}
return self.__search_jackett(params)
def __search_jackett(self, params: dict) -> list[IndexerQueryResult]:
futures = [] futures = []
with ThreadPoolExecutor() as executor, requests.Session() as session: with ThreadPoolExecutor() as executor, requests.Session() as session:
for indexer in self.indexers: for indexer in self.indexers:
future = executor.submit( future = executor.submit(
self.get_torrents_by_indexer, indexer, query, is_tv, session self.get_torrents_by_indexer, indexer, params, session
) )
futures.append(future) futures.append(future)
@@ -51,14 +73,103 @@ class Jackett(GenericIndexer, TorznabMixin):
return responses return responses
def get_torrents_by_indexer( def __get_search_capabilities(
self, indexer: str, query: str, is_tv: bool, session: requests.Session self, indexer: str, session: requests.Session
) -> list[IndexerQueryResult]: ) -> IndexerInfo:
url = ( url = (
self.url self.url
+ f"/api/v2.0/indexers/{indexer}/results/torznab/api?apikey={self.api_key}&t={'tvsearch' if is_tv else 'movie'}&q={query}" + f"/api/v2.0/indexers/{indexer}/results/torznab/api?apikey={self.api_key}&t=caps"
) )
response = session.get(url, timeout=self.timeout_seconds) response = session.get(url, timeout=self.timeout_seconds)
if response.status_code != 200:
msg = f"Cannot get search capabilities for Indexer {indexer}"
log.error(msg)
raise RuntimeError(msg)
xml = response.text
xml_tree = ET.fromstring(xml) # noqa: S314 # trusted source, since it is user controlled
tv_search = xml_tree.find("./*/tv-search")
movie_search = xml_tree.find("./*/movie-search")
log.debug(tv_search.attrib)
log.debug(movie_search.attrib)
tv_search_capabilities = []
movie_search_capabilities = []
tv_search_available = (tv_search is not None) and (
tv_search.attrib["available"] == "yes"
)
movie_search_available = (movie_search is not None) and (
movie_search.attrib["available"] == "yes"
)
if tv_search_available:
tv_search_capabilities = tv_search.attrib["supportedParams"].split(",")
if movie_search_available:
movie_search_capabilities = movie_search.attrib["supportedParams"].split(
","
)
return IndexerInfo(
supports_tv_search=tv_search_available,
supports_tv_search_imdb="tmdbid" in tv_search_capabilities,
supports_tv_search_tmdb="tmdbid" in tv_search_capabilities,
supports_tv_search_tvdb="tvdbid" in tv_search_capabilities,
supports_tv_search_season="season" in tv_search_capabilities,
supports_tv_search_episode="ep" in tv_search_capabilities,
supports_movie_search=movie_search_available,
supports_movie_search_imdb="imdbid" in movie_search_capabilities,
supports_movie_search_tmdb="tmdbid" in movie_search_capabilities,
supports_movie_search_tvdb="tvdbid" in movie_search_capabilities,
)
def __get_optimal_query_parameters(
self, indexer: str, session: requests.Session, params: dict
) -> dict[str, str]:
query_params = {"apikey": self.api_key, "t": params["t"]}
search_capabilities = self.__get_search_capabilities(
indexer=indexer, session=session
)
if params["t"] == "tvsearch":
if not search_capabilities.supports_tv_search:
msg = f"Indexer {indexer} does not support TV search"
raise RuntimeError(msg)
if search_capabilities.supports_tv_search_season and "season" in params:
query_params["season"] = params["season"]
if search_capabilities.supports_tv_search_episode and "ep" in params:
query_params["ep"] = params["ep"]
if search_capabilities.supports_tv_search_imdb and "imdbid" in params:
query_params["imdbid"] = params["imdbid"]
elif search_capabilities.supports_tv_search_tvdb and "tvdbid" in params:
query_params["tvdbid"] = params["tvdbid"]
elif search_capabilities.supports_tv_search_tmdb and "tmdbid" in params:
query_params["tmdbid"] = params["tmdbid"]
else:
query_params["q"] = params["q"]
if params["t"] == "movie":
if not search_capabilities.supports_movie_search:
msg = f"Indexer {indexer} does not support Movie search"
raise RuntimeError(msg)
if search_capabilities.supports_movie_search_imdb and "imdbid" in params:
query_params["imdbid"] = params["imdbid"]
elif search_capabilities.supports_tv_search_tvdb and "tvdbid" in params:
query_params["tvdbid"] = params["tvdbid"]
elif search_capabilities.supports_tv_search_tmdb and "tmdbid" in params:
query_params["tmdbid"] = params["tmdbid"]
else:
query_params["q"] = params["q"]
return query_params
def get_torrents_by_indexer(
self, indexer: str, params: dict, session: requests.Session
) -> list[IndexerQueryResult]:
url = f"{self.url}/api/v2.0/indexers/{indexer}/results/torznab/api"
query_params = self.__get_optimal_query_parameters(
indexer=indexer, session=session, params=params
)
response = session.get(url, timeout=self.timeout_seconds, params=query_params)
log.debug(f"Indexer {indexer} url: {response.url}")
if response.status_code != 200: if response.status_code != 200:
log.error( log.error(
@@ -75,8 +186,23 @@ class Jackett(GenericIndexer, TorznabMixin):
self, query: str, show: Show, season_number: int self, query: str, show: Show, season_number: int
) -> list[IndexerQueryResult]: ) -> list[IndexerQueryResult]:
log.debug(f"Searching for season {season_number} of show {show.name}") log.debug(f"Searching for season {season_number} of show {show.name}")
return self.search(query=query, is_tv=True) params = {
"t": "tvsearch",
"season": season_number,
"q": query,
}
if show.imdb_id:
params["imdbid"] = show.imdb_id
params[show.metadata_provider + "id"] = show.external_id
return self.__search_jackett(params=params)
def search_movie(self, query: str, movie: Movie) -> list[IndexerQueryResult]: def search_movie(self, query: str, movie: Movie) -> list[IndexerQueryResult]:
log.debug(f"Searching for movie {movie.name}") log.debug(f"Searching for movie {movie.name}")
return self.search(query=query, is_tv=False) params = {
"t": "movie",
"q": query,
}
if movie.imdb_id:
params["imdbid"] = movie.imdb_id
params[movie.metadata_provider + "id"] = movie.external_id
return self.__search_jackett(params=params)

View File

@@ -64,16 +64,12 @@ class TorznabMixin:
title = item.find("title").text title = item.find("title").text
size_str = item.find("size") size_str = item.find("size")
if size_str is None or size_str.text is None: if size_str is None or size_str.text is None:
log.warning( log.warning(f"Torznab item {title} has no size, skipping.")
f"Torznab item {title} has no size, skipping."
)
continue continue
try: try:
size = int(size_str.text or "0") size = int(size_str.text or "0")
except ValueError: except ValueError:
log.warning( log.warning(f"Torznab item {title} has invalid size, skipping.")
f"Torznab item {title} has invalid size, skipping."
)
continue continue
result = IndexerQueryResult( result = IndexerQueryResult(

View File

@@ -21,13 +21,20 @@ LOG_FILE = Path(os.getenv("LOG_FILE", "/app/config/media_manager.log"))
LOGGING_CONFIG = { LOGGING_CONFIG = {
"version": 1, "version": 1,
"disable_existing_loggers": False, "disable_existing_loggers": False,
"filters": {
"correlation_id": {
"()": "asgi_correlation_id.CorrelationIdFilter",
"uuid_length": 32,
"default_value": "-",
},
},
"formatters": { "formatters": {
"default": { "default": {
"format": "%(asctime)s - %(levelname)s - %(name)s - %(funcName)s(): %(message)s" "format": "%(asctime)s - [%(correlation_id)s] %(levelname)s - %(name)s - %(funcName)s(): %(message)s"
}, },
"json": { "json": {
"()": ISOJsonFormatter, "()": ISOJsonFormatter,
"format": "%(asctime)s %(levelname)s %(name)s %(message)s", "format": "%(asctime)s %(correlation_id)s %(levelname)s %(name)s %(message)s",
"rename_fields": { "rename_fields": {
"levelname": "level", "levelname": "level",
"asctime": "timestamp", "asctime": "timestamp",
@@ -39,11 +46,13 @@ LOGGING_CONFIG = {
"console": { "console": {
"class": "logging.StreamHandler", "class": "logging.StreamHandler",
"formatter": "default", "formatter": "default",
"filters": ["correlation_id"],
"stream": sys.stdout, "stream": sys.stdout,
}, },
"file": { "file": {
"class": "logging.handlers.RotatingFileHandler", "class": "logging.handlers.RotatingFileHandler",
"formatter": "json", "formatter": "json",
"filters": ["correlation_id"],
"filename": str(LOG_FILE), "filename": str(LOG_FILE),
"maxBytes": 10485760, "maxBytes": 10485760,
"backupCount": 5, "backupCount": 5,

View File

@@ -2,6 +2,7 @@ import logging
import os import os
import uvicorn import uvicorn
from asgi_correlation_id import CorrelationIdMiddleware
from fastapi import APIRouter, FastAPI, Request, Response from fastapi import APIRouter, FastAPI, Request, Response
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
@@ -71,6 +72,7 @@ app.add_middleware(
allow_credentials=True, allow_credentials=True,
allow_methods=["GET", "PUT", "POST", "DELETE", "PATCH", "HEAD", "OPTIONS"], allow_methods=["GET", "PUT", "POST", "DELETE", "PATCH", "HEAD", "OPTIONS"],
) )
app.add_middleware(CorrelationIdMiddleware, header_name="X-Correlation-ID")
api_app = APIRouter(prefix="/api/v1") api_app = APIRouter(prefix="/api/v1")

View File

@@ -15,7 +15,7 @@ def download_poster_image(storage_path: Path, poster_url: str, uuid: UUID) -> bo
res = requests.get(poster_url, stream=True, timeout=60) res = requests.get(poster_url, stream=True, timeout=60)
if res.status_code == 200: if res.status_code == 200:
image_file_path = storage_path.joinpath(str(uuid)).with_suffix("jpg") image_file_path = storage_path.joinpath(str(uuid)).with_suffix(".jpg")
image_file_path.write_bytes(res.content) image_file_path.write_bytes(res.content)
original_image = Image.open(image_file_path) original_image = Image.open(image_file_path)

View File

@@ -1,8 +1,7 @@
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings
class QbittorrentConfig(BaseSettings): class QbittorrentConfig(BaseSettings):
model_config = SettingsConfigDict(env_prefix="QBITTORRENT_")
host: str = "localhost" host: str = "localhost"
port: int = 8080 port: int = 8080
username: str = "admin" username: str = "admin"
@@ -14,7 +13,6 @@ class QbittorrentConfig(BaseSettings):
class TransmissionConfig(BaseSettings): class TransmissionConfig(BaseSettings):
model_config = SettingsConfigDict(env_prefix="TRANSMISSION_")
path: str = "/transmission/rpc" path: str = "/transmission/rpc"
https_enabled: bool = True https_enabled: bool = True
host: str = "localhost" host: str = "localhost"
@@ -25,7 +23,6 @@ class TransmissionConfig(BaseSettings):
class SabnzbdConfig(BaseSettings): class SabnzbdConfig(BaseSettings):
model_config = SettingsConfigDict(env_prefix="SABNZBD_")
host: str = "localhost" host: str = "localhost"
port: int = 8080 port: int = 8080
api_key: str = "" api_key: str = ""

View File

@@ -9,6 +9,7 @@ import bencoder
import libtorrent import libtorrent
import patoolib import patoolib
import requests import requests
from pathvalidate import sanitize_filename
from requests.exceptions import InvalidSchema from requests.exceptions import InvalidSchema
from media_manager.config import MediaManagerConfig from media_manager.config import MediaManagerConfig
@@ -132,7 +133,8 @@ def get_torrent_hash(torrent: IndexerQueryResult) -> str:
:return: The hash of the torrent. :return: The hash of the torrent.
""" """
torrent_filepath = ( torrent_filepath = (
MediaManagerConfig().misc.torrent_directory / f"{torrent.title}.torrent" MediaManagerConfig().misc.torrent_directory
/ f"{sanitize_filename(torrent.title)}.torrent"
) )
if torrent_filepath.exists(): if torrent_filepath.exists():
log.warning(f"Torrent file already exists at: {torrent_filepath}") log.warning(f"Torrent file already exists at: {torrent_filepath}")

View File

@@ -145,8 +145,30 @@ else
echo "Config file found at: $CONFIG_FILE" echo "Config file found at: $CONFIG_FILE"
fi fi
# check if running as root, if yes, fix permissions
if [ "$(id -u)" = '0' ]; then
echo "Running as root. Ensuring file permissions for mediamanager user..."
chown -R mediamanager:mediamanager "$CONFIG_DIR"
if [ -d "/data" ]; then
if [ "$(stat -c '%U' /data)" != "mediamanager" ]; then
echo "Fixing ownership of /data (this may take a while for large media libraries)..."
chown -R mediamanager:mediamanager /data
else
echo "/data ownership is already correct."
fi
fi
else
echo "Running as non-root user ($(id -u)). Skipping permission fixes."
echo "Note: Ensure your host volumes are manually set to the correct permissions."
fi
echo "Running DB migrations..." echo "Running DB migrations..."
uv run alembic upgrade head if [ "$(id -u)" = '0' ]; then
gosu mediamanager uv run alembic upgrade head
else
uv run alembic upgrade head
fi
echo "Starting MediaManager backend service..." echo "Starting MediaManager backend service..."
echo "" echo ""
@@ -159,9 +181,16 @@ echo ""
DEVELOPMENT_MODE=${MEDIAMANAGER_MISC__DEVELOPMENT:-FALSE} DEVELOPMENT_MODE=${MEDIAMANAGER_MISC__DEVELOPMENT:-FALSE}
PORT=${PORT:-8000} PORT=${PORT:-8000}
if [ "$DEVELOPMENT_MODE" == "TRUE" ]; then if [ "$DEVELOPMENT_MODE" == "TRUE" ]; then
echo "Development mode is enabled, enabling auto-reload..." echo "Development mode is enabled, enabling auto-reload..."
uv run fastapi run /app/media_manager/main.py --port "$PORT" --proxy-headers --reload DEV_OPTIONS="--reload"
else else
uv run fastapi run /app/media_manager/main.py --port "$PORT" --proxy-headers DEV_OPTIONS=""
fi
if [ "$(id -u)" = '0' ]; then
exec gosu mediamanager uv run fastapi run /app/media_manager/main.py --port "$PORT" --proxy-headers $DEV_OPTIONS
else
exec uv run fastapi run /app/media_manager/main.py --port "$PORT" --proxy-headers $DEV_OPTIONS
fi fi

View File

@@ -8,23 +8,25 @@ RUN apt-get update && apt-get install -y ca-certificates && \
apt-get clean && \ apt-get clean && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
# Create a non-root user and group
RUN groupadd -g 1000 mediamanager && \ RUN groupadd -g 1000 mediamanager && \
useradd -m -u 1000 -g mediamanager mediamanager useradd -m -u 1000 -g mediamanager mediamanager
WORKDIR /app WORKDIR /app
# Ensure mediamanager owns the app directory
RUN chown -R mediamanager:mediamanager /app RUN chown -R mediamanager:mediamanager /app
USER mediamanager
# Set uv cache to a writable home directory and use copy mode for volume compatibility
ENV UV_CACHE_DIR=/home/mediamanager/.cache/uv \ ENV UV_CACHE_DIR=/home/mediamanager/.cache/uv \
UV_LINK_MODE=copy UV_LINK_MODE=copy \
UV_COMPILE_BYTECODE=1
COPY --chown=mediamanager:mediamanager pyproject.toml uv.lock ./
USER mediamanager
RUN --mount=type=cache,target=/home/mediamanager/.cache/uv,uid=1000,gid=1000 \
uv sync --frozen --no-install-project --no-dev
COPY --chown=mediamanager:mediamanager . . COPY --chown=mediamanager:mediamanager . .
RUN --mount=type=cache,target=/home/mediamanager/.cache/uv,uid=1000,gid=1000 \
uv sync --locked RUN uv sync --frozen --no-dev
EXPOSE 8000 EXPOSE 8000
CMD ["uv", "run", "fastapi", "run", "/app/main.py"] CMD ["uv", "run", "fastapi", "run", "/app/main.py", "--port", "8000", "--proxy-headers"]

70
mkdocs.yml Normal file
View File

@@ -0,0 +1,70 @@
site_name: "MediaManager Documentation"
theme:
name: "material"
logo: "assets/logo.svg"
favicon: "assets/logo.svg"
features:
- navigation.sections
- navigation.expand
- navigation.indexes
- content.code.copy
- navigation.footer
palette:
- scheme: default
primary: indigo
accent: indigo
toggle:
icon: material/brightness-7
name: Switch to dark mode
- scheme: slate
primary: black
accent: black
toggle:
icon: material/brightness-4
name: Switch to light mode
markdown_extensions:
- admonition
- pymdownx.details
- pymdownx.superfences
- attr_list
- md_in_html
- pymdownx.snippets:
base_path: ["."]
nav:
- Welcome: index.md
- Installation:
- installation/README.md
- Docker Compose: installation/docker.md
- Nix Flakes [Community]: installation/flakes.md
- Usage:
- Importing existing media: importing-existing-media.md
- Configuration:
- configuration/README.md
- Backend: configuration/backend.md
- Authentication: configuration/authentication.md
- Database: configuration/database.md
- Download Clients: configuration/download-clients.md
- Indexers: configuration/indexers.md
- Scoring Rulesets: configuration/scoring-rulesets.md
- Notifications: configuration/notifications.md
- Custom Libraries: configuration/custom-libraries.md
- Logging: configuration/logging.md
- Advanced Features:
- qBittorrent Category: advanced-features/qbittorrent-category.md
- URL Prefix: advanced-features/url-prefix.md
- Metadata Provider Configuration: advanced-features/metadata-provider-configuration.md
- Custom port: advanced-features/custom-port.md
- Follow symlinks in frontend files: advanced-features/follow-symlinks-in-frontend-files.md
- Disable startup ascii art: advanced-features/disable-startup-ascii-art.md
- Troubleshooting: troubleshooting.md
- API Reference: api-reference.md
- Screenshots: screenshots.md
- Contributing to MediaManager:
- Developer Guide: contributing-to-mediamanager/developer-guide.md
- Documentation: contributing-to-mediamanager/documentation.md
extra:
version:
provider: mike

View File

@@ -33,6 +33,8 @@ dependencies = [
"sabnzbd-api>=0.1.2", "sabnzbd-api>=0.1.2",
"transmission-rpc>=7.0.11", "transmission-rpc>=7.0.11",
"libtorrent>=2.0.11", "libtorrent>=2.0.11",
"pathvalidate>=3.3.1",
"asgi-correlation-id>=4.3.4",
] ]
[dependency-groups] [dependency-groups]

26
uv.lock generated
View File

@@ -105,6 +105,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" },
] ]
[[package]]
name = "asgi-correlation-id"
version = "4.3.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
{ name = "starlette" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f4/ff/a6538245ac1eaa7733ec6740774e9d5add019e2c63caa29e758c16c0afdd/asgi_correlation_id-4.3.4.tar.gz", hash = "sha256:ea6bc310380373cb9f731dc2e8b2b6fb978a76afe33f7a2384f697b8d6cd811d", size = 20075, upload-time = "2024-10-17T11:44:30.324Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d9/ab/6936e2663c47a926e0659437b9333ad87d1ff49b1375d239026e0a268eba/asgi_correlation_id-4.3.4-py3-none-any.whl", hash = "sha256:36ce69b06c7d96b4acb89c7556a4c4f01a972463d3d49c675026cbbd08e9a0a2", size = 15262, upload-time = "2024-10-17T11:44:28.739Z" },
]
[[package]] [[package]]
name = "attrs" name = "attrs"
version = "25.4.0" version = "25.4.0"
@@ -854,6 +867,7 @@ source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "alembic" }, { name = "alembic" },
{ name = "apscheduler" }, { name = "apscheduler" },
{ name = "asgi-correlation-id" },
{ name = "bencoder" }, { name = "bencoder" },
{ name = "cachetools" }, { name = "cachetools" },
{ name = "fastapi", extra = ["standard"] }, { name = "fastapi", extra = ["standard"] },
@@ -864,6 +878,7 @@ dependencies = [
{ name = "httpx-oauth" }, { name = "httpx-oauth" },
{ name = "jsonschema" }, { name = "jsonschema" },
{ name = "libtorrent" }, { name = "libtorrent" },
{ name = "pathvalidate" },
{ name = "patool" }, { name = "patool" },
{ name = "pillow" }, { name = "pillow" },
{ name = "psycopg", extra = ["binary"] }, { name = "psycopg", extra = ["binary"] },
@@ -893,6 +908,7 @@ dev = [
requires-dist = [ requires-dist = [
{ name = "alembic", specifier = ">=1.16.1" }, { name = "alembic", specifier = ">=1.16.1" },
{ name = "apscheduler", specifier = ">=3.11.0" }, { name = "apscheduler", specifier = ">=3.11.0" },
{ name = "asgi-correlation-id", specifier = ">=4.3.4" },
{ name = "bencoder", specifier = ">=0.2.0" }, { name = "bencoder", specifier = ">=0.2.0" },
{ name = "cachetools", specifier = ">=6.0.0" }, { name = "cachetools", specifier = ">=6.0.0" },
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.12" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.115.12" },
@@ -903,6 +919,7 @@ requires-dist = [
{ name = "httpx-oauth", specifier = ">=0.16.1" }, { name = "httpx-oauth", specifier = ">=0.16.1" },
{ name = "jsonschema", specifier = ">=4.24.0" }, { name = "jsonschema", specifier = ">=4.24.0" },
{ name = "libtorrent", specifier = ">=2.0.11" }, { name = "libtorrent", specifier = ">=2.0.11" },
{ name = "pathvalidate", specifier = ">=3.3.1" },
{ name = "patool", specifier = ">=4.0.1" }, { name = "patool", specifier = ">=4.0.1" },
{ name = "pillow", specifier = ">=11.3.0" }, { name = "pillow", specifier = ">=11.3.0" },
{ name = "psycopg", extras = ["binary"], specifier = ">=3.2.9" }, { name = "psycopg", extras = ["binary"], specifier = ">=3.2.9" },
@@ -946,6 +963,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
] ]
[[package]]
name = "pathvalidate"
version = "3.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fa/2a/52a8da6fe965dea6192eb716b357558e103aea0a1e9a8352ad575a8406ca/pathvalidate-3.3.1.tar.gz", hash = "sha256:b18c07212bfead624345bb8e1d6141cdcf15a39736994ea0b94035ad2b1ba177", size = 63262, upload-time = "2025-06-15T09:07:20.736Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/70/875f4a23bfc4731703a5835487d0d2fb999031bd415e7d17c0ae615c18b7/pathvalidate-3.3.1-py3-none-any.whl", hash = "sha256:5263baab691f8e1af96092fa5137ee17df5bdfbd6cff1fcac4d6ef4bc2e1735f", size = 24305, upload-time = "2025-06-15T09:07:19.117Z" },
]
[[package]] [[package]]
name = "patool" name = "patool"
version = "4.0.3" version = "4.0.3"

156
web/package-lock.json generated
View File

@@ -134,9 +134,9 @@
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.27.2", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
"integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -151,9 +151,9 @@
} }
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.27.2", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
"integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -168,9 +168,9 @@
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.27.2", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
"integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -185,9 +185,9 @@
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.27.2", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
"integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -202,9 +202,9 @@
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.27.2", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
"integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -219,9 +219,9 @@
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.27.2", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
"integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -236,9 +236,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.2", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
"integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -253,9 +253,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.27.2", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
"integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -270,9 +270,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.27.2", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
"integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -287,9 +287,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.27.2", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
"integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -304,9 +304,9 @@
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.27.2", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
"integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -321,9 +321,9 @@
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.27.2", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
"integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@@ -338,9 +338,9 @@
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.27.2", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
"integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@@ -355,9 +355,9 @@
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.27.2", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
"integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -372,9 +372,9 @@
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.27.2", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
"integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -389,9 +389,9 @@
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.27.2", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
"integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -406,9 +406,9 @@
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.27.2", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
"integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -423,9 +423,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-arm64": { "node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.2", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
"integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -440,9 +440,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.27.2", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
"integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -457,9 +457,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-arm64": { "node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.2", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
"integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -474,9 +474,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.27.2", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
"integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -491,9 +491,9 @@
} }
}, },
"node_modules/@esbuild/openharmony-arm64": { "node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.2", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
"integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -508,9 +508,9 @@
} }
}, },
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/sunos-x64": {
"version": "0.27.2", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
"integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -525,9 +525,9 @@
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.27.2", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
"integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -542,9 +542,9 @@
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.27.2", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
"integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -559,9 +559,9 @@
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.27.2", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
"integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],

View File

@@ -127,13 +127,27 @@
<Table.Row> <Table.Row>
<Table.Cell> <Table.Cell>
{#if isShow} {#if isShow}
<a
href={resolve('/dashboard/tv/[showId]', {
showId: (request as components['schemas']['RichSeasonRequest']).show.id!
})}
class="text-primary hover:underline"
>
{getFullyQualifiedMediaName( {getFullyQualifiedMediaName(
(request as components['schemas']['RichSeasonRequest']).show (request as components['schemas']['RichSeasonRequest']).show
)} )}
</a>
{:else} {:else}
<a
href={resolve('/dashboard/movies/[movieId]', {
movieId: (request as components['schemas']['RichMovieRequest']).movie.id!
})}
class="text-primary hover:underline"
>
{getFullyQualifiedMediaName( {getFullyQualifiedMediaName(
(request as components['schemas']['RichMovieRequest']).movie (request as components['schemas']['RichMovieRequest']).movie
)} )}
</a>
{/if} {/if}
</Table.Cell> </Table.Cell>
{#if isShow} {#if isShow}

View File

@@ -14,14 +14,19 @@
import DeleteTorrentDialog from '$lib/components/torrents/delete-torrent-dialog.svelte'; import DeleteTorrentDialog from '$lib/components/torrents/delete-torrent-dialog.svelte';
import EditTorrentDialog from '$lib/components/torrents/edit-torrent-dialog.svelte'; import EditTorrentDialog from '$lib/components/torrents/edit-torrent-dialog.svelte';
import { invalidateAll } from '$app/navigation'; import { invalidateAll } from '$app/navigation';
import { resolve } from '$app/paths';
let { let {
torrents, torrents,
isShow = true isShow = true,
showId,
movieId
}: { }: {
torrents: torrents:
| components['schemas']['MovieTorrent'][] | components['schemas']['MovieTorrent'][]
| components['schemas']['RichSeasonTorrent'][]; | components['schemas']['RichSeasonTorrent'][];
isShow: boolean; isShow: boolean;
showId?: string;
movieId?: string;
} = $props(); } = $props();
let user: () => components['schemas']['UserRead'] = getContext('user'); let user: () => components['schemas']['UserRead'] = getContext('user');
@@ -68,7 +73,23 @@
{#each torrents as torrent (torrent.torrent_id)} {#each torrents as torrent (torrent.torrent_id)}
<Table.Row> <Table.Row>
<Table.Cell class="font-medium"> <Table.Cell class="font-medium">
{#if isShow && showId}
<a
href={resolve('/dashboard/tv/[showId]', { showId })}
class="text-primary hover:underline"
>
{torrent.torrent_title} {torrent.torrent_title}
</a>
{:else if !isShow && movieId}
<a
href={resolve('/dashboard/movies/[movieId]', { movieId })}
class="text-primary hover:underline"
>
{torrent.torrent_title}
</a>
{:else}
{torrent.torrent_title}
{/if}
</Table.Cell> </Table.Cell>
{#if isShow} {#if isShow}
<Table.Cell> <Table.Cell>

View File

@@ -112,6 +112,13 @@
width="80px" width="80px"
/></a /></a
>&nbsp;&nbsp; >&nbsp;&nbsp;
<a href="https://buymeacoffee.com/maxdorninger"
><img
src="https://cdn.buymeacoffee.com/uploads/profile_pictures/default/v2/EC9689/SY.png"
width="80px"
alt="syn"
/></a
>
</div> </div>
<p> <p>

View File

@@ -159,7 +159,7 @@
<Card.Description>A list of all torrents associated with this movie.</Card.Description> <Card.Description>A list of all torrents associated with this movie.</Card.Description>
</Card.Header> </Card.Header>
<Card.Content class="flex flex-col gap-4"> <Card.Content class="flex flex-col gap-4">
<TorrentTable isShow={false} torrents={movie.torrents} /> <TorrentTable isShow={false} torrents={movie.torrents} movieId={movie.id} />
</Card.Content> </Card.Content>
</Card.Root> </Card.Root>
</div> </div>

View File

@@ -55,7 +55,7 @@
</Card.Title> </Card.Title>
</Card.Header> </Card.Header>
<Card.Content> <Card.Content>
<TorrentTable isShow={false} torrents={movie.torrents} /> <TorrentTable isShow={false} torrents={movie.torrents} movieId={movie.movie_id} />
</Card.Content> </Card.Content>
</Card.Root> </Card.Root>
</div> </div>

View File

@@ -210,7 +210,7 @@
</Card.Header> </Card.Header>
<Card.Content class="w-full overflow-x-auto"> <Card.Content class="w-full overflow-x-auto">
<TorrentTable isShow={true} torrents={torrents.torrents} /> <TorrentTable isShow={true} torrents={torrents.torrents} showId={show.id} />
</Card.Content> </Card.Content>
</Card.Root> </Card.Root>
</div> </div>

View File

@@ -55,7 +55,7 @@
</Card.Title> </Card.Title>
</Card.Header> </Card.Header>
<Card.Content> <Card.Content>
<TorrentTable isShow={true} torrents={show.torrents} /> <TorrentTable isShow={true} torrents={show.torrents} showId={show.show_id} />
</Card.Content> </Card.Content>
</Card.Root> </Card.Root>
</div> </div>

View File

@@ -31,7 +31,7 @@
<a <a
target="_blank" target="_blank"
class="underline" class="underline"
href="https://maximilian-dorninger.gitbook.io/mediamanager/troubleshooting" href="https://maxdorninger.github.io/MediaManager/latest/troubleshooting/"
> >
Trouble logging in? Trouble logging in?
</a> </a>