feat(serve): hot-reload service repos on a timer without a restart

Add serve.services_refresh_interval; refresh pulls the repos and re-imports only the changed services, staging any with active jobs until they finish. Report skipped/broken services in the log and on /api/services.
This commit is contained in:
imSp4rky
2026-08-26 23:14:23 -06:00
parent 4964869258
commit 5128fef473
11 changed files with 350 additions and 52 deletions
+12 -4
View File
@@ -62,7 +62,7 @@ If the update check fails (for example, no network), `update_available` and `lat
### `GET /api/services`
Show the streaming services available on this server, filtered by your allowlist. Each entry gives the service's tag, matching rules, capability flags, and its CLI parameters (useful when you make a UI that accepts service-specific options).
Show the streaming services available on this server, filtered by your allowlist. Each entry gives the service's tag, matching rules, capability flags, and its CLI parameters (useful when you make a UI that accepts service-specific options). `load_errors` lists the services the server skipped because they failed to import, at startup or after a service repository refresh; the server also writes each one to its log. An entry marked `pending_update` has newer code staged: it keeps its current code until its running or queued jobs finish.
=== "Request"
@@ -100,7 +100,8 @@ Show the streaming services available on this server, filtered by your allowlist
"has_drm": true,
"auth_methods": ["cookies"]
}
]
],
"load_errors": []
}
```
@@ -118,6 +119,7 @@ Field notes:
| `needs_auth` | Whether the service overrides `authenticate()`. |
| `has_search` | Whether the service supports `POST /api/search`. |
| `has_drm` | Whether the service overrides a Widevine or PlayReady license hook. |
| `pending_update` | Set to `true` while a service repository refresh waits for this service's jobs to finish before it swaps in the new code. Absent otherwise. |
| `auth_methods` | Declared or inferred auth methods, e.g. `["cookies"]`, `["credentials"]`. |
| Status | Meaning |
@@ -773,7 +775,7 @@ The `409` guard is about active file I/O, not an arbitrary lock. An in-flight jo
### `POST /api/maintenance/refresh-services`
git-pull every service repository configured under `directories.services`. `refreshed` becomes `true` when all repos updated successfully, or when you configure no repository.
git-pull every service repository configured under `directories.services` and re-import the services that changed, without a restart. `refreshed` becomes `true` when all repos updated successfully, or when you configure no repository. `deferred` lists the services that have a running or queued job: they keep their current code until that job finishes, then swap. `load_errors` lists the services whose new code failed to import; the server drops them until the next successful refresh.
=== "Response `200`"
@@ -781,7 +783,13 @@ git-pull every service repository configured under `directories.services`. `refr
{
"refreshed": true,
"repos": [
{ "spec": "github.com/example/services", "updated": true, "changes": ["~EXAMPLE"] }
{
"spec": "github.com/example/services",
"updated": true,
"changes": ["~EXAMPLE", "+EXAMPLE2"],
"deferred": ["EXAMPLE"],
"load_errors": []
}
]
}
```
+1 -1
View File
@@ -183,7 +183,7 @@ request and response in full.
|---|---|---|
| `POST` | `/api/maintenance/clear-cache` | Clear the cache directory. |
| `POST` | `/api/maintenance/clear-temp` | Clear the temp directory. |
| `POST` | `/api/maintenance/refresh-services` | `git pull` each configured service repository. |
| `POST` | `/api/maintenance/refresh-services` | `git pull` each configured service repository and re-import the services that changed. |
### Remote-download sessions
+1 -1
View File
@@ -550,7 +550,7 @@ unshackle util COMMAND [ARGS]...
unshackle util refresh-services
```
Force a refresh (git pull / hard reset) of all service repos configured under `directories.services`.
Force a refresh (git pull / hard reset) of all service repos configured under `directories.services`. This updates the clones on disk. A server that is already running reloads the changed services through `POST /api/maintenance/refresh-services` or `serve.services_refresh_interval` instead.
### `util crop PATH ASPECT`
@@ -76,6 +76,14 @@ directories:
services_repo_force: true
```
!!! info "Refresh inside `serve`"
A running server does not restart to pick up a refresh. Both the periodic refresh
(`serve.services_refresh_interval`, see [serve](services.md#serve)) and the manual
`POST /api/maintenance/refresh-services` endpoint hard-reset the clones and then re-import
only the services that changed. A service with a running or queued download job keeps its
current code until that job finishes, then swaps; `GET /api/services` marks it with
`pending_update` in the meantime.
!!! warning "Read-only installs and reinstalls"
If the services directory lives inside the installed package, a reinstall can delete the
`_repos` clones. They are re-cloned on next use. On **read-only installs** you must
+3 -1
View File
@@ -117,6 +117,7 @@ is the [REST API](../../dev/rest-api/index.md) section. These are the config key
| `max_sessions` | int | `100` | Maximum concurrent sessions. |
| `history_limit` | int | `100` | How many finished jobs to retain in history. |
| `compression_level` | int | `1` | gzip level for responses. |
| `services_refresh_interval` | int (s) | `0` | How often the server pulls the git-backed service repositories in `directories.services` and hot-reloads the services that changed. `0` turns it off. A service with a running or queued job swaps to the new code as soon as its last job finishes. |
| `global_speed_limit` | str | *(unlimited)* | Server-wide download speed cap, e.g. `10M`, `1.5G` or plain bytes/sec (same format as `speed_limit`). One shared budget across all concurrent jobs; per-job speed limits are ignored while it is set. |
| `cdm_overrides` | list or bool | *(unset)* | Allowed per-request CDM overrides: a list of permitted device names, or `true` for any. Unset rejects every override. |
| `allow_job_credentials` | bool | `false` | Permit clients to supply credentials per job. |
@@ -141,11 +142,12 @@ serve:
api_secret: change-me
remote_only: true
services: [EXAMPLE1, EXAMPLE2]
services_refresh_interval: 3600 # pull the service repositories hourly and reload what changed
# server-wide download defaults (same keys as the `dl:` block)
downloads: 3
best_available: true
users:
a1b2c3d4: # this user's API key
a1b2c3d4: # this user's API key
services: [EXAMPLE1] # may only use EXAMPLE1
e5f6a7b8:
server_cdm: true # this key may have the server do the licensing