feat: Add library ID support to media scanner and worker
Add default library ID functionality to improve library targeting during media scans. Service changes in internal/services/media_scanner.go: - Add defaultLibraryID field to MediaScanner struct - Add SetLibraryID() method to set default library - Modify processMediaFile() to use defaultLibraryID when set - Prioritizes defaultLibraryID over folder-based library detection - Provides explicit library targeting for scans Service changes in internal/services/worker.go: - Add libraryUUID conversion from string to pgtype.UUID - Call scanner.SetLibraryID() before ScanFolders() - Ensures scanner respects the job's library ID These changes enable more precise library targeting during media scans, allowing scans to be directed to specific libraries rather than relying solely on folder-based detection.
This commit is contained in:
+31
-29
@@ -1,24 +1,24 @@
|
||||
"use strict";
|
||||
// Header functionality
|
||||
const toggleThemeDropdown = () => {
|
||||
const dropdown = document.getElementById('theme-dropdown');
|
||||
const dropdown = document.getElementById("theme-dropdown");
|
||||
if (dropdown) {
|
||||
dropdown.classList.toggle('hidden');
|
||||
dropdown.classList.toggle("hidden");
|
||||
// Close user menu if open
|
||||
const userMenu = document.getElementById('user-menu');
|
||||
if (userMenu && !dropdown.classList.contains('hidden')) {
|
||||
userMenu.classList.add('hidden');
|
||||
const userMenu = document.getElementById("user-menu");
|
||||
if (userMenu && !dropdown.classList.contains("hidden")) {
|
||||
userMenu.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
};
|
||||
const toggleUserMenu = () => {
|
||||
const menu = document.getElementById('user-menu');
|
||||
const menu = document.getElementById("user-menu");
|
||||
if (menu) {
|
||||
menu.classList.toggle('hidden');
|
||||
menu.classList.toggle("hidden");
|
||||
// Close theme dropdown if open
|
||||
const themeDropdown = document.getElementById('theme-dropdown');
|
||||
if (themeDropdown && !menu.classList.contains('hidden')) {
|
||||
themeDropdown.classList.add('hidden');
|
||||
const themeDropdown = document.getElementById("theme-dropdown");
|
||||
if (themeDropdown && !menu.classList.contains("hidden")) {
|
||||
themeDropdown.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -28,43 +28,45 @@ const changeThemeTo = (theme) => {
|
||||
window.applyTheme(theme);
|
||||
}
|
||||
// Save to server if logged in
|
||||
const token = localStorage.getItem('token');
|
||||
const token = localStorage.getItem("token");
|
||||
if (token) {
|
||||
fetch('/api/auth/theme', {
|
||||
method: 'PUT',
|
||||
fetch("/api/auth/theme", {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ theme })
|
||||
}).catch(err => console.log('Theme save failed', err));
|
||||
body: JSON.stringify({ theme }),
|
||||
}).catch((err) => console.log("Theme save failed", err));
|
||||
}
|
||||
// Close dropdown
|
||||
const dropdown = document.getElementById('theme-dropdown');
|
||||
const dropdown = document.getElementById("theme-dropdown");
|
||||
if (dropdown) {
|
||||
dropdown.classList.add('hidden');
|
||||
dropdown.classList.add("hidden");
|
||||
}
|
||||
};
|
||||
const logout = () => {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
window.location.href = '/';
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("user");
|
||||
window.location.href = "/";
|
||||
};
|
||||
// Close dropdowns when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
document.addEventListener("click", (e) => {
|
||||
const target = e.target;
|
||||
const themeDropdown = document.getElementById('theme-dropdown');
|
||||
const userMenu = document.getElementById('user-menu');
|
||||
const themeDropdown = document.getElementById("theme-dropdown");
|
||||
const userMenu = document.getElementById("user-menu");
|
||||
const themeButton = target?.closest('button[onclick="toggleThemeDropdown()"]');
|
||||
const userButton = target?.closest('button[onclick="toggleUserMenu()"]');
|
||||
if (!themeButton && themeDropdown && !themeDropdown.classList.contains('hidden')) {
|
||||
if (!themeButton &&
|
||||
themeDropdown &&
|
||||
!themeDropdown.classList.contains("hidden")) {
|
||||
if (!themeDropdown.contains(target)) {
|
||||
themeDropdown.classList.add('hidden');
|
||||
themeDropdown.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
if (!userButton && userMenu && !userMenu.classList.contains('hidden')) {
|
||||
if (!userButton && userMenu && !userMenu.classList.contains("hidden")) {
|
||||
if (!userMenu.contains(target)) {
|
||||
userMenu.classList.add('hidden');
|
||||
userMenu.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
+68
-65
@@ -3,22 +3,24 @@ let searchInputTimeout = null;
|
||||
const SEARCH_DEBOUNCE_MS = 300;
|
||||
const SEARCH_MIN_CHARS = 2;
|
||||
function initializeSearch() {
|
||||
const searchInput = document.getElementById('header-search');
|
||||
const searchInput = document.getElementById("header-search");
|
||||
if (!searchInput) {
|
||||
console.warn('Search input not found');
|
||||
console.warn("Search input not found");
|
||||
return;
|
||||
}
|
||||
searchInput.addEventListener('input', handleSearchInput);
|
||||
searchInput.addEventListener('keydown', handleSearchKeydown);
|
||||
searchInput.addEventListener('focus', () => {
|
||||
searchInput.addEventListener("input", handleSearchInput);
|
||||
searchInput.addEventListener("keydown", handleSearchKeydown);
|
||||
searchInput.addEventListener("focus", () => {
|
||||
if (searchInput.value.length >= SEARCH_MIN_CHARS) {
|
||||
performSearch(searchInput.value);
|
||||
}
|
||||
});
|
||||
document.addEventListener('click', (e) => {
|
||||
const searchResults = document.getElementById('search-results');
|
||||
const searchInputEl = document.getElementById('header-search');
|
||||
if (searchResults && !searchResults.contains(e.target) && e.target !== searchInputEl) {
|
||||
document.addEventListener("click", (e) => {
|
||||
const searchResults = document.getElementById("search-results");
|
||||
const searchInputEl = document.getElementById("header-search");
|
||||
if (searchResults &&
|
||||
!searchResults.contains(e.target) &&
|
||||
e.target !== searchInputEl) {
|
||||
hideSearchResults();
|
||||
}
|
||||
});
|
||||
@@ -38,70 +40,70 @@ function handleSearchInput(e) {
|
||||
}, SEARCH_DEBOUNCE_MS);
|
||||
}
|
||||
function handleSearchKeydown(e) {
|
||||
const searchResults = document.getElementById('search-results');
|
||||
if (!searchResults || searchResults.classList.contains('hidden')) {
|
||||
const searchResults = document.getElementById("search-results");
|
||||
if (!searchResults || searchResults.classList.contains("hidden")) {
|
||||
return;
|
||||
}
|
||||
const items = searchResults.querySelectorAll('.search-result-item');
|
||||
const currentIndex = parseInt(searchResults.dataset.selectedIndex || '-1');
|
||||
if (e.key === 'ArrowDown') {
|
||||
const items = searchResults.querySelectorAll(".search-result-item");
|
||||
const currentIndex = parseInt(searchResults.dataset.selectedIndex || "-1");
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
const nextIndex = Math.min(currentIndex + 1, items.length - 1);
|
||||
selectSearchResult(items, nextIndex);
|
||||
}
|
||||
else if (e.key === 'ArrowUp') {
|
||||
else if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
const prevIndex = Math.max(currentIndex - 1, -1);
|
||||
selectSearchResult(items, prevIndex);
|
||||
}
|
||||
else if (e.key === 'Enter') {
|
||||
else if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
if (currentIndex >= 0 && items[currentIndex]) {
|
||||
const link = items[currentIndex].querySelector('a');
|
||||
const link = items[currentIndex].querySelector("a");
|
||||
if (link)
|
||||
link.click();
|
||||
}
|
||||
}
|
||||
else if (e.key === 'Escape') {
|
||||
else if (e.key === "Escape") {
|
||||
hideSearchResults();
|
||||
}
|
||||
}
|
||||
function selectSearchResult(items, index) {
|
||||
items.forEach((item, i) => {
|
||||
if (i === index) {
|
||||
item.classList.add('bg-opacity-80');
|
||||
item.classList.add("bg-opacity-80");
|
||||
}
|
||||
else {
|
||||
item.classList.remove('bg-opacity-80');
|
||||
item.classList.remove("bg-opacity-80");
|
||||
}
|
||||
});
|
||||
const searchResults = document.getElementById('search-results');
|
||||
const searchResults = document.getElementById("search-results");
|
||||
if (searchResults) {
|
||||
searchResults.dataset.selectedIndex = index.toString();
|
||||
}
|
||||
}
|
||||
function performSearch(query) {
|
||||
const token = localStorage.getItem('token');
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) {
|
||||
console.warn('No authentication token found');
|
||||
console.warn("No authentication token found");
|
||||
return;
|
||||
}
|
||||
showSearchLoading();
|
||||
fetch(`/api/media-items/search?q=${encodeURIComponent(query)}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(response => {
|
||||
.then((response) => {
|
||||
if (response.status === 404) {
|
||||
return { error: 'no results found', results: [] };
|
||||
return { error: "no results found", results: [] };
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
hideSearchLoading();
|
||||
if (data && 'error' in data && data.error === 'no results found') {
|
||||
if (data && "error" in data && data.error === "no results found") {
|
||||
showNoResults(query);
|
||||
}
|
||||
else if (Array.isArray(data) && data.length > 0) {
|
||||
@@ -114,15 +116,15 @@ function performSearch(query) {
|
||||
showNoResults(query);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
hideSearchLoading();
|
||||
console.error('Search error:', error);
|
||||
console.error("Search error:", error);
|
||||
showSearchError();
|
||||
});
|
||||
}
|
||||
function showSearchLoading() {
|
||||
createSearchResultsContainer();
|
||||
const searchResults = document.getElementById('search-results');
|
||||
const searchResults = document.getElementById("search-results");
|
||||
if (!searchResults)
|
||||
return;
|
||||
searchResults.innerHTML = `
|
||||
@@ -131,33 +133,32 @@ function showSearchLoading() {
|
||||
<p class="mt-2 text-sm">Searching...</p>
|
||||
</div>
|
||||
`;
|
||||
searchResults.classList.remove('hidden');
|
||||
}
|
||||
function hideSearchLoading() {
|
||||
searchResults.classList.remove("hidden");
|
||||
}
|
||||
function hideSearchLoading() { }
|
||||
function showSearchResults(results, query) {
|
||||
createSearchResultsContainer();
|
||||
const searchResults = document.getElementById('search-results');
|
||||
const searchResults = document.getElementById("search-results");
|
||||
if (!searchResults)
|
||||
return;
|
||||
searchResults.dataset.selectedIndex = '-1';
|
||||
searchResults.dataset.selectedIndex = "-1";
|
||||
const libraryIconMap = {
|
||||
'ebooks': '📚',
|
||||
'comics': '📖',
|
||||
'manga': '🗾'
|
||||
ebooks: "📚",
|
||||
comics: "📖",
|
||||
manga: "🗾",
|
||||
};
|
||||
let html = `
|
||||
<div class="p-3 border-b" style="border-color: var(--border)">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide" style="color: var(--text-secondary)">
|
||||
${results.length} result${results.length !== 1 ? 's' : ''} for "${searchEscapeHtml(query)}"
|
||||
${results.length} result${results.length !== 1 ? "s" : ""} for "${searchEscapeHtml(query)}"
|
||||
</p>
|
||||
</div>
|
||||
<div class="max-h-96 overflow-y-auto">
|
||||
`;
|
||||
results.forEach((item, index) => {
|
||||
const icon = libraryIconMap[item.library_type_name] || '📁';
|
||||
const icon = libraryIconMap[item.library_type_name] || "📁";
|
||||
const titleHtml = highlightMatch(item.title, query);
|
||||
const authorHtml = item.author ? highlightMatch(item.author, query) : '';
|
||||
const authorHtml = item.author ? highlightMatch(item.author, query) : "";
|
||||
html += `
|
||||
<div class="search-result-item p-3 border-b hover:bg-opacity-50 transition-colors cursor-pointer"
|
||||
style="border-color: var(--border); background-color: var(--bg-secondary)"
|
||||
@@ -171,7 +172,7 @@ function showSearchResults(results, query) {
|
||||
<h4 class="text-sm font-medium truncate" style="color: var(--text-primary)">
|
||||
${titleHtml}
|
||||
</h4>
|
||||
${authorHtml ? `<p class="text-xs truncate" style="color: var(--text-secondary)">${authorHtml}</p>` : ''}
|
||||
${authorHtml ? `<p class="text-xs truncate" style="color: var(--text-secondary)">${authorHtml}</p>` : ""}
|
||||
<p class="text-xs mt-1" style="color: var(--text-secondary)">
|
||||
${searchEscapeHtml(item.library_name)}
|
||||
</p>
|
||||
@@ -191,11 +192,11 @@ function showSearchResults(results, query) {
|
||||
</div>
|
||||
`;
|
||||
searchResults.innerHTML = html;
|
||||
searchResults.classList.remove('hidden');
|
||||
searchResults.classList.remove("hidden");
|
||||
}
|
||||
function showNoResults(query) {
|
||||
createSearchResultsContainer();
|
||||
const searchResults = document.getElementById('search-results');
|
||||
const searchResults = document.getElementById("search-results");
|
||||
if (!searchResults)
|
||||
return;
|
||||
searchResults.innerHTML = `
|
||||
@@ -205,11 +206,11 @@ function showNoResults(query) {
|
||||
<p class="text-xs mt-1" style="color: var(--text-secondary)">Try different keywords</p>
|
||||
</div>
|
||||
`;
|
||||
searchResults.classList.remove('hidden');
|
||||
searchResults.classList.remove("hidden");
|
||||
}
|
||||
function showSearchError() {
|
||||
createSearchResultsContainer();
|
||||
const searchResults = document.getElementById('search-results');
|
||||
const searchResults = document.getElementById("search-results");
|
||||
if (!searchResults)
|
||||
return;
|
||||
searchResults.innerHTML = `
|
||||
@@ -219,24 +220,26 @@ function showSearchError() {
|
||||
<p class="text-xs mt-1" style="color: var(--text-secondary)">Please try again</p>
|
||||
</div>
|
||||
`;
|
||||
searchResults.classList.remove('hidden');
|
||||
searchResults.classList.remove("hidden");
|
||||
}
|
||||
function hideSearchResults() {
|
||||
const searchResults = document.getElementById('search-results');
|
||||
const searchResults = document.getElementById("search-results");
|
||||
if (searchResults) {
|
||||
searchResults.classList.add('hidden');
|
||||
searchResults.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
function createSearchResultsContainer() {
|
||||
let searchResults = document.getElementById('search-results');
|
||||
let searchResults = document.getElementById("search-results");
|
||||
if (!searchResults) {
|
||||
searchResults = document.createElement('div');
|
||||
searchResults.id = 'search-results';
|
||||
searchResults.className = 'hidden absolute z-50 w-full max-w-2xl mt-2 rounded-lg shadow-lg border';
|
||||
searchResults.style.cssText = 'background-color: var(--bg-secondary); border-color: var(--border)';
|
||||
const searchInput = document.getElementById('header-search');
|
||||
searchResults = document.createElement("div");
|
||||
searchResults.id = "search-results";
|
||||
searchResults.className =
|
||||
"hidden absolute z-50 w-full max-w-2xl mt-2 rounded-lg shadow-lg border";
|
||||
searchResults.style.cssText =
|
||||
"background-color: var(--bg-secondary); border-color: var(--border)";
|
||||
const searchInput = document.getElementById("header-search");
|
||||
if (searchInput) {
|
||||
const searchContainer = searchInput.closest('.relative');
|
||||
const searchContainer = searchInput.closest(".relative");
|
||||
if (searchContainer) {
|
||||
searchContainer.appendChild(searchResults);
|
||||
}
|
||||
@@ -245,20 +248,20 @@ function createSearchResultsContainer() {
|
||||
}
|
||||
function highlightMatch(text, query) {
|
||||
if (!text)
|
||||
return '';
|
||||
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const regex = new RegExp(`(${escapedQuery})`, 'gi');
|
||||
return "";
|
||||
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const regex = new RegExp(`(${escapedQuery})`, "gi");
|
||||
return searchEscapeHtml(text).replace(regex, '<mark style="background-color: var(--accent); color: var(--bg-primary); padding: 0 2px; border-radius: 2px;">$1</mark>');
|
||||
}
|
||||
function searchEscapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
function selectLibraryAndBook(libraryId, bookId) {
|
||||
localStorage.setItem('selectedLibrary', libraryId);
|
||||
localStorage.setItem('selectedBook', bookId);
|
||||
localStorage.setItem("selectedLibrary", libraryId);
|
||||
localStorage.setItem("selectedBook", bookId);
|
||||
hideSearchResults();
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', initializeSearch);
|
||||
document.addEventListener("DOMContentLoaded", initializeSearch);
|
||||
window.selectLibraryAndBook = selectLibraryAndBook;
|
||||
|
||||
Reference in New Issue
Block a user