Hardcover tweaks (#720)

This commit is contained in:
Alex
2026-03-07 18:16:40 +00:00
committed by GitHub
parent 9d08bb3ef1
commit a7db7f04e9
8 changed files with 46 additions and 7 deletions

View File

@@ -2188,6 +2188,8 @@ def api_metadata_search() -> Union[Response, Tuple[Response, int]]:
}
if search_result.source_url:
response_data["source_url"] = search_result.source_url
if search_result.source_title:
response_data["source_title"] = search_result.source_title
return jsonify(response_data)
except Exception as e:
logger.error_trace(f"Metadata search error: {e}")

View File

@@ -311,6 +311,7 @@ class SearchResult:
total_found: int = 0 # Total matching results (if known)
has_more: bool = False # True if more results available
source_url: Optional[str] = None # External URL for the result set (e.g. Hardcover list page)
source_title: Optional[str] = None # Display title for the result set (e.g. list name)
class MetadataProvider(ABC):

View File

@@ -59,6 +59,7 @@ query LookupListsBySlug($slug: String!) {
LIST_BOOKS_BY_ID_QUERY = """
query GetListBooksById($id: Int!, $limit: Int!, $offset: Int!) {
lists(where: {id: {_eq: $id}}, limit: 1) {
name
slug
user {
username
@@ -909,8 +910,9 @@ class HardcoverProvider(MetadataProvider):
list_books = list_data.get("list_books", []) if isinstance(list_data, dict) else []
books_count_raw = list_data.get("books_count", 0) if isinstance(list_data, dict) else 0
# Build source URL from slug and owner username
# Build source URL and title from list metadata
source_url = None
source_title = str(list_data.get("name") or "").strip() or None
list_slug = str(list_data.get("slug") or "").strip()
user_data = list_data.get("user", {})
owner_username = str(user_data.get("username") or "").strip() if isinstance(user_data, dict) else ""
@@ -937,7 +939,7 @@ class HardcoverProvider(MetadataProvider):
logger.debug(f"Failed to parse Hardcover list book for list_id={list_id}: {exc}")
has_more = offset + len(list_books) < books_count
return SearchResult(books=books, page=page, total_found=books_count, has_more=has_more, source_url=source_url)
return SearchResult(books=books, page=page, total_found=books_count, has_more=has_more, source_url=source_url, source_title=source_title)
@cacheable(ttl_key="METADATA_CACHE_SEARCH_TTL", ttl_default=300, key_prefix="hardcover:list:slug")
def _fetch_list_books(self, slug: str, owner_username: Optional[str], page: int, limit: int) -> SearchResult:

View File

@@ -79,6 +79,7 @@ import {
import { bookFromRequestData } from './utils/requestFulfil';
import { emitBookTargetChange, onBookTargetChange } from './utils/bookTargetEvents';
import { bookSupportsTargets } from './utils/bookTargetLoader';
import { getDynamicOptionGroup } from './components/shared/DynamicDropdown';
import { policyTrace } from './utils/policyTrace';
import { SearchModeProvider } from './contexts/SearchModeContext';
import { useSocket } from './contexts/SocketContext';
@@ -1089,16 +1090,28 @@ function App() {
[]
);
// When downloading a book while browsing a Hardcover list, automatically
// remove it from that list (fire-and-forget).
// When downloading a book while browsing a Hardcover list the user owns,
// automatically remove it from that list (fire-and-forget).
const searchFieldLabelsRef = useRef(searchFieldLabels);
searchFieldLabelsRef.current = searchFieldLabels;
const metadataConfigRef = useRef(activeMetadataConfig);
metadataConfigRef.current = activeMetadataConfig;
const removeBookFromActiveList = useCallback((book: Book) => {
if (!bookSupportsTargets(book)) return;
const activeList = searchFieldValuesRef.current.hardcover_list;
if (!activeList) return;
const target = String(activeList);
// Only auto-remove from lists the user owns (My Books / My Lists)
const listField = metadataConfigRef.current?.search_fields.find(
(f) => f.key === 'hardcover_list' && f.type === 'DynamicSelectSearchField',
);
if (listField && listField.type === 'DynamicSelectSearchField') {
const group = getDynamicOptionGroup(listField.options_endpoint, target);
if (group && group !== 'My Books' && group !== 'My Lists') return;
}
void setBookTargetState(book.provider!, book.provider_id!, target, false).then((result) => {
if (result.changed) {
emitBookTargetChange({
@@ -2135,7 +2148,7 @@ function App() {
getButtonState={getDirectActionButtonState}
getUniversalButtonState={getUniversalActionButtonState}
sortValue={visibleResultsSort}
showSortControl={!activeQueryUsesSeriesBrowse && !activeQueryUsesListBrowse}
showSortControl={!activeQueryUsesSeriesBrowse && !activeQueryUsesListBrowse && !resultsSourceUrl}
onSortChange={(value) => {
const request = buildCurrentSearchRequest(value);
const shouldPersistAppliedSort = !(

View File

@@ -217,7 +217,7 @@ export const BookTargetDropdown = ({
<button
type="button"
onClick={(e) => { e.stopPropagation(); toggle(); }}
className={`flex items-center justify-center rounded-full transition-all duration-200 focus:outline-none ${className ?? 'p-1.5 sm:p-2 text-gray-600 dark:text-gray-200 hover-action'}`}
className={`flex items-center justify-center rounded-full transition-colors duration-200 focus:outline-none ${className ?? 'p-1.5 sm:p-2 text-gray-600 dark:text-gray-200 hover-action'}`}
aria-label="Hardcover Lists"
title={count > 0 ? `On ${count} Hardcover list${count > 1 ? 's' : ''}` : 'Hardcover Lists'}
>

View File

@@ -25,6 +25,13 @@ const buildOptions = (
}));
};
/** Look up the group of a cached option by endpoint and value. */
export const getDynamicOptionGroup = (endpoint: string, value: string): string | undefined => {
const cached = optionsCache.get(endpoint);
if (!cached) return undefined;
return cached.find((o) => o.value === value)?.group;
};
export const DynamicDropdown = ({
endpoint,
value,

View File

@@ -49,8 +49,9 @@ interface UseSearchReturn {
isLoadingMore: boolean;
loadMore: (config: AppConfig | null, searchMode?: SearchMode) => Promise<void>;
totalFound: number;
// Source URL for the current result set (e.g. Hardcover list page)
// Source URL and title for the current result set (e.g. Hardcover list page)
resultsSourceUrl: string | undefined;
resultsSourceTitle: string | undefined;
}
export function useSearch(options: UseSearchOptions): UseSearchReturn {
@@ -82,6 +83,7 @@ export function useSearch(options: UseSearchOptions): UseSearchReturn {
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [totalFound, setTotalFound] = useState(0);
const [resultsSourceUrl, setResultsSourceUrl] = useState<string | undefined>();
const [resultsSourceTitle, setResultsSourceTitle] = useState<string | undefined>();
// Store last search params for loadMore
const lastSearchParamsRef = useRef<{
@@ -167,6 +169,7 @@ export function useSearch(options: UseSearchOptions): UseSearchReturn {
setTotalFound(0);
setCurrentPage(1);
setResultsSourceUrl(undefined);
setResultsSourceTitle(undefined);
lastSearchParamsRef.current = null;
return;
}
@@ -193,6 +196,11 @@ export function useSearch(options: UseSearchOptions): UseSearchReturn {
setHasMore(result.hasMore);
setTotalFound(result.totalFound);
setResultsSourceUrl(result.sourceUrl);
setResultsSourceTitle(result.sourceTitle);
// Replace URL in search input with list title for display
if (result.sourceTitle && searchQuery) {
setSearchInput(result.sourceTitle);
}
// Store params for loadMore
lastSearchParamsRef.current = {
query: searchQuery,
@@ -206,6 +214,7 @@ export function useSearch(options: UseSearchOptions): UseSearchReturn {
setHasMore(false);
setTotalFound(0);
setResultsSourceUrl(undefined);
setResultsSourceTitle(undefined);
showToast('No results found', 'error');
}
} catch (error) {
@@ -276,6 +285,7 @@ export function useSearch(options: UseSearchOptions): UseSearchReturn {
setHasMore(false);
setTotalFound(0);
setResultsSourceUrl(undefined);
setResultsSourceTitle(undefined);
lastSearchParamsRef.current = null;
}, [onSearchReset]);
@@ -340,5 +350,6 @@ export function useSearch(options: UseSearchOptions): UseSearchReturn {
loadMore,
totalFound,
resultsSourceUrl,
resultsSourceTitle,
};
}

View File

@@ -225,6 +225,7 @@ interface MetadataSearchResponse {
total_found?: number;
has_more?: boolean;
source_url?: string;
source_title?: string;
}
// Metadata search result with pagination info
@@ -234,6 +235,7 @@ export interface MetadataSearchResult {
totalFound: number;
hasMore: boolean;
sourceUrl?: string;
sourceTitle?: string;
}
export interface DynamicFieldOption {
@@ -300,6 +302,7 @@ export const searchMetadata = async (
totalFound: response.total_found || 0,
hasMore: response.has_more || false,
sourceUrl: response.source_url,
sourceTitle: response.source_title,
};
};