diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index fe24f4f8..2afdb9db 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -1,20 +1,19 @@
----
-name: Documentation Issue Report
-about: Create a report to help us improve
-title: ''
-labels: ''
-assignees: ''
-
----
-
-**Describe the Issue**
-A clear and concise description of where in the docs you are seeing an issue.
-
-**The Docs Page**
-What page are you seeing something that is incorrect?
-
-**Suggested Correction**
-Do you have a suggestion on how we can correct it?
-
-**Additional context**
-Add any other context about the problem here.
+---
+name: Documentation Issue Report
+about: Create a report to help us improve
+title: ""
+labels: ""
+assignees: ""
+---
+
+**Describe the Issue**
+A clear and concise description of where in the docs you are seeing an issue.
+
+**The Docs Page**
+What page are you seeing something that is incorrect?
+
+**Suggested Correction**
+Do you have a suggestion on how we can correct it?
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 4727bb2b..d0ad7217 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,8 +1,8 @@
-blank_issues_enabled: false
-contact_links:
- - name: 🚀 Feature Requests
- url: https://features.maintainerr.info
- about: Please share and vote on feature ideas here.
- - name: 🙋 Discord Support
- url: https://discord.maintainerr.info
- about: Join our Discord community to ask questions and get help.
\ No newline at end of file
+blank_issues_enabled: false
+contact_links:
+ - name: 🚀 Feature Requests
+ url: https://features.maintainerr.info
+ about: Please share and vote on feature ideas here.
+ - name: 🙋 Discord Support
+ url: https://discord.maintainerr.info
+ about: Join our Discord community to ask questions and get help.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..16271e91
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,16 @@
+version: 2
+updates:
+ - package-ecosystem: npm
+ directory: "/"
+ schedule:
+ interval: weekly
+ groups:
+ docusaurus:
+ patterns:
+ - "@docusaurus/*"
+ - "@easyops-cn/docusaurus-search-local"
+
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: weekly
diff --git a/.github/workflows/dependabot_merge.yml b/.github/workflows/dependabot_merge.yml
new file mode 100644
index 00000000..192efae2
--- /dev/null
+++ b/.github/workflows/dependabot_merge.yml
@@ -0,0 +1,47 @@
+name: Dependabot auto-merge
+on: pull_request_target
+
+# Least privilege: every write (approve + merge) is performed with the
+# GitHub App token below, so the workflow's own GITHUB_TOKEN stays read-only.
+permissions:
+ contents: read
+ pull-requests: read
+
+jobs:
+ dependabot:
+ runs-on: ubuntu-latest
+ if: ${{ github.actor == 'dependabot[bot]' }}
+ steps:
+ - name: Create GitHub App token
+ id: app-token
+ uses: actions/create-github-app-token@v3
+ with:
+ app-id: ${{ secrets.DOCS_APP_ID }}
+ private-key: ${{ secrets.DOCS_APP_KEY }}
+
+ - name: Dependabot metadata
+ id: metadata
+ uses: dependabot/fetch-metadata@v3.1.0
+ with:
+ github-token: "${{ steps.app-token.outputs.token }}"
+ skip-commit-verification: true
+
+ - name: Approve PR + merge after required checks pass
+ if: ${{ steps.metadata.outputs.update-type != 'version-update:semver-major' }}
+ run: |
+ set -euo pipefail
+ review_decision=$(gh pr view "$PR_URL" --json reviewDecision -q .reviewDecision)
+ if [ "$review_decision" != "APPROVED" ]; then
+ gh pr review --approve "$PR_URL"
+ else
+ echo "PR already approved."
+ fi
+
+ echo "Waiting for required checks to complete successfully..."
+ gh pr checks "$PR_URL" --required --watch
+
+ echo "Required checks passed. Merging..."
+ gh pr merge --admin --squash "$PR_URL"
+ env:
+ PR_URL: ${{ github.event.pull_request.html_url }}
+ GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
diff --git a/.github/workflows/docusaurus_ci.yml b/.github/workflows/docusaurus_ci.yml
index 665fcde7..93d8bebf 100644
--- a/.github/workflows/docusaurus_ci.yml
+++ b/.github/workflows/docusaurus_ci.yml
@@ -1,12 +1,15 @@
name: Docusaurus Deploy
on:
+ push:
+ branches:
+ - main
repository_dispatch:
types: [maintainerr-release]
workflow_dispatch:
inputs:
versionInput:
- description: 'Input version from Maintainerr release'
+ description: "Input version from Maintainerr release"
required: false
permissions:
@@ -39,7 +42,7 @@ jobs:
echo "email=${user_id}+${APP_SLUG}[bot]@users.noreply.github.com" >> "$GITHUB_OUTPUT"
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.ref || github.ref }}
@@ -70,9 +73,9 @@ jobs:
fi
- name: Setup Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@v6
with:
- node-version: 20
+ node-version: 24
cache: npm
- name: Install dependencies
diff --git a/.github/workflows/pr_build.yml b/.github/workflows/pr_build.yml
new file mode 100644
index 00000000..b6c3ed56
--- /dev/null
+++ b/.github/workflows/pr_build.yml
@@ -0,0 +1,32 @@
+name: PR Build
+
+on:
+ pull_request:
+ branches:
+ - main
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: 24
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Build Docusaurus site
+ run: npm run build
diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
new file mode 100644
index 00000000..8276e890
--- /dev/null
+++ b/.github/workflows/quality.yml
@@ -0,0 +1,33 @@
+name: Code quality checks
+
+on:
+ pull_request:
+ branches:
+ - main
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ formatting:
+ name: Formatting
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: 24
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Check formatting
+ run: npm run format:check
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 00000000..67f21116
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,16 @@
+# Build output & generated artifacts
+build/
+.docusaurus/
+node_modules/
+
+# Lockfile (managed by npm)
+package-lock.json
+
+# Frozen documentation snapshots — created by `docusaurus docs:version`.
+# These mirror past releases and should stay byte-stable, so we don't reformat them.
+versioned_docs/
+versioned_sidebars/
+
+# Vendored / generated assets we don't own
+static/assets/
+static/openapi-spec/
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 00000000..634de20d
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,5 @@
+{
+ "proseWrap": "preserve",
+ "singleQuote": false,
+ "trailingComma": "all"
+}
diff --git a/blog/posts/Tutorial_S01E01.md b/blog/posts/Tutorial_S01E01.md
index 65d51037..bc834888 100644
--- a/blog/posts/Tutorial_S01E01.md
+++ b/blog/posts/Tutorial_S01E01.md
@@ -19,21 +19,21 @@ Let's take the above movie as an example. This isn't a real movie, but for the p
**Plex**
-| Added | Last Viewed | Times Viewed | Audience Rating |
-| -------|-------------|--------------|---------------- |
-| 3Nov2023 | 10Jan2024 | 4 | 7.3 |
+| Added | Last Viewed | Times Viewed | Audience Rating |
+| -------- | ----------- | ------------ | --------------- |
+| 3Nov2023 | 10Jan2024 | 4 | 7.3 |
**Overseerr**
| Requested by | Requested Date | Times Requested by Anyone |
| ------------ | -------------- | ------------------------- |
-| user_girl123 | 2Nov2023 | 4 |
+| user_girl123 | 2Nov2023 | 4 |
**Radarr**
-| Release Date | Is Monitored | Runtime |
-| ------------ | ------------ | ------- |
-| 31Oct2023 | True | 114 minutes |
+| Release Date | Is Monitored | Runtime |
+| ------------ | ------------ | ----------- |
+| 31Oct2023 | True | 114 minutes |
:::tip
This information can be looked at for an actual item in your Plex library. You can do it through the Plex UI or you can parse through the XML of an item.
@@ -90,7 +90,7 @@ Now we are getting into the details. After the settings above have been configur
Community rules are just that, from the community. They may not work like they say they do. They may not be formatted properly. It can be a nice place to start, but I personally wouldn't rely on them as everyone's situation is different.
:::
-Below the *Community* button there are two other buttons: Import and Export. You can import a rule from a txt file in YAML format. This is an advanced method, but it is an option.
+Below the _Community_ button there are two other buttons: Import and Export. You can import a rule from a txt file in YAML format. This is an advanced method, but it is an option.
:::tip
If you want to go down this path, I would choose one of the Community rules, it doesn't really matter which one, and then use the export button. You will get a generated YAML format of the rule. If you are familiar with YAML this will be a good place to start.
@@ -106,7 +106,7 @@ If you are looking at a rule that you have already created, you can export the r
Understanding rules and sections can be crucial to getting your rule setup properly and achieving your desired outcome.
-When you first start, you will be in the first *section*: Section 1, Rule 1. A section is a grouped set of rules with one output over the whole section, depending on what was matched in the rules within that section.
+When you first start, you will be in the first _section_: Section 1, Rule 1. A section is a grouped set of rules with one output over the whole section, depending on what was matched in the rules within that section.
### AND
@@ -116,7 +116,7 @@ So let's try an **AND** example.
- Our rule 2 setup gains us output X.
- Our rule 2 is set up with an AND operator to rule 1.
- The output of this section would be X only.
- Since X was matched by rule 1 AND rule 2 but Y was only matched by rule 1, it will not be included in the section output because we said we want the output of the *section* to be everything that matched **(rule 1 AND rule 2)**.
+ Since X was matched by rule 1 AND rule 2 but Y was only matched by rule 1, it will not be included in the section output because we said we want the output of the _section_ to be everything that matched **(rule 1 AND rule 2)**.
### OR
@@ -126,7 +126,7 @@ Now an **OR** example.
- Our rule 2 setup gains us output X and output Z.
- Our rule 2 is set up with an OR operator to rule 1.
- The output of this section would be X, Y, and Z.
- Since X and Y were matched by rule 1, and X and Z were matched by rule 2, they are all included because we said we want the output of the *section* to be everything that matched **(rule 1 OR rule 2)**.
+ Since X and Y were matched by rule 1, and X and Z were matched by rule 2, they are all included because we said we want the output of the _section_ to be everything that matched **(rule 1 OR rule 2)**.
This is probably the simplest form of a rule setup that you can get, unless of course you are only using one rule because anything matched by that one rule becomes the output.
@@ -140,8 +140,8 @@ Now let's try a mixed **AND** / **OR** example.
- The output is X, as before.
- Rule 3 setup gains us output G and output M.
- Rule 3 is set up with an OR operator to rule 2.
-- The output of the whole *section* would be X, G, and M.
- Since X was matched by rule 1 AND rule 2 but Y was only matched by rule 1, the output of rule 2 will be only X. Since G and M were matched by rule 3, and we said we want the output of the *section* to be anything that matches **((rule 1 AND 2) OR rule 3)**, we get X, G, and M.
+- The output of the whole _section_ would be X, G, and M.
+ Since X was matched by rule 1 AND rule 2 but Y was only matched by rule 1, the output of rule 2 will be only X. Since G and M were matched by rule 3, and we said we want the output of the _section_ to be anything that matches **((rule 1 AND 2) OR rule 3)**, we get X, G, and M.
---
diff --git a/blog/posts/Tutorial_S01E02.md b/blog/posts/Tutorial_S01E02.md
index 239ebab2..6cae228b 100644
--- a/blog/posts/Tutorial_S01E02.md
+++ b/blog/posts/Tutorial_S01E02.md
@@ -21,21 +21,21 @@ This movie has the following attributes across Plex, Overseerr, and Radarr:
**Plex**
-| Added | Last Viewed | Times Viewed | Audience Rating |
-| -------|-------------|--------------|---------------- |
-| 3Nov2023 | 10Jan2024 | 4 | 7.3 |
+| Added | Last Viewed | Times Viewed | Audience Rating |
+| -------- | ----------- | ------------ | --------------- |
+| 3Nov2023 | 10Jan2024 | 4 | 7.3 |
**Overseerr**
| Requested by | Requested Date | Times Requested by Anyone |
| ------------ | -------------- | ------------------------- |
-| user_girl123 | 2Nov2023 | 4 |
+| user_girl123 | 2Nov2023 | 4 |
**Radarr**
-| Release Date | Is Monitored | Runtime |
-| ------------ | ------------ | ------- |
-| 31Oct2023 | True | 114 minutes |
+| Release Date | Is Monitored | Runtime |
+| ------------ | ------------ | ----------- |
+| 31Oct2023 | True | 114 minutes |
With this information, we have quite a few options that we can use as rule parameters and filters.
@@ -46,23 +46,23 @@ With this information, we have quite a few options that we can use as rule param
- 1: We can use one rule that states `Plex-Date Added` `before` `amount of days` `60`.
- This will match in our special tutorial scenario because the day the Movie was added to Plex happened 60 days or more "before" today's date.
- 2: We could use a rule that states `Plex-Times Viewed` `bigger` `number` `3`.
- - This would get added because it has a *Times Viewed* value of 4, which is bigger than 3.
+ - This would get added because it has a _Times Viewed_ value of 4, which is bigger than 3.
- 3: We could also use a rule that states `Plex-Audience Rating (scale 1-10)` `bigger` `number` `5`.
- - This rule would catch our movie because its *Audience Rating* is 7.3, which is bigger than 5.
+ - This rule would catch our movie because its _Audience Rating_ is 7.3, which is bigger than 5.
## Simple AND
- 1: We could add Rule 1 that states `Plex-Date Added` `before` `amount of days` `60`. Rule 2 then states AND `Plex-Times Viewed` `bigger` `number` `5`.
- - This would not catch our movie because it has a *Times Viewed* value of 4 and we need it to match **(rule 1 AND rule 2)**. It does match rule 1 but it does not match rule 2. If another movie in the library was added 60 days ago or more **AND** it had a view count of more than 5, it **would** get added to this rule.
+ - This would not catch our movie because it has a _Times Viewed_ value of 4 and we need it to match **(rule 1 AND rule 2)**. It does match rule 1 but it does not match rule 2. If another movie in the library was added 60 days ago or more **AND** it had a view count of more than 5, it **would** get added to this rule.
**Let's try one more.**
- 2: Rule 1 states `Plex-Times Viewed` `bigger` `number` `3`. Rule 2 states AND `Overseerr-Amount of Requests` `equals` `Plex-Times Viewed`.
- - This rule set **would** add our movie because its *Times Viewed* amount is 4, which is bigger than 3, **AND** the *Amount of Requests* from Overseerr equals the *Times Viewed* amount from Plex: **(Rule 1 AND Rule 2)**.
+ - This rule set **would** add our movie because its _Times Viewed_ amount is 4, which is bigger than 3, **AND** the _Amount of Requests_ from Overseerr equals the _Times Viewed_ amount from Plex: **(Rule 1 AND Rule 2)**.
-Those are some fairly simple AND examples, and hopefully it is starting to become obvious what is going on. Within a *section*, and only using AND operators, each item also needs to match the rule before it to be counted as a match and added to the collection.
+Those are some fairly simple AND examples, and hopefully it is starting to become obvious what is going on. Within a _section_, and only using AND operators, each item also needs to match the rule before it to be counted as a match and added to the collection.
-Another way to look at these examples is that within a *section*, each rule is making a list. The next rule is checking that list to see if anything also has that value, plus the value of its own rule.
+Another way to look at these examples is that within a _section_, each rule is making a list. The next rule is checking that list to see if anything also has that value, plus the value of its own rule.
### Visual Example
@@ -79,10 +79,10 @@ title:>Rule-set: Rule1 AND Rule2]
We don't have to go too far in depth with this because of what we have already learned. We will just give a quick example, then a visual.
- 1: We can use one rule that states `Plex-Date Added` `before` `amount of days` `90`.
- - This will not match in our special tutorial scenario because the day the Movie was added to Plex happened only *60* days before today's date. Not quite *90* days yet.
+ - This will not match in our special tutorial scenario because the day the Movie was added to Plex happened only _60_ days before today's date. Not quite _90_ days yet.
- 2: Our next rule states OR `Overseerr-Requested by user (Plex or local username)` `Contains (Partial list match)` `text` `user_girl123`.
- This would match because, as we can see, that is who requested the movie.
-- 3: This rule set **would** add our movie because it meets one **OR** the other of our criteria. It was added *60* days ago so it does not meet our criteria of *before 90 days*, but it did match the Overseerr requested-by-user rule. It gets added because we said we wanted **(Rule 1 OR Rule 2)**.
+- 3: This rule set **would** add our movie because it meets one **OR** the other of our criteria. It was added _60_ days ago so it does not meet our criteria of _before 90 days_, but it did match the Overseerr requested-by-user rule. It gets added because we said we wanted **(Rule 1 OR Rule 2)**.
Now let's get a visual.
diff --git a/blog/posts/Tutorial_S01E03.md b/blog/posts/Tutorial_S01E03.md
index 73baf17a..13cc99b2 100644
--- a/blog/posts/Tutorial_S01E03.md
+++ b/blog/posts/Tutorial_S01E03.md
@@ -93,7 +93,7 @@ B-.AND.->C
C ==> D(Results)
```
-This is the same thing as putting all of those rules in one section: `Section 1 results` AND `Section 2 results` AND `Section 3 results`. There is no need to do this and you should keep them all in one section: *(Section 1: Rule 1 AND Rule 2 AND Rule 3)*.
+This is the same thing as putting all of those rules in one section: `Section 1 results` AND `Section 2 results` AND `Section 3 results`. There is no need to do this and you should keep them all in one section: _(Section 1: Rule 1 AND Rule 2 AND Rule 3)_.
## Closing
diff --git a/docs/API.md b/docs/API.md
index b59f41e8..e79446cb 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -9,12 +9,11 @@ hide:
status: recent
---
-
-
:::danger
:fire: :fire: The API, and all of Maintainerr for that matter, does not have an authentication method. There are certain API calls, that if you make your instance public facing, will expose your entire settings configuration. This could include all of your service's API keys. Proceed with extreme caution if you choose to expose Maintainerr to the public. :fire: :fire:
:::
+
## API endpoints
:::info
@@ -31,16 +30,20 @@ These are some of the newer user-facing API groups that are relevant to the curr
Maintainerr exposes lightweight health endpoints under `/api/health` (prefixed with `BASE_PATH` when set) for orchestration probes and uptime monitoring.
-| Endpoint | Purpose |
-| --- | --- |
-| `GET /api/health/live` | Liveness probe; returns `200` while the process is running and does not touch the database |
-| `GET /api/health/ready` | Readiness probe; runs a database `SELECT 1` check and returns `200` or `503` |
-| `GET /api/health` | Convenience alias that mirrors `/api/health/ready` |
+| Endpoint | Purpose |
+| ----------------------- | ------------------------------------------------------------------------------------------ |
+| `GET /api/health/live` | Liveness probe; returns `200` while the process is running and does not touch the database |
+| `GET /api/health/ready` | Readiness probe; runs a database `SELECT 1` check and returns `200` or `503` |
+| `GET /api/health` | Convenience alias that mirrors `/api/health/ready` |
`GET /api/health/live` returns a lightweight envelope such as:
```json
-{ "status": "ok", "uptimeSeconds": 1234, "timestamp": "2026-06-05T12:00:00.000Z" }
+{
+ "status": "ok",
+ "uptimeSeconds": 1234,
+ "timestamp": "2026-06-05T12:00:00.000Z"
+}
```
`GET /api/health/ready` and `GET /api/health` include database status:
@@ -58,83 +61,83 @@ Maintainerr exposes lightweight health endpoints under `/api/health` (prefixed w
### Collections
-| Endpoint | Purpose |
-| --- | --- |
-| `GET /api/collections/overlay-data` | Returns collections with full media membership for overlay consumers, including the Calendar page |
-| `POST /api/collections/media/handle` | Run the configured collection action immediately for one item from the collection detail modal |
-| `GET /api/collections/:id/poster` | Return the stored custom collection poster as `image/jpeg`, or `404` when none exists |
-| `POST /api/collections/:id/poster` | Upload a custom collection poster with multipart field `poster`; returns `{ pushed, attempted }` |
-| `DELETE /api/collections/:id/poster` | Clear the stored poster and return `{ cleared, refreshRequested }` |
+| Endpoint | Purpose |
+| ------------------------------------ | ------------------------------------------------------------------------------------------------- |
+| `GET /api/collections/overlay-data` | Returns collections with full media membership for overlay consumers, including the Calendar page |
+| `POST /api/collections/media/handle` | Run the configured collection action immediately for one item from the collection detail modal |
+| `GET /api/collections/:id/poster` | Return the stored custom collection poster as `image/jpeg`, or `404` when none exists |
+| `POST /api/collections/:id/poster` | Upload a custom collection poster with multipart field `poster`; returns `{ pushed, attempted }` |
+| `DELETE /api/collections/:id/poster` | Clear the stored poster and return `{ cleared, refreshRequested }` |
### Metadata
-| Endpoint | Purpose |
-| --- | --- |
-| `GET /api/metadata/backdrop/:type` | Resolve a backdrop image for a movie or show from the configured metadata providers |
-| `GET /api/metadata/image/:type` | Resolve a poster image for a movie or show from the configured metadata providers |
-| `GET /api/settings/tmdb` | Read the saved TMDB API key state |
-| `POST /api/settings/tmdb` | Save a TMDB API key |
-| `DELETE /api/settings/tmdb` | Remove the saved TMDB API key |
-| `GET /api/settings/tvdb` | Read the saved TVDB API key state |
-| `POST /api/settings/tvdb` | Save a TVDB API key |
-| `DELETE /api/settings/tvdb` | Remove the saved TVDB API key |
-| `GET /api/settings/metadata-provider` | Read which metadata provider is currently primary |
-| `POST /api/settings/metadata-provider` | Change the primary metadata provider |
-| `POST /api/settings/metadata/refresh/:provider` | Clear cached metadata for TMDB or TVDB and queue a media-server refresh pass |
+| Endpoint | Purpose |
+| ----------------------------------------------- | ----------------------------------------------------------------------------------- |
+| `GET /api/metadata/backdrop/:type` | Resolve a backdrop image for a movie or show from the configured metadata providers |
+| `GET /api/metadata/image/:type` | Resolve a poster image for a movie or show from the configured metadata providers |
+| `GET /api/settings/tmdb` | Read the saved TMDB API key state |
+| `POST /api/settings/tmdb` | Save a TMDB API key |
+| `DELETE /api/settings/tmdb` | Remove the saved TMDB API key |
+| `GET /api/settings/tvdb` | Read the saved TVDB API key state |
+| `POST /api/settings/tvdb` | Save a TVDB API key |
+| `DELETE /api/settings/tvdb` | Remove the saved TVDB API key |
+| `GET /api/settings/metadata-provider` | Read which metadata provider is currently primary |
+| `POST /api/settings/metadata-provider` | Change the primary metadata provider |
+| `POST /api/settings/metadata/refresh/:provider` | Clear cached metadata for TMDB or TVDB and queue a media-server refresh pass |
### Media server settings
-| Endpoint | Purpose |
-| --- | --- |
-| `GET /api/settings/emby` | Read the saved Emby URL, API key, and selected admin user |
-| `POST /api/settings/emby/test` | Test an Emby URL and API key, and return available admin users |
-| `POST /api/settings/emby` | Save Emby connection settings |
-| `DELETE /api/settings/emby` | Remove the saved Emby connection settings |
+| Endpoint | Purpose |
+| ------------------------------- | ----------------------------------------------------------------------------------------------- |
+| `GET /api/settings/emby` | Read the saved Emby URL, API key, and selected admin user |
+| `POST /api/settings/emby/test` | Test an Emby URL and API key, and return available admin users |
+| `POST /api/settings/emby` | Save Emby connection settings |
+| `DELETE /api/settings/emby` | Remove the saved Emby connection settings |
| `POST /api/settings/emby/login` | Authenticate with an Emby admin username/password and return an API key plus admin-user choices |
### Streamystats (Jellyfin only)
-| Endpoint | Purpose |
-| --- | --- |
-| `GET /api/settings/streamystats` | Read the saved Streamystats base URL |
-| `POST /api/settings/test/streamystats` | Test a Streamystats URL using the currently configured Jellyfin API key |
-| `POST /api/settings/streamystats` | Save the Streamystats base URL |
-| `DELETE /api/settings/streamystats` | Remove the saved Streamystats base URL |
-| `GET /api/streamystats/info` | Return the configured Streamystats URL plus the resolved Jellyfin server id used for deep links |
-| `GET /api/streamystats/items/:itemId` | Return Streamystats watch-history totals, per-user stats, and episode progress for one Jellyfin item |
+| Endpoint | Purpose |
+| -------------------------------------- | ---------------------------------------------------------------------------------------------------- |
+| `GET /api/settings/streamystats` | Read the saved Streamystats base URL |
+| `POST /api/settings/test/streamystats` | Test a Streamystats URL using the currently configured Jellyfin API key |
+| `POST /api/settings/streamystats` | Save the Streamystats base URL |
+| `DELETE /api/settings/streamystats` | Remove the saved Streamystats base URL |
+| `GET /api/streamystats/info` | Return the configured Streamystats URL plus the resolved Jellyfin server id used for deep links |
+| `GET /api/streamystats/items/:itemId` | Return Streamystats watch-history totals, per-user stats, and episode progress for one Jellyfin item |
### Overlays
-| Endpoint | Purpose |
-| --- | --- |
-| `GET /api/overlays/settings` | Read global overlay settings |
-| `PUT /api/overlays/settings` | Update global overlay settings |
-| `GET /api/overlays/sections` | List media server library sections used by the template preview picker |
-| `GET /api/overlays/random-item` | Get a random media item for poster-template preview |
-| `GET /api/overlays/random-episode` | Get a random episode for title-card preview |
-| `GET /api/overlays/poster` | Proxy media artwork for template preview |
-| `GET /api/overlays/status` | Read the latest overlay processing status |
-| `POST /api/overlays/process` | Run overlay processing for all eligible collections |
-| `POST /api/overlays/process/:collectionId` | Run overlay processing for one collection |
-| `POST /api/overlays/revert/:collectionId` | Revert overlays for one collection |
-| `DELETE /api/overlays/reset` | Revert all overlays |
-| `GET /api/overlays/fonts` | List available fonts |
-| `GET /api/overlays/fonts/:name` | Read a bundled or uploaded font file |
-| `POST /api/overlays/fonts` | Upload a `.ttf`, `.otf`, or `.woff` font |
-| `GET /api/overlays/images` | List uploaded overlay image assets |
-| `GET /api/overlays/images/:name` | Read an uploaded overlay image asset |
-| `POST /api/overlays/images` | Upload a `.png`, `.jpg`/`.jpeg`, or `.webp` image for template image elements |
-| `DELETE /api/overlays/images/:name` | Delete an uploaded overlay image asset |
-| `GET /api/overlays/templates` | List overlay templates |
-| `GET /api/overlays/templates/:id` | Fetch one template |
-| `POST /api/overlays/templates` | Create a template |
-| `PUT /api/overlays/templates/:id` | Update a template |
-| `DELETE /api/overlays/templates/:id` | Delete a non-preset template |
-| `POST /api/overlays/templates/:id/duplicate` | Clone a template into an editable copy |
-| `POST /api/overlays/templates/:id/default` | Set a template as the default for its mode |
-| `POST /api/overlays/templates/:id/export` | Export a template as JSON |
-| `POST /api/overlays/templates/import` | Import a template from JSON |
-| `POST /api/overlays/templates/:id/preview` | Render a server-side preview of a template on real artwork |
+| Endpoint | Purpose |
+| -------------------------------------------- | ----------------------------------------------------------------------------- |
+| `GET /api/overlays/settings` | Read global overlay settings |
+| `PUT /api/overlays/settings` | Update global overlay settings |
+| `GET /api/overlays/sections` | List media server library sections used by the template preview picker |
+| `GET /api/overlays/random-item` | Get a random media item for poster-template preview |
+| `GET /api/overlays/random-episode` | Get a random episode for title-card preview |
+| `GET /api/overlays/poster` | Proxy media artwork for template preview |
+| `GET /api/overlays/status` | Read the latest overlay processing status |
+| `POST /api/overlays/process` | Run overlay processing for all eligible collections |
+| `POST /api/overlays/process/:collectionId` | Run overlay processing for one collection |
+| `POST /api/overlays/revert/:collectionId` | Revert overlays for one collection |
+| `DELETE /api/overlays/reset` | Revert all overlays |
+| `GET /api/overlays/fonts` | List available fonts |
+| `GET /api/overlays/fonts/:name` | Read a bundled or uploaded font file |
+| `POST /api/overlays/fonts` | Upload a `.ttf`, `.otf`, or `.woff` font |
+| `GET /api/overlays/images` | List uploaded overlay image assets |
+| `GET /api/overlays/images/:name` | Read an uploaded overlay image asset |
+| `POST /api/overlays/images` | Upload a `.png`, `.jpg`/`.jpeg`, or `.webp` image for template image elements |
+| `DELETE /api/overlays/images/:name` | Delete an uploaded overlay image asset |
+| `GET /api/overlays/templates` | List overlay templates |
+| `GET /api/overlays/templates/:id` | Fetch one template |
+| `POST /api/overlays/templates` | Create a template |
+| `PUT /api/overlays/templates/:id` | Update a template |
+| `DELETE /api/overlays/templates/:id` | Delete a non-preset template |
+| `POST /api/overlays/templates/:id/duplicate` | Clone a template into an editable copy |
+| `POST /api/overlays/templates/:id/default` | Set a template as the default for its mode |
+| `POST /api/overlays/templates/:id/export` | Export a template as JSON |
+| `POST /api/overlays/templates/import` | Import a template from JSON |
+| `POST /api/overlays/templates/:id/preview` | Render a server-side preview of a template on real artwork |
`POST /api/overlays/process` accepts an optional `{ force: true }` body to reapply overlays even when the saved day-count state is already current. Its run summary now always reports `processed`, `reverted`, `skipped`, and `errors`.
@@ -142,10 +145,10 @@ Maintainerr exposes lightweight health endpoints under `/api/health` (prefixed w
### Storage Metrics
-| Endpoint | Purpose |
-| --- | --- |
-| `GET /api/storage-metrics` | Return aggregated disk usage, instance health, collection-size summaries, and cumulative cleanup totals |
-| `GET /api/storage-metrics/library-sizes` | Compute per-library sizes on demand; potentially slow on large libraries |
+| Endpoint | Purpose |
+| ---------------------------------------- | ------------------------------------------------------------------------------------------------------- |
+| `GET /api/storage-metrics` | Return aggregated disk usage, instance health, collection-size summaries, and cumulative cleanup totals |
+| `GET /api/storage-metrics/library-sizes` | Compute per-library sizes on demand; potentially slow on large libraries |
`GET /api/storage-metrics` now includes `cleanupTotals` counters for `itemsHandled`, `moviesHandled`, `showsHandled`, `seasonsHandled`, and `episodesHandled`, plus reclaimed-byte totals in `bytesHandled`, `movieBytesHandled`, `showBytesHandled`, `seasonBytesHandled`, and `episodeBytesHandled`.
diff --git a/docs/Calendar.md b/docs/Calendar.md
index 773cb3ce..68c6220a 100644
--- a/docs/Calendar.md
+++ b/docs/Calendar.md
@@ -53,4 +53,4 @@ That means:
- removing media from a collection removes it from the future schedule
- changing `Take action after days` changes future scheduled dates
-- changing a collection action changes how that future entry is labeled
\ No newline at end of file
+- changing a collection action changes how that future entry is labeled
diff --git a/docs/Collections.md b/docs/Collections.md
index 6a5632c3..f5e33360 100644
--- a/docs/Collections.md
+++ b/docs/Collections.md
@@ -10,12 +10,13 @@ A collection is auto generated when defining a rule. A collection holds all medi
When the specified amount of days that media must live in the collection is passed, the collection handler job will perform the necessary cleanup actions.
:::note Collection Handling
- Collection handling is a batch process that runs every 12 hours. You can manually trigger it with the `Handle Collections` button on the Collections page.
- This runs each collection's configured action (such as delete, unmonitor, or do nothing), but it does not remove items from collections on its own.
+Collection handling is a batch process that runs every 12 hours. You can manually trigger it with the `Handle Collections` button on the Collections page.
+This runs each collection's configured action (such as delete, unmonitor, or do nothing), but it does not remove items from collections on its own.
If a rule-managed item is still in the collection but its most recent rule evaluation failed, Maintainerr skips the automatic handling action for that item until the rule can be evaluated cleanly again. Manually added items are still eligible for handling.
When a delete-style action removes files, Maintainerr also prunes that media from any other Maintainerr-managed collections that still list it. This prevents already-deleted items from being re-processed while Jellyfin or Emby are still catching up on their next library scan.
If eligible media is actively being streamed, Maintainerr defers it to the next collection-handler run instead of acting on it mid-playback. This is a best-effort snapshot taken once per run, so playback that starts later is only protected on the following pass.
+
:::
## Media Server
@@ -83,7 +84,7 @@ The collection-poster endpoints live under `/api/collections/:id`.
You can manually add media to a collection on the `Overview` page, by using the `Add` button on the media. Using the button will open a popup where you are able to pick the collection you wish to add the media to.
:::warning
- Please note that the first option selected is to **remove** media from all collections. However, if the media was added by the rule handler, it will be added again. If you wish to counter this behaviour, you must also exclude it from all collections.
+Please note that the first option selected is to **remove** media from all collections. However, if the media was added by the rule handler, it will be added again. If you wish to counter this behaviour, you must also exclude it from all collections.
:::
### Removing
@@ -93,7 +94,7 @@ As mentioned in the section above, you are able to remove media from all collect
However, if you wish to just remove media from 1 collection it's easier to click on the collection's name on the `collections` page. This will show all media currently added to the collection. There you're able to remove specific media from the collection by using the `Remove` button.
:::note
- This will also exclude media from rule handling for this collection, so it won't be added again.
+This will also exclude media from rule handling for this collection, so it won't be added again.
:::
### Excluding
diff --git a/docs/Common.md b/docs/Common.md
index 65f92b10..6ce20e89 100644
--- a/docs/Common.md
+++ b/docs/Common.md
@@ -5,7 +5,6 @@ description: Common problems, and their solutions.
title: Common Problems
---
-
:::note
These suggestions will not solve every issue, but they cover the most common problems people run into. If you try the steps below and still cannot get Maintainerr working as expected, reach out on [Discord](https://discord.maintainerr.info).
:::
diff --git a/docs/Configuration.md b/docs/Configuration.md
index 2599f7d5..5fb37d0a 100644
--- a/docs/Configuration.md
+++ b/docs/Configuration.md
@@ -5,7 +5,6 @@ description: Information on how to get Maintainerr up and running.
title: Configuration
---
-
All configuration is done inside the application. No extra config files are required.
When you first access the web UI, you should be redirected to the settings page. If that does not happen, try refreshing the page.
@@ -22,10 +21,10 @@ All Base URL settings are to be entered without the leading slash.
These settings are OK for most installations.
-| Setting | Description |
-| --- | --- |
-| Hostname | The hostname or IP address of the host running Maintainerr |
-| API key | Maintainerr's API key. It is currently reserved for future use. |
+| Setting | Description |
+| -------- | --------------------------------------------------------------- |
+| Hostname | The hostname or IP address of the host running Maintainerr |
+| API key | Maintainerr's API key. It is currently reserved for future use. |
## Media Server
@@ -45,11 +44,11 @@ After you authenticate with a Plex **admin** account, Maintainerr validates the
Proper DNS is preferred. Plex discovery and failover can depend on resolvable Plex endpoints, and Docker users in particular may run into intermittent connection or discovery problems when container DNS is unstable. If possible, make sure your environment has working DNS resolution for Plex-related hostnames and service names.
:::
-| Setting / Control | Description |
-| --- | --- |
-| Authentication | Authenticate with Plex using an **admin** account. Until this succeeds, the Plex server controls stay disabled. |
-| Server | Shows the currently selected discovered server, or lets you choose one from the discovered server list. |
-| Refresh icon | Re-runs Plex server discovery for the authenticated account. Use this if the server list is stale or discovery failed the first time. |
+| Setting / Control | Description |
+| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
+| Authentication | Authenticate with Plex using an **admin** account. Until this succeeds, the Plex server controls stay disabled. |
+| Server | Shows the currently selected discovered server, or lets you choose one from the discovered server list. |
+| Refresh icon | Re-runs Plex server discovery for the authenticated account. Use this if the server list is stale or discovery failed the first time. |
:::tip
`Test Connection` is disabled until you are authenticated and have either selected a discovered server or enabled manual override with saved settings.
@@ -76,11 +75,11 @@ If you want Maintainerr to connect securely in manual mode, use your `*.plex.dir
Jellyfin can also be used as your media server connection.
-| Setting | Description |
-| --- | --- |
+| Setting | Description |
+| ------------ | ---------------------------------------------------------------- |
| Jellyfin URL | The domain name or local IP address of the host running Jellyfin |
-| API key | A Jellyfin API key generated from your Jellyfin server |
-| Admin User | Test Connection to load the available admin users |
+| API key | A Jellyfin API key generated from your Jellyfin server |
+| Admin User | Test Connection to load the available admin users |
## Streamystats
@@ -90,52 +89,52 @@ Streamystats is only available for Jellyfin users
The separate `Settings -> Streamystats` page only appears when Jellyfin is the active media server. Maintainerr reuses your saved Jellyfin API key for authentication, so you only need to provide the Streamystats base URL.
-| Setting | Description |
-| --- | --- |
-| URL | The base URL of your Streamystats instance, such as `http://localhost:3000` or `https://streamystats.example.com` |
+| Setting | Description |
+| ------- | ----------------------------------------------------------------------------------------------------------------- |
+| URL | The base URL of your Streamystats instance, such as `http://localhost:3000` or `https://streamystats.example.com` |
## Emby
Use your Emby server URL directly. Maintainerr supports either entering an API key manually or using `Sign in with Emby` to authenticate with an admin username and password and let Maintainerr populate the API key for you. Emby Connect is not supported because Maintainerr uses direct server authentication and does not implement Emby's cloud-based Connect flow.
-| Setting | Description |
-| --- | --- |
-| Emby URL | The domain name or local IP address of the host running Emby |
-| API key | An Emby API key generated from `Dashboard -> Advanced -> API Keys`, or the token returned by `Sign in with Emby` |
+| Setting | Description |
+| ---------- | -------------------------------------------------------------------------------------------------------------------------------------- |
+| Emby URL | The domain name or local IP address of the host running Emby |
+| API key | An Emby API key generated from `Dashboard -> Advanced -> API Keys`, or the token returned by `Sign in with Emby` |
| Admin User | Test Connection to load the available admin users, or use `Sign in with Emby` to populate the saved admin-user selection automatically |
## Seerr
Seerr configuration is required if you want to use Seerr-related rule parameters or remove Seerr requests.
-| Setting | Description |
-| --- | --- |
-| URL | The domain name or local IP address of the host running Seerr |
-| API key | The API key from Seerr settings |
+| Setting | Description |
+| ------- | ------------------------------------------------------------- |
+| URL | The domain name or local IP address of the host running Seerr |
+| API key | The API key from Seerr settings |
## Radarr
Radarr's configuration is required to use its parameters in rules and to remove or unmonitor movies.
-| Setting | Description |
-| --- | --- |
-| Server Name | A friendly name to help identify the server |
+| Setting | Description |
+| -------------- | -------------------------------------------------------------- |
+| Server Name | A friendly name to help identify the server |
| Hostname or IP | The domain name or local IP address of the host running Radarr |
-| Port | The port Radarr runs on |
-| Base URL | The URL base configured in Radarr, if one is set |
-| API key | The API key from Radarr settings |
+| Port | The port Radarr runs on |
+| Base URL | The URL base configured in Radarr, if one is set |
+| API key | The API key from Radarr settings |
## Sonarr
Sonarr's configuration is required to use its parameters in rules and to remove or unmonitor shows.
-| Setting | Description |
-| --- | --- |
-| Server Name | A friendly name to help identify the server |
+| Setting | Description |
+| -------------- | -------------------------------------------------------------- |
+| Server Name | A friendly name to help identify the server |
| Hostname or IP | The domain name or local IP address of the host running Sonarr |
-| Port | The port Sonarr runs on |
-| Base URL | The URL base configured in Sonarr, if one is set |
-| API key | The API key from Sonarr settings |
+| Port | The port Sonarr runs on |
+| Base URL | The URL base configured in Sonarr, if one is set |
+| API key | The API key from Sonarr settings |
## Metadata
@@ -143,12 +142,12 @@ Maintainerr has a separate `Settings -> Metadata` page for poster, backdrop, and
This page is mainly useful when you want better artwork fallback, more reliable cross-provider ID resolution, or more control over which metadata source Maintainerr prefers.
-| Setting | Description |
-| --- | --- |
-| TMDB API key | Optional. If you leave this empty, Maintainerr uses its built-in shared TMDB key. Add your own key if you want an isolated quota or your own TMDB account access. |
-| TVDB API key | Optional. Enables TVDB as an additional metadata source and fallback for ID cross-references. TVDB cannot be selected as primary until it is configured. |
-| Primary | Chooses whether Maintainerr prefers TMDB or TVDB first when resolving posters, backdrops, and related metadata lookups. |
-| Refresh metadata | Clears cached metadata for that provider and asks your media server to refresh matching items that already have provider IDs stored. |
+| Setting | Description |
+| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| TMDB API key | Optional. If you leave this empty, Maintainerr uses its built-in shared TMDB key. Add your own key if you want an isolated quota or your own TMDB account access. |
+| TVDB API key | Optional. Enables TVDB as an additional metadata source and fallback for ID cross-references. TVDB cannot be selected as primary until it is configured. |
+| Primary | Chooses whether Maintainerr prefers TMDB or TVDB first when resolving posters, backdrops, and related metadata lookups. |
+| Refresh metadata | Clears cached metadata for that provider and asks your media server to refresh matching items that already have provider IDs stored. |
Typical usage:
@@ -173,7 +172,7 @@ Tautulli is only available for Plex users
Tautulli's configuration is required to use its parameters in rules.
-| Setting | Description |
-| --- | --- |
-| URL| The domain name or local IP address of the host running Tautulli |
-| API key | The API key from Tautulli settings |
+| Setting | Description |
+| ------- | ---------------------------------------------------------------- |
+| URL | The domain name or local IP address of the host running Tautulli |
+| API key | The API key from Tautulli settings |
diff --git a/docs/Downgrade.md b/docs/Downgrade.md
index 1c53e1fe..3ea0e6b7 100644
--- a/docs/Downgrade.md
+++ b/docs/Downgrade.md
@@ -5,7 +5,6 @@ title: Downgrade
description: How to install an older Maintainerr version using a database backup.
---
-
If you need to run an older Maintainerr version, you must use a database backup from before you upgraded.
:::note Notice
diff --git a/docs/Glossary.md b/docs/Glossary.md
index 5820125d..e965c124 100644
--- a/docs/Glossary.md
+++ b/docs/Glossary.md
@@ -1064,7 +1064,7 @@ List of Jellyfin usernames who have marked the current item or its parent items
**Useful Rule - Movies started but never finished:**
-``` text
+```text
playCount >= 1 AND viewCount = 0
```
diff --git a/docs/Installation.mdx b/docs/Installation.mdx
index 65b6abc2..7e7df6b2 100644
--- a/docs/Installation.mdx
+++ b/docs/Installation.mdx
@@ -5,23 +5,24 @@ description: Install methods and information.
title: Installation
---
-import Tabs from '@theme/Tabs'
-import TabItem from '@theme/TabItem'
-import Details from '@theme/Details'
-import AnnotatedCodeBlock from '@site/src/components/AnnotatedCodeBlock'
+import Tabs from "@theme/Tabs";
+import TabItem from "@theme/TabItem";
+import Details from "@theme/Details";
+import AnnotatedCodeBlock from "@site/src/components/AnnotatedCodeBlock";
Docker is Maintainerr's supported method of installation.
Images for amd64 & arm64 are available under `maintainerr/maintainerr` and `ghcr.io/maintainerr/maintainerr`.
The container's data location is `/opt/data`. A Docker [volume][tooltip] is strongly encouraged to persist your configuration.
-[tooltip]: https://docs.docker.com/storage/volumes/#start-a-container-with-a-volume 'Click here to be taken to the Docker documentation page on volumes.'
+[tooltip]: https://docs.docker.com/storage/volumes/#start-a-container-with-a-volume "Click here to be taken to the Docker documentation page on volumes."
:::note
Maintainerr uses the configured `user:group` as its runtime user inside the container, and any files it creates in your host data directory will be owned by that user and group.
If you do not set one explicitly, the default `UID:GID` is `1000:1000`, so make sure your host data directory is read/writeable by that UID:GID. If needed, update it with `chown -R 1000:1000 /opt/data`.
:::
+
## Docker
Compose is recommended for most installs. Choose the Docker example that matches your workflow.
@@ -49,20 +50,20 @@ Define the Maintainerr service in your docker-compose.yml as follows:
annotations={[
{
line: 3,
- label: '+',
+ label: "+",
tooltip:
- 'You can also use maintainerr/maintainerr here if you prefer the Docker Hub image.',
+ "You can also use maintainerr/maintainerr here if you prefer the Docker Hub image.",
},
{
line: 7,
- label: '+',
+ label: "+",
tooltip:
- 'Bind a host directory to /opt/data so configuration and database files persist outside the container.',
+ "Bind a host directory to /opt/data so configuration and database files persist outside the container.",
},
{
line: 11,
- label: '+',
- tooltip: 'Port mappings are defined as host:container.',
+ label: "+",
+ tooltip: "Port mappings are defined as host:container.",
},
]}
/>
@@ -92,20 +93,20 @@ Run Maintainerr with the following command:
annotations={[
{
line: 4,
- label: '+',
+ label: "+",
tooltip:
- 'Bind a host directory to /opt/data so configuration and database files persist outside the container.',
+ "Bind a host directory to /opt/data so configuration and database files persist outside the container.",
},
{
line: 6,
- label: '+',
- tooltip: 'Port mappings are defined as host:container.',
+ label: "+",
+ tooltip: "Port mappings are defined as host:container.",
},
{
line: 8,
- label: '+',
+ label: "+",
tooltip:
- 'You can also use maintainerr/maintainerr here if you prefer the Docker Hub image.',
+ "You can also use maintainerr/maintainerr here if you prefer the Docker Hub image.",
},
]}
/>
@@ -263,7 +264,7 @@ services:
maintainerr:
image: ghcr.io/maintainerr/maintainerr:latest
healthcheck:
- test: ['CMD', '/opt/app/healthcheck.sh']
+ test: ["CMD", "/opt/app/healthcheck.sh"]
interval: 30s
timeout: 5s
start_period: 40s
@@ -274,14 +275,14 @@ services:
A list of all available environment variables are below. No other env variables are officially supported by Maintainerr. These are added either into the compose file or your docker run command.
-| Variable | Default Value | Description |
-| ------------ | --------------- | ------------------------------------------------------------------------------------------------------------------ |
-| TZ | _host timezone_ | Controls date formatting in logs. |
-| UI_HOSTNAME | 0.0.0.0 | The listen host of the web server. Can be set to :: for IPv6. |
-| UI_PORT | 6246 | The listen port of the web server. |
-| BASE_PATH | | If reverse proxying with a subfolder you'll want to set this. Must be in the format of `/subfolder`. |
+| Variable | Default Value | Description |
+| ------------ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| TZ | _host timezone_ | Controls date formatting in logs. |
+| UI_HOSTNAME | 0.0.0.0 | The listen host of the web server. Can be set to :: for IPv6. |
+| UI_PORT | 6246 | The listen port of the web server. |
+| BASE_PATH | | If reverse proxying with a subfolder you'll want to set this. Must be in the format of `/subfolder`. |
| LOG_LEVEL | info | Overrides the persisted log level for the current container process only. Accepted values are `debug`, `verbose`, `info`, `warn`, `error`, and `fatal`. |
-| GITHUB_TOKEN | | GitHub Personal Access Token for higher API rate limits |
+| GITHUB_TOKEN | | GitHub Personal Access Token for higher API rate limits |
:::tip
If `BASE_PATH` is set, remember to prefix health-check probe paths accordingly (for example `/maintainerr/api/health/ready`).
diff --git a/docs/Introduction.md b/docs/Introduction.md
index 409535c7..c767b2e8 100644
--- a/docs/Introduction.md
+++ b/docs/Introduction.md
@@ -64,7 +64,7 @@ hide_title: true
- See an overview of your media server library contents.
- Manually add an item to one of the above mentioned collections.
- Manually exclude an item from one of the collections, even if it meets the rule criteria.
-- Show your new collection on the *Home* screen.
+- Show your new collection on the _Home_ screen.
- Set a number of days the collection will exist before it is deleted.
- Set Radarr and Sonarr to either **remove** or **unmonitor** media in the collection.
- Auto clear requests from Seerr
diff --git a/docs/Migration.md b/docs/Migration.md
index a96f40ea..b25af6e2 100644
--- a/docs/Migration.md
+++ b/docs/Migration.md
@@ -4,7 +4,7 @@ title: Migration
---
!!! tip
- Backup `/opt/data/maintainerr.db` before major changes.
+Backup `/opt/data/maintainerr.db` before major changes.
## Media Server Switching
@@ -64,7 +64,7 @@ After migration:
!!! warning
- Rule groups are **deactivated** after switching and libraries must be re-assigned before they will run. Collections won't function until libraries are set.
+Rule groups are **deactivated** after switching and libraries must be re-assigned before they will run. Collections won't function until libraries are set.
### Incompatible Properties
@@ -166,4 +166,4 @@ Same automatic migration as YAML imports.
Import any community rule regardless of origin server. Maintainerr migrates media-server properties to your configured server the same way as YAML imports: `firstVal` and `lastVal` are handled independently, non-media-server apps are left untouched, and rules with no equivalent target-server property are skipped with a user-visible skipped count.
!!! note
- Community rules from much older Maintainerr versions may not work due to schema changes.
+Community rules from much older Maintainerr versions may not work due to schema changes.
diff --git a/docs/Notifications.md b/docs/Notifications.md
index a7073411..473f8a43 100644
--- a/docs/Notifications.md
+++ b/docs/Notifications.md
@@ -6,13 +6,13 @@ description: Configure and manage notification agents for automated alerts and u
title: Notifications
---
-
Notifications allow Maintainerr to send automated alerts and updates about your media collections through various messaging platforms and services. You can configure multiple notification agents and specify which types of events should trigger notifications.
:::note Beta Feature
The notification system is currently in beta. Some agents have not been tested extensively.
:::
+
## Overview
The notification system works by connecting configured notification agents to your rules. When specific events occur (such as media being added to or removed from collections), Maintainerr will send notifications to the configured agents that are subscribed to those event types.
@@ -25,26 +25,26 @@ Navigate to **Settings → Notifications** to manage your notification agents. H
Each notification agent requires the following common settings:
-| Parameter | Description |
-| --------- | ----------- |
-| Name | A descriptive name for this notification configuration |
-| Enabled | Whether this agent is active and will send notifications |
-| Agent | The notification service to use (Discord, Email, etc.) |
-| Types | Which notification types this agent should receive |
+| Parameter | Description |
+| ---------------------------- | ---------------------------------------------------------------------------------------------------------- |
+| Name | A descriptive name for this notification configuration |
+| Enabled | Whether this agent is active and will send notifications |
+| Agent | The notification service to use (Discord, Email, etc.) |
+| Types | Which notification types this agent should receive |
| Notify x days before removal | For "Media About to be Handled" notifications, how many days before removal to send the alert (default: 3) |
### Notification Types
Maintainerr supports several notification types that you can enable for each agent:
-| Type | Description |
-| ---- | ----------- |
-| Media Added to Collection | Sent when media items are added to a collection |
-| Media Removed from Collection | Sent when media items are removed from a collection |
-| Media About to be Handled | Advance warning that media will be processed/deleted in X days |
-| Media Handled | Confirmation that media has been processed/deleted |
-| Rule Handling Failed | Alert when there's an error processing rules |
-| Collection Handling Failed | Alert when there's an error processing collections. When Maintainerr can tie the failure to one collection, the message names that collection. |
+| Type | Description |
+| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
+| Media Added to Collection | Sent when media items are added to a collection |
+| Media Removed from Collection | Sent when media items are removed from a collection |
+| Media About to be Handled | Advance warning that media will be processed/deleted in X days |
+| Media Handled | Confirmation that media has been processed/deleted |
+| Rule Handling Failed | Alert when there's an error processing rules |
+| Collection Handling Failed | Alert when there's an error processing collections. When Maintainerr can tie the failure to one collection, the message names that collection. |
Infrastructure-level collection failures that happen before Maintainerr can identify a specific collection still send the generic `Collection Handling Failed` message.
@@ -68,21 +68,21 @@ You'll need to create a Discord webhook for your channel. Follow Discord's guide
Send notifications via SMTP email.
-| Parameter | Required | Description |
-| --------- | -------- | ----------- |
-| Email From | Yes | Sender email address |
-| Sender Name | Yes | Display name for the sender |
-| Email To | Yes | Recipient email address |
-| SMTP Host | Yes | SMTP server hostname |
-| SMTP Port | Yes | SMTP server port (usually 587 or 465) |
-| Secure | No | Use implicit TLS |
-| Ignore TLS | No | Disable TLS entirely |
-| Require TLS | No | Always use STARTTLS |
-| Auth User | No | SMTP authentication username |
-| Auth Pass | No | SMTP authentication password |
-| Allow Self Signed | No | Accept self-signed certificates |
-| PGP Key | No | PGP public key for encryption |
-| PGP Password | No | Password for PGP key |
+| Parameter | Required | Description |
+| ----------------- | -------- | ------------------------------------- |
+| Email From | Yes | Sender email address |
+| Sender Name | Yes | Display name for the sender |
+| Email To | Yes | Recipient email address |
+| SMTP Host | Yes | SMTP server hostname |
+| SMTP Port | Yes | SMTP server port (usually 587 or 465) |
+| Secure | No | Use implicit TLS |
+| Ignore TLS | No | Disable TLS entirely |
+| Require TLS | No | Always use STARTTLS |
+| Auth User | No | SMTP authentication username |
+| Auth Pass | No | SMTP authentication password |
+| Allow Self Signed | No | Accept self-signed certificates |
+| PGP Key | No | PGP public key for encryption |
+| PGP Password | No | Password for PGP key |
### Gotify
@@ -189,25 +189,26 @@ You'll need to create a Telegram bot and get your chat ID. Follow these steps:
Send notifications to custom webhook endpoints. Requests are sent using the POST request method.
-| Parameter | Required | Description |
-| --------- | -------- | ----------- |
-| Webhook URL | Yes | Target webhook endpoint URL |
-| JSON Payload | Yes | Custom JSON payload template |
-| Auth Header | No | [Authorization](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Authorization) header value |
+| Parameter | Required | Description |
+| ------------ | -------- | --------------------------------------------------------------------------------------------------------------- |
+| Webhook URL | Yes | Target webhook endpoint URL |
+| JSON Payload | Yes | Custom JSON payload template |
+| Auth Header | No | [Authorization](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Authorization) header value |
#### Webhook Variables
The webhook agent supports variable replacement in the JSON payload. You can use the following variables:
-| Variable | Description |
-| -------- | ----------- |
+| Variable | Description |
+| ----------------------- | ----------------------------------- |
| `{{notification_type}}` | The type of notification being sent |
-| `{{subject}}` | The notification subject/title |
-| `{{message}}` | The notification message content |
-| `{{image}}` | Associated image URL (if available) |
-| `{{extra}}` | Additional data fields |
+| `{{subject}}` | The notification subject/title |
+| `{{message}}` | The notification message content |
+| `{{image}}` | Associated image URL (if available) |
+| `{{extra}}` | Additional data fields |
Example JSON payload:
+
```json
{
"content": "{{subject}}",
diff --git a/docs/ReverseProxy.md b/docs/ReverseProxy.md
index 6da40ccf..17c437e5 100644
--- a/docs/ReverseProxy.md
+++ b/docs/ReverseProxy.md
@@ -5,7 +5,6 @@ description: Working configurations that should get you started.
title: Reverse Proxy
---
-
We have tried to outline some working configurations. At the very least, these should get you started in the right direction.
## NGINX
diff --git a/docs/Rules.mdx b/docs/Rules.mdx
index d0e58b80..a87b0587 100644
--- a/docs/Rules.mdx
+++ b/docs/Rules.mdx
@@ -5,8 +5,8 @@ description: Rules configurations and basic information about using rules.
title: Rules
---
-import CodeBlock from '@theme/CodeBlock'
-import InlineTooltip from '@site/src/components/InlineTooltip'
+import CodeBlock from "@theme/CodeBlock";
+import InlineTooltip from "@site/src/components/InlineTooltip";
Rules are the core of Maintainerr. They evaluate media from your configured media server based on the parameters you set. If a media item matches a rule, it is added to a collection.
@@ -58,7 +58,7 @@ General info about the rule. Some of the information specified here will be show
| Radarr server | The server that Radarr specific rules and actions will be applied to |
| Radarr action | Delete, unmonitor, or change the quality profile of movies in Radarr |
| Sonarr server | The server that Sonarr specific rules and actions will be applied to |
-| Sonarr action | Delete, unmonitor, or change the quality profile in Sonarr. Season rules can also delete or unmonitor the parent show if it becomes empty. |
+| Sonarr action | Delete, unmonitor, or change the quality profile in Sonarr. Season rules can also delete or unmonitor the parent show if it becomes empty. |
| Media server action | Delete media from your media server directly. Only applicable when no \*arr server is selected. |
| Do nothing action | No action will be taken on the media in this collection. |
| Active | If inactive, the rule won't run |
@@ -69,7 +69,7 @@ General info about the rule. Some of the information specified here will be show
| Use rules | Disable the rule engine, for when you want to add media to the collection manually |
| Force reset Seerr record | Force resets the Seerr record by deleting any requests instead of relying on availability-sync. 'Enable CSRF Protection' needs to be disabled in Seerr's settings for this to work.
{lines.map((line, index) => {
- const lineNumber = index + 1
- const lineAnnotations = annotationsByLine.get(lineNumber) ?? []
+ const lineNumber = index + 1;
+ const lineAnnotations = annotationsByLine.get(lineNumber) ?? [];
return (
- {line || ' '}
+ {line || " "}
{lineAnnotations.length > 0 ? (
{lineAnnotations.map((annotation) => (
@@ -39,11 +39,11 @@ export default function AnnotatedCodeBlock({
) : null}
- )
+ );
})}
Thanks for the feedback.
} - {selection === 'improve' && ( + {selection === "helpful" && ( +Thanks for the feedback.
+ )} + {selection === "improve" && (- Thanks for the feedback. Help us improve this page by opening an issue in the{' '} - docs repo. + Thanks for the feedback. Help us improve this page by opening an issue + in the docs repo.
)} - ) + ); } diff --git a/src/components/InlineTooltip/index.jsx b/src/components/InlineTooltip/index.jsx index e0bec250..01aa96fe 100644 --- a/src/components/InlineTooltip/index.jsx +++ b/src/components/InlineTooltip/index.jsx @@ -1,40 +1,40 @@ -import React, { useEffect, useId, useState } from 'react' -import { createPortal } from 'react-dom' -import styles from './styles.module.css' +import React, { useEffect, useId, useState } from "react"; +import { createPortal } from "react-dom"; +import styles from "./styles.module.css"; export default function InlineTooltip({ label, tooltip }) { - const tooltipId = useId() - const [position, setPosition] = useState(null) + const tooltipId = useId(); + const [position, setPosition] = useState(null); function updatePosition(event) { - const rect = event.currentTarget.getBoundingClientRect() + const rect = event.currentTarget.getBoundingClientRect(); setPosition({ left: rect.left + rect.width / 2, top: rect.top - 10, - }) + }); } function hideTooltip() { - setPosition(null) + setPosition(null); } useEffect(() => { if (!position) { - return undefined + return undefined; } function dismissTooltip() { - setPosition(null) + setPosition(null); } - window.addEventListener('scroll', dismissTooltip, true) - window.addEventListener('resize', dismissTooltip) + window.addEventListener("scroll", dismissTooltip, true); + window.addEventListener("resize", dismissTooltip); return () => { - window.removeEventListener('scroll', dismissTooltip, true) - window.removeEventListener('resize', dismissTooltip) - } - }, [position]) + window.removeEventListener("scroll", dismissTooltip, true); + window.removeEventListener("resize", dismissTooltip); + }; + }, [position]); return ( <> @@ -49,7 +49,7 @@ export default function InlineTooltip({ label, tooltip }) { > {label} - {position && typeof document !== 'undefined' + {position && typeof document !== "undefined" ? createPortal( {tooltip} , - document.body + document.body, ) : null} > - ) + ); } diff --git a/src/components/SiteConsent/index.jsx b/src/components/SiteConsent/index.jsx index 6ecc9ba6..557b6f4b 100644 --- a/src/components/SiteConsent/index.jsx +++ b/src/components/SiteConsent/index.jsx @@ -1,127 +1,142 @@ -import React, {useEffect, useMemo, useState} from 'react' -import useDocusaurusContext from '@docusaurus/useDocusaurusContext' -import {useLocation} from '@docusaurus/router' -import styles from './styles.module.css' +import React, { useEffect, useMemo, useState } from "react"; +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; +import { useLocation } from "@docusaurus/router"; +import styles from "./styles.module.css"; -const CONSENT_KEY = 'maintainerr-docs-consent' -const MATOMO_SCRIPT_ID = 'maintainerr-matomo-script' +const CONSENT_KEY = "maintainerr-docs-consent"; +const MATOMO_SCRIPT_ID = "maintainerr-matomo-script"; function getStoredConsent() { - if (typeof window === 'undefined') { - return null + if (typeof window === "undefined") { + return null; } - return window.localStorage.getItem(CONSENT_KEY) + return window.localStorage.getItem(CONSENT_KEY); } -function loadMatomo({trackerUrl, siteId}) { +function loadMatomo({ trackerUrl, siteId }) { if ( - typeof window === 'undefined' || + typeof window === "undefined" || !trackerUrl || !siteId || document.getElementById(MATOMO_SCRIPT_ID) ) { - return + return; } - const normalizedTrackerUrl = trackerUrl.endsWith('/') + const normalizedTrackerUrl = trackerUrl.endsWith("/") ? trackerUrl - : `${trackerUrl}/` - - window._paq = window._paq || [] - window._paq.push(['disableCookies']) - window._paq.push(['trackPageView']) - window._paq.push(['enableLinkTracking']) - window._paq.push(['setTrackerUrl', `${normalizedTrackerUrl}matomo.php`]) - window._paq.push(['setSiteId', siteId]) - - const script = document.createElement('script') - script.id = MATOMO_SCRIPT_ID - script.async = true - script.src = `${normalizedTrackerUrl}matomo.js` - document.head.appendChild(script) + : `${trackerUrl}/`; + + window._paq = window._paq || []; + window._paq.push(["disableCookies"]); + window._paq.push(["trackPageView"]); + window._paq.push(["enableLinkTracking"]); + window._paq.push(["setTrackerUrl", `${normalizedTrackerUrl}matomo.php`]); + window._paq.push(["setSiteId", siteId]); + + const script = document.createElement("script"); + script.id = MATOMO_SCRIPT_ID; + script.async = true; + script.src = `${normalizedTrackerUrl}matomo.js`; + document.head.appendChild(script); } function trackPageView(location) { - if (typeof window === 'undefined' || !window._paq) { - return + if (typeof window === "undefined" || !window._paq) { + return; } - const fullPath = `${location.pathname}${location.search}${location.hash}` - window._paq.push(['setCustomUrl', fullPath]) - window._paq.push(['setDocumentTitle', document.title]) - window._paq.push(['trackPageView']) + const fullPath = `${location.pathname}${location.search}${location.hash}`; + window._paq.push(["setCustomUrl", fullPath]); + window._paq.push(["setDocumentTitle", document.title]); + window._paq.push(["trackPageView"]); } export function trackMatomoEvent(category, action, name) { - if (typeof window === 'undefined' || !window._paq) { - return + if (typeof window === "undefined" || !window._paq) { + return; } - window._paq.push(['trackEvent', category, action, name]) + window._paq.push(["trackEvent", category, action, name]); } export default function SiteConsent() { - const {siteConfig} = useDocusaurusContext() - const location = useLocation() - const [consent, setConsent] = useState(null) - const [isReady, setIsReady] = useState(false) - const matomoConfig = useMemo(() => siteConfig.customFields?.matomo ?? {}, [siteConfig]) + const { siteConfig } = useDocusaurusContext(); + const location = useLocation(); + const [consent, setConsent] = useState(null); + const [isReady, setIsReady] = useState(false); + const matomoConfig = useMemo( + () => siteConfig.customFields?.matomo ?? {}, + [siteConfig], + ); const privacyPolicyUrl = - siteConfig.customFields?.privacyPolicyUrl ?? 'https://maintainerr.info/privacy.html' + siteConfig.customFields?.privacyPolicyUrl ?? + "https://maintainerr.info/privacy.html"; useEffect(() => { - setConsent(getStoredConsent()) - setIsReady(true) - }, []) + setConsent(getStoredConsent()); + setIsReady(true); + }, []); useEffect(() => { - if (consent !== 'accepted' || !matomoConfig.enabled) { - return + if (consent !== "accepted" || !matomoConfig.enabled) { + return; } - loadMatomo(matomoConfig) - }, [consent, matomoConfig]) + loadMatomo(matomoConfig); + }, [consent, matomoConfig]); useEffect(() => { - if (consent !== 'accepted' || !matomoConfig.enabled) { - return + if (consent !== "accepted" || !matomoConfig.enabled) { + return; } - trackPageView(location) - }, [consent, location, matomoConfig.enabled]) + trackPageView(location); + }, [consent, location, matomoConfig.enabled]); function updateConsent(nextConsent) { - window.localStorage.setItem(CONSENT_KEY, nextConsent) - setConsent(nextConsent) + window.localStorage.setItem(CONSENT_KEY, nextConsent); + setConsent(nextConsent); - if (nextConsent === 'accepted') { - trackMatomoEvent('cookie-consent', 'accept', 'analytics') + if (nextConsent === "accepted") { + trackMatomoEvent("cookie-consent", "accept", "analytics"); } } if (!isReady || consent !== null) { - return null + return null; } return ( -Help us, help you.
- We use privacy-friendly analytics to understand which docs are useful and where people - get stuck. You can read the full policy at{' '} + We use privacy-friendly analytics to understand which docs are useful + and where people get stuck. You can read the full policy at{" "} Maintainerr Privacy Policy.