From 8eae1a9bcf548db93ca6a70d69b4889e7a2969b9 Mon Sep 17 00:00:00 2001 From: enoch85 Date: Mon, 28 Sep 2026 13:58:04 +0000 Subject: [PATCH 1/2] docs: cover Ombi, the Services section and the watch statistics API The drift report for the next release (#177) lists Ombi as a second request service, a settings navigation rework, per-server exclusion tagging, a new media-server sort field and three new endpoints. Ombi gets a Configuration section, because the settings page's docs button links to Configuration/#ombi and that anchor did not exist. Its seven rule properties are in the glossary with the quirks a reader cannot infer from the names: Ombi stamps an approval only when someone approves by hand, holds one request per movie and one per user for a show, and stores air dates only for the episodes someone requested. The "Force delete Ombi request" toggle is described next to the Seerr one, including what a season removal leaves in place, since Ombi can only delete a whole request. Service connections moved out of Settings into their own Services section, so the Settings -> X references now say Services -> X. The Radarr and Sonarr field tables still described a hostname, port and base URL form that the card layout replaced, and the exclusion tag is set per server rather than per service, so both were rewritten against the current form. The API reference gains an Ombi page and a watch statistics page for the Tautulli and Tracearr per-item endpoints. A new page has no route into the site without a sidebars.js entry, which is the only edit outside docs/ and static/openapi-spec/. --- docs/API.md | 8 +- docs/Collections.md | 4 +- docs/Configuration.md | 70 ++++++----- docs/Glossary.md | 86 ++++++++++++++ docs/Migration.md | 2 + docs/Notifications.md | 32 ++--- docs/Overlays.md | 2 + docs/Rules.mdx | 3 + docs/Security.md | 2 +- docs/api/ombi.md | 45 +++++++ docs/api/settings.md | 60 ++++++++++ docs/api/watch-statistics.md | 83 +++++++++++++ sidebars.js | 2 + .../openapi-spec/maintainerr_api_specs.yaml | 112 ++++++++++++++++++ 14 files changed, 461 insertions(+), 50 deletions(-) create mode 100644 docs/api/ombi.md create mode 100644 docs/api/watch-statistics.md diff --git a/docs/API.md b/docs/API.md index 6a1a38548..f57af6bd7 100644 --- a/docs/API.md +++ b/docs/API.md @@ -13,13 +13,13 @@ hide: ::: -Maintainerr exposes 219 HTTP endpoints. Every one of them is documented in the pages below, grouped by area. +Maintainerr exposes 226 HTTP endpoints. Every one of them is documented in the pages below, grouped by area. ## Endpoints by area | Page | Endpoints | Covers | | ------------------------------------------------------------- | --------- | ---------------------------------------------------------------------------------- | -| [Settings](./api/settings.md) | 74 | Every integration's connection settings, connection tests, the media server switch | +| [Settings](./api/settings.md) | 78 | Every integration's connection settings, connection tests, the media server switch | | [Rules](./api/rules.md) | 29 | Rule groups, execution, exclusions, community rules, YAML import and export | | [Overlays](./api/overlays.md) | 28 | Overlay settings, processing runs, templates, fonts and images | | [Collections](./api/collections.md) | 25 | Collections, membership, bulk media actions, handling, posters, logs | @@ -31,6 +31,8 @@ Maintainerr exposes 219 HTTP endpoints. Every one of them is documented in the p | [Metadata, storage and events](./api/metadata-and-storage.md) | 6 | Metadata provider lookups, storage metrics, the events stream | | [Servarr](./api/servarr.md) | 5 | Radarr, Sonarr and Sportarr disk space and quality profiles | | [Streamystats](./api/streamystats.md) | 2 | Streamystats server info and per-item watch statistics | +| [Watch statistics](./api/watch-statistics.md) | 2 | Per-item Tautulli and Tracearr watch statistics | +| [Ombi](./api/ombi.md) | 1 | Ombi requester names | ## API conventions @@ -66,7 +68,7 @@ Bodies are validated per endpoint. Where a schema exists, a failure returns: `errors` holds the individual validation problems. -Validation is not universal. Of the 70 endpoints that take a body, **17 have no schema at all**, so the body reaches the service unchecked. Each of those 17 says so on its own entry. Most numeric path parameters are checked and reject a non-numeric value with a `400` before the handler runs, but not all: `DELETE /api/notifications/configuration/{id}` declares a numeric id without that check. +Validation is not universal. Of the 72 endpoints that take a body, **17 have no schema at all**, so the body reaches the service unchecked. Each of those 17 says so on its own entry. Most numeric path parameters are checked and reject a non-numeric value with a `400` before the handler runs, but not all: `DELETE /api/notifications/configuration/{id}` declares a numeric id without that check. ### Success and failure in the same status code diff --git a/docs/Collections.md b/docs/Collections.md index 5ea74f9bf..a21b2e9f0 100644 --- a/docs/Collections.md +++ b/docs/Collections.md @@ -34,9 +34,9 @@ When Plex is your configured media server, the rule group or collection form can - This is a Plex-only feature. Jellyfin and Emby do not expose a safe collection reorder API, so the sort control is not available there. - `Default (no custom sort)` leaves the collection order alone. -- The options are delete soonest or latest, title, air date or release date, rating, and watch count. +- The options are delete soonest or latest, title, date added, air date or release date, rating, and watch count. `Date Added` is the date the item was added to your media server, which is the same order the server itself uses. - Saving a new or changed sort applies it to Plex immediately; you do not need to wait for the next collection add cycle. -- Air date / release date, rating, watch count, and delete soonest / latest sorts break ties by title so Plex keeps the same order you see in Maintainerr. +- Air date / release date, date added, rating, watch count, and delete soonest / latest sorts break ties by title so Plex keeps the same order you see in Maintainerr. :::note Turning the custom sort back off does not restore Plex's previous order automatically. If you want the old order back, change it directly in Plex. diff --git a/docs/Configuration.md b/docs/Configuration.md index f8aed5acb..439164cec 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -9,6 +9,8 @@ All configuration is done inside the application. No extra config files are requ When you first access the web UI, you should be redirected to the settings page. If that does not happen, try refreshing the page. +Service connections live under `Services` in the sidebar. That page lists every service as a card, grouped by what it does - media server, requests, library managers, metadata, watch statistics, downloads and notifications - and each card says whether that service is set up. Open a card to configure it, then use the row of logos along the top of the page to move between services. `Settings` holds the rest: `General`, `Logs`, `Jobs` and `About`. A `/settings/` link lands on the same page under `/services`. + :::note Service URLs Every service URL field expects a base URL starting with `http://` or `https://`. Any trailing slashes are removed for you when you save or test the setting. ::: @@ -29,7 +31,7 @@ These settings are OK for most installations. ## Media Server -You need to configure one media server: Plex, Jellyfin, or Emby. Multiple media servers are not supported simultaneously. +You need to configure one media server: Plex, Jellyfin, or Emby. Multiple media servers are not supported simultaneously. Pick it on the media server card under `Services`; that page also holds its connection settings. ## Plex @@ -111,7 +113,7 @@ Jellyfin can also be used as your media server connection. 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. +The `Services -> 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 | | ------- | ----------------------------------------------------------------------------------------------------------------- | @@ -136,33 +138,44 @@ Seerr configuration is required if you want to use Seerr-related rule parameters | URL | The domain name or local IP address of the host running Seerr | | API key | The API key from Seerr settings | +## Ombi + +Ombi is a second request service alongside Seerr. Both can be configured at once. Ombi's configuration is required if you want to use Ombi-related rule parameters, remove Ombi requests, or see who requested media in pre-deletion notifications. + +| Setting | Description | +| ------- | ------------------------------------------------------------ | +| URL | The domain name or local IP address of the host running Ombi | +| API key | The API key from Ombi, under `Settings`, `Ombi`, `API Key` | + +Ombi holds one request per movie, and one request per user for a show, each covering the seasons and episodes that user asked for. That shapes what the rule properties answer, so read the [Ombi rules](./Glossary.md#ombi) in the glossary before you build a rule on them. + +Once a request has gone available, Ombi never lists it as unavailable again. If you want the request removed when Maintainerr deletes the media, turn on `Force delete Ombi request` on the rule group - see [Creating rules](./Rules.mdx#general). + ## Radarr Radarr's configuration is required to use its parameters in rules and to remove or unmonitor movies. -:::note -Enter the `Base URL` for Radarr and Sonarr without a leading slash - `radarr`, not `/radarr`. -::: +Each Radarr instance is its own card on the page. Use `Add Radarr server` to add another. -| 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 | +| Setting | Description | +| ----------- | ---------------------------------------------------------------------------------------------------- | +| Server Name | A friendly name to help identify the server | +| URL | The base URL of the Radarr instance, such as `http://localhost:7878` or `https://radarr.example.com` | +| Base Path | The URL base configured in Radarr, if one is set. Only editable once `URL` holds a complete address | +| 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 | -| 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 | +Each Sonarr instance is its own card on the page. Use `Add Sonarr server` to add another. + +| Setting | Description | +| ----------- | ---------------------------------------------------------------------------------------------------- | +| Server Name | A friendly name to help identify the server | +| URL | The base URL of the Sonarr instance, such as `http://localhost:8989` or `https://sonarr.example.com` | +| Base Path | The URL base configured in Sonarr, if one is set. Only editable once `URL` holds a complete address | +| API key | The API key from Sonarr settings | ## Sportarr @@ -176,21 +189,22 @@ shows come from the configured Sportarr connection. For a league none of your co Maintainerr can read that artwork from sportarr.net instead, which is off unless you set the `SPORTARR_NET` environment variable to `on`. Nothing is requested from sportarr.net otherwise. -Artwork and descriptions are cached. `Refresh Metadata` in the Sportarr server dialog drops that +Artwork and descriptions are cached. `Refresh metadata` on the Sportarr server card drops that cache, so the next view fetches them again. -| Setting | Description | -| ----------- | ------------------------------------------- | -| Server Name | A friendly name to help identify the server | -| URL | The base URL of the Sportarr instance | -| API key | The API key from Sportarr settings | +| Setting | Description | +| ----------- | ----------------------------------------------------------------------------------------------------- | +| Server Name | A friendly name to help identify the server | +| URL | The base URL of the Sportarr instance | +| Base Path | The URL base configured in Sportarr, if one is set. Only editable once `URL` holds a complete address | +| API key | The API key from Sportarr settings | ## Exclusion tag When Maintainerr excludes an item, it can apply a protective tag to the matching Radarr movie or Sonarr series, so the \*arr instance carries a single source of truth for "do not touch". This covers both global and collection-scoped exclusions. :::note -The `Exclusion tag` section appears on the `Settings -> Radarr` and `Settings -> Sonarr` pages once that service is configured. Radarr and Sonarr are configured independently: each has its own enable toggle, label, and removal policy. +These settings sit on each Radarr and Sonarr server's own card, under `Services`. Every server is configured independently, with its own enable toggle, label, and removal policy. A collection-scoped exclusion tags the item on that collection's server only. A global one tags it on every configured server that has tagging switched on and tracks the item, each with that server's own label. ::: | Setting | Description | @@ -204,7 +218,7 @@ Tagging is best-effort: the tag is added or removed through the Radarr/Sonarr ed ## Download Client :::note -The separate `Settings -> Download Client` page only appears once Radarr or Sonarr is configured. +The `Services -> Download Client` page only appears once Radarr or Sonarr is configured. ::: When media is removed through Radarr or Sonarr, Maintainerr can remove the matching completed download (and optionally its data) from your download client. Downloads are matched using the Radarr/Sonarr download history, so media removed without Radarr/Sonarr is left untouched. @@ -258,7 +272,7 @@ A `403 Forbidden` on the connection test (or in the logs) means qBittorrent acce ## Metadata -Maintainerr has a separate `Settings -> Metadata` page for poster, backdrop, and metadata-provider settings used across the UI. +Maintainerr has a `Services -> Metadata` page for poster, backdrop, and metadata-provider settings used across the UI. 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. diff --git a/docs/Glossary.md b/docs/Glossary.md index 5af46b3c6..41b856e54 100644 --- a/docs/Glossary.md +++ b/docs/Glossary.md @@ -2125,6 +2125,92 @@ Indicates whether the media was requested in Seerr. --- +### Ombi + +:::note +Ombi holds one request per movie, and one request per user for a show, each covering the seasons and episodes that user asked for. A season or episode property therefore answers from the requests that cover it, and a show property from all of them. + +::: + +#### Requested by user + +:::info +The Ombi username of the user who requested the media. A request made with Ombi's API key is listed under the alias that request carries. + +::: + +- Key: Ombi.addUser +- Availability: movies, shows, seasons, episodes +- Type: text + +#### Request date + +:::info +The date when the media was requested in Ombi. Where several requests cover the item, the earliest one. + +::: + +- Key: Ombi.requestDate +- Availability: movies, shows, seasons, episodes +- Type: date + +#### Release/air date + +:::info +The release date Ombi holds for the movie or show. For a season or episode it is the earliest air date among the requested episodes, because Ombi only stores air dates for the episodes someone actually requested. + +::: + +- Key: Ombi.releaseDate +- Availability: movies, shows, seasons, episodes +- Type: date + +#### Approval date + +:::info +The date when the media request was approved in Ombi. Ombi only stamps an approval that was given by hand, so an auto-approved request answers with its request date instead. + +::: + +- Key: Ombi.approvalDate +- Availability: movies, shows, seasons, episodes +- Type: date + +#### Media downloaded date + +:::info +The date when Ombi marked the request as available. + +::: + +- Key: Ombi.mediaAddedAt +- Availability: movies, shows, seasons, episodes +- Type: date + +#### Amount of requests + +:::info +The number of Ombi requests covering the media. A movie is always 1 or 0. + +::: + +- Key: Ombi.amountRequested +- Availability: movies, shows, seasons, episodes +- Type: number + +#### Requested in Ombi + +:::info +Indicates whether the media was requested in Ombi. + +::: + +- Key: Ombi.isRequested +- Availability: movies, shows, seasons, episodes +- Type: boolean + +--- + ### Tautulli :::note diff --git a/docs/Migration.md b/docs/Migration.md index 6b1ce05ab..7ab7a47dc 100644 --- a/docs/Migration.md +++ b/docs/Migration.md @@ -41,6 +41,8 @@ The database schema includes support for overlays. - a `sportarr_settings` table plus optional Sportarr server and quality-profile fields on collections. Existing collections remain unchanged until you configure Sportarr. - a `cleanupLeftoverFolders` collection field for the opt-in leftover-folder cleanup. Existing collections default to disabled. - three Tracearr settings (`tracearr_url`, `tracearr_api_key`, `tracearr_server_id`) for the Tracearr watch history integration. Existing configurations are unaffected. +- two Ombi settings (`ombi_url`, `ombi_api_key`) plus a `forceOmbi` collection field for the Ombi request service. Existing collections default to off, and nothing contacts Ombi until you configure it. +- a move of the Radarr and Sonarr exclusion-tag settings out of the global settings row and onto each server, as `tagExclusions`, `exclusionTag` and `untagOnUnexclude` columns on `radarr_settings` and `sonarr_settings`. The migration copies each service's settings onto every server of that service, so tagging behaves exactly as it did before the upgrade. Its `down()` does not copy them back. No manual database work should be required, but you should still keep a backup of `/opt/data/maintainerr.db` before upgrading and allow startup migrations to complete before using the overlay screens. diff --git a/docs/Notifications.md b/docs/Notifications.md index d7b424573..4df3e238f 100644 --- a/docs/Notifications.md +++ b/docs/Notifications.md @@ -18,7 +18,7 @@ The notification system works by connecting configured notification agents to yo ## Configuring Notification Agents -Navigate to **Settings → Notifications** to manage your notification agents. Here you can add, edit, and delete notification configurations. +Navigate to **Services → Notifications** to manage your notification agents. Here you can add, edit, and delete notification configurations. Each agent is a card you open to edit. ### General Configuration @@ -36,22 +36,22 @@ Each notification agent requires the following common settings: 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. When Seerr is configured, the message also names the requester (see note below). | -| 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. | -| Update Available | Sent when a newer Maintainerr build is available, naming the current and new version and linking the upgrade guide (see note below). | +| 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. When Seerr or Ombi is configured, the message also names the requester (see note below). | +| 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. | +| Update Available | Sent when a newer Maintainerr build is available, naming the current and new version and linking the upgrade guide (see note below). | Infrastructure-level collection failures that happen before Maintainerr can identify a specific collection still send the generic `Collection Handling Failed` message. Maintainerr checks for a newer build twice a day, and sends `Update Available` on the `latest`, `stable`, and `main` image tags. The `development` tag moves with every merged commit, so it only shows the sidebar update indicator instead of announcing each one. Release notes are linked only when the newer build is a published release. -:::note Seerr requester in pre-deletion warnings -When Seerr (Overseerr or Jellyseerr) is configured, the **Media About to be Handled** message includes who requested the item, for example: _'Some Title' (requested by alice) will be handled in 3 days_. The lookup is season-aware for TV content and best-effort by design: if Seerr is unreachable or the item was not requested through Seerr, the requester line is silently omitted and the warning is still sent. +:::note Requester in pre-deletion warnings +When Seerr (Overseerr or Jellyseerr) or Ombi is configured, the **Media About to be Handled** message includes who requested the item, for example: _'Some Title' (requested by alice) will be handled in 3 days_. Both services are asked when both are configured, and the names are merged. The lookup is scoped to the season or episode for TV content and best-effort by design: if the service is unreachable or the item was not requested there, the requester line is silently omitted and the warning is still sent. ::: ### How messages are grouped and delivered @@ -235,7 +235,7 @@ The `{{extra}}` block is also flattened into the top-level webhook payload as in | `dayAmount` | number | Days until the item is handled (`null` when not applicable) | | `mediaItems` | string | Stringified JSON array of media items (see example below) | -Example `mediaItems` value for a **Media About to be Handled** notification when Seerr is configured: +Example `mediaItems` value for a **Media About to be Handled** notification when a request service is configured: ```json [ @@ -257,13 +257,13 @@ Every entry contains `mediaServerId`. The rest are optional: | `type` | `movie`, `show`, `season` or `episode` | | `title` | The item's name, worded the same way as in the message text | | `providerIds` | The item's IMDb, TMDB and TVDB ids. Each is an array, and empty when there is no id. | -| `requestedBy` | The Seerr usernames who requested the item | +| `requestedBy` | The Seerr and Ombi usernames who requested the item | `type`, `title` and `providerIds` come from a snapshot Maintainerr takes just before it handles an item, so they still name it once the deletion has made its media server id useless. Only **Media About to be Handled** and **Media Handled** carry them, and they are left out for an item the media server could not be asked about at the time. The ids are the item's own, so a season or episode carries season or episode ids rather than the show's. If you match on them, resolve the show yourself. -`requestedBy` is left out when Seerr is not configured or the item was not requested there. +`requestedBy` is left out when neither Seerr nor Ombi is configured, or the item was not requested in either. Example JSON payload: @@ -295,7 +295,7 @@ To configure notifications for a rule: You can test any configured notification agent by: -1. Going to **Settings → Notifications** +1. Going to **Services → Notifications** 2. Editing an existing notification configuration 3. Clicking the **Test** button 4. A test notification will be sent to verify the configuration is working diff --git a/docs/Overlays.md b/docs/Overlays.md index 749fc8499..fec5973f6 100644 --- a/docs/Overlays.md +++ b/docs/Overlays.md @@ -90,6 +90,8 @@ A countdown says the media leaves the library on that date, so a collection whos Maintainerr re-renders overlays when the visible days-left value changes, and it can revert overlay artwork for a single collection or for all collections. +Before uploading, it compares the rendered image with the artwork the media server currently holds and skips the upload when the two match, so an item whose day count moved without changing the picture is counted as skipped in the run's log. Artwork another tool has replaced does not match, so it is drawn again. + Deleting a collection restores its overlays first, so its items keep their original artwork. If a run is going on at the time, that restore waits for the run to finish instead of being skipped. ## Media that gets deleted along with a collection diff --git a/docs/Rules.mdx b/docs/Rules.mdx index 86821859e..46b9c3ce4 100644 --- a/docs/Rules.mdx +++ b/docs/Rules.mdx @@ -98,12 +98,15 @@ General info about the rule. Some of the information specified here will be show | Clean up leftover folders | (BETA) When enabled, Maintainerr removes the folder the \*arr leaves behind after deleting files one at a time, along with its sidecars (subtitles, .nfo, artwork). Off by default. Only shown when the selected action strands a folder. Requires the media library mounted into Maintainerr at the same path as the \*arr. See [Leftover folder cleanup](./Collections.md#leftover-folder-cleanup). | | 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. | +| Force delete Ombi request | Deletes the matching Ombi request when the media is deleted. Without it the request stays in Ombi, still listed as available. | | 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 | | Keep in Maintainerr only | Keep this collection inside Maintainerr and create nothing on your media server. Rules, actions, overlays, and \*arr tags all keep working. Off by default, set per collection, and hidden while `Custom collection` is on. | 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. +`Force delete Ombi request` is shown for the same three rule types, for a similar reason: Ombi can only delete a whole request, so deleting one for a single episode would drop the requests for the episodes you still have. Ombi has no availability sync of its own either, so a request left in place stays listed as available even after the media is gone. On a season rule, Maintainerr deletes only the requests that cover nothing but that season; one that also covers another season is kept, and the log says so. Ombi drops a show's own request once its last remaining request is gone. + When you choose `Change quality profile and search` for Radarr or Sonarr, Maintainerr shows a quality profile selector for the chosen server. This action does not use `Take action after days`; it changes the item's quality profile immediately when the collection action runs. The `Tag this content` option keeps a Radarr/Sonarr tag named after the rule group on each matching item for as long as it stays in the collection, and removes it when the item leaves. Because Sonarr tags apply to the whole series, the option is offered for movie collections (Radarr) and whole-show collections (Sonarr) only - not season or episode collections. The tag is created on the \*arr instance if missing, the rule group's name is normalized to the tag character set Radarr/Sonarr accept (lowercase letters, numbers, and hyphens), and tagging is best-effort, so it never blocks a rule run. Renaming a rule group applies the new tag on the next membership change but leaves the previous tag in place until you remove it. diff --git a/docs/Security.md b/docs/Security.md index b55719048..eb1eb5561 100644 --- a/docs/Security.md +++ b/docs/Security.md @@ -101,7 +101,7 @@ If you want to run Maintainerr as safely as possible: Only needed if your Maintainerr was reachable from the internet without a login. -- [ ] Rotate the API key for every connected service: Plex token, Sonarr/Radarr/Sportarr API keys, Seerr API key, Tautulli API key, Tracearr API key, Jellyfin/Emby API key, TMDB API key, TVDB API key, and the qBittorrent download-client password. (Streamystats needs nothing separate - Maintainerr authenticates to it with the Jellyfin API key already listed here.) +- [ ] Rotate the API key for every connected service: Plex token, Sonarr/Radarr/Sportarr API keys, Seerr API key, Ombi API key, Tautulli API key, Tracearr API key, Jellyfin/Emby API key, TMDB API key, TVDB API key, and the download-client password. (Streamystats needs nothing separate - Maintainerr authenticates to it with the Jellyfin API key already listed here.) - [ ] Rotate any webhook URLs or SMTP credentials configured in Maintainerr's notification agents. - [ ] Review recent collection runs in Maintainerr's logs for unexpected deletions or rule changes. - [ ] Check Sonarr/Radarr/Seerr audit logs if available. diff --git a/docs/api/ombi.md b/docs/api/ombi.md new file mode 100644 index 000000000..5c45e9976 --- /dev/null +++ b/docs/api/ombi.md @@ -0,0 +1,45 @@ +--- +slug: /api/ombi +title: Ombi API +description: The Ombi requester lookup used by the media modal and the pre-deletion warning. +--- + +One read-only route that names who requested a title in [Ombi](https://ombi.io/). Ombi request _deletion_ has no route of its own: it happens inside collection handling when a collection has `Force delete Ombi request` on. + +Ombi is a separate integration from [Seerr](./seerr.md), with its own settings and its own rule properties. Both can be configured at once, and the media modal and the pre-deletion warning merge the names they return. + +See [API conventions](../API.md#api-conventions) for the rules that apply to every endpoint. + +## Endpoints + +### `GET /api/ombi/requests/{tmdbId}/users` + +**List the deduplicated Ombi usernames of everyone who requested a title, optionally narrowed to one season or episode.** + +| Parameter | Type | Required | Description | +| --------- | -------------- | -------- | --------------------------------------------------------------------------------------------------- | +| `tmdbId` | path, integer | Yes | TMDB id of the movie or show. Ombi keys movies by TMDB id and shows by the TMDB id it holds on them | +| `type` | query, string | Yes | `movie` or `tv`. Anything else is a `400` | +| `season` | query, integer | No | Season number. Only narrows a `tv` lookup | +| `episode` | query, integer | No | Episode number within `season`. Only narrows a `tv` lookup | + +Response: + +```json +["example-user", "another-user"] +``` + +| Status | Cause | +| ------ | ----------------------------------------------------------------------------------------- | +| `200` | Array of usernames, possibly empty | +| `400` | `tmdbId` is not an integer, `type` is missing or not `movie`/`tv`, or a number is not one | + +Unlike the Seerr route, `type` is required and validated, because Ombi keeps movie and show requests in separate lists and cannot be asked for both at once. + +A request made with Ombi's API key is recorded against Ombi's system user, so the name reported is the alias that request carries instead. + +An empty array conflates three states: nobody requested the title, Ombi is down, and Ombi is not configured. This is deliberate, so that a pre-deletion notification is never suppressed just because the requester could not be named. + +:::caution Cost trap +The first call after the cache is cleared reads **every** movie request and **every** show request in Ombi to build an index. Opening a media modal can therefore trigger a full request prefetch. Concurrent first callers are collapsed onto a single sweep, and a failed sweep is not cached so the next call retries. The index is held for an hour and is cleared at the start of every rule group run. +::: diff --git a/docs/api/settings.md b/docs/api/settings.md index 75ea7a967..f17afa255 100644 --- a/docs/api/settings.md +++ b/docs/api/settings.md @@ -682,6 +682,8 @@ This is the only place the version requirement is applied. A build whose version ## Requests +Seerr and Ombi are separate integrations with separate storage, and both can be configured at once. + The three Seerr settings routes each answer on `/api/settings/seerr`, `/api/settings/overseerr` and `/api/settings/jellyseerr`. They are aliases of one handler with one storage location, and `/api/settings/seerr` is canonical. ### `GET /api/settings/seerr` @@ -742,6 +744,64 @@ There is no "test what is currently stored" mode. Both fields are required in th The URL and key both come from the request body, so this route makes the server contact any address you supply. Private addresses are deliberately allowed, since self-hosted services need them. Combined with the lack of authentication, that makes this a way to have your server issue requests on someone else's behalf. ::: +### `GET /api/settings/ombi` + +**Return the stored Ombi URL and API key.** + +| Status | Cause | +| -------------------------- | ----------------------------------------------- | +| `200` | The settings, **with the API key in cleartext** | +| `200` with `status: "NOK"` | The read failed | + +### `POST /api/settings/ombi` + +**Store the Ombi URL and API key.** + +Request body needs both `url` and `api_key`. + +| Status | Cause | +| -------------------------- | ----------------- | +| `201` | Saved | +| `201` with `status: "NOK"` | The write failed | +| `400` | Validation failed | + +The connection is stored **unverified**. Use `POST /api/settings/test/ombi` first if you want verification. + +Both fields are required together, so you cannot clear Ombi here. Use the `DELETE` route. + +### `DELETE /api/settings/ombi` + +**Clear the stored Ombi settings.** + +| Status | Cause | +| -------------------------- | ---------------- | +| `200` | Cleared | +| `200` with `status: "NOK"` | The write failed | + +:::warning Destructive +Clears the Ombi URL and API key, with no copy kept. The Ombi client is dropped, so nothing queries Ombi afterwards. + +Rules that read Ombi values are not deleted or rewritten. They simply stop resolving, and a collection with `forceOmbi` set stops removing requests. +::: + +### `POST /api/settings/test/ombi` + +**Probe an Ombi instance with supplied credentials, without saving.** + +The probe reads `/api/v1/Settings/about` on the instance you name. + +| Status | Cause | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| `201` with `status: "OK"` | Connected. `message` is the Ombi version | +| `201` with `status: "NOK"` | `Failure, an unexpected response was returned. The URL is likely incorrect.`, or a classified connection failure such as `Invalid API key` | +| `400` | Validation failed | + +There is no "test what is currently stored" mode. Both fields are required in the body. + +:::caution This sends your credentials to whatever host you name +The same caveat as the Seerr test route above: the URL and key both come from the request body, so this route makes the server contact any address you supply. +::: + ## Watch statistics ### `GET /api/settings/tautulli` diff --git a/docs/api/watch-statistics.md b/docs/api/watch-statistics.md new file mode 100644 index 000000000..b677119e7 --- /dev/null +++ b/docs/api/watch-statistics.md @@ -0,0 +1,83 @@ +--- +slug: /api/watch-statistics +title: Watch statistics API +description: Per-item Tautulli and Tracearr watch statistics for the media modal. +--- + +Two read-only endpoints behind the watch statistics panels in the media modal: one for [Tautulli](https://tautulli.com/), one for [Tracearr](https://github.com/connorgallopo/Tracearr). They answer in the same shape and share the same status contract. + +Streamystats has its own richer payload, on the [Streamystats](./streamystats.md) page. + +See [API conventions](../API.md#api-conventions) for the rules that apply to every endpoint. + +## The shared response + +```json +{ + "url": "https://tracearr.example.com/media/2f1c...", + "plays": 7, + "watchTime": 18540, + "lastWatched": "2026-03-04T21:15:00.000Z", + "users": [ + { + "name": "example-user", + "plays": 5, + "watchTime": 13200, + "lastWatched": "2026-03-04T21:15:00.000Z" + } + ] +} +``` + +| Field | Description | +| ------------- | ------------------------------------------------------------------------- | +| `url` | The item's page on the statistics service. Left out when there is not one | +| `plays` | Total plays counted for the item | +| `watchTime` | Total watch time in **seconds** | +| `lastWatched` | ISO date-time of the newest play, or `null` | +| `users` | Per-user totals, each with `name`, `plays`, `watchTime` and `lastWatched` | + +Both endpoints share one status contract: + +| Status | Cause | +| ------ | -------------------------------------------------------- | +| `200` | The statistics above | +| `403` | Tautulli only: the active media server is not Plex | +| `404` | The service is not configured, or nobody played the item | +| `502` | The service could not be read | + +The split matters: `404` is a fact about the item, `502` is a failure to establish one. The media modal shows an empty panel for the first and an error for the second. + +## Endpoints + +### `GET /api/tautulli/items/{itemId}` + +**Return Tautulli watch statistics for one Plex item.** + +**Plex only.** With Jellyfin or Emby as the active media server this answers `403`, because Tautulli only knows Plex rating keys. + +| Parameter | Type | Required | Description | +| --------- | ------ | -------- | --------------------------------------------------------------- | +| `itemId` | string | Yes | Plex rating key. Not validated and passed to Tautulli unescaped | + +The per-user totals come from Tautulli's `get_item_user_stats`. Those rows carry no dates, so every user's `lastWatched` is `null` and the overall `lastWatched` is read separately from the newest history row for the item. History is scoped by the item's own rating key for a movie or episode, by its parent for a season, and by its grandparent for a show, which is the same scope the Tautulli rule properties use. + +`url` points at the item's history page on your Tautulli instance. + +### `GET /api/tracearr/items/{itemId}` + +**Return Tracearr watch statistics for one media server item.** + +Works with Plex, Jellyfin and Emby. Only the Tracearr server bound in your settings is read, so plays recorded against another server are not counted. + +| Parameter | Type | Required | Description | +| --------- | ------ | -------- | --------------------------------------------------------------------------- | +| `itemId` | string | Yes | Media server item id, the same id used by `GET /api/media-server/meta/{id}` | + +Statistics are read from Tracearr's history rather than from its per-title statistics, because those follow Tracearr's own media identity, which can merge two different items of one server and credit the wrong one with the other's plays. + +- A movie or episode is looked up by its own rating key. +- A show or season has to be found by provider id first, trying TVDB, then TMDB, then IMDb. A show Tracearr cannot place that way answers `404` rather than reporting zero. Its plays are then narrowed to the show, or to that season's number. +- `url` points at the item's page on your Tracearr instance, and is left out when Tracearr holds no id for it. + +An item with more than 5000 plays answers `502` instead of a partial total, since a truncated number would read as a fact. diff --git a/sidebars.js b/sidebars.js index 9e646935e..e5761bbc6 100644 --- a/sidebars.js +++ b/sidebars.js @@ -68,6 +68,8 @@ const sidebars = { "api/metadata-and-storage", "api/servarr", "api/streamystats", + "api/watch-statistics", + "api/ombi", ], }, ], diff --git a/static/openapi-spec/maintainerr_api_specs.yaml b/static/openapi-spec/maintainerr_api_specs.yaml index 7f4672158..6dc167dc1 100644 --- a/static/openapi-spec/maintainerr_api_specs.yaml +++ b/static/openapi-spec/maintainerr_api_specs.yaml @@ -987,6 +987,52 @@ paths: type: object tags: - settings + /api/settings/ombi: + get: + operationId: SettingsController_getOmbiSetting + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + tags: + - settings + post: + operationId: SettingsController_updateOmbiSetting + parameters: [] + responses: + '201': + description: '' + content: + application/json: + schema: + type: object + tags: + - settings + delete: + operationId: SettingsController_removeOmbiSetting + parameters: [] + responses: + '200': + description: '' + tags: + - settings + /api/settings/test/ombi: + post: + operationId: SettingsController_testOmbi + parameters: [] + responses: + '201': + description: '' + content: + application/json: + schema: + type: object + tags: + - settings /api/settings/jellyfin: get: operationId: SettingsController_getJellyfinSetting @@ -2055,6 +2101,72 @@ paths: type: boolean tags: - SeerrApi + /api/ombi/requests/{tmdbId}/users: + get: + operationId: OmbiApiController_getRequestedByUsernames + parameters: + - name: tmdbId + required: true + in: path + schema: + type: number + - name: type + required: true + in: query + schema: + enum: + - movie + - tv + type: string + - name: season + required: false + in: query + schema: + type: number + - name: episode + required: false + in: query + schema: + type: number + responses: + '200': + description: '' + content: + application/json: + schema: + type: array + items: + type: string + tags: + - OmbiApi + /api/tautulli/items/{itemId}: + get: + operationId: TautulliApiController_getItemStats + parameters: + - name: itemId + required: true + in: path + schema: + type: string + responses: + '200': + description: '' + tags: + - TautulliApi + /api/tracearr/items/{itemId}: + get: + operationId: TracearrApiController_getItemStats + parameters: + - name: itemId + required: true + in: path + schema: + type: string + responses: + '200': + description: '' + tags: + - TracearrApi /api/streamystats/info: get: operationId: StreamystatsApiController_getInfo From 5cb302e36a8ff37ca2a7277370dbd9b8d6af281d Mon Sep 17 00:00:00 2001 From: enoch85 Date: Mon, 28 Sep 2026 14:09:32 +0000 Subject: [PATCH 2/2] docs: note that a new API page needs a sidebars.js entry The doc-only rule for drift PRs forbids touching sidebars, which reads as forbidding the one edit a new page under docs/api/ cannot do without: a page with no sidebars.js entry has no route into the site. Spell out the exception so the next drift PR does not have to weigh it again, and keep the requirement to call it out in the PR description. --- AGENTS.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index f5672e41c..df7402037 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,7 +36,9 @@ Maintainerr's `HEAD`. These rules are hard rules when you open the PR for one. release. No speculative additions and no broad rewrites. - **Doc-only changes.** Restrict edits to `docs/` and `static/openapi-spec/`. Do not touch sidebars, config, or unrelated assets unless a doc edit strictly - requires it. + requires it. A new page under `docs/api/` does require one line in + `sidebars.js`, because without it the page has no route into the site. Add it + and say so in the PR description. - **Keep the PR tied to the code diff**, not to the issue summary alone. - **Structure the PR description** in two parts: 1. **What was added**, one short bullet per doc edit, citing the upstream