diff --git a/shelfmark/main.py b/shelfmark/main.py index 438b91e..1259459 100644 --- a/shelfmark/main.py +++ b/shelfmark/main.py @@ -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}") diff --git a/shelfmark/metadata_providers/__init__.py b/shelfmark/metadata_providers/__init__.py index 0f187e4..e302579 100644 --- a/shelfmark/metadata_providers/__init__.py +++ b/shelfmark/metadata_providers/__init__.py @@ -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): diff --git a/shelfmark/metadata_providers/hardcover.py b/shelfmark/metadata_providers/hardcover.py index cf8215e..6b86fed 100644 --- a/shelfmark/metadata_providers/hardcover.py +++ b/shelfmark/metadata_providers/hardcover.py @@ -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: diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index f18077d..52d795b 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -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 = !( diff --git a/src/frontend/src/components/BookTargetDropdown.tsx b/src/frontend/src/components/BookTargetDropdown.tsx index eba57aa..1a3fb74 100644 --- a/src/frontend/src/components/BookTargetDropdown.tsx +++ b/src/frontend/src/components/BookTargetDropdown.tsx @@ -217,7 +217,7 @@ export const BookTargetDropdown = ({