Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ assignees: ''

## Steps To Reproduce
<!-- Steps to reproduce the behavior -->
1.
2.
3.
1.
2.
3.

## Expected Behavior
<!-- A clear and concise description of what you expected to happen -->
Expand All @@ -34,4 +34,4 @@ When running in debug mode, a DEBUG button will appear in the interface. Please
<!-- If applicable, please provide your full docker-compose (redacted from any secrets) -->

## Additional Context
<!-- Add any other context about the problem here -->
<!-- Add any other context about the problem here -->
2 changes: 1 addition & 1 deletion .github/workflows/build-and-publish-docker-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ jobs:
- name: Get current date
id: date
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT

- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

Expand Down
13 changes: 13 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
repos:
- repo: builtin
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-added-large-files

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.10
hooks:
- id: ruff-check
args: [--fix]
- id: ruff-format
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,4 @@
]
}
]
}
}
12 changes: 11 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help install install-python-dev dev build preview typecheck frontend-test clean up up down docker-build refresh restart build-serve python-lint python-lint-fix python-format python-format-check python-typecheck python-dead-code python-checks python-test-lint python-test-lint-fix python-test-format python-test-format-check python-test-typecheck python-test-checks
.PHONY: help install install-python-dev dev build preview typecheck frontend-test clean up up down docker-build refresh restart build-serve python-lint python-lint-fix python-format python-format-check python-typecheck python-dead-code python-checks python-test-lint python-test-lint-fix python-test-format python-test-format-check python-test-typecheck python-test-checks python-coverage prek-install

# Frontend directory
FRONTEND_DIR := src/frontend
Expand Down Expand Up @@ -32,6 +32,8 @@ help:
@echo " python-test-format-check - Check Python test formatting with Ruff"
@echo " python-test-typecheck - Run lightweight BasedPyright checks against Python tests"
@echo " python-test-checks - Run all relaxed Python test static analysis checks"
@echo " python-coverage - Run tests with coverage report"
@echo " prek-install - Install prek git hooks"
@echo " clean - Remove node_modules and build artifacts"
@echo ""
@echo "Backend (Docker):"
Expand Down Expand Up @@ -127,6 +129,14 @@ python-test-typecheck:

python-test-checks: python-test-lint python-test-format-check python-test-typecheck

python-coverage:
@echo "Running tests with coverage..."
uv run pytest tests/ -x --tb=short -m "not integration and not e2e" --cov --cov-report=term-missing

prek-install:
@echo "Installing prek git hooks..."
uv run prek install

