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 ## File names of downloaded books
"Download ePub" on a story page sends the book to your browser with a file name. A template gives "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 the name. The default template is `{title_sort} - {authors}`. For "The Book" by Ann Writer, it gives
Writer, it gives `Book, The - Ann Writer.epub`. `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, 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 clear the field and save. The template follows
the rules of [Calibre's templates](https://manual.calibre-ebook.com/template_lang.html) for file 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, names. Bindery adds `.epub`. For example, `{series:|| - }{series_index:0>2s|| - }{title} - {authors}`
`{series:|| - }{series_index:0>2s|| - }{title} - {authors}` gives gives `The Saga - 02 - The Book - Ann Writer.epub`.
`Saga, The - 02 - Book, The - 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 | | Field | Value |
| --- | --- | | --- | --- |
| `{title}` | The title. A leading "The", "A" or "An" moves to the end, as in Calibre: `Book, The`. | | `{title}` | The title: `The Book`. |
| `{title_sort}` | The same value as `{title}`. | | `{title_sort}` | The title with a leading "The", "A" or "An" at the end: `Book, The`. |
| `{authors}`, `{author}` | The authors, with ` & ` between them. | | `{authors}`, `{author}` | The authors, with ` & ` between them. |
| `{author_sort}` | The authors with the last name first: `Writer, Ann`. | | `{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`. | | `{series_index}` | The series number: `2`, or `2.5`. |
| `{tags}` | The tags in sorted order, with `, ` between them. | | `{tags}` | The tags in sorted order, with `, ` between them. |
| `{languages}` | The language, as Bindery stores it. | | `{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. `%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. 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 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. 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. """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 Bindery follows Calibre 9.2.1 with its default tweaks and the English articles. prepare() takes the tweak for
articles. Dates take a strftime format, with Calibre's "%b, %Y" as the default. Bindery takes {field}, the order of titles and series as an argument. Dates take a strftime format, with Calibre's "%b, %Y" as the
{field[0]}, {field:format|prefix|suffix} and {{ }}. It refuses template functions and the program modes. It never default. Bindery takes {field}, {field[0]}, {field:format|prefix|suffix} and {{ }}. It refuses template
runs template text as code. 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. 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 from bindery.ingest import sanitize_filename
# Calibre's default template for a file that it sends to a device. # The name that Calibre's default template, {title} - {authors}, gives with its default tweaks.
DEFAULT_TEMPLATE = "{title} - {authors}" DEFAULT_TEMPLATE = "{title_sort} - {authors}"
DEFAULT_DATE_FORMAT = "%b, %Y" DEFAULT_DATE_FORMAT = "%b, %Y"
MAX_TEMPLATE_LENGTH = 1024 MAX_TEMPLATE_LENGTH = 1024
MAX_DATE_FORMAT_LENGTH = 100 MAX_DATE_FORMAT_LENGTH = 100
@@ -29,7 +29,7 @@ MAX_NAME_BYTES = 200
# a book without that value. # a book without that value.
FIELDS = { FIELDS = {
"title": "The title", "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", "authors": "The authors",
"author": "The authors", "author": "The authors",
"author_sort": "The author sort string. To use only the first letter of the name, use {author_sort[0]}", "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"] book_id = story["calibre_id"] if config.CWA_ENABLED and story["calibre_id"] is not None else story["id"]
name = "" name = ""
try: 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) name = filename_template.file_name(_download_setting("download_template"), values)
except filename_template.TemplateError as e: except filename_template.TemplateError as e:
logger.warning(f"The download template fails for {story['url']}: {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' }, { class: 'hint' },
'This template gives the file name of a book that you download from a story page. To go back to the ' + 'This template gives the file name of a book that you download from a story page. To go back to the ' +
'default, ', 'default, ',
code('{title} - {authors}'), code('{title_sort} - {authors}'),
', clear the field and save.', ', clear the field and save.',
), ),
h( 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. ' + ' 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, ', 'A field with no value becomes empty text. For example, ',
code('{series:|| - }{series_index:0>2s|| - }{title} - {authors}'), 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('p', { class: 'hint' }, 'You can use these fields:'),
h( h(
@@ -208,8 +208,9 @@ export function settingsPage(main, _params, config) {
h( h(
'li', 'li',
{}, {},
'The title and the series move a leading "The", "A" or "An" to the end, as Calibre does. ' + '{title} and {series} keep a leading "The", "A" or "An". {title_sort} moves it to the end, so ' +
'"The Book" becomes "Book, The".', '"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( h(
'li', 'li',
+1 -1
View File
@@ -472,7 +472,7 @@ def test_settings(client, monkeypatch, tmp_path):
"pause_uploads": False, "pause_uploads": False,
"ingest_available": True, "ingest_available": True,
"personal_ini": "[defaults]\nis_adult:true\n", "personal_ini": "[defaults]\nis_adult:true\n",
"download_template": "{title} - {authors}", "download_template": "{title_sort} - {authors}",
"download_date_format": "%b, %Y", "download_date_format": "%b, %Y",
"story_jobs_hours": 36, "story_jobs_hours": 36,
"story_jobs_count": 10, "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") 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) saved = client.put("/api/settings", json={"download_template": " {series:|| - }{title} - {id} "}, headers=H)
assert saved.json["download_template"] == "{series:|| - }{title} - {id}" 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. # 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]}) 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) name = download_name(client, sid)
assert 'filename="Cafe - 77.epub"' in name and "filename*=UTF-8''Caf%C3%A9%20-%2077.epub" in name 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. # 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) assert "filename*=UTF-8''Caf%C3%A9.epub" in download_name(client, sid)
# An empty template restores the default. # An empty template restores the default.
saved = client.put("/api/settings", json={"download_template": " "}, headers=H).json 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) assert "filename*=UTF-8''Caf%C3%A9%20-%20Ann%20Writer.epub" in download_name(client, sid)