mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-09-28 22:06:05 +01:00
Feature: Show the AA search result stats (#1362)
When doing a direct search for a book, show the stats of the AA results. For example if we search for "The Great Gatsby", AA reports it has 240 hits and shows the first page of 50. Provide this stats info in the shelfmark webUI via ResultsSection.tsx and ReleaseModal.tsx. This table shows what should be displayed based on the total number of hits found: |Total |Display| |-------|-------| |1 |"Result 1 (1 Total)"| |6 |"Results 1-6 (6 Total)"| |144 |"Results 1-50 (144 Total)"| |500+ |"Results 1-50 (500+ Total)"| Currently, shelfmark also only shows us the first 50 search hits even if there were more available from AA. This could be added later if considered desirable. As usual, a picture is worth a 1000 words: <img width="1012" height="610" alt="direct-results-stat" src="https://github.com/user-attachments/assets/8c0c688f-444e-482a-a9a3-2dee5a5563b8" /> <img width="1013" height="741" alt="universal-results-info" src="https://github.com/user-attachments/assets/399bbb79-87f2-4432-a3d0-64937795f5f1" /> Coded with llama.cpp, opencode and 🤖
This commit is contained in:
@@ -114,7 +114,7 @@ def run_warmup() -> bool:
|
||||
from shelfmark.core.models import SearchFilters
|
||||
from shelfmark.release_sources.direct_download import search_books
|
||||
|
||||
results = search_books(query, SearchFilters())
|
||||
results, _ = search_books(query, SearchFilters())
|
||||
except Exception:
|
||||
# Broad by design: a warm-up must never take the app down, and the source
|
||||
# raises everything from network errors to parse failures.
|
||||
|
||||
+6
-1
@@ -3049,8 +3049,13 @@ def api_releases() -> Response | tuple[Response, int]:
|
||||
|
||||
search_info = {}
|
||||
for source_name, source_instance in source_instances.items():
|
||||
info: dict[str, str | int | None] = {}
|
||||
if hasattr(source_instance, "last_search_type") and source_instance.last_search_type:
|
||||
search_info[source_name] = {"search_type": source_instance.last_search_type}
|
||||
info["search_type"] = source_instance.last_search_type
|
||||
if hasattr(source_instance, "total_results"):
|
||||
info["total_results"] = source_instance.total_results
|
||||
if info:
|
||||
search_info[source_name] = info
|
||||
|
||||
response = {
|
||||
"releases": releases_data,
|
||||
|
||||
@@ -687,7 +687,23 @@ def _fetch_search_table_uncached(
|
||||
return "", None
|
||||
|
||||
|
||||
def search_books(query: str, filters: SearchFilters) -> list[BrowseRecord]:
|
||||
def _extract_total_results_from_html(html: str) -> int | str | None:
|
||||
"""Extract the total results count from Anna's Archive search page HTML.
|
||||
|
||||
AA renders a summary like: Results 1-50 (92 total)
|
||||
Or for 500+ results: Results 1-50 (500+ total)
|
||||
Returns the total count as int, or "500+" for capped results, or None if not found.
|
||||
"""
|
||||
match = re.search(r"Results\s+\d+\s*-\s*\d+\s*\((\d+(?:\+\s*)?)\s+total\)", html)
|
||||
if match:
|
||||
raw = match.group(1)
|
||||
if "+" in raw:
|
||||
return "500+"
|
||||
return int(raw)
|
||||
return None
|
||||
|
||||
|
||||
def search_books(query: str, filters: SearchFilters) -> tuple[list[BrowseRecord], int | str | None]:
|
||||
"""Search for books matching the query.
|
||||
|
||||
Args:
|
||||
@@ -695,7 +711,7 @@ def search_books(query: str, filters: SearchFilters) -> list[BrowseRecord]:
|
||||
filters: Search filters (language, format, content type, etc.)
|
||||
|
||||
Returns:
|
||||
List[BrowseRecord]: List of matching books
|
||||
Tuple of (List[BrowseRecord], total_count | None): Matching books and total result count
|
||||
|
||||
Raises:
|
||||
SearchUnavailableError: If Anna's Archive cannot be reached
|
||||
@@ -752,10 +768,11 @@ def search_books(query: str, filters: SearchFilters) -> list[BrowseRecord]:
|
||||
# AA gates /search behind a DDoS-Guard JS challenge, which every mirror shares. Rotating
|
||||
# to another mirror only collects another 403, so let the bypasser solve it.
|
||||
html, tbody = _fetch_search_table(url, selector)
|
||||
total_count = _extract_total_results_from_html(html) if tbody is not None else None
|
||||
if tbody is None:
|
||||
if "No files found." in html:
|
||||
logger.info("No books found for query: %s", query)
|
||||
return []
|
||||
return ([], None)
|
||||
logger.warning("No results table found for query: %s", query)
|
||||
msg = "No books found. Please try another query."
|
||||
raise RuntimeError(msg)
|
||||
@@ -791,7 +808,7 @@ def search_books(query: str, filters: SearchFilters) -> list[BrowseRecord]:
|
||||
if books:
|
||||
_enrich_search_results_with_downloads(books)
|
||||
|
||||
return books
|
||||
return (books, total_count)
|
||||
|
||||
|
||||
def _fetch_download_count_inline(book_id: str) -> int | None:
|
||||
@@ -1897,11 +1914,16 @@ class AnnasArchiveProvider:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._last_search_type = "title_author"
|
||||
self._total_results: int | str | None = None
|
||||
|
||||
@property
|
||||
def last_search_type(self) -> str:
|
||||
return self._last_search_type
|
||||
|
||||
@property
|
||||
def total_results(self) -> int | str | None:
|
||||
return self._total_results
|
||||
|
||||
def is_enabled(self) -> bool:
|
||||
from shelfmark.core import mirrors
|
||||
|
||||
@@ -1940,8 +1962,9 @@ class AnnasArchiveProvider:
|
||||
search_label: str,
|
||||
) -> list[BrowseRecord]:
|
||||
"""Retry AA queries without a language filter when filtered search returns nothing."""
|
||||
results = search_books(query, filters)
|
||||
results, total_count = self._search_books(query, filters)
|
||||
if results or not filters.lang:
|
||||
self._total_results = total_count
|
||||
return results
|
||||
|
||||
logger.debug(
|
||||
@@ -1949,7 +1972,17 @@ class AnnasArchiveProvider:
|
||||
search_label,
|
||||
filters.lang,
|
||||
)
|
||||
return search_books(query, replace(filters, lang=None))
|
||||
results, total_count = self._search_books(query, replace(filters, lang=None))
|
||||
self._total_results = total_count
|
||||
return results
|
||||
|
||||
def _search_books(
|
||||
self,
|
||||
query: str,
|
||||
filters: SearchFilters,
|
||||
) -> tuple[list[BrowseRecord], int | str | None]:
|
||||
"""Call search_books and capture the total result count."""
|
||||
return search_books(query, filters)
|
||||
|
||||
def search(
|
||||
self,
|
||||
@@ -2019,10 +2052,11 @@ class AnnasArchiveProvider:
|
||||
filters = SearchFilters(isbn=[isbn])
|
||||
filters.lang = lang_filter if lang_filter is not None else []
|
||||
try:
|
||||
results = search_books(isbn, filters)
|
||||
results, isbn_total = search_books(isbn, filters)
|
||||
if results:
|
||||
logger.info("Found %s releases via ISBN", len(results))
|
||||
self._last_search_type = "isbn"
|
||||
self._total_results = isbn_total
|
||||
return results
|
||||
logger.debug("No ISBN results, falling back to title+author")
|
||||
except SearchUnavailableError:
|
||||
@@ -2052,7 +2086,9 @@ class AnnasArchiveProvider:
|
||||
logger.debug("Searching direct_download: title_author='%s', langs=%s", query, langs)
|
||||
filters = SearchFilters(lang=langs if langs is not None else [])
|
||||
try:
|
||||
for bi in search_books(query, filters):
|
||||
books, title_total = search_books(query, filters)
|
||||
self._total_results = title_total
|
||||
for bi in books:
|
||||
if bi.id not in seen_ids:
|
||||
seen_ids.add(bi.id)
|
||||
all_results.append(bi)
|
||||
@@ -2079,7 +2115,8 @@ class AnnasArchiveProvider:
|
||||
|
||||
logger.debug("Searching direct_download: title_author='%s', langs=[]", query)
|
||||
try:
|
||||
for bi in search_books(query, SearchFilters()):
|
||||
books, _ = search_books(query, SearchFilters())
|
||||
for bi in books:
|
||||
if bi.id not in seen_ids:
|
||||
seen_ids.add(bi.id)
|
||||
all_results.append(bi)
|
||||
|
||||
@@ -110,6 +110,12 @@ class DirectDownloadSource(ReleaseSource):
|
||||
provider = registry.provider_by_id("annas_archive", self._providers)
|
||||
return str(getattr(provider, "last_search_type", "title_author"))
|
||||
|
||||
@property
|
||||
def total_results(self) -> int | None:
|
||||
"""Returns the total result count from the last search."""
|
||||
provider = registry.provider_by_id("annas_archive", self._providers)
|
||||
return getattr(provider, "total_results", None)
|
||||
|
||||
def get_column_config(self) -> ReleaseColumnConfig:
|
||||
"""Column configuration for Direct Download source.
|
||||
|
||||
|
||||
@@ -487,6 +487,7 @@ function App() {
|
||||
isLoadingMore,
|
||||
loadMore,
|
||||
totalFound,
|
||||
directTotalResults,
|
||||
resultsSourceUrl,
|
||||
} = useSearch({
|
||||
showToast,
|
||||
@@ -2607,6 +2608,7 @@ function App() {
|
||||
void loadMore(config, effectiveSearchMode);
|
||||
}}
|
||||
totalFound={totalFound}
|
||||
directTotalResults={directTotalResults}
|
||||
onShowToast={showToast}
|
||||
resultsSourceUrl={resultsSourceUrl}
|
||||
/>
|
||||
|
||||
@@ -1769,7 +1769,7 @@ const ReleaseModalSession = ({
|
||||
</div>
|
||||
)}
|
||||
{!sourcesLoading && allTabs.length > 0 && (
|
||||
<div className="flex items-center justify-between px-5">
|
||||
<div className="flex items-center px-5">
|
||||
{/* Tabs - scrollable on narrow screens */}
|
||||
<div className="scrollbar-hide min-w-0 flex-1 overflow-x-auto">
|
||||
<div className="relative flex gap-1">
|
||||
@@ -1801,7 +1801,35 @@ const ReleaseModalSession = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 pr-1 pl-2">
|
||||
{/* Center: Results count from release sources */}
|
||||
<div className="flex flex-1 justify-center">
|
||||
{(() => {
|
||||
const searchInfo = releasesBySource[activeTab]?.search_info?.[activeTab];
|
||||
const totalCount = searchInfo?.total_results;
|
||||
if (totalCount === undefined || totalCount === null || totalCount === 0)
|
||||
return null;
|
||||
const isCapped = totalCount === '500+';
|
||||
const totalCountNum = isCapped ? 500 : Number(totalCount);
|
||||
// AA-style: show page range + total
|
||||
if (totalCountNum === 1) {
|
||||
return (
|
||||
<span className="mx-2 shrink-0 text-xs whitespace-nowrap text-gray-500 dark:text-gray-400">
|
||||
Result 1 (1 Total)
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const shownEnd = Math.min(totalCountNum, 50);
|
||||
const totalStr = isCapped ? '500+' : String(totalCountNum);
|
||||
return (
|
||||
<span className="mx-2 shrink-0 text-xs whitespace-nowrap text-gray-500 dark:text-gray-400">
|
||||
Results 1-{shownEnd} ({totalStr} Total)
|
||||
</span>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* Right: Controls */}
|
||||
<div className="ml-auto flex items-center gap-3 pr-1 pl-2">
|
||||
{/* Multi-book pack toggle (fallback for releases that can't be inspected) */}
|
||||
{!isCombinedMode && (
|
||||
<button
|
||||
|
||||
@@ -33,6 +33,8 @@ interface ResultsSectionProps {
|
||||
isLoadingMore?: boolean;
|
||||
onLoadMore?: () => void;
|
||||
totalFound?: number;
|
||||
// Direct mode: total result count from release sources
|
||||
directTotalResults?: number | string | null;
|
||||
onShowToast?: (message: string, type: 'success' | 'error' | 'info') => void;
|
||||
resultsSourceUrl?: string;
|
||||
}
|
||||
@@ -53,6 +55,7 @@ export const ResultsSection = ({
|
||||
isLoadingMore,
|
||||
onLoadMore,
|
||||
totalFound,
|
||||
directTotalResults,
|
||||
onShowToast,
|
||||
resultsSourceUrl,
|
||||
}: ResultsSectionProps) => {
|
||||
@@ -88,34 +91,62 @@ export const ResultsSection = ({
|
||||
|
||||
return (
|
||||
<section id="results-section" className="mb-4 w-full sm:mb-8">
|
||||
<div className="relative z-10 mb-2 flex items-center justify-between sm:mb-3">
|
||||
{showSortControl && (
|
||||
<SortControl
|
||||
value={sortValue}
|
||||
onChange={onSortChange}
|
||||
metadataSortOptions={metadataSortOptions}
|
||||
/>
|
||||
)}
|
||||
{!showSortControl && resultsSourceUrl && (
|
||||
<a
|
||||
href={resultsSourceUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="animate-pop-up inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium text-gray-600 transition-colors hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
>
|
||||
View list on Hardcover
|
||||
<svg className="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
)}
|
||||
<div className="relative z-10 mb-2 flex items-center sm:mb-3">
|
||||
{/* Left: Sort control or Hardcover link */}
|
||||
<div>
|
||||
{showSortControl && (
|
||||
<SortControl
|
||||
value={sortValue}
|
||||
onChange={onSortChange}
|
||||
metadataSortOptions={metadataSortOptions}
|
||||
/>
|
||||
)}
|
||||
{!showSortControl && resultsSourceUrl && (
|
||||
<a
|
||||
href={resultsSourceUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="animate-pop-up inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium text-gray-600 transition-colors hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
>
|
||||
View list on Hardcover
|
||||
<svg className="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* View toggle buttons - Desktop: show all 3, Mobile: show Compact and List only */}
|
||||
{/* Center: Results count */}
|
||||
<div className="flex flex-1 justify-center">
|
||||
{(() => {
|
||||
const count = directTotalResults ?? null;
|
||||
if (count === null || count === 0) return null;
|
||||
const isCapped = directTotalResults === '500+';
|
||||
const totalCount = isCapped ? 500 : Number(count);
|
||||
// AA-style: show page range + total
|
||||
if (totalCount === 1) {
|
||||
return (
|
||||
<span className="mx-2 mt-4 px-2 text-xs whitespace-nowrap text-gray-500 dark:text-gray-400">
|
||||
Result 1 (1 Total)
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const shownEnd = Math.min(totalCount, 50);
|
||||
const totalStr = isCapped ? '500+' : String(totalCount);
|
||||
return (
|
||||
<span className="mx-2 mt-4 px-2 text-xs whitespace-nowrap text-gray-500 dark:text-gray-400">
|
||||
Results 1-{shownEnd} ({totalStr} Total)
|
||||
</span>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* Right: View toggle buttons - Desktop: show all 3, Mobile: show Compact and List only */}
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
{isDesktop && (
|
||||
<button
|
||||
|
||||
@@ -288,6 +288,7 @@ export function useReleaseSearchSession(
|
||||
[tabName]: {
|
||||
...existing,
|
||||
releases: [...existing.releases, ...mergedReleases],
|
||||
search_info: response.search_info,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -53,6 +53,8 @@ interface UseSearchReturn {
|
||||
isLoadingMore: boolean;
|
||||
loadMore: (config: AppConfig | null, searchMode?: SearchMode) => Promise<void>;
|
||||
totalFound: number;
|
||||
// Direct mode: total result count from release sources (e.g., "500+" for capped)
|
||||
directTotalResults: number | string | null;
|
||||
// Source URL and title for the current result set (e.g. Hardcover list page)
|
||||
resultsSourceUrl: string | undefined;
|
||||
resultsSourceTitle: string | undefined;
|
||||
@@ -92,6 +94,8 @@ export function useSearch(options: UseSearchOptions): UseSearchReturn {
|
||||
const [hasMore, setHasMore] = useState(false);
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||
const [totalFound, setTotalFound] = useState(0);
|
||||
// Direct mode: total result count from release sources (e.g., Anna's Archive)
|
||||
const [directTotalResults, setDirectTotalResults] = useState<number | string | null>(null);
|
||||
const [resultsSourceUrl, setResultsSourceUrl] = useState<string | undefined>();
|
||||
const [resultsSourceTitle, setResultsSourceTitle] = useState<string | undefined>();
|
||||
|
||||
@@ -274,7 +278,7 @@ export function useSearch(options: UseSearchOptions): UseSearchReturn {
|
||||
setLastSearchQuery(query);
|
||||
|
||||
try {
|
||||
const results = await searchBooks(query);
|
||||
const { books: results, totalResults } = await searchBooks(query);
|
||||
|
||||
if (results.length > 0) {
|
||||
// When no explicit server-side sort is selected (empty or "downloads"),
|
||||
@@ -284,8 +288,10 @@ export function useSearch(options: UseSearchOptions): UseSearchReturn {
|
||||
const isDownloadsSort = !effectiveSort || effectiveSort === 'downloads';
|
||||
const sorted = isDownloadsSort ? sortBooksByDownloads(results) : results;
|
||||
setBooks(sorted);
|
||||
setDirectTotalResults(totalResults);
|
||||
} else {
|
||||
showToast('No results found', 'error');
|
||||
setDirectTotalResults(null);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof AuthenticationError) {
|
||||
@@ -335,6 +341,7 @@ export function useSearch(options: UseSearchOptions): UseSearchReturn {
|
||||
setCurrentPage(1);
|
||||
setHasMore(false);
|
||||
setTotalFound(0);
|
||||
setDirectTotalResults(null);
|
||||
setResultsSourceUrl(undefined);
|
||||
setResultsSourceTitle(undefined);
|
||||
lastSearchParamsRef.current = null;
|
||||
@@ -412,6 +419,7 @@ export function useSearch(options: UseSearchOptions): UseSearchReturn {
|
||||
isLoadingMore,
|
||||
loadMore,
|
||||
totalFound,
|
||||
directTotalResults,
|
||||
resultsSourceUrl,
|
||||
resultsSourceTitle,
|
||||
};
|
||||
|
||||
@@ -279,14 +279,20 @@ async function fetchJSON<T>(
|
||||
}
|
||||
|
||||
// API functions
|
||||
export const searchBooks = async (query: string): Promise<Book[]> => {
|
||||
if (!query) return [];
|
||||
export const searchBooks = async (
|
||||
query: string,
|
||||
): Promise<{ books: Book[]; totalResults: number | string | null }> => {
|
||||
if (!query) return { books: [], totalResults: null };
|
||||
const response = await fetchJSON<ReleasesResponse>(
|
||||
`${API_BASE}/releases?source=direct_download&${query}`,
|
||||
{},
|
||||
searchTimeoutMs,
|
||||
);
|
||||
return response.releases.map(transformReleaseToDirectBook);
|
||||
// Extract total count from search_info if available
|
||||
const firstSource = response.sources_searched?.[0];
|
||||
const searchInfo = firstSource ? response.search_info?.[firstSource] : undefined;
|
||||
const totalResults = searchInfo?.total_results ?? null;
|
||||
return { books: response.releases.map(transformReleaseToDirectBook), totalResults };
|
||||
};
|
||||
|
||||
// Metadata search response type (internal)
|
||||
|
||||
@@ -475,6 +475,7 @@ export interface Release {
|
||||
// Search info returned by release sources
|
||||
export interface SourceSearchInfo {
|
||||
search_type: 'isbn' | 'title_author' | 'categories' | 'expanded' | 'manual' | 'query';
|
||||
total_results?: number | string | null;
|
||||
}
|
||||
|
||||
// Response from /api/releases endpoint
|
||||
|
||||
@@ -99,7 +99,7 @@ def test_title_variants_stop_once_the_budget_is_spent(monkeypatch):
|
||||
deadline = search_deadline.current()
|
||||
if deadline is not None:
|
||||
deadline.event.set()
|
||||
return []
|
||||
return ([], None)
|
||||
|
||||
monkeypatch.setattr(aa, "search_books", fake_search_books)
|
||||
monkeypatch.setattr(aa, "ensure_available", lambda: None)
|
||||
@@ -114,7 +114,7 @@ def test_title_variants_stop_once_the_budget_is_spent(monkeypatch):
|
||||
|
||||
def test_all_title_variants_run_within_budget(monkeypatch):
|
||||
queries: list[str] = []
|
||||
monkeypatch.setattr(aa, "search_books", lambda q, _f: queries.append(q) or [])
|
||||
monkeypatch.setattr(aa, "search_books", lambda q, _f: queries.append(q) or ([], None))
|
||||
monkeypatch.setattr(aa, "ensure_available", lambda: None)
|
||||
|
||||
source = dd.DirectDownloadSource()
|
||||
@@ -135,7 +135,7 @@ def test_language_filter_retry_is_skipped_on_a_spent_budget(monkeypatch):
|
||||
deadline = search_deadline.current()
|
||||
if deadline is not None:
|
||||
deadline.event.set()
|
||||
return []
|
||||
return ([], None)
|
||||
|
||||
monkeypatch.setattr(aa, "search_books", fake_search_books)
|
||||
monkeypatch.setattr(aa, "ensure_available", lambda: None)
|
||||
|
||||
@@ -33,7 +33,7 @@ class TestDirectDownloadSearchQueries:
|
||||
|
||||
def fake_search_books(query: str, filters):
|
||||
captured.append(query)
|
||||
return []
|
||||
return ([], None)
|
||||
|
||||
_enable_direct_download(monkeypatch)
|
||||
|
||||
@@ -75,7 +75,7 @@ class TestDirectDownloadSearchQueries:
|
||||
|
||||
def fake_search_books(query: str, filters):
|
||||
captured.append((query, filters.lang))
|
||||
return records_by_query[query]
|
||||
return (records_by_query[query], None)
|
||||
|
||||
_enable_direct_download(monkeypatch)
|
||||
|
||||
@@ -118,8 +118,8 @@ class TestDirectDownloadSearchQueries:
|
||||
def fake_search_books(query: str, filters):
|
||||
captured.append((query, filters.lang))
|
||||
if filters.lang:
|
||||
return []
|
||||
return fallback_results[query]
|
||||
return ([], None)
|
||||
return (fallback_results[query], None)
|
||||
|
||||
_enable_direct_download(monkeypatch)
|
||||
|
||||
@@ -156,8 +156,8 @@ class TestDirectDownloadSearchQueries:
|
||||
def fake_search_books(query: str, filters):
|
||||
captured.append((query, filters.lang, filters.format))
|
||||
if filters.lang:
|
||||
return []
|
||||
return [_browse_record("manual-1", "Manual result")]
|
||||
return ([], None)
|
||||
return ([_browse_record("manual-1", "Manual result")], None)
|
||||
|
||||
_enable_direct_download(monkeypatch)
|
||||
|
||||
@@ -336,7 +336,7 @@ def test_search_books_filters_locally_when_path_language_enabled(monkeypatch):
|
||||
|
||||
monkeypatch.setattr(aa.downloader, "html_get_page", _fake_html_get_page)
|
||||
|
||||
records = aa.search_books("demo", SearchFilters(lang=["fr"], format=["pdf"]))
|
||||
records, _ = aa.search_books("demo", SearchFilters(lang=["fr"], format=["pdf"]))
|
||||
|
||||
assert "&lang=" not in captured_url["url"]
|
||||
assert len(records) == 1
|
||||
@@ -380,7 +380,7 @@ def test_search_books_keeps_server_language_matches_when_path_language_disabled(
|
||||
|
||||
monkeypatch.setattr(aa.downloader, "html_get_page", _fake_html_get_page)
|
||||
|
||||
records = aa.search_books("demo", SearchFilters(lang=["en"], format=["pdf"]))
|
||||
records, _ = aa.search_books("demo", SearchFilters(lang=["en"], format=["pdf"]))
|
||||
|
||||
# AA already narrowed by &lang=; its free-text language cells must not be re-matched.
|
||||
assert "&lang=en" in captured_url["url"]
|
||||
|
||||
@@ -22,7 +22,7 @@ def test_warmup_runs_when_nobody_has_searched(monkeypatch):
|
||||
searched: list[str] = []
|
||||
monkeypatch.setattr(
|
||||
"shelfmark.release_sources.direct_download.search_books",
|
||||
lambda query, _filters: searched.append(query) or ["a result"],
|
||||
lambda query, _filters: (searched.append(query), ["a result"], None)[1:],
|
||||
)
|
||||
|
||||
assert warmup.run_warmup() is True
|
||||
|
||||
Reference in New Issue
Block a user