diff --git a/docs/environment-variables.md b/docs/environment-variables.md
index 71521ef..bd3a05b 100644
--- a/docs/environment-variables.md
+++ b/docs/environment-variables.md
@@ -14,6 +14,7 @@ This document lists all configuration options that can be set via environment va
- [Network](#network)
- [Advanced](#advanced)
- [Prowlarr](#prowlarr)
+- [Newznab](#newznab)
- [AudiobookBay](#audiobookbay)
- [IRC](#irc)
- [Download Clients](#download-clients)
@@ -124,6 +125,7 @@ Show the onboarding wizard on first run. Set to false to skip (useful for epheme
| Variable | Description | Type | Default |
|----------|-------------|------|---------|
+| `SEARCH_PAGE_TITLE` | Title shown above the main search box on the homepage. | string | `Shelfmark` |
| `CALIBRE_WEB_URL` | Adds a navigation button to your book library (Calibre-Web Automated, Grimmory, etc). | string | _none_ |
| `AUDIOBOOK_LIBRARY_URL` | Adds a separate navigation button for your audiobook library (Audiobookshelf, Plex, etc). When both URLs are set, icons are shown instead of text. | string | _none_ |
| `SUPPORTED_FORMATS` | Book formats to include in search results. ZIP/RAR archives are extracted automatically and book files are used if found. | string (comma-separated) | `epub,mobi,azw3,fb2,djvu,cbz,cbr` |
@@ -133,6 +135,15 @@ Show the onboarding wizard on first run. Set to false to skip (useful for epheme
Detailed descriptions
+#### `SEARCH_PAGE_TITLE`
+
+**Search Page Title**
+
+Title shown above the main search box on the homepage.
+
+- **Type:** string
+- **Default:** `Shelfmark`
+
#### `CALIBRE_WEB_URL`
**Library URL**
@@ -294,8 +305,8 @@ The release source tab to open by default in the release modal for audiobooks. U
| `BOOKS_OUTPUT_MODE` | Choose where completed book files are sent. | string (choice) | `folder` |
| `INGEST_DIR` | Directory where downloaded files are saved. Use {User} for per-user folders (e.g. /books/{User}). | string | `/books` |
| `FILE_ORGANIZATION` | Choose how downloaded book files are named and organized. | string (choice) | `rename` |
-| `TEMPLATE_RENAME` | Variables: {Author}, {Title}, {Year}, {User}, {OriginalName} (source filename without extension). Universal adds: {Series}, {SeriesPosition}, {Subtitle}. Use arbitrary prefix/suffix: {Vol. SeriesPosition - } outputs 'Vol. 2 - ' when set, nothing when empty. Rename templates are filename-only (no '/' or '\'); use Organize for folders. Applies to single-file downloads. | string | `{Author} - {Title} ({Year})` |
-| `TEMPLATE_ORGANIZE` | Use / to create folders. Variables: {Author}, {Title}, {Year}, {User}, {OriginalName} (source filename without extension). Universal adds: {Series}, {SeriesPosition}, {Subtitle}. Use arbitrary prefix/suffix: {Vol. SeriesPosition - } outputs 'Vol. 2 - ' when set, nothing when empty. | string | `{Author}/{Title} ({Year})` |
+| `TEMPLATE_RENAME` | Variables: {Author}, {Title}, {Year}, {User}, {OriginalName} (source filename without extension). Universal adds: {Series}, {SeriesPosition}, {Subtitle}, {PrimaryTitle}. Use arbitrary prefix/suffix: {Vol. SeriesPosition - } outputs 'Vol. 2 - ' when set, nothing when empty. Rename templates are filename-only (no '/' or '\'); use Organize for folders. Applies to single-file downloads. | string | `{Author} - {Title} ({Year})` |
+| `TEMPLATE_ORGANIZE` | Use / to create folders. Variables: {Author}, {Title}, {Year}, {User}, {OriginalName} (source filename without extension). Universal adds: {Series}, {SeriesPosition}, {Subtitle}, {PrimaryTitle}. Use arbitrary prefix/suffix: {Vol. SeriesPosition - } outputs 'Vol. 2 - ' when set, nothing when empty. | string | `{Author}/{Title} ({Year})` |
| `HARDLINK_TORRENTS` | Create hardlinks instead of copying. Preserves seeding but archives won't be extracted. Don't use if destination is a library ingest folder. | boolean | `false` |
| `BOOKLORE_HOST` | Base URL of your Grimmory instance | string | _none_ |
| `BOOKLORE_USERNAME` | Grimmory account username | string | _none_ |
@@ -311,13 +322,13 @@ The release source tab to open by default in the release modal for audiobooks. U
| `EMAIL_SMTP_USERNAME` | SMTP username (leave empty for no authentication). | string | _none_ |
| `EMAIL_SMTP_PASSWORD` | SMTP password (required if Username is set). | string (secret) | _none_ |
| `EMAIL_FROM` | From address used for the email. You can include a display name (e.g., Shelfmark ). Leave blank to default to the SMTP username (when it is an email address). | string | _none_ |
-| `EMAIL_SUBJECT_TEMPLATE` | Email subject. Variables: {Author}, {Title}, {Year}, {Series}, {SeriesPosition}, {Subtitle}, {Format}. | string | `{Title}` |
+| `EMAIL_SUBJECT_TEMPLATE` | Email subject. Variables: {Author}, {Title}, {PrimaryTitle}, {Year}, {Series}, {SeriesPosition}, {Subtitle}, {Format}. | string | `{Title}` |
| `EMAIL_SMTP_TIMEOUT_SECONDS` | How long to wait for SMTP operations before failing. | number | `60` |
| `EMAIL_ALLOW_UNVERIFIED_TLS` | Disable TLS certificate verification (not recommended). | boolean | `false` |
| `DESTINATION_AUDIOBOOK` | Directory where downloaded audiobook files are saved. Leave empty to use the Books destination. | string | _none_ |
| `FILE_ORGANIZATION_AUDIOBOOK` | Choose how downloaded audiobook files are named and organized. | string (choice) | `rename` |
-| `TEMPLATE_AUDIOBOOK_RENAME` | Variables: {Author}, {Title}, {Year}, {User}, {OriginalName} (source filename without extension), {Series}, {SeriesPosition}, {Subtitle}, {PartNumber}. Use arbitrary prefix/suffix: {Vol. SeriesPosition - } outputs 'Vol. 2 - ' when set, nothing when empty. Rename templates are filename-only (no '/' or '\'); use Organize for folders. Applies to single-file downloads. | string | `{Author} - {Title}` |
-| `TEMPLATE_AUDIOBOOK_ORGANIZE` | Use / to create folders. Variables: {Author}, {Title}, {Year}, {User}, {OriginalName} (source filename without extension), {Series}, {SeriesPosition}, {Subtitle}, {PartNumber}. Use arbitrary prefix/suffix: {Vol. SeriesPosition - } outputs 'Vol. 2 - ' when set, nothing when empty. | string | `{Author}/{Title}` |
+| `TEMPLATE_AUDIOBOOK_RENAME` | Variables: {Author}, {Title}, {Year}, {User}, {OriginalName} (source filename without extension), {Series}, {SeriesPosition}, {Subtitle}, {PrimaryTitle}, {PartNumber}. Use arbitrary prefix/suffix: {Vol. SeriesPosition - } outputs 'Vol. 2 - ' when set, nothing when empty. Rename templates are filename-only (no '/' or '\'); use Organize for folders. Applies to single-file downloads. | string | `{Author} - {Title}` |
+| `TEMPLATE_AUDIOBOOK_ORGANIZE` | Use / to create folders. Variables: {Author}, {Title}, {Year}, {User}, {OriginalName} (source filename without extension), {Series}, {SeriesPosition}, {Subtitle}, {PrimaryTitle}, {PartNumber}. Use arbitrary prefix/suffix: {Vol. SeriesPosition - } outputs 'Vol. 2 - ' when set, nothing when empty. | string | `{Author}/{Title}/{Title}` |
| `HARDLINK_TORRENTS_AUDIOBOOK` | Create hardlinks instead of copying. Preserves seeding but archives won't be extracted. Don't use if destination is a library ingest folder. | boolean | `true` |
| `AUTO_OPEN_DOWNLOADS_SIDEBAR` | Automatically open the downloads sidebar when a new download is queued. | boolean | `false` |
| `DOWNLOAD_TO_BROWSER_CONTENT_TYPES` | Automatically download completed files to your browser for the selected content types. | string (comma-separated) | _empty list_ |
@@ -361,7 +372,7 @@ Choose how downloaded book files are named and organized.
**Naming Template**
-Variables: {Author}, {Title}, {Year}, {User}, {OriginalName} (source filename without extension). Universal adds: {Series}, {SeriesPosition}, {Subtitle}. Use arbitrary prefix/suffix: {Vol. SeriesPosition - } outputs 'Vol. 2 - ' when set, nothing when empty. Rename templates are filename-only (no '/' or '\'); use Organize for folders. Applies to single-file downloads.
+Variables: {Author}, {Title}, {Year}, {User}, {OriginalName} (source filename without extension). Universal adds: {Series}, {SeriesPosition}, {Subtitle}, {PrimaryTitle}. Use arbitrary prefix/suffix: {Vol. SeriesPosition - } outputs 'Vol. 2 - ' when set, nothing when empty. Rename templates are filename-only (no '/' or '\'); use Organize for folders. Applies to single-file downloads.
- **Type:** string
- **Default:** `{Author} - {Title} ({Year})`
@@ -370,7 +381,7 @@ Variables: {Author}, {Title}, {Year}, {User}, {OriginalName} (source filename wi
**Path Template**
-Use / to create folders. Variables: {Author}, {Title}, {Year}, {User}, {OriginalName} (source filename without extension). Universal adds: {Series}, {SeriesPosition}, {Subtitle}. Use arbitrary prefix/suffix: {Vol. SeriesPosition - } outputs 'Vol. 2 - ' when set, nothing when empty.
+Use / to create folders. Variables: {Author}, {Title}, {Year}, {User}, {OriginalName} (source filename without extension). Universal adds: {Series}, {SeriesPosition}, {Subtitle}, {PrimaryTitle}. Use arbitrary prefix/suffix: {Vol. SeriesPosition - } outputs 'Vol. 2 - ' when set, nothing when empty.
- **Type:** string
- **Default:** `{Author}/{Title} ({Year})`
@@ -524,7 +535,7 @@ From address used for the email. You can include a display name (e.g., Shelfmark
**Subject Template**
-Email subject. Variables: {Author}, {Title}, {Year}, {Series}, {SeriesPosition}, {Subtitle}, {Format}.
+Email subject. Variables: {Author}, {Title}, {PrimaryTitle}, {Year}, {Series}, {SeriesPosition}, {Subtitle}, {Format}.
- **Type:** string
- **Default:** `{Title}`
@@ -571,7 +582,7 @@ Choose how downloaded audiobook files are named and organized.
**Naming Template**
-Variables: {Author}, {Title}, {Year}, {User}, {OriginalName} (source filename without extension), {Series}, {SeriesPosition}, {Subtitle}, {PartNumber}. Use arbitrary prefix/suffix: {Vol. SeriesPosition - } outputs 'Vol. 2 - ' when set, nothing when empty. Rename templates are filename-only (no '/' or '\'); use Organize for folders. Applies to single-file downloads.
+Variables: {Author}, {Title}, {Year}, {User}, {OriginalName} (source filename without extension), {Series}, {SeriesPosition}, {Subtitle}, {PrimaryTitle}, {PartNumber}. Use arbitrary prefix/suffix: {Vol. SeriesPosition - } outputs 'Vol. 2 - ' when set, nothing when empty. Rename templates are filename-only (no '/' or '\'); use Organize for folders. Applies to single-file downloads.
- **Type:** string
- **Default:** `{Author} - {Title}`
@@ -580,10 +591,10 @@ Variables: {Author}, {Title}, {Year}, {User}, {OriginalName} (source filename wi
**Path Template**
-Use / to create folders. Variables: {Author}, {Title}, {Year}, {User}, {OriginalName} (source filename without extension), {Series}, {SeriesPosition}, {Subtitle}, {PartNumber}. Use arbitrary prefix/suffix: {Vol. SeriesPosition - } outputs 'Vol. 2 - ' when set, nothing when empty.
+Use / to create folders. Variables: {Author}, {Title}, {Year}, {User}, {OriginalName} (source filename without extension), {Series}, {SeriesPosition}, {Subtitle}, {PrimaryTitle}, {PartNumber}. Use arbitrary prefix/suffix: {Vol. SeriesPosition - } outputs 'Vol. 2 - ' when set, nothing when empty.
- **Type:** string
-- **Default:** `{Author}/{Title}`
+- **Default:** `{Author}/{Title}/{Title}`
#### `HARDLINK_TORRENTS_AUDIOBOOK`
@@ -639,7 +650,7 @@ How long to keep completed/failed downloads in the queue display.
| Variable | Description | Type | Default |
|----------|-------------|------|---------|
-| `AUTH_METHOD` | Select the authentication method for accessing Shelfmark. | string (choice) | `none` |
+| `AUTH_METHOD` | Select the authentication method for accessing Shelfmark. Restart container after changing Calibre-Web passwords. | string (choice) | `none` |
| `PROXY_AUTH_USER_HEADER` | The HTTP header your proxy uses to pass the authenticated username. | string | `X-Auth-User` |
| `PROXY_AUTH_LOGOUT_URL` | The URL to redirect users to for logging out. Leave empty to disable logout functionality. | string | _empty string_ |
| `PROXY_AUTH_ADMIN_GROUP_HEADER` | Optional: header your proxy uses to pass user groups/roles. | string | `X-Auth-Groups` |
@@ -661,7 +672,7 @@ How long to keep completed/failed downloads in the queue display.
**Authentication Method**
-Select the authentication method for accessing Shelfmark.
+Select the authentication method for accessing Shelfmark. Restart container after changing Calibre-Web passwords.
- **Type:** string (choice)
- **Default:** `none`
@@ -1115,6 +1126,57 @@ Automatically retry search without category filtering if no results are found
+## Newznab
+
+| Variable | Description | Type | Default |
+|----------|-------------|------|---------|
+| `NEWZNAB_ENABLED` | Enable searching for books via a Newznab-compatible indexer | boolean | `false` |
+| `NEWZNAB_URL` | Base URL of your Newznab indexer or aggregator | string | _none_ |
+| `NEWZNAB_API_KEY` | Your Newznab API key (leave blank if not required) | string (secret) | _none_ |
+| `NEWZNAB_AUTO_EXPAND` | Automatically retry search without category filtering if no results are found | boolean | `false` |
+
+
+Detailed descriptions
+
+#### `NEWZNAB_ENABLED`
+
+**Enable Newznab source**
+
+Enable searching for books via a Newznab-compatible indexer
+
+- **Type:** boolean
+- **Default:** `false`
+
+#### `NEWZNAB_URL`
+
+**Newznab URL**
+
+Base URL of your Newznab indexer or aggregator
+
+- **Type:** string
+- **Default:** _none_
+- **Required:** Yes
+
+#### `NEWZNAB_API_KEY`
+
+**API Key**
+
+Your Newznab API key (leave blank if not required)
+
+- **Type:** string (secret)
+- **Default:** _none_
+
+#### `NEWZNAB_AUTO_EXPAND`
+
+**Auto-expand search on no results**
+
+Automatically retry search without category filtering if no results are found
+
+- **Type:** boolean
+- **Default:** `false`
+
+
+
## AudiobookBay
| Variable | Description | Type | Default |
diff --git a/scripts/generate_env_docs.py b/scripts/generate_env_docs.py
index d0e6aac..2907166 100755
--- a/scripts/generate_env_docs.py
+++ b/scripts/generate_env_docs.py
@@ -310,7 +310,7 @@ def generate_env_docs() -> 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
+ from shelfmark.core.settings_registry import iter_value_fields
lines = []
@@ -323,17 +323,9 @@ def _generate_tab_docs(tab: Any, group_prefix: str | None = None) -> list[str]:
lines.append("")
# Collect env-supported fields
- env_fields = []
- for field in tab.fields:
- # Skip non-value fields
- if isinstance(field, (ActionButton, CustomComponentField, HeadingField)):
- continue
-
- # Skip fields that don't support ENV vars
- if not getattr(field, "env_supported", True):
- continue
-
- env_fields.append(field)
+ env_fields = [
+ field for field in iter_value_fields(tab) if getattr(field, "env_supported", True)
+ ]
if not env_fields:
lines.append("_No environment variables for this section._")
diff --git a/shelfmark/config/settings.py b/shelfmark/config/settings.py
index 3ebd38e..18da163 100644
--- a/shelfmark/config/settings.py
+++ b/shelfmark/config/settings.py
@@ -938,7 +938,7 @@ def download_settings() -> list[SettingsField]:
SelectField(
key="FILE_ORGANIZATION",
label="File Organization",
- description="Choose how downloaded book files are named and organized. ",
+ description="Choose how downloaded book files are named and organized.",
options=[
{
"value": "none",
@@ -966,7 +966,14 @@ def download_settings() -> list[SettingsField]:
_naming_template_field(
key="TEMPLATE_RENAME",
label="Naming Template",
- description="Filename template for single-file book downloads.",
+ description=(
+ "Variables: {Author}, {Title}, {Year}, {User}, {OriginalName} "
+ "(source filename without extension). Universal adds: {Series}, "
+ "{SeriesPosition}, {Subtitle}, {PrimaryTitle}. Use arbitrary prefix/suffix: "
+ "{Vol. SeriesPosition - } outputs 'Vol. 2 - ' when set, nothing when empty. "
+ "Rename templates are filename-only (no '/' or '\\'); use Organize for folders. "
+ "Applies to single-file downloads."
+ ),
default="{Author} - {Title} ({Year})",
placeholder="{Author} - {Title} ({Year})",
show_when=[
@@ -978,7 +985,12 @@ def download_settings() -> list[SettingsField]:
_naming_template_field(
key="TEMPLATE_ORGANIZE",
label="Path Template",
- description="Folder and filename template for book downloads.",
+ description=(
+ "Use / to create folders. Variables: {Author}, {Title}, {Year}, {User}, "
+ "{OriginalName} (source filename without extension). Universal adds: {Series}, "
+ "{SeriesPosition}, {Subtitle}, {PrimaryTitle}. Use arbitrary prefix/suffix: "
+ "{Vol. SeriesPosition - } outputs 'Vol. 2 - ' when set, nothing when empty."
+ ),
default="{Author}/{Title} ({Year})",
placeholder="{Author}/{Series/}{Title} ({Year})",
show_when=[
@@ -1236,7 +1248,14 @@ def download_settings() -> list[SettingsField]:
_naming_template_field(
key="TEMPLATE_AUDIOBOOK_RENAME",
label="Naming Template",
- description="Filename template for single-file audiobook downloads.",
+ description=(
+ "Variables: {Author}, {Title}, {Year}, {User}, {OriginalName} "
+ "(source filename without extension), {Series}, {SeriesPosition}, {Subtitle}, "
+ "{PrimaryTitle}, {PartNumber}. Use arbitrary prefix/suffix: "
+ "{Vol. SeriesPosition - } outputs 'Vol. 2 - ' when set, nothing when empty. "
+ "Rename templates are filename-only (no '/' or '\\'); use Organize for folders. "
+ "Applies to single-file downloads."
+ ),
default="{Author} - {Title}",
placeholder="{Author} - {Title}{ - Part }{PartNumber}",
show_when={"field": "FILE_ORGANIZATION_AUDIOBOOK", "value": "rename"},
@@ -1246,8 +1265,13 @@ def download_settings() -> list[SettingsField]:
_naming_template_field(
key="TEMPLATE_AUDIOBOOK_ORGANIZE",
label="Path Template",
- description="Folder and filename template for audiobook downloads.",
- default="{Author}/{Title}",
+ description=(
+ "Use / to create folders. Variables: {Author}, {Title}, {Year}, {User}, "
+ "{OriginalName} (source filename without extension), {Series}, {SeriesPosition}, "
+ "{Subtitle}, {PrimaryTitle}, {PartNumber}. Use arbitrary prefix/suffix: "
+ "{Vol. SeriesPosition - } outputs 'Vol. 2 - ' when set, nothing when empty."
+ ),
+ default="{Author}/{Title}/{Title}",
placeholder="{Author}/{Series/}{Title}{ - Part }{PartNumber}",
show_when={"field": "FILE_ORGANIZATION_AUDIOBOOK", "value": "organize"},
universal_only=True,
diff --git a/shelfmark/core/settings_registry.py b/shelfmark/core/settings_registry.py
index c8b6d3a..cc4e67a 100644
--- a/shelfmark/core/settings_registry.py
+++ b/shelfmark/core/settings_registry.py
@@ -333,7 +333,7 @@ def get_all_settings_tabs() -> list[SettingsTab]:
return sorted(_SETTINGS_REGISTRY.values(), key=lambda t: (t.order, t.name))
-def _iter_value_fields(tab: SettingsTab) -> Iterator[FieldBase]:
+def iter_value_fields(tab: SettingsTab) -> Iterator[FieldBase]:
"""Yield value-bearing fields for a tab."""
for settings_field in tab.fields:
if isinstance(settings_field, CustomComponentField):
@@ -360,7 +360,7 @@ def get_settings_field_map(
field_map: dict[str, tuple[FieldBase, str]] = {}
for tab in tabs:
- for settings_field in _iter_value_fields(tab):
+ for settings_field in iter_value_fields(tab):
field_map[settings_field.key] = (settings_field, tab.name)
return field_map
@@ -494,7 +494,7 @@ def initialize_default_configs() -> bool:
# Collect default values for all fields
defaults = {}
- for field in _iter_value_fields(tab):
+ for field in iter_value_fields(tab):
# Only include fields that have a non-None default
if field.default is not None:
defaults[field.key] = field.default
@@ -536,7 +536,7 @@ def sync_env_to_config() -> None:
for tab in get_all_settings_tabs():
values_to_sync = {}
- for settings_field in _iter_value_fields(tab):
+ for settings_field in iter_value_fields(tab):
# Skip fields that don't support ENV vars
if not getattr(settings_field, "env_supported", True):
continue
diff --git a/src/frontend/src/components/settings/customFields/NamingTemplateField.tsx b/src/frontend/src/components/settings/customFields/NamingTemplateField.tsx
index 5db7485..9b159f5 100644
--- a/src/frontend/src/components/settings/customFields/NamingTemplateField.tsx
+++ b/src/frontend/src/components/settings/customFields/NamingTemplateField.tsx
@@ -106,7 +106,6 @@ export const NamingTemplateField = ({
disabled={fieldDisabled}
className="w-full rounded-lg border border-(--border-muted) bg-(--bg-soft) px-3 py-2 text-sm transition-colors focus:border-sky-500 focus:ring-2 focus:ring-sky-500/50 focus:outline-hidden disabled:cursor-not-allowed disabled:opacity-60"
/>
- {boundField.description &&
{boundField.description}
}
{(hasPathSeparatorInFilename || preview.unknownTokens.length > 0) && (
diff --git a/src/frontend/src/tests/namingTemplatePreview.test.ts b/src/frontend/src/tests/namingTemplatePreview.test.ts
index 94a469c..7116a3e 100644
--- a/src/frontend/src/tests/namingTemplatePreview.test.ts
+++ b/src/frontend/src/tests/namingTemplatePreview.test.ts
@@ -2,11 +2,18 @@ import { describe, expect, it } from 'vitest';
import {
buildNamingTemplatePreview,
+ NAMING_TEMPLATE_TOKENS,
renderNamingTemplate,
SAMPLE_NAMING_METADATA,
} from '../utils/namingTemplatePreview';
describe('namingTemplatePreview', () => {
+ it('groups primary title with universal variables', () => {
+ expect(NAMING_TEMPLATE_TOKENS.find((token) => token.token === 'PrimaryTitle')?.group).toBe(
+ 'Universal',
+ );
+ });
+
it('renders primary title in path previews', () => {
const preview = buildNamingTemplatePreview(
'{Author}/{Series/}{SeriesPosition - }{PrimaryTitle} ({Year})',
diff --git a/src/frontend/src/utils/namingTemplatePreview.ts b/src/frontend/src/utils/namingTemplatePreview.ts
index 63c41b4..d1dc970 100644
--- a/src/frontend/src/utils/namingTemplatePreview.ts
+++ b/src/frontend/src/utils/namingTemplatePreview.ts
@@ -39,7 +39,7 @@ export const NAMING_TEMPLATE_TOKENS: NamingTemplateToken[] = [
label: 'Primary title',
description: 'Title without the subtitle suffix',
value: 'The Hound of the Baskervilles',
- group: 'Core',
+ group: 'Universal',
},
{
token: 'Year',
diff --git a/tests/config/test_download_settings.py b/tests/config/test_download_settings.py
index 3f45aef..556b1a1 100644
--- a/tests/config/test_download_settings.py
+++ b/tests/config/test_download_settings.py
@@ -156,6 +156,21 @@ def test_download_settings_naming_templates_use_wrapped_custom_component():
assert value_key not in fields_by_key
+def test_download_settings_naming_template_value_fields_are_registered():
+ import shelfmark.config.settings # noqa: F401
+ from shelfmark.core import settings_registry
+
+ field_map = settings_registry.get_settings_field_map(tab_name="downloads")
+
+ for value_key in (
+ "TEMPLATE_RENAME",
+ "TEMPLATE_ORGANIZE",
+ "TEMPLATE_AUDIOBOOK_RENAME",
+ "TEMPLATE_AUDIOBOOK_ORGANIZE",
+ ):
+ assert value_key in field_map
+
+
def test_download_settings_naming_template_serialization_keeps_value_fields_hidden():
from shelfmark.config.settings import download_settings
from shelfmark.core import settings_registry
diff --git a/tests/config/test_generate_env_docs.py b/tests/config/test_generate_env_docs.py
index cbdccda..98e3525 100644
--- a/tests/config/test_generate_env_docs.py
+++ b/tests/config/test_generate_env_docs.py
@@ -38,3 +38,22 @@ def test_generated_env_docs_describe_mirror_lists_as_comma_separated_strings() -
"| `LIBGEN_MIRROR_URLS` | Mirrors are tried in the order you add them until one works. | "
"string (comma-separated) | _empty list_ |"
) in docs
+
+
+def test_generated_env_docs_include_custom_component_value_fields() -> None:
+ docs = generate_env_docs()
+
+ for env_var in (
+ "TEMPLATE_RENAME",
+ "TEMPLATE_ORGANIZE",
+ "TEMPLATE_AUDIOBOOK_RENAME",
+ "TEMPLATE_AUDIOBOOK_ORGANIZE",
+ ):
+ assert f"`{env_var}`" in docs
+
+ assert (
+ "| `TEMPLATE_AUDIOBOOK_ORGANIZE` | Use / to create folders. Variables: "
+ "{Author}, {Title}, {Year}, {User}, {OriginalName} "
+ "(source filename without extension), {Series}, {SeriesPosition}, {Subtitle}, "
+ "{PrimaryTitle}, {PartNumber}. Use arbitrary prefix/suffix:"
+ ) in docs