Rename Booklore to Grimmory (#791)

This commit is contained in:
Alex
2026-03-21 15:38:38 +00:00
committed by GitHub
parent 8f949a73d5
commit 698eb07e71
7 changed files with 66 additions and 48 deletions
+5 -5
View File
@@ -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:
+12 -12
View File
@@ -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"},
+26 -25
View File
@@ -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(
@@ -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',
+1 -1
View File
@@ -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 });
}
+17
View File
@@ -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
+2 -2
View File
@@ -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"])