Keep the article in {title} and {series}

The default template is now {title_sort} - {authors}. It gives the same name as Calibre's default.
This commit is contained in:
tim
2026-10-03 08:09:41 +02:00
parent 42e7287389
commit dcdd8e4ac7
6 changed files with 36 additions and 24 deletions
+13 -9
View File
@@ -448,23 +448,27 @@ downloaded before.
## File names of downloaded books
"Download ePub" on a story page sends the book to your browser with a file name. A template gives
the name. The default template is `{title} - {authors}`, as in Calibre. For "The Book" by Ann
Writer, it gives `Book, The - Ann Writer.epub`.
the name. The default template is `{title_sort} - {authors}`. For "The Book" by Ann Writer, it gives
`Book, The - Ann Writer.epub`. That is the name that Calibre's default template gives.
An admin can change the template under "Book Downloads" in Settings. To go back to the default,
clear the field and save. The template follows
the rules of [Calibre's templates](https://manual.calibre-ebook.com/template_lang.html) for file
names, so a template from Calibre gives the same name in Bindery. Bindery adds `.epub`. For example,
`{series:|| - }{series_index:0>2s|| - }{title} - {authors}` gives
`Saga, The - 02 - Book, The - Ann Writer.epub`.
names. Bindery adds `.epub`. For example, `{series:|| - }{series_index:0>2s|| - }{title} - {authors}`
gives `The Saga - 02 - The Book - Ann Writer.epub`.
In Calibre, `{title}` and `{series}` give the title sort in a file name, for example `Book, The`.
In Bindery, they keep the article. This is the same as Calibre with the tweak
`save_template_title_series_sorting` set to `strictly_alphabetic`. Use `{title_sort}` for the title
with the article at the end.
| Field | Value |
| --- | --- |
| `{title}` | The title. A leading "The", "A" or "An" moves to the end, as in Calibre: `Book, The`. |
| `{title_sort}` | The same value as `{title}`. |
| `{title}` | The title: `The Book`. |
| `{title_sort}` | The title with a leading "The", "A" or "An" at the end: `Book, The`. |
| `{authors}`, `{author}` | The authors, with ` & ` between them. |
| `{author_sort}` | The authors with the last name first: `Writer, Ann`. |
| `{series}` | The series. A leading article moves to the end. |
| `{series}` | The series. |
| `{series_index}` | The series number: `2`, or `2.5`. |
| `{tags}` | The tags in sorted order, with `, ` between them. |
| `{languages}` | The language, as Bindery stores it. |
@@ -495,7 +499,7 @@ gives the day, `%b` the month name, `%m` the month number and `%Y` the year. The
`%b, %Y`, as in Calibre. It gives `May, 2020`. To go back to the default, clear the field and save.
A `/` in a date becomes `_`, as in Calibre.
Bindery follows Calibre 9.2.1 with its default tweaks, for English. The order of tags with letters
Bindery follows Calibre 9.2.1 for English, with the tweak above. The order of tags with letters
outside ASCII can differ from Calibre. The template applies only to downloads in the browser. It
does not change the names of the files in `INGEST_DIR`. The browser can ignore the name.
+7 -7
View File
@@ -1,9 +1,9 @@
"""Calibre's templates for file names, for the name of a downloaded book.
Bindery follows Calibre 9.2.1 with its default tweaks: titles and series in library order with the English
articles. Dates take a strftime format, with Calibre's "%b, %Y" as the default. Bindery takes {field},
{field[0]}, {field:format|prefix|suffix} and {{ }}. It refuses template functions and the program modes. It never
runs template text as code.
Bindery follows Calibre 9.2.1 with its default tweaks and the English articles. prepare() takes the tweak for
the order of titles and series as an argument. Dates take a strftime format, with Calibre's "%b, %Y" as the
default. Bindery takes {field}, {field[0]}, {field:format|prefix|suffix} and {{ }}. It refuses template
functions and the program modes. It never runs template text as code.
The tags sort close to Calibre's ICU rules, but their order can differ for characters outside ASCII.
"""
@@ -16,8 +16,8 @@ from typing import Any
from bindery.ingest import sanitize_filename
# Calibre's default template for a file that it sends to a device.
DEFAULT_TEMPLATE = "{title} - {authors}"
# The name that Calibre's default template, {title} - {authors}, gives with its default tweaks.
DEFAULT_TEMPLATE = "{title_sort} - {authors}"
DEFAULT_DATE_FORMAT = "%b, %Y"
MAX_TEMPLATE_LENGTH = 1024
MAX_DATE_FORMAT_LENGTH = 100
@@ -29,7 +29,7 @@ MAX_NAME_BYTES = 200
# a book without that value.
FIELDS = {
"title": "The title",
"title_sort": "The title in sort order",
"title_sort": 'The title with a leading "The", "A" or "An" at the end, such as "Book, The"',
"authors": "The authors",
"author": "The authors",
"author_sort": "The author sort string. To use only the first letter of the name, use {author_sort[0]}",
+4 -1
View File
@@ -675,7 +675,10 @@ def _download_name(story: dict[str, Any]) -> str:
book_id = story["calibre_id"] if config.CWA_ENABLED and story["calibre_id"] is not None else story["id"]
name = ""
try:
values = filename_template.prepare(meta, book_id, date_format=_download_setting("download_date_format"))
# {title} and {series} keep a leading article, as in Calibre with this value of its tweak
# save_template_title_series_sorting. {title_sort} moves it to the end.
date_format = _download_setting("download_date_format")
values = filename_template.prepare(meta, book_id, order="strictly_alphabetic", date_format=date_format)
name = filename_template.file_name(_download_setting("download_template"), values)
except filename_template.TemplateError as e:
logger.warning(f"The download template fails for {story['url']}: {e}")
+5 -4
View File
@@ -168,7 +168,7 @@ export function settingsPage(main, _params, config) {
{ class: 'hint' },
'This template gives the file name of a book that you download from a story page. To go back to the ' +
'default, ',
code('{title} - {authors}'),
code('{title_sort} - {authors}'),
', clear the field and save.',
),
h(
@@ -183,7 +183,7 @@ export function settingsPage(main, _params, config) {
' for file names. Bindery replaces each field in braces with the value of the book, and adds .epub. ' +
'A field with no value becomes empty text. For example, ',
code('{series:|| - }{series_index:0>2s|| - }{title} - {authors}'),
' gives "Saga, The - 02 - Book, The - Ann Writer.epub".',
' gives "The Saga - 02 - The Book - Ann Writer.epub".',
),
h('p', { class: 'hint' }, 'You can use these fields:'),
h(
@@ -208,8 +208,9 @@ export function settingsPage(main, _params, config) {
h(
'li',
{},
'The title and the series move a leading "The", "A" or "An" to the end, as Calibre does. ' +
'"The Book" becomes "Book, The".',
'{title} and {series} keep a leading "The", "A" or "An". {title_sort} moves it to the end, so ' +
'"The Book" becomes "Book, The". Calibre puts the title sort in {title} as well, unless you set its ' +
'tweak save_template_title_series_sorting to strictly_alphabetic.',
),
h(
'li',
+1 -1
View File
@@ -472,7 +472,7 @@ def test_settings(client, monkeypatch, tmp_path):
"pause_uploads": False,
"ingest_available": True,
"personal_ini": "[defaults]\nis_adult:true\n",
"download_template": "{title} - {authors}",
"download_template": "{title_sort} - {authors}",
"download_date_format": "%b, %Y",
"story_jobs_hours": 36,
"story_jobs_count": 10,
+6 -2
View File
@@ -160,9 +160,13 @@ def test_download_template_names_the_file(client, story):
assert client.get("/api/config").json["download_fields"]["id"].startswith("The CWA book ID")
saved = client.put("/api/settings", json={"download_template": " {series:|| - }{title} - {id} "}, headers=H)
assert saved.json["download_template"] == "{series:|| - }{title} - {id}"
assert f'filename="Saga, The - Book, The - {sid}.epub"' in download_name(client, sid)
assert f'filename="The Saga - The Book - {sid}.epub"' in download_name(client, sid)
# The default moves the article to the end, as Calibre's default does.
client.put("/api/settings", json={"download_template": ""}, headers=H)
assert 'filename="Book, The - Ann Writer.epub"' in download_name(client, sid)
# The ID is that of the CWA book, when the story has one.
o.store.update_story(sid, calibre_id=77, pinned={"title": "Caf\u00e9", "series": ["", None]})
client.put("/api/settings", json={"download_template": "{series:|| - }{title} - {id}"}, headers=H)
name = download_name(client, sid)
assert 'filename="Cafe - 77.epub"' in name and "filename*=UTF-8''Caf%C3%A9%20-%2077.epub" in name
# A template with folders gives the name of the file alone.
@@ -170,7 +174,7 @@ def test_download_template_names_the_file(client, story):
assert "filename*=UTF-8''Caf%C3%A9.epub" in download_name(client, sid)
# An empty template restores the default.
saved = client.put("/api/settings", json={"download_template": " "}, headers=H).json
assert saved["download_template"] == "{title} - {authors}"
assert saved["download_template"] == "{title_sort} - {authors}"
assert "filename*=UTF-8''Caf%C3%A9%20-%20Ann%20Writer.epub" in download_name(client, sid)