diff --git a/README.md b/README.md index 881fe2e..0a186c9 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,50 @@ Need machine-readable status updates? The dashboard calls a small set of helper More automation hooks are planned; contributions are very welcome if you need additional routes. +## Audiobookshelf integration +Abogen can push finished audiobooks directly into Audiobookshelf. Configure this under **Settings → Integrations → Audiobookshelf** by providing: + +- **Base URL** – the HTTPS origin (and optional path prefix) where your Audiobookshelf server is reachable, for example `https://abs.example.com` or `https://media.example.com/abs`. Do **not** append `/api`. +- **Library ID** – the identifier of the target Audiobookshelf library (copy it from the library’s settings page in ABS). +- **API token** – a personal access token generated in Audiobookshelf under *Account → API tokens*. + +You can enable automatic uploads for future jobs or trigger individual uploads from the queue once the connection succeeds. + +### Reverse proxy checklist (Nginx Proxy Manager) +When Audiobookshelf sits behind Nginx Proxy Manager (NPM), make sure the API paths and headers reach the backend untouched: + +1. Create a **Proxy Host** that points to your ABS container or host (default forward port `13378`). +2. Under the **SSL** tab, enable your certificate and tick **Force SSL** if you want HTTPS only. +3. In the **Advanced** tab, append the snippet below so bearer tokens, client IPs, and large uploads survive the proxy hop: + ```nginx + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header Authorization $http_authorization; + client_max_body_size 5g; + proxy_read_timeout 300s; + proxy_connect_timeout 300s; + ``` +4. Disable **Block Common Exploits** (it strips Authorization headers in some NPM builds). +5. Enable **Websockets Support** on the main proxy screen (Audiobookshelf uses it for the web UI, and it keeps the reverse proxy configuration consistent). +6. If you publish Audiobookshelf under a path prefix (for example `/abs`), add a **Custom Location** with `Location: /abs/` and set the **Forward Path** to `/`. That rewrite strips the `/abs` prefix before traffic reaches Audiobookshelf so `/abs/api/...` on the internet becomes `/api/...` on the backend. Use the same prefixed URL in Abogen’s “Base URL” field. + +After saving the proxy host, test the API from the machine running Abogen: + +```bash +curl -i "https://abs.example.com/api/uploads" \ + -H "Authorization: Bearer YOUR_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"libraryId":"YOUR_LIBRARY_ID","mediaType":"audiobook"}' +``` + +If you still receive `Cannot GET /api/...`, the proxy is rewriting paths. Double-check the **Custom Locations** table (the `Forward Path` column should be empty for `/abs/`) and review the NPM access/error logs while issuing the curl request to confirm the backend sees the full `/api/uploads` URL. + +A JSON response containing an `id` or `uploadId` confirms the proxy is routing API calls correctly. You can then use **Test connection** in Abogen’s settings and the “Send to Audiobookshelf” button on completed jobs. + ## Configuration reference Most behaviour is controlled through the UI, but a few environment variables are helpful for automation: - `ABOGEN_SECRET_KEY` – provide your own random secret when deploying across multiple replicas. diff --git a/abogen/integrations/audiobookshelf.py b/abogen/integrations/audiobookshelf.py index 2dd8209..03c8002 100644 --- a/abogen/integrations/audiobookshelf.py +++ b/abogen/integrations/audiobookshelf.py @@ -22,6 +22,7 @@ class AudiobookshelfConfig: api_token: str library_id: str collection_id: Optional[str] = None + folder_id: Optional[str] = None verify_ssl: bool = True send_cover: bool = True send_chapters: bool = True diff --git a/abogen/web/routes.py b/abogen/web/routes.py index 8512eab..45fad94 100644 --- a/abogen/web/routes.py +++ b/abogen/web/routes.py @@ -2129,6 +2129,7 @@ def _integration_defaults() -> Dict[str, Dict[str, Any]]: "api_token": "", "library_id": "", "collection_id": "", + "folder_id": "", "verify_ssl": True, "send_cover": True, "send_chapters": True, @@ -2433,6 +2434,12 @@ def _audiobookshelf_settings_from_payload(payload: Mapping[str, Any]) -> Dict[st or stored.get("collection_id") or "" ).strip() + folder_id = str( + payload.get("folder_id") + or payload.get("audiobookshelf_folder_id") + or stored.get("folder_id") + or "" + ).strip() token_input = str( payload.get("api_token") or payload.get("audiobookshelf_api_token") @@ -2502,6 +2509,7 @@ def _audiobookshelf_settings_from_payload(payload: Mapping[str, Any]) -> Dict[st "base_url": base_url, "library_id": library_id, "collection_id": collection_id, + "folder_id": folder_id, "api_token": api_token, "verify_ssl": verify_ssl, "send_cover": send_cover, @@ -2527,6 +2535,7 @@ def _build_audiobookshelf_config(settings: Mapping[str, Any]) -> Optional[Audiob api_token=api_token, library_id=library_id, collection_id=(str(settings.get("collection_id") or "").strip() or None), + folder_id=(str(settings.get("folder_id") or "").strip() or None), verify_ssl=_coerce_bool(settings.get("verify_ssl"), True), send_cover=_coerce_bool(settings.get("send_cover"), True), send_chapters=_coerce_bool(settings.get("send_chapters"), True), @@ -2608,6 +2617,7 @@ def _apply_integration_form(cfg: Dict[str, Any], form: Mapping[str, Any]) -> Non abs_base = str(form.get("audiobookshelf_base_url") or current_abs.get("base_url") or "").strip() abs_library = str(form.get("audiobookshelf_library_id") or current_abs.get("library_id") or "").strip() abs_collection = str(form.get("audiobookshelf_collection_id") or current_abs.get("collection_id") or "").strip() + abs_folder = str(form.get("audiobookshelf_folder_id") or current_abs.get("folder_id") or "").strip() abs_token_input = str(form.get("audiobookshelf_api_token") or "") abs_token_clear = _coerce_bool(form.get("audiobookshelf_api_token_clear"), False) if abs_token_input: @@ -2632,6 +2642,7 @@ def _apply_integration_form(cfg: Dict[str, Any], form: Mapping[str, Any]) -> Non "api_token": abs_token, "library_id": abs_library, "collection_id": abs_collection, + "folder_id": abs_folder, "verify_ssl": abs_verify, "send_cover": abs_send_cover, "send_chapters": abs_send_chapters, @@ -3095,6 +3106,7 @@ def test_audiobookshelf() -> ResponseReturnValue: "message": f"Connected to Audiobookshelf library '{library_name}'.", "library_id": library_id, "collection_id": collection_id or None, + "folder_id": settings.get("folder_id") or None, }) diff --git a/abogen/web/service.py b/abogen/web/service.py index 294f891..4e1fce6 100644 --- a/abogen/web/service.py +++ b/abogen/web/service.py @@ -956,6 +956,7 @@ class ConversionService: api_token=api_token, library_id=library_id, collection_id=(str(integration_cfg.get("collection_id") or "").strip() or None), + folder_id=(str(integration_cfg.get("folder_id") or "").strip() or None), verify_ssl=self._coerce_bool(integration_cfg.get("verify_ssl"), True), send_cover=self._coerce_bool(integration_cfg.get("send_cover"), True), send_chapters=self._coerce_bool(integration_cfg.get("send_chapters"), True), diff --git a/abogen/web/static/settings.js b/abogen/web/static/settings.js index c65f4e3..195ab05 100644 --- a/abogen/web/static/settings.js +++ b/abogen/web/static/settings.js @@ -305,6 +305,7 @@ function collectAudiobookshelfFields() { const baseUrl = form.querySelector('#audiobookshelf_base_url')?.value?.trim() || ''; const libraryId = form.querySelector('#audiobookshelf_library_id')?.value?.trim() || ''; const collectionId = form.querySelector('#audiobookshelf_collection_id')?.value?.trim() || ''; + const folderId = form.querySelector('#audiobookshelf_folder_id')?.value?.trim() || ''; const tokenInput = form.querySelector('#audiobookshelf_api_token'); const apiToken = tokenInput?.value?.trim() || ''; const hasSecret = tokenInput?.dataset.hasSecret === 'true'; @@ -319,6 +320,7 @@ function collectAudiobookshelfFields() { base_url: baseUrl, library_id: libraryId, collection_id: collectionId, + folder_id: folderId, api_token: apiToken, use_saved_token: useSavedToken, clear_saved_token: clearToken, diff --git a/abogen/web/templates/settings.html b/abogen/web/templates/settings.html index dc721d8..0722c99 100644 --- a/abogen/web/templates/settings.html +++ b/abogen/web/templates/settings.html @@ -444,6 +444,10 @@ +