feat: Add folder browsing functionality to Audiobookshelf integration and enhance related settings UI

This commit is contained in:
JB
2025-11-01 06:07:29 -07:00
parent 1d9dcf8800
commit 28652d9e78
5 changed files with 232 additions and 47 deletions
+38 -1
View File
@@ -2535,7 +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),
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),
@@ -3038,6 +3038,43 @@ def test_calibre_opds() -> ResponseReturnValue:
})
@api_bp.post("/integrations/audiobookshelf/folders")
def list_audiobookshelf_folders() -> ResponseReturnValue:
if not request.is_json:
return jsonify({"error": "Expected JSON payload."}), 400
payload = request.get_json(silent=True) or {}
settings = _audiobookshelf_settings_from_payload(payload)
config = _build_audiobookshelf_config(settings)
if config is None:
return jsonify({"error": "Provide base URL, API token, and library ID before listing folders."}), 400
try:
client = AudiobookshelfClient(config)
except ValueError as exc:
return jsonify({"error": str(exc)}), 400
try:
folders = client.list_folders()
except AudiobookshelfUploadError as exc:
cause = exc.__cause__
status_code = getattr(getattr(cause, "response", None), "status_code", None)
http_status = 502 if status_code and status_code >= 500 else 400
return jsonify({"error": str(exc)}), http_status
if not folders:
return jsonify({
"message": "No folders found for this library.",
"folders": [],
})
total = len(folders)
label = "folder" if total == 1 else "folders"
return jsonify({
"message": f"Found {total} {label} in this library.",
"folders": folders,
})
@api_bp.post("/integrations/audiobookshelf/test")
def test_audiobookshelf() -> ResponseReturnValue:
if not request.is_json:
+78 -1
View File
@@ -320,7 +320,7 @@ function collectAudiobookshelfFields() {
base_url: baseUrl,
library_id: libraryId,
collection_id: collectionId,
folder_id: folderId,
folder_id: folderId,
api_token: apiToken,
use_saved_token: useSavedToken,
clear_saved_token: clearToken,
@@ -406,6 +406,79 @@ async function testAudiobookshelf(button) {
}
}
async function listAudiobookshelfFolders(button) {
const status = statusSelectors.audiobookshelf;
const fields = collectAudiobookshelfFields();
if (!status) {
return;
}
const hasToken = Boolean(fields.api_token) || Boolean(fields.use_saved_token);
if (!fields.base_url || !hasToken || !fields.library_id) {
setStatus(status, 'Enter the base URL, API token, and library ID before browsing folders.', 'error');
return;
}
clearStatus(status);
setStatus(status, 'Loading folders…');
button.disabled = true;
try {
const response = await fetch('/api/integrations/audiobookshelf/folders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(fields),
});
const payload = await response.json();
if (!response.ok) {
throw new Error(payload.error || 'Folder lookup failed.');
}
const folders = Array.isArray(payload.folders) ? payload.folders : [];
if (!folders.length) {
setStatus(status, payload.message || 'No folders found for this library.', 'info');
return;
}
const list = folders
.map((entry, index) => {
const id = String(entry.id || '').trim();
const name = String(entry.name || '').trim();
const path = String(entry.path || '').trim();
const descriptorParts = [];
if (name) {
descriptorParts.push(name);
}
if (path && (!name || path.toLowerCase() !== name.toLowerCase())) {
descriptorParts.push(path);
}
if (!descriptorParts.length && id) {
descriptorParts.push(id);
}
const descriptor = descriptorParts.join(' — ') || '(unnamed folder)';
return `${index + 1}. ${descriptor}`;
})
.join('\n');
const choice = window.prompt(`Select a folder by number:\n\n${list}`, '1');
if (choice === null) {
setStatus(status, 'Folder selection cancelled.', 'info');
return;
}
const selection = Number.parseInt(choice, 10);
if (!Number.isFinite(selection) || selection < 1 || selection > folders.length) {
setStatus(status, 'Enter a number from the list to select a folder.', 'error');
return;
}
const selected = folders[selection - 1];
const folderInput = form.querySelector('#audiobookshelf_folder_id');
if (folderInput) {
folderInput.value = selected.id || '';
folderInput.dispatchEvent(new Event('input', { bubbles: true }));
}
const label = String(selected.name || selected.path || selected.id || '').trim() || 'selected folder';
setStatus(status, `Selected folder '${label}'.`, 'success');
} catch (error) {
setStatus(status, error instanceof Error ? error.message : 'Folder lookup failed.', 'error');
} finally {
button.disabled = false;
}
}
async function previewNormalization(button) {
const status = statusSelectors.normalization;
const output = outputAreas.normalization;
@@ -515,6 +588,10 @@ function initActions() {
if (audiobookshelfTestButton) {
audiobookshelfTestButton.addEventListener('click', () => testAudiobookshelf(audiobookshelfTestButton));
}
const audiobookshelfBrowseButton = document.querySelector('[data-action="audiobookshelf-list-folders"]');
if (audiobookshelfBrowseButton) {
audiobookshelfBrowseButton.addEventListener('click', () => listAudiobookshelfFolders(audiobookshelfBrowseButton));
}
}
function initLLMStateWatchers() {
+6 -5
View File
@@ -444,11 +444,12 @@
<label for="audiobookshelf_collection_id">Collection ID (optional)</label>
<input type="text" id="audiobookshelf_collection_id" name="audiobookshelf_collection_id" value="{{ integrations.audiobookshelf.collection_id }}">
</div>
<div class="field__group">
<label for="audiobookshelf_folder_id">Folder (name or ID)</label>
<input type="text" id="audiobookshelf_folder_id" name="audiobookshelf_folder_id" value="{{ integrations.audiobookshelf.folder_id }}">
<p class="hint">Enter the folder exactly as it appears in Audiobookshelf or paste the folder ID from the address bar.</p>
</div>
<div class="field__group">
<label for="audiobookshelf_folder_id">Folder (name or ID)</label>
<input type="text" id="audiobookshelf_folder_id" name="audiobookshelf_folder_id" value="{{ integrations.audiobookshelf.folder_id }}">
<p class="hint">Enter the folder exactly as it appears in Audiobookshelf or paste the folder ID. You can also browse the available folders below.</p>
<button type="button" class="button button--ghost button--small" data-action="audiobookshelf-list-folders">Browse folders</button>
</div>
</div>
<div class="field field--inline">
<div class="field__group">