# Run frontend unit tests
frontend-test:
@echo "Running frontend unit tests..."
Expand Down
2 changes: 1 addition & 1 deletion data/book-languages.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,4 @@
{ "language": "Uyghur", "code": "ug" },
{ "language": "Armenian", "code": "hy" },
{ "language": "Shan", "code": "shn" }
]
]
2 changes: 1 addition & 1 deletion docs/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ Directory where downloaded files are saved. Use {User} for per-user folders (e.g

**File Organization**

Choose how downloaded book files are named and organized.
Choose how downloaded book files are named and organized.

- **Type:** string (choice)
- **Default:** `rename`
Expand Down
2 changes: 1 addition & 1 deletion entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ if [ "$DEBUG" = "true" ] && [ "$USING_EXTERNAL_BYPASSER" != "true" ]; then
--enable-logging --v=1 --log-level=0 \
--log-file=/tmp/chrome_entrypoint_test.log \
--crash-dumps-dir=/tmp/chrome_crash_dumps \
< /dev/null
< /dev/null
EXIT_CODE=$?
echo "Chrome exit code: $EXIT_CODE"
ls -lh /tmp/chrome_entrypoint_test.log
Expand Down
1 change: 0 additions & 1 deletion genDebug.sh
Original file line number Diff line number Diff line change
Expand Up @@ -199,4 +199,3 @@ else
echo "Failed to create debug archive"
exit 1
fi

15 changes: 15 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ browser = [
[dependency-groups]
dev = [
"basedpyright>=1.39.0",
"prek",
"pytest",
"pytest-cov",
"pytest-xdist>=3.8.0",
"ruff==0.15.10",
"vulture>=2.14",
Expand Down Expand Up @@ -93,6 +95,11 @@ select = [
ignore = ["D", "EM", "FBT", "PLR2004", "UP035", "TRY003", "E501", "TD002", "S104", "S603"]

[tool.ruff.lint.per-file-ignores]
"scripts/**/*.py" = [
"BLE001",
"S",
"TRY",
]
"tests/**/*.py" = [
"ANN",
"BLE001",
Expand Down Expand Up @@ -153,5 +160,13 @@ ignore_decorators = [
min_confidence = 90
sort_by_size = true

[tool.coverage.run]
source = ["shelfmark"]
branch = true

[tool.coverage.report]
show_missing = true
skip_empty = true

[tool.uv]
package = false
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ That's it! Configure settings through the web interface as needed.
volumes:
- /your/config/path:/config # Config, database, and artwork cache directory
- /your/download/path:/books # Downloaded books
- /client/path:/client/path # Optional: For Torrent/Usenet downloads, match your client directory exactly.
- /client/path:/client/path # Optional: For Torrent/Usenet downloads, match your client directory exactly.
```

> **Tip**: Point the download volume to your CWA or Grimmory ingest folder for automatic import.
Expand Down
68 changes: 32 additions & 36 deletions scripts/generate_env_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,15 @@

import argparse
import sys
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional
from typing import Any

# Add project root to path
project_root = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(project_root))


def get_field_type_name(field) -> str:
def get_field_type_name(field: Any) -> str:
"""Get a human-readable type name for a field."""
from shelfmark.core.settings_registry import (
CheckboxField,
Expand All @@ -40,49 +39,47 @@ def get_field_type_name(field) -> str:

if isinstance(field, CheckboxField):
return "boolean"
elif isinstance(field, NumberField):
if isinstance(field, NumberField):
return "number"
elif isinstance(field, SelectField):
if isinstance(field, SelectField):
return "string (choice)"
elif isinstance(field, MultiSelectField):
if isinstance(field, MultiSelectField):
return "string (comma-separated)"
elif isinstance(field, OrderableListField):
if isinstance(field, OrderableListField):
return "JSON array"
elif isinstance(field, PasswordField):
if isinstance(field, PasswordField):
return "string (secret)"
elif isinstance(field, TextField):
return "string"
else:
if isinstance(field, TextField):
return "string"
return "string"


def format_default_value(field) -> str:
def format_default_value(field: Any) -> str:
"""Format the default value for display."""
default = field.default

if default is None:
return "_none_"
elif isinstance(default, bool):
if isinstance(default, bool):
return f"`{str(default).lower()}`"
elif isinstance(default, (int, float)):
if isinstance(default, (int, float)):
return f"`{default}`"
elif isinstance(default, str):
if isinstance(default, str):
if default == "":
return "_empty string_"
return f"`{default}`"
elif isinstance(default, list):
if isinstance(default, list):
if not default:
return "_empty list_"
# For simple lists, show comma-separated values
if all(isinstance(item, str) for item in default):
return f"`{','.join(default)}`"
# For complex lists (e.g., OrderableListField defaults), summarize
return f"_see UI for defaults_"
else:
return f"`{default}`"
return "_see UI for defaults_"
return f"`{default}`"


def get_select_options(field) -> Optional[List[str]]:
def get_select_options(field: Any) -> list[str] | None:
"""Get the available options for a SelectField.

Returns options formatted as 'value (label)' or just 'value' if they match,
Expand Down Expand Up @@ -119,7 +116,7 @@ def get_select_options(field) -> Optional[List[str]]:
return result


def _generate_bootstrap_env_docs() -> List[str]:
def _generate_bootstrap_env_docs() -> list[str]:
"""Generate documentation for bootstrap environment variables from env.py."""
# These are environment variables defined in env.py that are used before
# the settings registry is available
Expand Down Expand Up @@ -195,8 +192,10 @@ def _generate_bootstrap_env_docs() -> List[str]:
"|----------|-------------|------|---------|",
]

for var in bootstrap_vars:
lines.append(f"| `{var['name']}` | {var['description']} | {var['type']} | `{var['default']}` |")
lines.extend(
f"| `{var['name']}` | {var['description']} | {var['type']} | `{var['default']}` |"
for var in bootstrap_vars
)

lines.append("")
lines.append("<details>")
Expand All @@ -221,17 +220,14 @@ def _generate_bootstrap_env_docs() -> List[str]:
def generate_env_docs() -> str:
"""Generate markdown documentation for all environment variables."""
# Import settings modules to ensure all settings are registered
import shelfmark.config.settings # noqa: F401
import shelfmark.config.security # noqa: F401
import shelfmark.release_sources.irc.settings # noqa: F401
import shelfmark.config.security
import shelfmark.config.settings
import shelfmark.metadata_providers.googlebooks
import shelfmark.metadata_providers.hardcover
import shelfmark.metadata_providers.openlibrary
import shelfmark.release_sources.irc.settings
import shelfmark.release_sources.prowlarr.settings # noqa: F401
import shelfmark.metadata_providers.hardcover # noqa: F401
import shelfmark.metadata_providers.openlibrary # noqa: F401
import shelfmark.metadata_providers.googlebooks # noqa: F401

from shelfmark.core.settings_registry import (
ActionButton,
HeadingField,
get_all_groups,
get_all_settings_tabs,
)
Expand All @@ -240,7 +236,7 @@ def generate_env_docs() -> str:
groups = {g.name: g for g in get_all_groups()}

# Organize tabs by group
grouped_tabs: Dict[Optional[str], List] = {None: []}
grouped_tabs: dict[str | None, list] = {None: []}
for group_name in groups:
grouped_tabs[group_name] = []

Expand Down Expand Up @@ -309,7 +305,7 @@ def generate_env_docs() -> str:
return "\n".join(lines)


def _generate_tab_docs(tab, group_prefix: Optional[str] = None) -> List[str]:
def _generate_tab_docs(tab: Any, group_prefix: str | None = None) -> list[str]:
"""Generate documentation for a single settings tab."""
from shelfmark.core.settings_registry import ActionButton, CustomComponentField, HeadingField

Expand All @@ -318,7 +314,6 @@ def _generate_tab_docs(tab, group_prefix: Optional[str] = None) -> List[str]:
# Section header
if group_prefix:
lines.append(f"### {group_prefix}: {tab.display_name}")
anchor_id = f"{group_prefix}-{tab.display_name}".lower().replace(" ", "-")
else:
lines.append(f"## {tab.display_name}")

Expand Down Expand Up @@ -391,6 +386,7 @@ def _generate_tab_docs(tab, group_prefix: Optional[str] = None) -> List[str]:

# Show constraints for NumberField
from shelfmark.core.settings_registry import NumberField

if isinstance(field, NumberField):
constraints = []
if field.min_value is not None:
Expand All @@ -408,7 +404,7 @@ def _generate_tab_docs(tab, group_prefix: Optional[str] = None) -> List[str]:
return lines


def main():
def main() -> None:
parser = argparse.ArgumentParser(
description="Generate markdown documentation for environment variables"
)
Expand Down
Loading
Loading