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. | | Custom collection | Use a manually created collection. Maintainerr will never automatically add or remove this collection from your media server. | -| Custom collection name | The name of the existing manually managed collection to use | +| Custom collection name | The name of the existing manually managed collection to use | Maintainerr only shows `Force reset Seerr record` for movie, show, and season rules. Episode rules always rely on Seerr's normal availability sync because Seerr tracks requests at the season level, not per episode. If an older episode rule still has this option saved, re-saving the rule clears it. @@ -170,28 +170,28 @@ The output of the rule will then be passed on to the next rule. The action defines the way the `first value` and `second value` will be compared. The available actions are dependent on the type of the `first value` -| Action | Description | Types | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | -| bigger | Is the `first value` bigger than the `second value` ? | number | -| smaller | Is the `first value` smaller than the `second value` ? | number | -| contains | Does the `first value` contain the `second value`? Lists will confirm the existence of an exact match within the `first value` list | number, text | -| contains (partial) | Does the `first value` contain the `second value` ? Lists will confirm the existence of a partial match within the `first value` list | number, text | -| contains (all items) | Does the `first value` list contain all values from the `second value` list? | text[] | -| not contains | Does the `first value` lack the `second value`? Lists will indicate the absence of an exact match within the `first value` list. | number, text | -| not contains (partial) | Does the `first value` lack the `second value` ? Lists will indicate the absence of a partial match within the `first value` list. | number, text | -| not contains (all items) | Does the `first value` list fail to contain all values from the `second value` list? | text[] | -| equals | Is the `first value` equal to the `second value` ? | number, text, date | -| not equals | Is the `first value` unequal to the `second value` ? | number, text, date | -| count equals | Does the number of entries in the `first value` list equal the `second value`? | text[] | -| count not equals | Does the number of entries in the `first value` list differ from the `second value`? | text[] | -| count bigger | Is the number of entries in the `first value` list bigger than the `second value`? | text[] | -| count smaller | Is the number of entries in the `first value` list smaller than the `second value`? | text[] | -| before | Does the `first value` occur before the `second value` ? | date | -| after | Does the `first value` occur after the `second value` ? | date | -| in last | Does the `first value` occur in the last x amount of days ? | date | -| in next | Does the `first value` occur in the next x amount of days ? | date | -| exists | Does the `first value` exist at all? Useful for matching whether Maintainerr could resolve a value before comparing it. | number, text, date, boolean, text[] | -| not exists | Does the `first value` not exist? Useful for missing dates, empty watch history, or values that are not available for the item. | number, text, date, boolean, text[] | +| Action | Description | Types | +| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | +| bigger | Is the `first value` bigger than the `second value` ? | number | +| smaller | Is the `first value` smaller than the `second value` ? | number | +| contains | Does the `first value` contain the `second value`? Lists will confirm the existence of an exact match within the `first value` list | number, text | +| contains (partial) | Does the `first value` contain the `second value` ? Lists will confirm the existence of a partial match within the `first value` list | number, text | +| contains (all items) | Does the `first value` list contain all values from the `second value` list? | text[] | +| not contains | Does the `first value` lack the `second value`? Lists will indicate the absence of an exact match within the `first value` list. | number, text | +| not contains (partial) | Does the `first value` lack the `second value` ? Lists will indicate the absence of a partial match within the `first value` list. | number, text | +| not contains (all items) | Does the `first value` list fail to contain all values from the `second value` list? | text[] | +| equals | Is the `first value` equal to the `second value` ? | number, text, date | +| not equals | Is the `first value` unequal to the `second value` ? | number, text, date | +| count equals | Does the number of entries in the `first value` list equal the `second value`? | text[] | +| count not equals | Does the number of entries in the `first value` list differ from the `second value`? | text[] | +| count bigger | Is the number of entries in the `first value` list bigger than the `second value`? | text[] | +| count smaller | Is the number of entries in the `first value` list smaller than the `second value`? | text[] | +| before | Does the `first value` occur before the `second value` ? | date | +| after | Does the `first value` occur after the `second value` ? | date | +| in last | Does the `first value` occur in the last x amount of days ? | date | +| in next | Does the `first value` occur in the next x amount of days ? | date | +| exists | Does the `first value` exist at all? Useful for matching whether Maintainerr could resolve a value before comparing it. | number, text, date, boolean, text[] | +| not exists | Does the `first value` not exist? Useful for missing dates, empty watch history, or values that are not available for the item. | number, text, date, boolean, text[] | The difference between **_Contains/Contains (exact)_** and **_Contains (partial)_** is only apparent with list values. When comparing a text list, **_Contains (exact)_** will only return true if the `second value` _exactly_ matches any value in the `first value` list. **_Contains (partial)_** will return true if the `first value` list has a value that _partially_ matches any value in the `second value` list. diff --git a/docs/Test-Media.mdx b/docs/Test-Media.mdx index 07b736ed..d07b6bd6 100644 --- a/docs/Test-Media.mdx +++ b/docs/Test-Media.mdx @@ -5,8 +5,8 @@ description: Use Test Media to see whether a specific item matches a rule and wh title: Test Media --- -import Link from '@docusaurus/Link' -import CodeBlock from '@theme/CodeBlock' +import Link from "@docusaurus/Link"; +import CodeBlock from "@theme/CodeBlock"; Maintainerr comes with a built-in feature to test your ruleset against your media, and display the results to you. This can be done without ever running a rule or creating collections. Sometimes, it is hard for you to determine why something was or wasn't added to a collection. Using the Test Media feature can be an extremely useful tool in helping you figure out what is going on. @@ -33,12 +33,12 @@ Click on the name of the collection that you want to test rules for. You will be Depending on what type of library/media this collection is for, you will have different options at the top of this popup. -| Item | Value | -| ----- | ------- | -| Media | Name of a Movie or TVShow that you want to test | -| Season | Select which season you want to test (if TV) | -| Episode | Select the episode you want to test (if TV) | -| Output | The test results in YAML format | +| Item | Value | +| ------- | ----------------------------------------------- | +| Media | Name of a Movie or TVShow that you want to test | +| Season | Select which season you want to test (if TV) | +| Episode | Select the episode you want to test (if TV) | +| Output | The test results in YAML format | ### Test your media @@ -114,7 +114,7 @@ This is helpful when you are trying to test a specific rule, usually one that is Test Media results do not always include the result of every rule in your ruleset. As mentioned elsewhere, the rules run in order. -For example: If Rule 2 is an AND to Rule 1, and Rule 1 is determined to be `FALSE`, then only the output of Rule 1 will be shown. This is because Maintainerr didn't even test Rule 2. +For example: If Rule 2 is an AND to Rule 1, and Rule 1 is determined to be `FALSE`, then only the output of Rule 1 will be shown. This is because Maintainerr didn't even test Rule 2. It is logically impossible for something to be `1 AND 2`, if it is not `1` to begin with. There is no point in testing Rule 2, because it will not have an impact on the results. diff --git a/docs/Works.mdx b/docs/Works.mdx index f223e8c9..290d4e5a 100644 --- a/docs/Works.mdx +++ b/docs/Works.mdx @@ -5,8 +5,8 @@ title: How it works description: Explanation of the basics and how Maintainerr was designed to be used. --- -import Details from '@theme/Details' -import CodeBlock from '@theme/CodeBlock' +import Details from "@theme/Details"; +import CodeBlock from "@theme/CodeBlock"; ## Basic Idea diff --git a/docusaurus.config.js b/docusaurus.config.js index cf744cae..f51691f7 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -1,155 +1,155 @@ const config = { - title: 'Maintainerr Documentation', - tagline: 'Documentation for the Maintainerr project.', - favicon: 'img/favicon.ico', - url: 'https://docs.maintainerr.info', - baseUrl: '/', - organizationName: 'Maintainerr', - projectName: 'Maintainerr_docs', + title: "Maintainerr Documentation", + tagline: "Documentation for the Maintainerr project.", + favicon: "img/favicon.ico", + url: "https://docs.maintainerr.info", + baseUrl: "/", + organizationName: "Maintainerr", + projectName: "Maintainerr_docs", customFields: { feedbackIssueUrl: - 'https://github.com/Maintainerr/Maintainerr_docs/issues/new/choose', - privacyPolicyUrl: 'https://maintainerr.info/privacy', + "https://github.com/Maintainerr/Maintainerr_docs/issues/new/choose", + privacyPolicyUrl: "https://maintainerr.info/privacy", matomo: { - siteId: '2', - trackerUrl: 'https://analytics.maintainerr.info/', + siteId: "2", + trackerUrl: "https://analytics.maintainerr.info/", enabled: true, }, }, trailingSlash: true, - onBrokenLinks: 'warn', - staticDirectories: ['static'], + onBrokenLinks: "warn", + staticDirectories: ["static"], markdown: { hooks: { - onBrokenMarkdownLinks: 'warn', + onBrokenMarkdownLinks: "warn", }, }, plugins: [ [ - require.resolve('@easyops-cn/docusaurus-search-local'), + require.resolve("@easyops-cn/docusaurus-search-local"), { hashed: true, indexDocs: true, indexBlog: true, - docsRouteBasePath: '/', - blogRouteBasePath: '/blog', + docsRouteBasePath: "/", + blogRouteBasePath: "/blog", }, ], ], presets: [ [ - 'classic', + "classic", { docs: { - path: 'docs', - routeBasePath: '/', - sidebarPath: require.resolve('./sidebars.js'), + path: "docs", + routeBasePath: "/", + sidebarPath: require.resolve("./sidebars.js"), showLastUpdateAuthor: true, showLastUpdateTime: true, includeCurrentVersion: true, }, blog: { - blogTitle: 'Maintainerr Walkthroughs', + blogTitle: "Maintainerr Walkthroughs", blogDescription: - 'A collection of walkthroughs and tutorials for using Maintainerr.', - postsPerPage: 'ALL', - sortPosts: 'ascending', + "A collection of walkthroughs and tutorials for using Maintainerr.", + postsPerPage: "ALL", + sortPosts: "ascending", }, theme: { - customCss: require.resolve('./src/css/custom.css'), + customCss: require.resolve("./src/css/custom.css"), }, }, ], ], themeConfig: { colorMode: { - defaultMode: 'dark', + defaultMode: "dark", disableSwitch: true, respectPrefersColorScheme: false, }, - image: 'img/docs_image.png', + image: "img/docs_image.png", tableOfContents: { maxHeadingLevel: 4, }, navbar: { hideOnScroll: true, - title: 'Maintainerr Docs', + title: "Maintainerr Docs", logo: { - alt: 'Maintainerr logo', - src: 'img/logo_icon.svg', + alt: "Maintainerr logo", + src: "img/logo_icon.svg", }, items: [ - { to: '/installation', label: 'Get Started', position: 'left' }, - { to: '/configuration', label: 'Configuration', position: 'left' }, - { to: '/rules', label: 'Rules', position: 'left' }, + { to: "/installation", label: "Get Started", position: "left" }, + { to: "/configuration", label: "Configuration", position: "left" }, + { to: "/rules", label: "Rules", position: "left" }, { - type: 'docsVersionDropdown', - position: 'right', + type: "docsVersionDropdown", + position: "right", dropdownActiveClassDisabled: true, }, - { to: '/blog', label: 'Walkthroughs', position: 'left' }, + { to: "/blog", label: "Walkthroughs", position: "left" }, { - href: 'https://github.com/maintainerr/maintainerr', - label: 'GitHub', - position: 'right', + href: "https://github.com/maintainerr/maintainerr", + label: "GitHub", + position: "right", }, ], }, footer: { - style: 'dark', + style: "dark", links: [ { - title: 'Docs', + title: "Docs", items: [ - { label: 'Installation', to: '/installation' }, - { label: 'Configuration', to: '/configuration' }, - { label: 'API', to: '/api' }, + { label: "Installation", to: "/installation" }, + { label: "Configuration", to: "/configuration" }, + { label: "API", to: "/api" }, ], }, { - title: 'Community', + title: "Community", items: [ { - label: 'Changelog', - href: 'https://github.com/Maintainerr/Maintainerr/releases', + label: "Changelog", + href: "https://github.com/Maintainerr/Maintainerr/releases", }, { - label: 'Status of Services', - href: 'https://status.maintainerr.info', + label: "Status of Services", + href: "https://status.maintainerr.info", }, { - label: 'Feature Requests', - href: 'https://features.maintainerr.info/?view=most-wanted', + label: "Feature Requests", + href: "https://features.maintainerr.info/?view=most-wanted", }, { - label: 'Discord', - href: 'https://discord.maintainerr.info', + label: "Discord", + href: "https://discord.maintainerr.info", }, ], }, { - title: 'More', + title: "More", items: [ { - label: 'Docker Hub', - href: 'https://hub.docker.com/r/maintainerr/maintainerr/', + label: "Docker Hub", + href: "https://hub.docker.com/r/maintainerr/maintainerr/", }, { - label: 'GHCR Package', - href: 'https://ghcr.io/maintainerr/maintainerr', + label: "GHCR Package", + href: "https://ghcr.io/maintainerr/maintainerr", }, ], }, ], logo: { - alt: 'Maintainerr logo', - src: 'img/logo.svg', + alt: "Maintainerr logo", + src: "img/logo.svg", width: 250, height: 72, }, copyright: `Copyright (c) ${new Date().getFullYear()} Maintainerr`, }, }, -} +}; -module.exports = config +module.exports = config; diff --git a/package-lock.json b/package-lock.json index 0a6ae5d7..e519c7b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,12 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@docusaurus/core": "^3.9.2", - "@docusaurus/preset-classic": "^3.9.2", - "@easyops-cn/docusaurus-search-local": "^0.55.1" + "@docusaurus/core": "^3.10.1", + "@docusaurus/preset-classic": "^3.10.1", + "@easyops-cn/docusaurus-search-local": "^0.55.2" + }, + "devDependencies": { + "prettier": "^3.8.3" } }, "node_modules/@algolia/abtesting": { @@ -4963,9 +4966,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4982,9 +4982,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5001,9 +4998,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5020,9 +5014,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -15610,6 +15601,22 @@ "postcss": "^8.4.31" } }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-error": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", diff --git a/package.json b/package.json index 8929e13f..cefb58ab 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,9 @@ "build": "docusaurus build", "serve": "docusaurus serve --dir build", "clear": "docusaurus clear", - "docs:version": "docusaurus docs:version" + "docs:version": "docusaurus docs:version", + "format": "prettier --write .", + "format:check": "prettier --check ." }, "repository": { "type": "git", @@ -22,11 +24,14 @@ }, "homepage": "https://github.com/Maintainerr/Maintainerr_docs#readme", "dependencies": { - "@docusaurus/core": "^3.9.2", - "@docusaurus/preset-classic": "^3.9.2", - "@easyops-cn/docusaurus-search-local": "^0.55.1" + "@docusaurus/core": "^3.10.1", + "@docusaurus/preset-classic": "^3.10.1", + "@easyops-cn/docusaurus-search-local": "^0.55.2" }, "overrides": { "encoding-sniffer": "1.0.2" + }, + "devDependencies": { + "prettier": "^3.8.3" } } diff --git a/sidebars.js b/sidebars.js index 22b61158..df3013db 100644 --- a/sidebars.js +++ b/sidebars.js @@ -1,50 +1,54 @@ /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ const sidebars = { docsSidebar: [ - 'introduction', + "introduction", { - type: 'category', - label: 'Getting Started', + type: "category", + label: "Getting Started", items: [ - 'installation', - 'configuration', - 'works', - 'common', - 'reverseproxy', - 'downgrade', + "installation", + "configuration", + "works", + "common", + "reverseproxy", + "downgrade", ], }, { - type: 'category', - label: 'Rules', - items: ['rules', 'glossary'], + type: "category", + label: "Rules", + items: ["rules", "glossary"], }, { - type: 'category', - label: 'Collections', - items: ['collections', 'test-media'], + type: "category", + label: "Collections", + items: ["collections", "test-media"], }, { - type: 'category', - label: 'Features', - items: ['calendar-feature', 'overlays-feature', 'storage-metrics-feature'], + type: "category", + label: "Features", + items: [ + "calendar-feature", + "overlays-feature", + "storage-metrics-feature", + ], }, { - type: 'category', - label: 'Notifications', - items: ['notifications'], + type: "category", + label: "Notifications", + items: ["notifications"], }, { - type: 'category', - label: 'Community', - items: ['contributing', 'changelog'], + type: "category", + label: "Community", + items: ["contributing", "changelog"], }, { - type: 'category', - label: 'API', - items: ['api'], + type: "category", + label: "API", + items: ["api"], }, ], -} +}; -module.exports = sidebars +module.exports = sidebars; diff --git a/src/components/AnnotatedCodeBlock/index.jsx b/src/components/AnnotatedCodeBlock/index.jsx index 9f3186a0..812737b5 100644 --- a/src/components/AnnotatedCodeBlock/index.jsx +++ b/src/components/AnnotatedCodeBlock/index.jsx @@ -1,19 +1,19 @@ -import React from 'react' -import InlineTooltip from '../InlineTooltip' -import styles from './styles.module.css' +import React from "react"; +import InlineTooltip from "../InlineTooltip"; +import styles from "./styles.module.css"; export default function AnnotatedCodeBlock({ code, - language = 'text', + language = "text", annotations = [], }) { - const lines = code.replace(/\n$/, '').split('\n') + const lines = code.replace(/\n$/, "").split("\n"); const annotationsByLine = annotations.reduce((map, annotation) => { - const list = map.get(annotation.line) ?? [] - list.push(annotation) - map.set(annotation.line, list) - return map - }, new Map()) + const list = map.get(annotation.line) ?? []; + list.push(annotation); + map.set(annotation.line, list); + return map; + }, new Map()); return (
@@ -21,12 +21,12 @@ export default function AnnotatedCodeBlock({
           
             {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}
                 
-              )
+              );
             })}
           
         
