diff --git a/src/server/api/setup_endpoints.py b/src/server/api/setup_endpoints.py index da73583..de11a8f 100644 --- a/src/server/api/setup_endpoints.py +++ b/src/server/api/setup_endpoints.py @@ -224,17 +224,25 @@ async def resolve_unresolved_folder( ) +class SearchFolderRequest(BaseModel): + """Request model for searching an unresolved folder with custom query.""" + query: Optional[str] = Field(None, description="Custom search query override") + + @router.post("/unresolved/{folder_name}/search", response_model=UnresolvedFolderResponse) async def search_unresolved_folder( folder_name: str, + request: Optional[SearchFolderRequest] = None, db=Depends(get_database_session), ) -> UnresolvedFolderResponse: """Re-search for a specific unresolved folder to get fresh suggestions. - Performs a new search using the folder's title and caches the results. + Performs a new search using the folder's title or a custom query. + Caches the results for subsequent display. Args: folder_name: URL-encoded folder name to search for + request: Optional SearchFolderRequest with custom query override Returns: UnresolvedFolderResponse with updated search suggestions @@ -258,10 +266,13 @@ async def search_unresolved_folder( detail=f"Folder already resolved: {folder_name}" ) + # Use custom query if provided, otherwise fall back to folder title + search_query = request.query if request and request.query else folder.title + # Perform search series_app = get_series_app() try: - results = await series_app.search(folder.title) + results = await series_app.search(search_query) search_result_json = json.dumps(results) if results else "[]" except Exception as e: logger.warning( @@ -278,7 +289,7 @@ async def search_unresolved_folder( folder_name=folder.folder_name, title=folder.title, year=folder.year, - search_attempts=folder.search_attempts, + search_attempts=folder.search_attempts + 1, search_suggestions=results, ) diff --git a/src/server/web/templates/unresolved.html b/src/server/web/templates/unresolved.html index 719fbb4..6151aee 100644 --- a/src/server/web/templates/unresolved.html +++ b/src/server/web/templates/unresolved.html @@ -238,6 +238,63 @@ opacity: 0.7; } + .search-again-row { + display: flex; + gap: 0.5rem; + margin-top: 0.5rem; + align-items: center; + } + + .search-again-input { + flex: 1; + padding: 0.5rem 0.75rem; + border: 1px solid var(--color-border); + border-radius: var(--border-radius-md); + font-size: 0.85rem; + background: var(--color-surface); + color: var(--color-text); + } + + .search-again-input:focus { + outline: none; + border-color: var(--color-accent); + } + + .search-again-row .search-again-btn { + margin-top: 0; + } + + .search-again-btn.searching { + pointer-events: none; + opacity: 0.7; + } + + .search-again-row { + display: flex; + gap: 0.5rem; + margin-top: 0.5rem; + align-items: center; + } + + .search-again-input { + flex: 1; + padding: 0.5rem 0.75rem; + border: 1px solid var(--color-border); + border-radius: var(--border-radius-md); + font-size: 0.85rem; + background: var(--color-surface); + color: var(--color-text); + } + + .search-again-input:focus { + outline: none; + border-color: var(--color-accent); + } + + .search-again-row .search-again-btn { + margin-top: 0; + } + /* Empty state */ .empty-state { text-align: center; @@ -471,12 +528,17 @@ return res.json(); } - async function reSearchFolder(folderName) { + async function reSearchFolder(folderName, customQuery) { const token = localStorage.getItem('auth_token'); const encodedName = encodeURIComponent(folderName); + const body = customQuery ? JSON.stringify({ query: customQuery }) : '{}'; const res = await fetch(`/api/setup/unresolved/${encodedName}/search`, { method: 'POST', - headers: { 'Authorization': `Bearer ${token}` } + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: body }); return res.json(); } @@ -497,15 +559,21 @@ ? folder.search_suggestions.map(s => `
- ${s.title} + ${s.name || s.title}
`).join('') : '
No suggestions found
'; const searchAgainBtn = (folder.search_suggestions && folder.search_suggestions.length === 0) - ? `` + ? `
+ + +
` : ''; return ` @@ -650,25 +718,29 @@ btn.addEventListener('click', async (e) => { const folder = e.target.dataset.folder || e.target.closest('button').dataset.folder; const item = document.querySelector(`.folder-item[data-folder="${folder}"]`); + const searchInput = item.querySelector('.search-again-input'); + const customQuery = searchInput ? searchInput.value.trim() : null; btn.classList.add('searching'); btn.innerHTML = ' Searching...'; try { - const result = await reSearchFolder(folder); + const result = await reSearchFolder(folder, customQuery); // Update suggestions in place const suggestionsEl = item.querySelector('.suggestion-list'); if (result.search_suggestions && result.search_suggestions.length > 0) { suggestionsEl.innerHTML = result.search_suggestions.map(s => `
- ${s.title} + ${s.name || s.title}
`).join(''); } else { suggestionsEl.innerHTML = '
No suggestions found
'; } - btn.remove(); + // Remove the search row since we now have suggestions (or none) + const searchRow = item.querySelector('.search-again-row'); + if (searchRow) searchRow.remove(); } catch (err) { showToast('Search failed', 'error'); } finally {