diff --git a/.github/workflows/build-push-backend.yml b/.github/workflows/build-push-backend.yml
index cec9d99..5a658c3 100644
--- a/.github/workflows/build-push-backend.yml
+++ b/.github/workflows/build-push-backend.yml
@@ -6,6 +6,15 @@ on:
- master
tags:
- 'v*.*.*'
+ - 'v*.*'
+ paths:
+ - 'media_manager/**'
+ - 'alembic/**'
+ - 'alembic.ini'
+ - 'Dockerfile'
+ - 'pyproject.toml'
+ - 'uv.lock'
+ - '.github/workflows/build-push-backend.yml'
workflow_dispatch:
jobs:
diff --git a/.github/workflows/build-push-frontend.yml b/.github/workflows/build-push-frontend.yml
index 4f482a4..fdc2893 100644
--- a/.github/workflows/build-push-frontend.yml
+++ b/.github/workflows/build-push-frontend.yml
@@ -6,6 +6,7 @@ on:
- master
tags:
- 'v*.*.*'
+ - 'v*.*'
paths:
- 'web/**'
- '.github/workflows/build-push-frontend.yml'
diff --git a/Writerside/Writerside_libraries.tree b/Writerside/Writerside_libraries.tree
new file mode 100644
index 0000000..c396ef3
--- /dev/null
+++ b/Writerside/Writerside_libraries.tree
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Writerside/cfg/buildprofiles.xml b/Writerside/cfg/buildprofiles.xml
index 39f7ce5..8299504 100644
--- a/Writerside/cfg/buildprofiles.xml
+++ b/Writerside/cfg/buildprofiles.xml
@@ -7,12 +7,11 @@
false
- 2000favicon.icologo.svg
- Get MediaManager
- https://github.com/maxdorninger/MediaManager/releases
+ Buy me a coffee ☕
+ https://buymeacoffee.com/maxdorningertruehttps://github.com/maxdorninger/MediaManager
diff --git a/Writerside/topics/Indexer-Settings.md b/Writerside/topics/Indexer-Settings.md
index 5a61923..212bfc7 100644
--- a/Writerside/topics/Indexer-Settings.md
+++ b/Writerside/topics/Indexer-Settings.md
@@ -2,8 +2,36 @@
## Prowlarr
-| Variable | Description | Default | Example | Required (if Prowlarr enabled) |
-|--------------------|-------------------------------------|-------------------------|------------------------|--------------------------------|
-| `PROWLARR_ENABLED` | Set to `True` to enable Prowlarr. | `True` | `true` | No |
-| `PROWLARR_API_KEY` | Your Prowlarr API key. | - | `prowlarr_api_key` | Yes |
-| `PROWLARR_URL` | Base URL of your Prowlarr instance. | `http://localhost:9696` | `http://prowlarr:9696` | No |
+### `PROWLARR_ENABLED`
+
+Set to `True` to enable Prowlarr. Default is `False`. Example: `true`.
+
+### `PROWLARR_API_KEY`
+
+This is your Prowlarr API key. Example: `prowlarr_api_key`.
+
+### `PROWLARR_URL`
+
+Base URL of your Prowlarr instance. Default is `http://localhost:9696`. Example: `http://prowlarr:9696`.
+
+## Jackett
+
+### `JACKETT_ENABLED`
+
+Set to `True` to enable Jackett. Default is `False`. Example: `true`.
+
+### `JACKETT_API_KEY`
+
+This is your Prowlarr API key. Example: `jackett_api_key`.
+
+### `JACKETT_URL`
+
+Base URL of your Prowlarr instance. Default is `http://localhost:9117`. Example: `http://prowlarr:9117`.
+
+### `JACKETT_INDEXERS`
+
+List of all indexers for Jackett to search through. Default is `all`. Example: `["1337x","0magnet"]`.
+
+
+
+
\ No newline at end of file
diff --git a/Writerside/topics/User-Guide.md b/Writerside/topics/User-Guide.md
index b29d75c..1ed1244 100644
--- a/Writerside/topics/User-Guide.md
+++ b/Writerside/topics/User-Guide.md
@@ -3,8 +3,8 @@
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:
-__
+qualities of a show/movie necessitate such a paradigm shift.
+__So here is a quick step-by-step guide to get you started:__
diff --git a/Writerside/topics/authentication-setup.md b/Writerside/topics/authentication-setup.md
index 0e40c65..422d5de 100644
--- a/Writerside/topics/authentication-setup.md
+++ b/Writerside/topics/authentication-setup.md
@@ -3,32 +3,61 @@
MediaManager supports multiple authentication methods. Email/password authentication is the default, but you can also
enable OpenID Connect (OAuth 2.0) for integration with external identity providers.
-
Note the lack of a trailing slash in some env vars like FRONTEND_URL. This is important.
-| Variable | Description | Default | Example | Required |
-|-------------------------|--------------------------------------------------------------------------|-----------------|-------------------------------------------|----------|
-| `AUTH_TOKEN_SECRET` | Strong secret key for signing JWTs (create with `openssl rand -hex 32`). | - | `AUTH_TOKEN_SECRET=your_super_secret_key` | Yes |
-| `AUTH_SESSION_LIFETIME` | Lifetime of user sessions in seconds. | `86400` (1 day) | `AUTH_SESSION_LIFETIME=604800` (1 week) | No |
-| `AUTH_ADMIN_EMAIL` | Email address of the administrator accounts. | - | `AUTH_ADMIN_EMAIL=admin@example.com` | Yes |
-| `FRONTEND_URL` | The url the frontend will be accessed from. | - | `https://mediamanager.example` | Yes |
+## General Authentication Settings
+
+### `AUTH_TOKEN_SECRET`
+
+Strong secret key for signing JWTs (create with `openssl rand -hex 32`). This is a required field. Example:
+`AUTH_TOKEN_SECRET=your_super_secret_key`.
+
+### `AUTH_SESSION_LIFETIME`
+
+Lifetime of user sessions in seconds. Default is `86400` (1 day). Example: `AUTH_SESSION_LIFETIME=604800` (1 week).
+
+### `AUTH_ADMIN_EMAIL`
+
+A list of email addresses for administrator accounts. This is a required field. Example:
+`AUTH_ADMIN_EMAIL=admin@example.com`.
+
+### `FRONTEND_URL`
+
+The URL the frontend will be accessed from. This is a required field. Example: `https://mediamanager.example`.
-On login/registration, every user whose email is in `AUTH_ADMIN_EMAIL` will be granted admin privileges.
-Users whose email is not in `AUTH_ADMIN_EMAIL` will be regular users and will need to be verified by an administrator,
+On login/registration, every user whose email is in AUTH_ADMIN_EMAIL will be granted admin privileges.
+Users whose email is not in AUTH_ADMIN_EMAIL will be regular users and will need to be verified by an administrator,
this can be done in the settings page.
+
+
+
+
## OpenID Connect (OAuth 2.0)
-| Variable | Description | Default | Example |
-|---------------------------------|--------------------------------------------------------------------------------------------------|----------|---------------------------------------------------------------------------------------------|
-| `OPENID_ENABLED` | Enables OpenID authentication | `FALSE` | `TRUE` |
-| `OPENID_CLIENT_ID` | Client ID from your OpenID provider. | - | - |
-| `OPENID_CLIENT_SECRET` | Client Secret from your OpenID provider. | - | - |
-| `OPENID_CONFIGURATION_ENDPOINT` | URL of your OpenID provider's discovery document (e.g., `.../.well-known/openid-configuration`). | - | `https://authentik.example.com/application/o/mediamanager/.well-known/openid-configuration` |
-| `OPENID_NAME` | Display name for this OpenID provider. | `OpenID` | `Authentik` |
+### `OPENID_ENABLED`
+
+Enables OpenID authentication. Default is `FALSE`. Example: `TRUE`.
+
+### `OPENID_CLIENT_ID`
+
+Client ID from your OpenID provider.
+
+### `OPENID_CLIENT_SECRET`
+
+Client Secret from your OpenID provider.
+
+### `OPENID_CONFIGURATION_ENDPOINT`
+
+URL of your OpenID provider's discovery document (e.g., `.../.well-known/openid-configuration`). Example:
+`https://authentik.example.com/application/o/mediamanager/.well-known/openid-configuration`.
+
+### `OPENID_NAME`
+
+Display name for this OpenID provider. Default is `OpenID`. Example: `Authentik`.
### Configuring OpenID Connect
diff --git a/Writerside/topics/configuration-backend.md b/Writerside/topics/configuration-backend.md
index a315ce1..25c03b8 100644
--- a/Writerside/topics/configuration-backend.md
+++ b/Writerside/topics/configuration-backend.md
@@ -2,33 +2,62 @@
These variables configure the core backend application, database connections, authentication, and integrations.
+
+
+
+
## General Settings
-| Variable | Description | Default |
-|-----------------|-----------------------------|-----------|
-| `API_BASE_PATH` | The url base of the backend | `/api/v1` |
+### `API_BASE_PATH`
+
+The url base of the backend. Default is `/api/v1`.
+
+### `CORS_URLS`
+
+Enter a list of origins you are going to access the api from. Example: `https://mm.example`.
## Database Settings
-| Variable | Description | Default | Example |
-|---------------|------------------------------------------|----------------|--------------|
-| `DB_HOST` | Hostname or IP of the PostgreSQL server. | `localhost` | `postgres` |
-| `DB_PORT` | Port number of the PostgreSQL server. | `5432` | `5432` |
-| `DB_USER` | Username for PostgreSQL connection. | `MediaManager` | `myuser` |
-| `DB_PASSWORD` | Password for the PostgreSQL user. | `MediaManager` | `mypassword` |
-| `DB_DBNAME` | Name of the PostgreSQL database. | `MediaManager` | `mydatabase` |
+### `DB_HOST`
+
+Hostname or IP of the PostgreSQL server. Default is `localhost`. Example: `postgres`.
+
+### `DB_PORT`
+
+Port number of the PostgreSQL server. Default is `5432`. Example: `5432`.
+
+### `DB_USER`
+
+Username for PostgreSQL connection. Default is `MediaManager`. Example: `myuser`.
+
+### `DB_PASSWORD`
+
+Password for the PostgreSQL user. Default is `MediaManager`. Example: `mypassword`.
+
+### `DB_DBNAME`
+
+Name of the PostgreSQL database. Default is `MediaManager`. Example: `mydatabase`.
## Download Client Settings
Currently, only qBittorrent is supported as a download client. But support for other clients isn't unlikely in the
future.
-| Variable | Description | Default | Example |
-|--------------------|-----------------------------|-------------|--------------------|
-| `QBITTORRENT_HOST` | Host of the QBittorrent API | `localhost` | `qbit.example.com` |
-| `QBITTORRENT_PORT` | Port of the QBittorrent API | `8080` | `443` |
-| `QBITTORRENT_USER` | Username for QBittorrent | `admin` | - |
-| `QBITTORRENT_PASS` | Password for QBittorrent | `admin` | - |
+### `QBITTORRENT_HOST`
+
+Host of the QBittorrent API. Default is `localhost`. Example: `qbit.example.com`.
+
+### `QBITTORRENT_PORT`
+
+Port of the QBittorrent API. Default is `8080`. Example: `443`.
+
+### `QBITTORRENT_USER`
+
+Username for QBittorrent. Default is `admin`.
+
+### `QBITTORRENT_PASS`
+
+Password for QBittorrent. Default is `admin`.
## Metadata Provider Settings
@@ -44,39 +73,48 @@ an account and generate a free API key in your account settings.
Other software like Jellyfin use TMDB as well, so there won't be any metadata discrepancies.
-| Variable | Default | Example |
-|----------------|---------|---------------------------------------|
-| `TMDB_API_KEY` | None | `TMDB_API_KEY=your_tmdb_api_key_here` |
+#### `TMDB_API_KEY`
+
+Your TMDB API key. Example: `your_tmdb_api_key_here`.
### TVDB (The TVDB)
- The TVDB might provide false metadata, also it doesn't support some features of MediaManager like to show overviews, therfore TMDB is the preferred metadata provider.
+ The TVDB might provide false metadata, also it doesn't support some features of MediaManager like to show overviews, therfore TMDB is the preferred metadata provider.
Get an API key from [The TVDB](https://thetvdb.com/auth/register) to use this provider. You can create an account and
generate a free API key in your account settings.
-| Variable | Default | Example |
-|----------------|---------|---------------------------------------|
-| `TVDB_API_KEY` | None | `TVDB_API_KEY=your_tvdb_api_key_here` |
+#### `TVDB_API_KEY`
+
+Your TVDB API key. Example: `your_tvdb_api_key_here`.
## Directory Settings
- Normally you don't need to change these, as the default mountpoints are usually sufficient. In your `docker-compose.yaml`, you can just mount `/any/directory` to `/data/torrents`.
+ Normally you don't need to change these, as the default mountpoints are usually sufficient. In your docker-compose.yaml, you can just mount /any/directory to /data/torrents.
-| Variable | Description | Default |
-|---------------------|---------------------------------------------------|------------------|
-| `IMAGE_DIRECTORY` | media images (posters, backdrops) will be stored. | `/data/images` |
-| `TV_DIRECTORY` | location of TV show files | `/data/tv` |
-| `MOVIE_DIRECTORY` | location of movie files | `/data/movies` |
-| `TORRENT_DIRECTORY` | location of torrent files and downloads | `/data/torrents` |
+### `IMAGE_DIRECTORY`
+
+Media images (posters, backdrops) will be stored here. Default is `/data/images`.
+
+### `TV_DIRECTORY`
+
+Location of TV show files. Default is `/data/tv`.
+
+### `MOVIE_DIRECTORY`
+
+Location of movie files. Default is `/data/movies`.
+
+### `TORRENT_DIRECTORY`
+
+Location of torrent files and downloads. Default is `/data/torrents`.
## Build Arguments (Dockerfile)
-| Argument | Description | Example (in build command) |
-|-----------|--------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------|
-| `VERSION` | Labels the Docker image with a version. Passed during build (e.g., by GitHub Actions). Frontend uses this as `PUBLIC_VERSION`. | `docker build --build-arg VERSION=1.2.3 .` |
+### `VERSION`
+Labels the Docker image with a version. Passed during build (e.g., by GitHub Actions). Frontend uses this as
+`PUBLIC_VERSION`. Example (in build command): `docker build --build-arg VERSION=1.2.3 .`
diff --git a/Writerside/topics/configuration-frontend.md b/Writerside/topics/configuration-frontend.md
index 69cf205..27edabc 100644
--- a/Writerside/topics/configuration-frontend.md
+++ b/Writerside/topics/configuration-frontend.md
@@ -1,10 +1,20 @@
# Frontend
-| Variable | Description | Default | Example |
-|----------------------|----------------------------------------------------------------|--------------------------------|-------------------------------------------|
-| `PUBLIC_WEB_SSR` | Enables/disables Server-Side Rendering. (this is experimental) | `false` | `true` |
-| `PUBLIC_API_URL` | You (the browser) mut reach the backend from this url. | `http://localhost:8000/api/v1` | `https://mediamanager.example.com/api/v1` |
-| `PUBLIC_SSR_API_URL` | The frontent container must reach the backend from this url. | `http://localhost:8000/api/v1` | `http://backend:8000/api/v1` |
+## Environment Variables
+
+### `PUBLIC_WEB_SSR`
+
+Enables/disables Server-Side Rendering. (this is experimental). Default is `false`. Example: `true`.
+
+### `PUBLIC_API_URL`
+
+You (the browser) must reach the backend from this url. Default is `http://localhost:8000/api/v1`. Example:
+`https://mediamanager.example.com/api/v1`.
+
+### `PUBLIC_SSR_API_URL`
+
+The frontend container must reach the backend from this url. Default is `http://localhost:8000/api/v1`. Example:
+`http://backend:8000/api/v1`.
## Build Arguments (web/Dockerfile)
@@ -13,8 +23,12 @@
To configure a url base path for the frontend, you need to build the frontend docker container, this is because
unfortunately SvelteKit needs to know the base path at build time.
-| Argument | Description | Example (in build command) |
-|------------|-----------------------------------------------------------------------------------------------------------|----------------------------------------------------------------|
-| `VERSION` | Sets the `PUBLIC_VERSION` environment variable at runtime in the frontend container. Passed during build. | `docker build --build-arg VERSION=1.2.3 -f web/Dockerfile .` |
-| `BASE_URL` | Sets the base url path, it must begin with a slash and not end | `docker build --build-arg BASE_URL=/media -f web/Dockerfile .` |
+### `VERSION`
+Sets the `PUBLIC_VERSION` environment variable at runtime in the frontend container. Passed during build. Example (in
+build command): `docker build --build-arg VERSION=1.2.3 -f web/Dockerfile .`
+
+### `BASE_URL`
+
+Sets the base url path, it must begin with a slash and not end with one. Example (in build command):
+`docker build --build-arg BASE_URL=/media -f web/Dockerfile .`
diff --git a/Writerside/topics/developer-guide.md b/Writerside/topics/developer-guide.md
index 698fd7f..11a562f 100644
--- a/Writerside/topics/developer-guide.md
+++ b/Writerside/topics/developer-guide.md
@@ -27,19 +27,16 @@ This section is for those who want to contribute to Media Manager or understand
### Backend
-- **Framework:** Python with FastAPI
-- **Database ORM:** SQLAlchemy
-- **Database Migrations:** Alembic
-- **Dependency Management:** uv
+- Python with FastAPI
+- SQLAlchemy
+- Pydantic and Pydantic-Settings
### Frontend
-- **Framework:** SvelteKit
-- **Language:** TypeScript
-- **Styling:** Tailwind CSS
-- **Components:** shadcn-svelte for UI components
+- TypeScript with SvelteKit
+- Tailwind CSS
+- shadcn-svelte
-### Deployment & CI/CD
+### CI/CD
-- Docker & Docker Compose
- GitHub Actions
\ No newline at end of file
diff --git a/Writerside/topics/notes.topic b/Writerside/topics/notes.topic
new file mode 100644
index 0000000..b289760
--- /dev/null
+++ b/Writerside/topics/notes.topic
@@ -0,0 +1,8 @@
+
+
+
+
+ Lists have to be formatted like this: ["item1", "item2", "item3"]. Note the double quotes.
+
+
\ No newline at end of file
diff --git a/Writerside/topics/troubleshooting.md b/Writerside/topics/troubleshooting.md
index 9193f17..e6df201 100644
--- a/Writerside/topics/troubleshooting.md
+++ b/Writerside/topics/troubleshooting.md
@@ -5,17 +5,15 @@
- Always check the container logs for more specific error messages
+ Always check the container and browser logs for more specific error messages
-## Authentication Issues
+## Authentication Issues (OIDC)
-* Double-check `AUTH_TOKEN_SECRET`. If it changes, existing sessions/tokens will be invalidated.
-* For OpenID:
- * Verify `OPENID_CLIENT_ID`, `OPENID_CLIENT_SECRET`, and `OPENID_CONFIGURATION_ENDPOINT` are correct.
- * Ensure the `FRONTEND_URL` is accurate and that your OpenID provider has the correct redirect URI whitelisted (
- e.g., `http://your-frontend-url/api/v1/auth/cookie/Authentik/callback`).
- * Check backend logs for errors from `httpx_oauth` or `fastapi-users`.
+* Verify `OPENID_CLIENT_ID`, `OPENID_CLIENT_SECRET`, and `OPENID_CONFIGURATION_ENDPOINT` are correct.
+* Ensure the `FRONTEND_URL` is accurate and that your OpenID provider has the correct redirect URI whitelisted (
+ e.g., `http://your-frontend-url/api/v1/auth/cookie/Authentik/callback`).
+* Check backend logs for errors from `httpx_oauth` or `fastapi-users`.
## CORS Errors
diff --git a/Writerside/writerside.cfg b/Writerside/writerside.cfg
index baed983..2e8cca9 100644
--- a/Writerside/writerside.cfg
+++ b/Writerside/writerside.cfg
@@ -5,4 +5,5 @@
+
\ No newline at end of file
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 0414354..facb639 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -4,17 +4,16 @@ services:
container_name: backend
ports:
- "8000:8000"
-
+ # In your reverse proxy you will probably need to set rule that only requests with a path prefix
+ # of /api/v1 will be forwarded to this container
+ # if you are using traefik the rule is going to look something like this:
+ # "traefik.http.routers.mm-api.rule=Host(`media.example`)&&PathPrefix(`/api/v1`)"
environment:
- QBITTORRENT_PASSWORD=
- QBITTORRENT_HOST=
- QBITTORRENT_USERNAME=
- QBITTORRENT_PORT=
- - PROWLARR_URL=http://prowlarr:9696
- - PROWLARR_ENABLED=TRUE
- - PROWLARR_API_KEY=
-
- TMDB_API_KEY=
- CORS_URLS=
@@ -54,7 +53,6 @@ services:
environment:
- PUBLIC_API_URL=http://localhost:8000/api/v1
- PUBLIC_SSR_API_URL=http://backend:8000/api/v1
- # - PUBLIC_WEB_SSR=false
db:
image: postgres:latest
restart: unless-stopped
diff --git a/media_manager/auth/config.py b/media_manager/auth/config.py
index 8e49eb7..1765c2e 100644
--- a/media_manager/auth/config.py
+++ b/media_manager/auth/config.py
@@ -7,7 +7,7 @@ class AuthConfig(BaseSettings):
model_config = SettingsConfigDict(env_prefix="AUTH_")
token_secret: str
session_lifetime: int = 60 * 60 * 24
- admin_email: str
+ admin_email: list[str] = []
@property
def jwt_signing_key(self):
diff --git a/media_manager/indexer/__init__.py b/media_manager/indexer/__init__.py
index db2d00b..9f59e42 100644
--- a/media_manager/indexer/__init__.py
+++ b/media_manager/indexer/__init__.py
@@ -1,7 +1,9 @@
import logging
+from media_manager.indexer.config import JackettConfig
+from media_manager.indexer.indexers.jackett import Jackett
from media_manager.indexer.config import ProwlarrConfig
-from media_manager.indexer.indexers.generic import GenericIndexer, IndexerQueryResult
+from media_manager.indexer.indexers.generic import GenericIndexer
from media_manager.indexer.indexers.prowlarr import Prowlarr
log = logging.getLogger(__name__)
@@ -10,3 +12,5 @@ indexers: list[GenericIndexer] = []
if ProwlarrConfig().enabled:
indexers.append(Prowlarr())
+if JackettConfig().enabled:
+ indexers.append(Jackett())
diff --git a/media_manager/indexer/config.py b/media_manager/indexer/config.py
index c878ee2..861e21e 100644
--- a/media_manager/indexer/config.py
+++ b/media_manager/indexer/config.py
@@ -3,6 +3,16 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
class ProwlarrConfig(BaseSettings):
model_config = SettingsConfigDict(env_prefix="PROWLARR_")
- enabled: bool = True
- api_key: str
+ enabled: bool | None = False
+ api_key: str | None
url: str = "http://localhost:9696"
+
+
+class JackettConfig(BaseSettings):
+ model_config = SettingsConfigDict(env_prefix="JACKETT_")
+ enabled: bool | None = False
+ api_key: str | None
+ url: str = "http://localhost:9696"
+ indexers: list[str] = [
+ "all"
+ ]
diff --git a/media_manager/indexer/indexers/generic.py b/media_manager/indexer/indexers/generic.py
index d008873..e0b1fa6 100644
--- a/media_manager/indexer/indexers/generic.py
+++ b/media_manager/indexer/indexers/generic.py
@@ -1,6 +1,3 @@
-from media_manager.indexer.schemas import IndexerQueryResult
-
-
class GenericIndexer(object):
name: str
@@ -10,7 +7,7 @@ class GenericIndexer(object):
else:
raise ValueError("indexer name must not be None")
- def get_search_results(self, query: str) -> list[IndexerQueryResult]:
+ def search(self, query: str) -> list["IndexerQueryResult"]:
"""
Sends a search request to the Indexer and returns the results.
diff --git a/media_manager/indexer/indexers/jackett.py b/media_manager/indexer/indexers/jackett.py
new file mode 100644
index 0000000..f416768
--- /dev/null
+++ b/media_manager/indexer/indexers/jackett.py
@@ -0,0 +1,76 @@
+import logging
+import xml.etree.ElementTree as ET
+from xml.etree.ElementTree import Element
+
+import requests
+
+from media_manager.indexer.indexers.generic import GenericIndexer
+from media_manager.indexer.config import JackettConfig
+from media_manager.indexer.schemas import IndexerQueryResult
+
+log = logging.getLogger(__name__)
+
+
+class Jackett(GenericIndexer):
+ def __init__(self, **kwargs):
+ """
+ A subclass of GenericIndexer for interacting with the Jacket API.
+
+ """
+ super().__init__(name="jackett")
+ config = JackettConfig()
+ self.api_key = config.api_key
+ self.url = config.url
+ self.indexers = config.indexers
+ log.debug("Registering Jacket as Indexer")
+
+ # TODO: change architecture to build query string in the torrent module, instead of tv module
+ # NOTE: this could be done in parallel, but if there aren't more than a dozen indexers, it shouldn't matter
+ def search(self, query: str) -> list[IndexerQueryResult]:
+ log.debug("Searching for " + query)
+
+ responses = []
+ for indexer in self.indexers:
+ log.debug(f"Searching in indexer: {indexer}")
+ url = (
+ self.url
+ + f"/api/v2.0/indexers/{indexer}/results/torznab/api?apikey={self.api_key}&t=tvsearch&q={query}"
+ )
+ response = requests.get(url)
+ responses.append(response)
+
+ xmlns = {
+ "torznab": "http://torznab.com/schemas/2015/feed",
+ "atom": "http://www.w3.org/2005/Atom",
+ }
+ result_list: list[IndexerQueryResult] = []
+ for response in responses:
+ if response.status_code == 200:
+ xml_tree = ET.fromstring(response.content)
+ for item in xml_tree.findall("channel/item"):
+ attributes: list[Element] = [
+ x for x in item.findall("torznab:attr", xmlns)
+ ]
+ for attribute in attributes:
+ if attribute.attrib["name"] == "seeders":
+ seeders = int(attribute.attrib["value"])
+ break
+ else:
+ log.warning(
+ f"Seeders not found in torrent: {item.find('title').text}, skipping this torrent"
+ )
+ continue
+
+ result = IndexerQueryResult(
+ title=item.find("title").text,
+ download_url=item.find("link").text,
+ seeders=seeders,
+ flags=[],
+ size=int(item.find("size").text),
+ )
+ result_list.append(result)
+ log.debug(f"Raw result: {result.model_dump()}")
+ else:
+ log.error(f"Jacket Error: {response.status_code}")
+ return []
+ return result_list
diff --git a/media_manager/indexer/indexers/prowlarr.py b/media_manager/indexer/indexers/prowlarr.py
index 1e3dd0a..a4f1268 100644
--- a/media_manager/indexer/indexers/prowlarr.py
+++ b/media_manager/indexer/indexers/prowlarr.py
@@ -2,7 +2,7 @@ import logging
import requests
-from media_manager.indexer import GenericIndexer
+from media_manager.indexer.indexers.generic import GenericIndexer
from media_manager.indexer.config import ProwlarrConfig
from media_manager.indexer.schemas import IndexerQueryResult
@@ -23,7 +23,7 @@ class Prowlarr(GenericIndexer):
self.url = config.url
log.debug("Registering Prowlarr as Indexer")
- def get_search_results(self, query: str) -> list[IndexerQueryResult]:
+ def search(self, query: str) -> list[IndexerQueryResult]:
log.debug("Searching for " + query)
url = self.url + "/api/v1/search"
headers = {"accept": "application/json", "X-Api-Key": self.api_key}
diff --git a/media_manager/indexer/schemas.py b/media_manager/indexer/schemas.py
index d9b5ed0..dc685be 100644
--- a/media_manager/indexer/schemas.py
+++ b/media_manager/indexer/schemas.py
@@ -10,7 +10,6 @@ from media_manager.torrent.models import Quality
IndexerQueryResultId = typing.NewType("IndexerQueryResultId", UUID)
-# TODO: use something like strategy pattern to make sorting more user customizable
class IndexerQueryResult(BaseModel):
model_config = ConfigDict(from_attributes=True)
diff --git a/media_manager/indexer/service.py b/media_manager/indexer/service.py
index 0a6b67d..ba69438 100644
--- a/media_manager/indexer/service.py
+++ b/media_manager/indexer/service.py
@@ -1,9 +1,9 @@
from sqlalchemy.orm import Session
import media_manager.indexer.repository
-from media_manager.indexer import IndexerQueryResult, log, indexers
+from media_manager.indexer import log, indexers
from media_manager.indexer.repository import save_result
-from media_manager.indexer.schemas import IndexerQueryResultId
+from media_manager.indexer.schemas import IndexerQueryResultId, IndexerQueryResult
def search(query: str, db: Session) -> list[IndexerQueryResult]:
@@ -12,7 +12,7 @@ def search(query: str, db: Session) -> list[IndexerQueryResult]:
log.debug(f"Searching for Torrent: {query}")
for i in indexers:
- results.extend(i.get_search_results(query))
+ results.extend(i.search(query))
for result in results:
save_result(result=result, db=db)
log.debug(f"Found Torrents: {results}")
diff --git a/media_manager/main.py b/media_manager/main.py
index a6e7a02..e5447e6 100644
--- a/media_manager/main.py
+++ b/media_manager/main.py
@@ -5,8 +5,6 @@ from logging.config import dictConfig
from pythonjsonlogger.json import JsonFormatter
-import torrent.service
-from database import SessionLocal
LOGGING_CONFIG = {
"version": 1,
@@ -59,6 +57,8 @@ from datetime import datetime
from contextlib import asynccontextmanager
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
+import media_manager.torrent.service
+from media_manager.database import SessionLocal
init_db()
log.info("Database initialized")
@@ -77,37 +77,31 @@ else:
def hourly_tasks():
log.info(f"Tasks are running at {datetime.now()}")
auto_download_all_approved_season_requests()
- torrent.service.TorrentService(db=SessionLocal()).import_all_torrents()
+ media_manager.torrent.service.TorrentService(
+ db=SessionLocal()
+ ).import_all_torrents()
scheduler = BackgroundScheduler()
trigger = CronTrigger(minute=0, hour="*")
scheduler.add_job(hourly_tasks, trigger)
+scheduler.start()
@asynccontextmanager
async def lifespan(app: FastAPI):
- scheduler.start()
yield
scheduler.shutdown()
-app = FastAPI(lifespan=lifespan)
-
-
base_path = os.getenv("API_BASE_PATH") or "/api/v1"
log.info("Base Path for API: %s", base_path)
-app = FastAPI(root_path=base_path)
+app = FastAPI(root_path=base_path, lifespan=lifespan)
-if basic_config.DEVELOPMENT:
- origins = [
- "*",
- ]
-else:
- origins = basic_config.CORS_URLS.split(",")
- log.info("CORS URLs activated for following origins:")
- for origin in origins:
- log.info(f" - {origin}")
+origins = basic_config.CORS_URLS.split(",")
+log.info("CORS URLs activated for following origins:")
+for origin in origins:
+ log.info(f" - {origin}")
app.add_middleware(
CORSMiddleware,
diff --git a/media_manager/torrent/service.py b/media_manager/torrent/service.py
index 327816f..84a00c7 100644
--- a/media_manager/torrent/service.py
+++ b/media_manager/torrent/service.py
@@ -15,7 +15,7 @@ import media_manager.torrent.repository
import media_manager.tv.repository
import media_manager.tv.service
from media_manager.config import BasicConfig
-from media_manager.indexer import IndexerQueryResult
+from media_manager.indexer.schemas import IndexerQueryResult
from media_manager.torrent.repository import (
get_seasons_files_of_torrent,
get_show_of_torrent,
diff --git a/media_manager/tv/service.py b/media_manager/tv/service.py
index 9b58287..7681758 100644
--- a/media_manager/tv/service.py
+++ b/media_manager/tv/service.py
@@ -5,7 +5,7 @@ import media_manager.metadataProvider
import media_manager.torrent.repository
import media_manager.tv.repository
from media_manager.database import SessionLocal
-from media_manager.indexer import IndexerQueryResult
+from media_manager.indexer.schemas import IndexerQueryResult
from media_manager.indexer.schemas import IndexerQueryResultId
from media_manager.metadataProvider.schemas import MetaDataProviderShowSearchResult
from media_manager.torrent.repository import get_seasons_files_of_torrent
diff --git a/web/src/lib/components/app-sidebar.svelte b/web/src/lib/components/app-sidebar.svelte
index a62de73..778890b 100644
--- a/web/src/lib/components/app-sidebar.svelte
+++ b/web/src/lib/components/app-sidebar.svelte
@@ -40,12 +40,12 @@
navSecondary: [
{
title: 'Support',
- url: '#',
+ url: 'https://github.com/maxdorninger/MediaManager/issues',
icon: LifeBuoy
},
{
title: 'Feedback',
- url: '#',
+ url: 'https://github.com/maxdorninger/MediaManager/issues',
icon: Send
},
{
diff --git a/web/src/lib/components/season-requests-table.svelte b/web/src/lib/components/season-requests-table.svelte
index 87ecc69..58d02f2 100644
--- a/web/src/lib/components/season-requests-table.svelte
+++ b/web/src/lib/components/season-requests-table.svelte
@@ -124,6 +124,7 @@
{request.authorized_by?.email ?? 'N/A'}
+
{#if user().is_superuser}