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 - 2000 favicon.ico logo.svg - Get MediaManager - https://github.com/maxdorninger/MediaManager/releases + Buy me a coffee ☕ + https://buymeacoffee.com/maxdorninger true https://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}