mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-11-14 17:18:26 +00:00
This is the biggest of the docs batch, and this time I don't think it was my PRs that caused it. The plugin guides fell behind as the plugin API changed over several releases, and nobody updated them along the way. Same arrangement as the others: Claude compared the guides to the code and wrote the changes, so another human look before this merges would be good. Someone who knows the plugin API well would catch things I would not. These are the parts I would read first: the search plan table, the `SourceUnavailableError` advice in the error handling section, and the `locating` status row. What was wrong and is now fixed, all checked against the code: Release sources guide - `search` takes `(book, plan, *, expand_search, content_type)`. Every sample had the old one-argument signature, so a source written from the guide raised a TypeError on its first search. There is now a short table of the search plan fields. - A source is registered by adding its module to `_BUILTIN_SOURCE_MODULES`. The guide said to add an import at the bottom of `__init__.py`. - `get_column_config` is an instance method, not a classmethod, and `ColumnRenderType.SEEDERS` does not exist (the real members are listed). - `download` can return a `HandoffResult`. `list_available_sources` lists every source with an `enabled` flag, it does not filter. - `group="sources"` is gone, since only `direct_download` and `metadata_providers` exist. - Field lists for `Release`, `DownloadTask`, `BookMetadata`, `ColumnSchema` and `ReleaseColumnConfig` are brought up to date, with the protocol enum and the `locating` status. - The optional hooks that were not mentioned (`post_process_cleanup`, `build_retry_resolution_fields`, `list_authors`, `seed_complaints`, `get_record`, `get_destination_override`, `search_results_are_releases`) are documented. Plugin settings guide - The examples imported from `shelfmark.metadata_providers.base` and `shelfmark.release_sources.base`, which do not exist. The metadata example also skipped the abstract `search_by_isbn` and the kwargs factory. - Value priority now starts with the environment variable, and `search_mode` is listed next to `general` as using `settings.json`. The `metadata_providers` group order is 12, not 50. - Documented `TagListField`, `OrderableListField`, `TableField`, `env_var`, `env_supported`, `user_overridable`, `universal_only`, `filter_by_field`, `register_on_save` and the `current_values` argument for action callbacks. Also in here: `docs/dev/index.md` was a TODO stub and is now a short index linked from `docs/index.md`, a dead link in the metadata providers README is fixed, and the README table gains Databáze knih and Moly.hu. To check the samples were not just plausible, I ran the quick start and the full BookNet example against the real registry. Both register, instantiate and return their column config. The old BookNet example failed on import with a NameError, because the Test Connection callback was defined below the settings function that references it. The decorator calls that function straight away, so both settings snippets now define the callback first.
764 lines
22 KiB
Markdown
764 lines
22 KiB
Markdown
# Plugin Settings Integration Guide
|
|
|
|
This guide explains how to add configuration settings to plugins (Metadata Providers and Release Sources) so they appear in the Settings UI.
|
|
|
|
## Overview
|
|
|
|
The settings system uses a decorator-based registration pattern. Plugins register their settings when their module is imported, and the frontend dynamically renders the appropriate UI based on the schema provided by the backend.
|
|
|
|
**Key features:**
|
|
- Settings are defined in Python and automatically rendered in the React frontend
|
|
- Values persist across container restarts via JSON config files
|
|
- Changes take effect immediately without restart (unless marked otherwise)
|
|
|
|
## Quick Start
|
|
|
|
Add settings to your plugin in 3 steps:
|
|
|
|
```python
|
|
from shelfmark.core.settings_registry import (
|
|
register_settings,
|
|
TextField,
|
|
PasswordField,
|
|
ActionButton,
|
|
)
|
|
|
|
|
|
@register_settings(
|
|
name="my_plugin", # Unique identifier
|
|
display_name="My Plugin", # Shown in sidebar
|
|
icon="wrench", # Icon name
|
|
order=100, # Sort order (lower = higher in list)
|
|
group="metadata_providers", # Optional: group in sidebar
|
|
)
|
|
def my_plugin_settings():
|
|
return [
|
|
PasswordField(
|
|
key="MY_PLUGIN_API_KEY",
|
|
label="API Key",
|
|
description="Your API key from the provider",
|
|
required=True,
|
|
),
|
|
ActionButton(
|
|
key="test_connection",
|
|
label="Test Connection",
|
|
style="primary",
|
|
callback=_test_connection,
|
|
),
|
|
]
|
|
|
|
|
|
def _test_connection():
|
|
# Perform connection test
|
|
return {"success": True, "message": "Connected successfully!"}
|
|
```
|
|
|
|
## Available Field Types
|
|
|
|
### TextField
|
|
|
|
Single-line text input for strings.
|
|
|
|
```python
|
|
TextField(
|
|
key="MY_SETTING", # Config key
|
|
label="Setting Name", # Display label
|
|
description="Help text", # Optional description below field
|
|
default="", # Default value
|
|
placeholder="Enter value", # Placeholder text
|
|
max_length=100, # Optional max characters
|
|
required=False, # Is this field required?
|
|
requires_restart=False, # Does changing this need a restart?
|
|
show_when=None, # Conditional visibility (see below)
|
|
disabled_when=None, # Conditional disable (see below)
|
|
)
|
|
```
|
|
|
|
### PasswordField
|
|
|
|
Masked input for sensitive values (API keys, passwords). Values are never echoed back to the frontend.
|
|
|
|
```python
|
|
PasswordField(
|
|
key="API_KEY",
|
|
label="API Key",
|
|
description="Your secret API key",
|
|
placeholder="sk-...",
|
|
required=True,
|
|
)
|
|
```
|
|
|
|
### NumberField
|
|
|
|
Numeric input with optional min/max constraints.
|
|
|
|
```python
|
|
NumberField(
|
|
key="TIMEOUT",
|
|
label="Timeout (seconds)",
|
|
description="Connection timeout in seconds",
|
|
default=30,
|
|
min_value=5,
|
|
max_value=300,
|
|
step=1, # Increment step
|
|
required=False,
|
|
)
|
|
```
|
|
|
|
### CheckboxField
|
|
|
|
Toggle switch for boolean values.
|
|
|
|
```python
|
|
CheckboxField(
|
|
key="ENABLE_FEATURE",
|
|
label="Enable Feature",
|
|
description="Turn this feature on or off",
|
|
default=False,
|
|
)
|
|
```
|
|
|
|
### SelectField
|
|
|
|
Dropdown for single-choice selection.
|
|
|
|
```python
|
|
SelectField(
|
|
key="LOG_LEVEL",
|
|
label="Log Level",
|
|
description="Logging verbosity",
|
|
default="info",
|
|
options=[
|
|
{"value": "debug", "label": "Debug"},
|
|
{"value": "info", "label": "Info"},
|
|
{"value": "warning", "label": "Warning"},
|
|
{"value": "error", "label": "Error"},
|
|
],
|
|
)
|
|
```
|
|
|
|
`SelectField` also takes `filter_by_field`, the key of another field whose value filters this field's options through each option's `childOf` property. `options` can be a list or a function that returns one, evaluated when the schema is built.
|
|
|
|
### MultiSelectField
|
|
|
|
Multi-choice selection from a list of options. `variant` is `"pills"` (default) or `"dropdown"` for a checkbox dropdown.
|
|
|
|
```python
|
|
MultiSelectField(
|
|
key="SUPPORTED_FORMATS",
|
|
label="Supported Formats",
|
|
description="Select which formats to support",
|
|
default=["epub", "mobi"],
|
|
options=[
|
|
{"value": "epub", "label": "EPUB"},
|
|
{"value": "mobi", "label": "MOBI"},
|
|
{"value": "pdf", "label": "PDF"},
|
|
{"value": "azw3", "label": "AZW3"},
|
|
],
|
|
)
|
|
```
|
|
|
|
### TagListField
|
|
|
|
Editable list of free-form strings, shown as tags. The value is a list of strings.
|
|
|
|
```python
|
|
TagListField(
|
|
key="MY_PLUGIN_MIRROR_URLS",
|
|
label="Mirror URLs",
|
|
description="Type a URL and press Enter to add it",
|
|
placeholder="https://mirror.example",
|
|
normalize_urls=True, # default: entries are cleaned up as URLs
|
|
)
|
|
```
|
|
|
|
### OrderableListField
|
|
|
|
A list of options that can be toggled on and off and reordered. The value is a list of `{"id": ..., "enabled": ...}` objects in priority order.
|
|
|
|
```python
|
|
OrderableListField(
|
|
key="MY_PLUGIN_SOURCE_PRIORITY",
|
|
label="Source priority",
|
|
options=[
|
|
{"id": "a", "label": "Source A"},
|
|
{"id": "b", "label": "Source B", "isLocked": True, "disabledReason": "Needs an API key"},
|
|
],
|
|
default=[{"id": "a", "enabled": True}, {"id": "b", "enabled": False}],
|
|
)
|
|
```
|
|
|
|
An option can set `isLocked` (the toggle is disabled), `isPinned` (it cannot be reordered), `description` and `section`.
|
|
|
|
### TableField
|
|
|
|
Editable table of structured rows. The value is a list of objects, one per row.
|
|
|
|
```python
|
|
TableField(
|
|
key="MY_PLUGIN_RULES",
|
|
label="Rules",
|
|
columns=[
|
|
{"key": "name", "label": "Name", "type": "text", "placeholder": "Rule name"},
|
|
{"key": "mode", "label": "Mode", "type": "select", "options": [...]},
|
|
],
|
|
add_label="Add rule",
|
|
empty_message="No rules yet",
|
|
default=[],
|
|
)
|
|
```
|
|
|
|
### ActionButton
|
|
|
|
Button that executes a callback function. Does not store a value.
|
|
|
|
```python
|
|
ActionButton(
|
|
key="test_connection", # Unique key for the action
|
|
label="Test Connection", # Button text
|
|
description="Test the API connection",
|
|
style="primary", # "default", "primary", or "danger"
|
|
callback=my_callback_fn, # Function to execute
|
|
)
|
|
|
|
|
|
def my_callback_fn():
|
|
"""Callback must return dict with 'success' and 'message' keys."""
|
|
try:
|
|
# Perform action
|
|
return {"success": True, "message": "Connection successful!"}
|
|
except Exception as e:
|
|
return {"success": False, "message": f"Failed: {str(e)}"}
|
|
```
|
|
|
|
A callback that declares a `current_values` parameter receives the form's current, unsaved values as a dict, so a "Test connection" button can test what the user just typed instead of what was last saved:
|
|
|
|
```python
|
|
def my_callback_fn(current_values: dict):
|
|
api_key = current_values.get("MY_PLUGIN_API_KEY", "")
|
|
...
|
|
```
|
|
|
|
The decorator calls your settings function as soon as the module is imported, so define the callback above `@register_settings`, not below it.
|
|
|
|
### HeadingField
|
|
|
|
Display-only section heading with optional link. Does not store a value.
|
|
|
|
```python
|
|
HeadingField(
|
|
key="section_heading", # Unique key
|
|
title="Configuration", # Heading text
|
|
description="Configure the plugin settings below",
|
|
link_url="https://example.com/docs", # Optional link
|
|
link_text="View Documentation", # Link text
|
|
)
|
|
```
|
|
|
|
### CustomComponentField
|
|
|
|
Render a frontend-registered custom settings component while still using the
|
|
decorator-based schema.
|
|
|
|
```python
|
|
from shelfmark.core.settings_registry import CustomComponentField
|
|
|
|
CustomComponentField(
|
|
key="request_policy_editor",
|
|
component="request_policy_grid", # frontend registry key
|
|
label="Request Policy Rules",
|
|
description="Custom editor for policy defaults and matrix rules.",
|
|
value_fields=[
|
|
SelectField(
|
|
key="REQUEST_POLICY_DEFAULT_EBOOK", label="Default Ebook Mode", default="download"
|
|
),
|
|
SelectField(
|
|
key="REQUEST_POLICY_DEFAULT_AUDIOBOOK",
|
|
label="Default Audiobook Mode",
|
|
default="download",
|
|
),
|
|
TableField(key="REQUEST_POLICY_RULES", label="Rules", columns=_rule_columns, default=[]),
|
|
],
|
|
wrap_in_field_wrapper=True, # use standard FieldWrapper label/description layout
|
|
)
|
|
```
|
|
|
|
When `value_fields` is provided, those backing fields are included in
|
|
serialization/save/validation automatically and are hidden from the default renderer.
|
|
|
|
## Common Field Properties
|
|
|
|
All field types support these common properties:
|
|
|
|
| Property | Type | Default | Description |
|
|
|----------|------|---------|-------------|
|
|
| `key` | `str` | Required | Unique identifier for this setting |
|
|
| `label` | `str` | Required | Display label in the UI |
|
|
| `description` | `str` | `""` | Help text shown below the field |
|
|
| `default` | `Any` | `None` | Default value if not set |
|
|
| `required` | `bool` | `False` | Whether the field must have a value |
|
|
| `env_var` | `str` | `None` | Environment variable name to use instead of `key` |
|
|
| `env_supported` | `bool` | `True` | Set `False` for a UI-only setting that cannot come from an environment variable |
|
|
| `user_overridable` | `bool` | `False` | Let an admin set a per-user override of this field |
|
|
| `universal_only` | `bool` | `False` | Only show the field in Universal search mode |
|
|
| `disabled` | `bool` | `False` | Disable the field (greyed out) |
|
|
| `disabled_reason` | `str` | `""` | Explanation shown when disabled |
|
|
| `requires_restart` | `bool` | `False` | Whether changes require container restart |
|
|
| `show_when` | `dict` | `None` | Conditional visibility (see below) |
|
|
| `disabled_when` | `dict` | `None` | Conditional disable (see below) |
|
|
| `hidden_in_ui` | `bool` | `False` | Hide from default renderer but keep in schema/save path |
|
|
|
|
## Conditional Visibility
|
|
|
|
Fields can be shown/hidden based on other field values using `show_when`:
|
|
|
|
```python
|
|
# Only show DNS servers field when custom DNS is selected
|
|
TextField(
|
|
key="CUSTOM_DNS_SERVERS",
|
|
label="DNS Servers",
|
|
description="Comma-separated DNS server IPs",
|
|
show_when={"field": "DNS_PROVIDER", "value": "manual"},
|
|
)
|
|
```
|
|
|
|
The field will only be visible when the referenced field has the specified value.
|
|
|
|
## Conditional Disable
|
|
|
|
Fields can be enabled/disabled based on other field values using `disabled_when`:
|
|
|
|
```python
|
|
# Disable timeout field when feature is disabled
|
|
NumberField(
|
|
key="FEATURE_TIMEOUT",
|
|
label="Timeout (seconds)",
|
|
description="Request timeout",
|
|
default=30,
|
|
disabled_when={
|
|
"field": "FEATURE_ENABLED",
|
|
"value": False,
|
|
"reason": "Enable the feature first",
|
|
},
|
|
)
|
|
```
|
|
|
|
The field will be greyed out with the specified reason when the condition is met.
|
|
|
|
## Settings Groups
|
|
|
|
Register a group to organize related settings tabs in the sidebar:
|
|
|
|
```python
|
|
from shelfmark.core.settings_registry import register_group
|
|
|
|
# Register a group (do this once, usually in a central config file)
|
|
register_group(
|
|
name="my_group",
|
|
display_name="My Group",
|
|
icon="folder",
|
|
order=50,
|
|
)
|
|
|
|
|
|
# Then register settings to the group
|
|
@register_settings(
|
|
name="plugin_a",
|
|
display_name="Plugin A",
|
|
icon="puzzle",
|
|
order=51,
|
|
group="my_group", # Assigns to the group
|
|
)
|
|
def plugin_a_settings():
|
|
return [...]
|
|
```
|
|
|
|
**Existing groups:**
|
|
- `direct_download` (order=20): For download-related settings
|
|
- `metadata_providers` (order=12): For metadata provider plugins
|
|
|
|
## Value Resolution Priority
|
|
|
|
Settings values are resolved in this order (highest priority first):
|
|
|
|
1. **Environment variable** - The field's key, or `env_var` when set. Skipped for a field with `env_supported=False`. A value that comes from an environment variable is shown locked in the UI
|
|
2. **Config File** - Stored in `CONFIG_DIR/plugins/<tab_name>.json`
|
|
3. **Field Default** - Value specified in the field definition
|
|
|
|
The `general` and `search_mode` tabs use `CONFIG_DIR/settings.json` instead of the plugins subdirectory.
|
|
|
|
## Validating or Transforming on Save
|
|
|
|
`register_on_save(tab_name, handler)` runs a function each time the tab is saved. The handler receives a copy of the values being saved and returns a dict. Return `{"error": True, "message": "..."}` to refuse the save and show the message, or `{"values": {...}}` to save changed values.
|
|
|
|
```python
|
|
from shelfmark.core.settings_registry import register_on_save
|
|
|
|
|
|
def _validate_my_plugin(values: dict) -> dict:
|
|
if values.get("MY_PLUGIN_TIMEOUT", 30) < 5:
|
|
return {"error": True, "message": "Timeout must be at least 5 seconds"}
|
|
return {"values": values}
|
|
|
|
|
|
register_on_save("my_plugin", _validate_my_plugin)
|
|
```
|
|
|
|
## Reading Setting Values
|
|
|
|
Use the `config` singleton to read setting values in your plugin code:
|
|
|
|
```python
|
|
from shelfmark.core.config import config
|
|
|
|
# Get a setting value with default fallback
|
|
api_key = config.get("MY_PLUGIN_API_KEY", "")
|
|
timeout = config.get("MY_PLUGIN_TIMEOUT", 30)
|
|
|
|
# Or access as attributes (raises AttributeError if not found)
|
|
api_key = config.MY_PLUGIN_API_KEY
|
|
|
|
# Check all cached settings
|
|
all_settings = config.get_all()
|
|
```
|
|
|
|
The config singleton:
|
|
- Resolves values in the order above, with the field default as the last fallback
|
|
- Caches values for performance
|
|
- Refreshes automatically when settings are updated via the UI
|
|
|
|
## Complete Example: Metadata Provider
|
|
|
|
Here's a complete example for a metadata provider plugin:
|
|
|
|
```python
|
|
# shelfmark/metadata_providers/my_provider.py
|
|
|
|
from shelfmark.metadata_providers import (
|
|
BookMetadata,
|
|
MetadataProvider,
|
|
MetadataSearchOptions,
|
|
register_provider,
|
|
register_provider_kwargs,
|
|
)
|
|
from shelfmark.core.settings_registry import (
|
|
register_settings,
|
|
HeadingField,
|
|
TextField,
|
|
PasswordField,
|
|
CheckboxField,
|
|
ActionButton,
|
|
)
|
|
from shelfmark.core.config import config
|
|
|
|
|
|
def _test_connection():
|
|
"""Test API connection callback."""
|
|
api_key = config.get("MY_PROVIDER_API_KEY", "")
|
|
if not api_key:
|
|
return {"success": False, "message": "API key not configured"}
|
|
|
|
try:
|
|
# Perform actual connection test
|
|
# response = requests.get(...)
|
|
return {"success": True, "message": "Connected to My Provider API"}
|
|
except Exception as e:
|
|
return {"success": False, "message": f"Connection failed: {str(e)}"}
|
|
|
|
|
|
@register_settings(
|
|
name="my_provider",
|
|
display_name="My Provider",
|
|
icon="book",
|
|
order=53,
|
|
group="metadata_providers",
|
|
)
|
|
def my_provider_settings():
|
|
"""Define settings for this metadata provider."""
|
|
return [
|
|
HeadingField(
|
|
key="my_provider_heading",
|
|
title="My Provider",
|
|
description="A metadata provider for book information",
|
|
link_url="https://myprovider.com",
|
|
link_text="Visit My Provider",
|
|
),
|
|
PasswordField(
|
|
key="MY_PROVIDER_API_KEY",
|
|
label="API Key",
|
|
description="Your My Provider API key",
|
|
placeholder="Enter your API key",
|
|
required=True,
|
|
),
|
|
CheckboxField(
|
|
key="MY_PROVIDER_INCLUDE_COVERS",
|
|
label="Include Cover Images",
|
|
description="Fetch cover images when searching",
|
|
default=True,
|
|
),
|
|
TextField(
|
|
key="MY_PROVIDER_BASE_URL",
|
|
label="API Base URL",
|
|
description="Override the default API endpoint",
|
|
default="https://api.myprovider.com/v1",
|
|
required=False,
|
|
),
|
|
ActionButton(
|
|
key="test_connection",
|
|
label="Test Connection",
|
|
description="Verify your API key works",
|
|
style="primary",
|
|
callback=_test_connection,
|
|
),
|
|
]
|
|
|
|
|
|
@register_provider_kwargs("my_provider")
|
|
def _my_provider_kwargs() -> dict:
|
|
"""Keyword arguments for MyProvider(...), read from settings each time one is created."""
|
|
return {"api_key": config.get("MY_PROVIDER_API_KEY", "")}
|
|
|
|
|
|
@register_provider("my_provider")
|
|
class MyProvider(MetadataProvider):
|
|
"""My Provider metadata implementation."""
|
|
|
|
name = "my_provider"
|
|
display_name = "My Provider"
|
|
requires_auth = True
|
|
|
|
def __init__(self, api_key: str = ""):
|
|
self.api_key = api_key
|
|
self.base_url = config.get("MY_PROVIDER_BASE_URL", "https://api.myprovider.com/v1")
|
|
|
|
def is_available(self) -> bool:
|
|
return bool(self.api_key)
|
|
|
|
def search(self, options: MetadataSearchOptions) -> list[BookMetadata]:
|
|
# Implementation...
|
|
return []
|
|
|
|
def get_book(self, book_id: str) -> BookMetadata | None:
|
|
# Implementation...
|
|
return None
|
|
|
|
def search_by_isbn(self, isbn: str) -> BookMetadata | None:
|
|
# Implementation...
|
|
return None
|
|
```
|
|
|
|
All four methods above are abstract, so a provider that leaves one out cannot be instantiated. The provider module also has to be imported once so its decorators run: add it to the list of imports at the bottom of `shelfmark/metadata_providers/__init__.py`.
|
|
|
|
## Complete Example: Release Source
|
|
|
|
Here's a complete example for a release source plugin:
|
|
|
|
```python
|
|
# shelfmark/release_sources/my_source.py
|
|
|
|
from shelfmark.release_sources import (
|
|
DownloadHandler,
|
|
ReleaseSource,
|
|
register_handler,
|
|
register_source,
|
|
)
|
|
from shelfmark.core.settings_registry import (
|
|
register_settings,
|
|
HeadingField,
|
|
TextField,
|
|
NumberField,
|
|
CheckboxField,
|
|
SelectField,
|
|
ActionButton,
|
|
)
|
|
from shelfmark.core.config import config
|
|
|
|
|
|
def _test_source():
|
|
"""Test source availability callback."""
|
|
base_url = config.get("MY_SOURCE_URL", "https://mysource.com")
|
|
try:
|
|
# Test connectivity
|
|
return {"success": True, "message": f"Source available at {base_url}"}
|
|
except Exception as e:
|
|
return {"success": False, "message": f"Source unavailable: {str(e)}"}
|
|
|
|
|
|
@register_settings(
|
|
name="my_source",
|
|
display_name="My Source",
|
|
icon="download",
|
|
order=25,
|
|
group="direct_download",
|
|
)
|
|
def my_source_settings():
|
|
"""Define settings for this release source."""
|
|
return [
|
|
HeadingField(
|
|
key="my_source_heading",
|
|
title="My Source Configuration",
|
|
description="Configure the My Source download provider",
|
|
),
|
|
CheckboxField(
|
|
key="MY_SOURCE_ENABLED",
|
|
label="Enable My Source",
|
|
description="Include My Source in download fallback chain",
|
|
default=True,
|
|
),
|
|
TextField(
|
|
key="MY_SOURCE_URL",
|
|
label="Source URL",
|
|
description="Base URL for the source",
|
|
default="https://mysource.com",
|
|
show_when={"field": "MY_SOURCE_ENABLED", "value": True},
|
|
),
|
|
NumberField(
|
|
key="MY_SOURCE_TIMEOUT",
|
|
label="Timeout (seconds)",
|
|
description="Request timeout",
|
|
default=30,
|
|
min_value=10,
|
|
max_value=120,
|
|
show_when={"field": "MY_SOURCE_ENABLED", "value": True},
|
|
),
|
|
SelectField(
|
|
key="MY_SOURCE_PRIORITY",
|
|
label="Priority",
|
|
description="Where in the fallback chain to try this source",
|
|
default="normal",
|
|
options=[
|
|
{"value": "high", "label": "High (try first)"},
|
|
{"value": "normal", "label": "Normal"},
|
|
{"value": "low", "label": "Low (try last)"},
|
|
],
|
|
show_when={"field": "MY_SOURCE_ENABLED", "value": True},
|
|
),
|
|
ActionButton(
|
|
key="test_source",
|
|
label="Test Source",
|
|
description="Check if the source is accessible",
|
|
style="primary",
|
|
callback=_test_source,
|
|
),
|
|
]
|
|
|
|
|
|
@register_source("my_source")
|
|
class MySource(ReleaseSource):
|
|
"""My Source release source implementation."""
|
|
|
|
name = "my_source"
|
|
display_name = "My Source"
|
|
|
|
def __init__(self):
|
|
self.enabled = config.get("MY_SOURCE_ENABLED", True)
|
|
self.base_url = config.get("MY_SOURCE_URL", "https://mysource.com")
|
|
self.timeout = config.get("MY_SOURCE_TIMEOUT", 30)
|
|
|
|
def is_available(self) -> bool:
|
|
return self.enabled
|
|
|
|
def search(self, book, plan, *, expand_search=False, content_type="ebook"):
|
|
# Implementation...
|
|
return []
|
|
|
|
|
|
@register_handler("my_source")
|
|
class MySourceHandler(DownloadHandler):
|
|
"""Handler for downloading from My Source."""
|
|
|
|
def download(self, task, cancel_flag, progress_callback, status_callback):
|
|
# Implementation...
|
|
return None
|
|
|
|
def cancel(self, task_id):
|
|
return False
|
|
```
|
|
|
|
The full source and handler interfaces, and how to register the module, are in the [Release Sources Plugin Guide](release-sources-plugin-guide.md).
|
|
|
|
## Best Practices
|
|
|
|
1. **Use descriptive keys**: Keys should be uppercase and prefixed with your plugin name (e.g., `MY_PLUGIN_API_KEY`)
|
|
|
|
2. **Provide helpful descriptions**: Include enough detail in descriptions to help users understand what each setting does
|
|
|
|
3. **Set sensible defaults**: Users should be able to get started without configuring everything
|
|
|
|
4. **Use conditional visibility**: Hide advanced options behind enabling checkboxes to reduce UI clutter
|
|
|
|
5. **Include a test button**: ActionButtons that test connections help users verify their configuration
|
|
|
|
6. **Mark restart-required settings**: Use `requires_restart=True` for settings that can't be applied live
|
|
|
|
7. **Group related settings**: Use HeadingField to visually separate sections, and put plugins in appropriate groups
|
|
|
|
8. **Handle missing values gracefully**: Always provide fallbacks when reading settings in your code
|
|
|
|
## API Reference
|
|
|
|
### Backend Routes
|
|
|
|
| Method | Endpoint | Description |
|
|
|--------|----------|-------------|
|
|
| GET | `/api/settings` | Get all settings tabs, groups, and values |
|
|
| GET | `/api/settings/<tab_name>` | Get a specific settings tab |
|
|
| PUT | `/api/settings/<tab_name>` | Update settings for a tab |
|
|
| POST | `/api/settings/<tab_name>/action/<action_key>` | Execute an action button callback |
|
|
|
|
### Response Format
|
|
|
|
**GET /api/settings**
|
|
```json
|
|
{
|
|
"groups": [
|
|
{"name": "direct_download", "displayName": "Direct Download", "icon": "download", "order": 20}
|
|
],
|
|
"tabs": [
|
|
{
|
|
"name": "my_plugin",
|
|
"displayName": "My Plugin",
|
|
"icon": "book",
|
|
"order": 53,
|
|
"group": "metadata_providers",
|
|
"fields": [
|
|
{
|
|
"type": "password",
|
|
"key": "MY_PLUGIN_API_KEY",
|
|
"label": "API Key",
|
|
"description": "Your API key",
|
|
"hasValue": true,
|
|
"value": "",
|
|
"required": true,
|
|
"disabled": false,
|
|
"requiresRestart": false
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**PUT /api/settings/<tab_name>**
|
|
```json
|
|
// Request
|
|
{"MY_PLUGIN_API_KEY": "new-value", "MY_PLUGIN_TIMEOUT": 60}
|
|
|
|
// Response
|
|
{
|
|
"success": true,
|
|
"message": "Settings updated",
|
|
"updated": ["MY_PLUGIN_API_KEY", "MY_PLUGIN_TIMEOUT"],
|
|
"requiresRestart": false
|
|
}
|
|
```
|
|
|
|
**POST /api/settings/<tab_name>/action/<action_key>**
|
|
```json
|
|
// Response
|
|
{
|
|
"success": true,
|
|
"message": "Connection successful!"
|
|
}
|
|
```
|