- ) + ); } diff --git a/src/components/DocFeedback/index.jsx b/src/components/DocFeedback/index.jsx index f0188dc0..c8eef515 100644 --- a/src/components/DocFeedback/index.jsx +++ b/src/components/DocFeedback/index.jsx @@ -1,22 +1,22 @@ -import React, {useMemo, useState} from 'react' -import useDocusaurusContext from '@docusaurus/useDocusaurusContext' -import {useLocation} from '@docusaurus/router' -import styles from './styles.module.css' -import {trackMatomoEvent} from '../SiteConsent' +import React, { useMemo, useState } from "react"; +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; +import { useLocation } from "@docusaurus/router"; +import styles from "./styles.module.css"; +import { trackMatomoEvent } from "../SiteConsent"; export default function DocFeedback() { - const {siteConfig} = useDocusaurusContext() - const location = useLocation() - const [selection, setSelection] = useState(null) - const issueUrl = siteConfig.customFields?.feedbackIssueUrl + const { siteConfig } = useDocusaurusContext(); + const location = useLocation(); + const [selection, setSelection] = useState(null); + const issueUrl = siteConfig.customFields?.feedbackIssueUrl; const pageId = useMemo( () => `${location.pathname}${location.search}${location.hash}`, [location.hash, location.pathname, location.search], - ) + ); function handleSelection(nextSelection) { - setSelection(nextSelection) - trackMatomoEvent('docs-feedback', nextSelection, pageId) + setSelection(nextSelection); + trackMatomoEvent("docs-feedback", nextSelection, pageId); } return ( @@ -25,24 +25,28 @@ export default function DocFeedback() {
- {selection === 'helpful' &&

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.

- -
- ) + ); } diff --git a/src/css/custom.css b/src/css/custom.css index f20ba683..588552a3 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -14,14 +14,14 @@ --ifm-footer-link-color: var(--ifm-navbar-link-color); --ifm-footer-title-color: var(--ifm-navbar-link-color); --ifm-heading-color: #111827; - --ifm-font-family-base: 'Segoe UI', sans-serif; + --ifm-font-family-base: "Segoe UI", sans-serif; --ifm-heading-font-family: var(--ifm-font-family-base); - --ifm-font-family-monospace: 'Cascadia Code', 'Consolas', 'SFMono-Regular', - Menlo, Monaco, monospace; + --ifm-font-family-monospace: + "Cascadia Code", "Consolas", "SFMono-Regular", Menlo, Monaco, monospace; --ifm-code-font-size: 95%; } -[data-theme='dark'] { +[data-theme="dark"] { --ifm-background-color: #111827; --ifm-heading-color: #f8fafc; } @@ -35,8 +35,9 @@ margin: 0 auto 2rem; padding: 2.25rem 1.5rem; border-radius: 1.25rem; - background: linear-gradient(rgba(17, 24, 39, 0.66), rgba(17, 24, 39, 0.66)), - url('/img/banner3.png') center 35% / cover no-repeat; + background: + linear-gradient(rgba(17, 24, 39, 0.66), rgba(17, 24, 39, 0.66)), + url("/img/banner3.png") center 35% / cover no-repeat; box-shadow: 0 18px 40px rgba(15, 23, 42, 0.16); } @@ -143,7 +144,8 @@ .hero-shell { min-height: calc(100vh - var(--ifm-navbar-height)); - background: radial-gradient( + background: + radial-gradient( circle at top left, rgba(217, 119, 6, 0.28), transparent 35% diff --git a/src/theme/DocItem/Footer/index.js b/src/theme/DocItem/Footer/index.js index 437e53cf..e1ce334f 100644 --- a/src/theme/DocItem/Footer/index.js +++ b/src/theme/DocItem/Footer/index.js @@ -1,16 +1,16 @@ -import React from 'react' -import DocItemFooter from '@theme-original/DocItem/Footer' -import DocFeedback from '@site/src/components/DocFeedback' -import {useDoc} from '@docusaurus/plugin-content-docs/client' +import React from "react"; +import DocItemFooter from "@theme-original/DocItem/Footer"; +import DocFeedback from "@site/src/components/DocFeedback"; +import { useDoc } from "@docusaurus/plugin-content-docs/client"; export default function DocItemFooterWrapper(props) { - const {metadata} = useDoc() - const hideFeedback = metadata.id === 'introduction' + const { metadata } = useDoc(); + const hideFeedback = metadata.id === "introduction"; return ( <> {!hideFeedback && } - ) + ); } diff --git a/src/theme/Root.js b/src/theme/Root.js index 7f46d163..c38c17ad 100644 --- a/src/theme/Root.js +++ b/src/theme/Root.js @@ -1,11 +1,11 @@ -import React from 'react' -import SiteConsent from '@site/src/components/SiteConsent' +import React from "react"; +import SiteConsent from "@site/src/components/SiteConsent"; -export default function Root({children}) { +export default function Root({ children }) { return ( <> {children} - ) + ); } diff --git a/static/stylesheets/extra.css b/static/stylesheets/extra.css index 909c5bde..7e9bf815 100644 --- a/static/stylesheets/extra.css +++ b/static/stylesheets/extra.css @@ -1,14 +1,18 @@ @keyframes dev { - 0%, 40%, 80%, 100% { - transform: scale(1); - } - 20%, 60% { - transform: scale(1.15); - } + 0%, + 40%, + 80%, + 100% { + transform: scale(1); } - .dev { - animation: dev 1500ms infinite; - } - .md-status--recent::after { - mask-image: url('data:image/svg+xml;charset=utf-8,plus-circle'); + 20%, + 60% { + transform: scale(1.15); } +} +.dev { + animation: dev 1500ms infinite; +} +.md-status--recent::after { + mask-image: url('data:image/svg+xml;charset=utf-8,plus-circle'); +}