feat: add custom query support for unresolved folder re-search
- Add SearchFolderRequest model for optional custom search query - Update search endpoint to use custom query if provided - Add search-again input field in UI for custom queries - Increment search_attempts counter on re-search
This commit is contained in:
@@ -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)
|
@router.post("/unresolved/{folder_name}/search", response_model=UnresolvedFolderResponse)
|
||||||
async def search_unresolved_folder(
|
async def search_unresolved_folder(
|
||||||
folder_name: str,
|
folder_name: str,
|
||||||
|
request: Optional[SearchFolderRequest] = None,
|
||||||
db=Depends(get_database_session),
|
db=Depends(get_database_session),
|
||||||
) -> UnresolvedFolderResponse:
|
) -> UnresolvedFolderResponse:
|
||||||
"""Re-search for a specific unresolved folder to get fresh suggestions.
|
"""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:
|
Args:
|
||||||
folder_name: URL-encoded folder name to search for
|
folder_name: URL-encoded folder name to search for
|
||||||
|
request: Optional SearchFolderRequest with custom query override
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
UnresolvedFolderResponse with updated search suggestions
|
UnresolvedFolderResponse with updated search suggestions
|
||||||
@@ -258,10 +266,13 @@ async def search_unresolved_folder(
|
|||||||
detail=f"Folder already resolved: {folder_name}"
|
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
|
# Perform search
|
||||||
series_app = get_series_app()
|
series_app = get_series_app()
|
||||||
try:
|
try:
|
||||||
results = await series_app.search(folder.title)
|
results = await series_app.search(search_query)
|
||||||
search_result_json = json.dumps(results) if results else "[]"
|
search_result_json = json.dumps(results) if results else "[]"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -278,7 +289,7 @@ async def search_unresolved_folder(
|
|||||||
folder_name=folder.folder_name,
|
folder_name=folder.folder_name,
|
||||||
title=folder.title,
|
title=folder.title,
|
||||||
year=folder.year,
|
year=folder.year,
|
||||||
search_attempts=folder.search_attempts,
|
search_attempts=folder.search_attempts + 1,
|
||||||
search_suggestions=results,
|
search_suggestions=results,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -238,6 +238,63 @@
|
|||||||
opacity: 0.7;
|
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 */
|
||||||
.empty-state {
|
.empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -471,12 +528,17 @@
|
|||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reSearchFolder(folderName) {
|
async function reSearchFolder(folderName, customQuery) {
|
||||||
const token = localStorage.getItem('auth_token');
|
const token = localStorage.getItem('auth_token');
|
||||||
const encodedName = encodeURIComponent(folderName);
|
const encodedName = encodeURIComponent(folderName);
|
||||||
|
const body = customQuery ? JSON.stringify({ query: customQuery }) : '{}';
|
||||||
const res = await fetch(`/api/setup/unresolved/${encodedName}/search`, {
|
const res = await fetch(`/api/setup/unresolved/${encodedName}/search`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: body
|
||||||
});
|
});
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
@@ -497,15 +559,21 @@
|
|||||||
? folder.search_suggestions.map(s => `
|
? folder.search_suggestions.map(s => `
|
||||||
<div class="suggestion-item">
|
<div class="suggestion-item">
|
||||||
<i class="fas fa-link"></i>
|
<i class="fas fa-link"></i>
|
||||||
<a href="${s.link}" class="suggestion-link" target="_blank">${s.title}</a>
|
<a href="${s.link}" class="suggestion-link" target="_blank">${s.name || s.title}</a>
|
||||||
</div>
|
</div>
|
||||||
`).join('')
|
`).join('')
|
||||||
: '<div class="no-suggestions"><i class="fas fa-info-circle"></i> No suggestions found</div>';
|
: '<div class="no-suggestions"><i class="fas fa-info-circle"></i> No suggestions found</div>';
|
||||||
|
|
||||||
const searchAgainBtn = (folder.search_suggestions && folder.search_suggestions.length === 0)
|
const searchAgainBtn = (folder.search_suggestions && folder.search_suggestions.length === 0)
|
||||||
? `<button class="search-again-btn" data-folder="${folder.folder_name}">
|
? `<div class="search-again-row">
|
||||||
<i class="fas fa-search"></i> Search Again
|
<input type="text" class="search-again-input"
|
||||||
</button>`
|
placeholder="Custom search..."
|
||||||
|
value="${folder.title || ''}"
|
||||||
|
data-folder="${folder.folder_name}">
|
||||||
|
<button class="search-again-btn" data-folder="${folder.folder_name}">
|
||||||
|
<i class="fas fa-search"></i> Search Again
|
||||||
|
</button>
|
||||||
|
</div>`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
@@ -650,25 +718,29 @@
|
|||||||
btn.addEventListener('click', async (e) => {
|
btn.addEventListener('click', async (e) => {
|
||||||
const folder = e.target.dataset.folder || e.target.closest('button').dataset.folder;
|
const folder = e.target.dataset.folder || e.target.closest('button').dataset.folder;
|
||||||
const item = document.querySelector(`.folder-item[data-folder="${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.classList.add('searching');
|
||||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Searching...';
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Searching...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await reSearchFolder(folder);
|
const result = await reSearchFolder(folder, customQuery);
|
||||||
// Update suggestions in place
|
// Update suggestions in place
|
||||||
const suggestionsEl = item.querySelector('.suggestion-list');
|
const suggestionsEl = item.querySelector('.suggestion-list');
|
||||||
if (result.search_suggestions && result.search_suggestions.length > 0) {
|
if (result.search_suggestions && result.search_suggestions.length > 0) {
|
||||||
suggestionsEl.innerHTML = result.search_suggestions.map(s => `
|
suggestionsEl.innerHTML = result.search_suggestions.map(s => `
|
||||||
<div class="suggestion-item">
|
<div class="suggestion-item">
|
||||||
<i class="fas fa-link"></i>
|
<i class="fas fa-link"></i>
|
||||||
<a href="${s.link}" class="suggestion-link" target="_blank">${s.title}</a>
|
<a href="${s.link}" class="suggestion-link" target="_blank">${s.name || s.title}</a>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).join('');
|
||||||
} else {
|
} else {
|
||||||
suggestionsEl.innerHTML = '<div class="no-suggestions"><i class="fas fa-info-circle"></i> No suggestions found</div>';
|
suggestionsEl.innerHTML = '<div class="no-suggestions"><i class="fas fa-info-circle"></i> No suggestions found</div>';
|
||||||
}
|
}
|
||||||
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) {
|
} catch (err) {
|
||||||
showToast('Search failed', 'error');
|
showToast('Search failed', 'error');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user