diff --git a/shelfmark/config/booklore_settings.py b/shelfmark/config/booklore_settings.py index 3d103dd9..c7d864f2 100644 --- a/shelfmark/config/booklore_settings.py +++ b/shelfmark/config/booklore_settings.py @@ -179,18 +179,18 @@ def test_booklore_connection(current_values: dict[str, Any] | None = None) -> di password = _get_value("BOOKLORE_PASSWORD", "") or "" if not base_url: - return {"success": False, "message": "Booklore URL is required"} + return {"success": False, "message": "Grimmory URL is required"} if not username: - return {"success": False, "message": "Booklore username is required"} + return {"success": False, "message": "Grimmory username is required"} if not password: - return {"success": False, "message": "Booklore password is required"} + return {"success": False, "message": "Grimmory password is required"} try: library_options, _ = _get_booklore_select_options(base_url, username, password) - message = "Connected to Booklore" + message = "Connected to Grimmory" if library_options: - message = f"Connected to Booklore ({len(library_options)} libraries)" + message = f"Connected to Grimmory ({len(library_options)} libraries)" return {"success": True, "message": message} except BookloreError as exc: diff --git a/shelfmark/config/settings.py b/shelfmark/config/settings.py index 9c1794bc..e856b781 100644 --- a/shelfmark/config/settings.py +++ b/shelfmark/config/settings.py @@ -385,7 +385,7 @@ def general_settings(): TextField( key="CALIBRE_WEB_URL", label="Library URL", - description="Adds a navigation button to your book library (Calibre-Web Automated, Booklore, etc).", + description="Adds a navigation button to your book library (Calibre-Web Automated, Grimmory, etc).", placeholder="http://calibre-web:8083", ), TextField( @@ -873,8 +873,8 @@ def download_settings(): }, { "value": "booklore", - "label": "Booklore (API)", - "description": "Upload files directly to Booklore", + "label": "Grimmory (API)", + "description": "Upload files directly to Grimmory", }, ], default="folder", @@ -957,14 +957,14 @@ def download_settings(): ), HeadingField( key="booklore_heading", - title="Booklore", - description="Upload books directly to Booklore via API. Audiobooks always use folder mode.", + title="Grimmory", + description="Upload books directly to Grimmory (Formerly Booklore) via API. Audiobooks always use folder mode.", show_when={"field": "BOOKS_OUTPUT_MODE", "value": "booklore"}, ), TextField( key="BOOKLORE_HOST", - label="Booklore URL", - description="Base URL of your Booklore instance", + label="Grimmory URL", + description="Base URL of your Grimmory instance", placeholder="http://booklore:6060", required=True, show_when={"field": "BOOKS_OUTPUT_MODE", "value": "booklore"}, @@ -972,14 +972,14 @@ def download_settings(): TextField( key="BOOKLORE_USERNAME", label="Username", - description="Booklore account username", + description="Grimmory account username", required=True, show_when={"field": "BOOKS_OUTPUT_MODE", "value": "booklore"}, ), PasswordField( key="BOOKLORE_PASSWORD", label="Password", - description="Booklore account password", + description="Grimmory account password", required=True, show_when={"field": "BOOKS_OUTPUT_MODE", "value": "booklore"}, ), @@ -1005,7 +1005,7 @@ def download_settings(): SelectField( key="BOOKLORE_LIBRARY_ID", label="Library", - description="Booklore library to upload into.", + description="Grimmory library to upload into.", options=get_booklore_library_options, required=True, user_overridable=True, @@ -1017,7 +1017,7 @@ def download_settings(): SelectField( key="BOOKLORE_PATH_ID", label="Path", - description="Booklore library path for uploads.", + description="Grimmory library path for uploads.", options=get_booklore_path_options, required=True, filter_by_field="BOOKLORE_LIBRARY_ID", @@ -1030,7 +1030,7 @@ def download_settings(): ActionButton( key="test_booklore", label="Test Connection", - description="Verify your Booklore configuration", + description="Verify your Grimmory configuration", style="primary", callback=test_booklore_connection, show_when={"field": "BOOKS_OUTPUT_MODE", "value": "booklore"}, diff --git a/shelfmark/download/outputs/booklore.py b/shelfmark/download/outputs/booklore.py index 0805fb7e..9a8b9dcd 100644 --- a/shelfmark/download/outputs/booklore.py +++ b/shelfmark/download/outputs/booklore.py @@ -24,6 +24,7 @@ BOOKLORE_SUPPORTED_EXTENSIONS = {".azw", ".azw3", ".cb7", ".cbr", ".cbz", ".epub BOOKLORE_SUPPORTED_FORMATS_LABEL = ", ".join( ext.lstrip(".").upper() for ext in sorted(BOOKLORE_SUPPORTED_EXTENSIONS) ) +BOOKLORE_DISPLAY_NAME = "Grimmory" class BookloreError(Exception): @@ -67,11 +68,11 @@ def build_booklore_config( password = values.get("BOOKLORE_PASSWORD", "") or "" if not base_url: - raise BookloreError("Booklore URL is required") + raise BookloreError(f"{BOOKLORE_DISPLAY_NAME} URL is required") if not username: - raise BookloreError("Booklore username is required") + raise BookloreError(f"{BOOKLORE_DISPLAY_NAME} username is required") if not password: - raise BookloreError("Booklore password is required") + raise BookloreError(f"{BOOKLORE_DISPLAY_NAME} password is required") destination = _parse_destination( values.get("BOOKLORE_DESTINATION", BOOKLORE_DESTINATION_LIBRARY) @@ -97,8 +98,8 @@ def build_booklore_config( library_id_val = values.get("BOOKLORE_LIBRARY_ID") path_id_val = values.get("BOOKLORE_PATH_ID") - library_id = _parse_int(library_id_val, "Booklore library ID") - path_id = _parse_int(path_id_val, "Booklore path ID") + library_id = _parse_int(library_id_val, f"{BOOKLORE_DISPLAY_NAME} library ID") + path_id = _parse_int(path_id_val, f"{BOOKLORE_DISPLAY_NAME} path ID") return BookloreConfig( base_url=base_url.rstrip("/"), @@ -119,28 +120,28 @@ def booklore_login(booklore_config: BookloreConfig) -> str: try: response = requests.post(url, json=payload, timeout=30, verify=booklore_config.verify_tls) except requests.exceptions.ConnectionError as exc: - raise BookloreError("Could not connect to Booklore") from exc + raise BookloreError(f"Could not connect to {BOOKLORE_DISPLAY_NAME}") from exc except requests.exceptions.Timeout as exc: - raise BookloreError("Booklore connection timed out") from exc + raise BookloreError(f"{BOOKLORE_DISPLAY_NAME} connection timed out") from exc except requests.exceptions.RequestException as exc: - raise BookloreError(f"Booklore login failed: {exc}") from exc + raise BookloreError(f"{BOOKLORE_DISPLAY_NAME} login failed: {exc}") from exc if response.status_code in {401, 403}: - raise BookloreError("Booklore authentication failed") + raise BookloreError(f"{BOOKLORE_DISPLAY_NAME} authentication failed") try: response.raise_for_status() except requests.exceptions.HTTPError as exc: - raise BookloreError(f"Booklore login failed ({response.status_code})") from exc + raise BookloreError(f"{BOOKLORE_DISPLAY_NAME} login failed ({response.status_code})") from exc try: data = response.json() except ValueError as exc: - raise BookloreError("Invalid Booklore login response") from exc + raise BookloreError(f"Invalid {BOOKLORE_DISPLAY_NAME} login response") from exc token = data.get("accessToken") if not token: - raise BookloreError("Booklore did not return an access token") + raise BookloreError(f"{BOOKLORE_DISPLAY_NAME} did not return an access token") return token @@ -153,12 +154,12 @@ def booklore_list_libraries(booklore_config: BookloreConfig, token: str) -> list response = requests.get(url, headers=headers, timeout=30, verify=booklore_config.verify_tls) response.raise_for_status() except requests.exceptions.RequestException as exc: - raise BookloreError(f"Failed to fetch Booklore libraries: {exc}") from exc + raise BookloreError(f"Failed to fetch {BOOKLORE_DISPLAY_NAME} libraries: {exc}") from exc try: return response.json() except ValueError as exc: - raise BookloreError("Invalid Booklore libraries response") from exc + raise BookloreError(f"Invalid {BOOKLORE_DISPLAY_NAME} libraries response") from exc def booklore_upload_file(booklore_config: BookloreConfig, token: str, file_path: Path) -> None: @@ -189,13 +190,13 @@ def booklore_upload_file(booklore_config: BookloreConfig, token: str, file_path: if message: message = f": {message[:200]}" status_code = response.status_code if response is not None else "unknown" - raise BookloreError(f"Booklore upload failed ({status_code}){message}") from exc + raise BookloreError(f"{BOOKLORE_DISPLAY_NAME} upload failed ({status_code}){message}") from exc except requests.exceptions.ConnectionError as exc: - raise BookloreError("Could not connect to Booklore") from exc + raise BookloreError(f"Could not connect to {BOOKLORE_DISPLAY_NAME}") from exc except requests.exceptions.Timeout as exc: - raise BookloreError("Booklore upload timed out") from exc + raise BookloreError(f"{BOOKLORE_DISPLAY_NAME} upload timed out") from exc except requests.exceptions.RequestException as exc: - raise BookloreError(f"Booklore upload failed: {exc}") from exc + raise BookloreError(f"{BOOKLORE_DISPLAY_NAME} upload failed: {exc}") from exc def booklore_refresh_library(booklore_config: BookloreConfig, token: str) -> None: @@ -206,7 +207,7 @@ def booklore_refresh_library(booklore_config: BookloreConfig, token: str) -> Non response = requests.put(url, headers=headers, timeout=30, verify=booklore_config.verify_tls) response.raise_for_status() except requests.exceptions.RequestException as exc: - raise BookloreError(f"Booklore refresh failed: {exc}") from exc + raise BookloreError(f"{BOOKLORE_DISPLAY_NAME} refresh failed: {exc}") from exc def _supports_booklore(task: DownloadTask) -> bool: @@ -231,7 +232,7 @@ def _booklore_format_error(rejected_files: List[Path]) -> str: rejected_exts = sorted(set(f.suffix.lower() for f in rejected_files)) rejected_list = ", ".join(rejected_exts) return ( - f"Booklore does not support {rejected_list}. " + f"{BOOKLORE_DISPLAY_NAME} does not support {rejected_list}. " f"Supported formats: {BOOKLORE_SUPPORTED_FORMATS_LABEL}" ) @@ -267,7 +268,7 @@ def _post_process_booklore( status_callback("error", str(e)) return None - status_callback("resolving", "Preparing Booklore upload") + status_callback("resolving", f"Preparing {BOOKLORE_DISPLAY_NAME} upload") stage_action = STAGE_NONE if is_managed_workspace_path(temp_file): @@ -314,7 +315,7 @@ def _post_process_booklore( if cancel_flag.is_set(): logger.info("Task %s: cancelled during Booklore upload", task.task_id) return None - status_callback("resolving", f"Uploading to Booklore ({index}/{len(prepared.files)})") + status_callback("resolving", f"Uploading to {BOOKLORE_DISPLAY_NAME} ({index}/{len(prepared.files)})") booklore_upload_file(booklore_config, token, file_path) if booklore_config.refresh_after_upload: @@ -361,9 +362,9 @@ def _post_process_booklore( if not maybe_run_custom_script(script_context, status_callback=status_callback): return None - message = "Uploaded to Booklore" + message = f"Uploaded to {BOOKLORE_DISPLAY_NAME}" if len(prepared.files) > 1: - message = f"Uploaded to Booklore ({len(prepared.files)} files)" + message = f"Uploaded to {BOOKLORE_DISPLAY_NAME} ({len(prepared.files)} files)" status_callback("complete", message) success = True return f"booklore://{task.task_id}" @@ -374,7 +375,7 @@ def _post_process_booklore( return None except Exception as e: logger.error_trace("Task %s: unexpected error uploading to Booklore: %s", task.task_id, e) - status_callback("error", f"Booklore upload failed: {e}") + status_callback("error", f"{BOOKLORE_DISPLAY_NAME} upload failed: {e}") return None finally: cleanup_output_staging( diff --git a/src/frontend/src/components/settings/users/UserOverridesSection.tsx b/src/frontend/src/components/settings/users/UserOverridesSection.tsx index 89ec5d4d..f3c734ba 100644 --- a/src/frontend/src/components/settings/users/UserOverridesSection.tsx +++ b/src/frontend/src/components/settings/users/UserOverridesSection.tsx @@ -20,7 +20,7 @@ interface UserOverridesSectionProps { const modeOptions = [ { value: 'folder', label: 'Folder' }, { value: 'email', label: 'Email (SMTP)' }, - { value: 'booklore', label: 'BookLore (API)' }, + { value: 'booklore', label: 'Grimmory (API)' }, ]; const fallbackOutputModeField: SelectFieldConfig = { @@ -53,7 +53,7 @@ const fallbackBookloreLibraryField: SelectFieldConfig = { type: 'SelectField', key: 'BOOKLORE_LIBRARY_ID', label: 'Library', - description: 'BookLore library to upload into.', + description: 'Grimmory library to upload into.', value: '', options: [], }; @@ -62,7 +62,7 @@ const fallbackBooklorePathField: SelectFieldConfig = { type: 'SelectField', key: 'BOOKLORE_PATH_ID', label: 'Path', - description: 'BookLore library path for uploads.', + description: 'Grimmory library path for uploads.', value: '', options: [], filterByField: 'BOOKLORE_LIBRARY_ID', diff --git a/src/frontend/src/hooks/useSettings.ts b/src/frontend/src/hooks/useSettings.ts index 2661640b..042feb3b 100644 --- a/src/frontend/src/hooks/useSettings.ts +++ b/src/frontend/src/hooks/useSettings.ts @@ -257,7 +257,7 @@ export function useSettings(): UseSettingsReturn { const result = await executeSettingsAction(tabName, actionKey, currentValues); // Re-fetch settings after successful action to pick up updated options - // (e.g., BookLore "Test Connection" refreshes library/path lists) + // (e.g., Grimmory "Test Connection" refreshes library/path lists) if (result.success) { await fetchSettings({ silent: true, preserveDirtyValues: true }); } diff --git a/tests/config/test_download_settings.py b/tests/config/test_download_settings.py index 34558562..51f3d384 100644 --- a/tests/config/test_download_settings.py +++ b/tests/config/test_download_settings.py @@ -59,6 +59,23 @@ def test_download_settings_booklore_destination_field_defaults_to_library(): assert option_values == {"library", "bookdrop"} +def test_download_settings_grimmory_copy_is_exposed_in_ui_metadata(): + from shelfmark.config.settings import download_settings + + fields = download_settings() + + output_mode_field = next(field for field in fields if getattr(field, "key", None) == "BOOKS_OUTPUT_MODE") + grimmory_option = next(option for option in output_mode_field.options if option["value"] == "booklore") + heading_field = next(field for field in fields if getattr(field, "key", None) == "booklore_heading") + url_field = next(field for field in fields if getattr(field, "key", None) == "BOOKLORE_HOST") + + assert grimmory_option["label"] == "Grimmory (API)" + assert grimmory_option["description"] == "Upload files directly to Grimmory" + assert heading_field.title == "Grimmory" + assert "(Formerly Booklore)" in heading_field.description + assert url_field.label == "Grimmory URL" + + def test_download_settings_booklore_library_and_path_depend_on_library_destination(): from shelfmark.config.settings import download_settings diff --git a/tests/core/test_processing_integration.py b/tests/core/test_processing_integration.py index fb826b2f..2c8bdf03 100644 --- a/tests/core/test_processing_integration.py +++ b/tests/core/test_processing_integration.py @@ -675,7 +675,7 @@ def test_booklore_mode_uploads_and_cleans_staging(tmp_path): assert uploaded_files assert not temp_file.exists() assert list(staging.iterdir()) == [] - assert any("Booklore" in (message or "") for _, message in statuses) + assert any("Grimmory" in (message or "") for _, message in statuses) def test_booklore_mode_rejects_unsupported_files(tmp_path): @@ -723,7 +723,7 @@ def test_booklore_mode_rejects_unsupported_files(tmp_path): errors = [call for call in status_cb.call_args_list if call.args[0] == "error"] assert errors - assert "Booklore does not support" in errors[-1].args[1] + assert "Grimmory does not support" in errors[-1].args[1] @pytest.mark.parametrize("organization", ["none", "rename", "organize"])