Manage indexed libraries from Jellyfin plugin

This commit is contained in:
Sleeps
2026-10-06 17:58:04 +01:00
parent bd1f2fa9ce
commit 0680379fcb
@@ -23,16 +23,13 @@
<div class="inputContainer">
<input is="emby-input" type="password" id="apiKey" label="API Key" />
<div class="fieldDescription">Must match SCENE_API_KEY in jellyfin-scene-search. Leave empty only if the API has no key configured.</div>
<div class="fieldDescription">Must match SCENE_API_KEY in jellyfin-scene-search.</div>
</div>
<label class="checkboxContainer">
<input is="emby-checkbox" type="checkbox" id="requirePrefix" />
<span>Require a search prefix</span>
</label>
<div class="fieldDescription" style="margin-left: 3em; margin-top: -1em; margin-bottom: 1em;">
Recommended while only part of the Jellyfin library is indexed. When enabled, ordinary searches keep using Jellyfin's built-in search.
</div>
<div class="inputContainer">
<input is="emby-input" type="text" id="searchPrefix" label="Search Prefix" />
@@ -45,9 +42,26 @@
<div class="inputContainer">
<input is="emby-input" type="number" id="timeoutSeconds" min="5" max="300" step="1" label="API Timeout (seconds)" />
<div class="fieldDescription">The first semantic search may take longer while SigLIP is loaded.</div>
</div>
<h2>Indexed Libraries</h2>
<div class="fieldDescription" style="margin-bottom:1em;">
Choose which Jellyfin libraries Scene Search should index. Selection is stored in this plugin, not in the Python .env file.
</div>
<div id="libraryList">Loading libraries…</div>
<div style="margin-top:1em; margin-bottom:1em;">
<button is="emby-button" type="button" id="indexNow" class="raised">
<span>Index selected libraries</span>
</button>
<label class="checkboxContainer" style="margin-top:1em;">
<input is="emby-checkbox" type="checkbox" id="forceIndex" />
<span>Force rebuild already-indexed items</span>
</label>
</div>
<div id="indexStatus" class="fieldDescription" style="margin-bottom:1.5em;"></div>
<button is="emby-button" type="submit" class="raised button-submit block">
<span>Save</span>
</button>
@@ -59,10 +73,112 @@
(function () {
var pluginId = "2d8d1bb5-3f0c-4d5a-9d59-934463c06d35";
var page = document.querySelector('#sceneSearchConfigPage');
var currentConfig = null;
var statusTimer = null;
function selectedLibraryIds() {
return Array.prototype.slice.call(
page.querySelectorAll('.sceneLibrary:checked')
).map(function (el) { return el.value; });
}
function renderLibraries(data) {
var container = page.querySelector('#libraryList');
var selected = new Set((currentConfig && currentConfig.SelectedLibraryIds) || []);
var libraries = (data && data.libraries) || [];
if (!libraries.length) {
container.textContent = 'No Jellyfin video libraries were returned by the Scene Search API.';
return;
}
container.innerHTML = libraries.map(function (library) {
var checked = selected.has(library.id) ? ' checked' : '';
var detail = library.indexed_items + ' indexed item' +
(library.indexed_items === 1 ? '' : 's') + ', ' +
library.indexed_frames + ' frames';
return '<label class="checkboxContainer">' +
'<input type="checkbox" class="sceneLibrary" value="' +
library.id.replace(/"/g, '&quot;') + '"' + checked + ' />' +
'<span>' + library.name.replace(/</g, '&lt;').replace(/>/g, '&gt;') + '</span>' +
'<div class="fieldDescription" style="margin-left:3em;">' + detail + '</div>' +
'</label>';
}).join('');
}
function renderStatus(status) {
var container = page.querySelector('#indexStatus');
if (!status) {
container.textContent = 'Index status unavailable.';
return;
}
if (status.running) {
var parts = ['Indexing'];
if (status.library) {
parts.push(status.library);
}
if (status.item) {
parts.push(status.item);
}
if (status.item_number && status.item_total) {
parts.push(status.item_number + '/' + status.item_total);
}
if (status.frames_total) {
parts.push((status.frames_processed || 0) + '/' + status.frames_total + ' frames');
}
if (status.stage === 'sync' || status.stage === 'syncing') {
parts = ['Rebuilding search index'];
if (status.documents_processed) {
parts.push(status.documents_processed + ' documents');
}
}
container.textContent = parts.join(' — ');
} else if (status.stage === 'failed') {
container.textContent = 'Index failed: ' + (status.error || 'unknown error');
} else if (status.stage === 'complete') {
container.textContent =
'Index complete — ' + (status.indexed || 0) + ' indexed, ' +
(status.skipped || 0) + ' unchanged, ' +
(status.failed || 0) + ' failed, ' +
(status.pruned || 0) + ' removed.';
} else {
container.textContent = 'Index idle.';
}
}
function refreshStatus() {
return ApiClient.getJSON(ApiClient.getUrl('Plugins/SceneSearch/IndexStatus'))
.then(function (status) {
renderStatus(status);
if (status && status.running) {
if (!statusTimer) {
statusTimer = setInterval(refreshStatus, 2000);
}
} else if (statusTimer) {
clearInterval(statusTimer);
statusTimer = null;
loadLibraries();
}
})
.catch(function () {
renderStatus(null);
});
}
function loadLibraries() {
return ApiClient.getJSON(ApiClient.getUrl('Plugins/SceneSearch/Libraries'))
.then(renderLibraries)
.catch(function () {
page.querySelector('#libraryList').textContent =
'Could not load libraries. Check the Scene Search API URL/key and that the Python service is running.';
});
}
page.addEventListener('pageshow', function () {
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(pluginId).then(function (config) {
currentConfig = config;
page.querySelector('#enabled').checked = config.Enabled;
page.querySelector('#apiUrl').value = config.ApiUrl || 'http://127.0.0.1:8097';
page.querySelector('#apiKey').value = config.ApiKey || '';
@@ -70,10 +186,44 @@
page.querySelector('#searchPrefix').value = config.SearchPrefix || 'scene:';
page.querySelector('#maxResults').value = config.MaxResults || 20;
page.querySelector('#timeoutSeconds').value = config.TimeoutSeconds || 60;
return Promise.all([loadLibraries(), refreshStatus()]);
}).finally(function () {
Dashboard.hideLoadingMsg();
});
});
page.addEventListener('pagehide', function () {
if (statusTimer) {
clearInterval(statusTimer);
statusTimer = null;
}
});
page.querySelector('#indexNow').addEventListener('click', function () {
var ids = selectedLibraryIds();
if (!ids.length) {
page.querySelector('#indexStatus').textContent = 'Select at least one library first.';
return;
}
page.querySelector('#indexStatus').textContent = 'Starting index…';
ApiClient.ajax({
type: 'POST',
url: ApiClient.getUrl('Plugins/SceneSearch/Index'),
contentType: 'application/json',
data: JSON.stringify({
library_ids: ids,
force: page.querySelector('#forceIndex').checked
})
}).then(function () {
refreshStatus();
}).catch(function () {
page.querySelector('#indexStatus').textContent =
'Could not start indexing. Check Jellyfin logs and Scene Search API status.';
});
});
page.querySelector('.sceneSearchConfigForm').addEventListener('submit', function (event) {
event.preventDefault();
Dashboard.showLoadingMsg();
@@ -86,6 +236,7 @@
config.SearchPrefix = page.querySelector('#searchPrefix').value.trim();
config.MaxResults = parseInt(page.querySelector('#maxResults').value, 10) || 20;
config.TimeoutSeconds = parseInt(page.querySelector('#timeoutSeconds').value, 10) || 60;
config.SelectedLibraryIds = selectedLibraryIds();
return ApiClient.updatePluginConfiguration(pluginId, config);
}).then(function (result) {