From dcdd8e4ac78911e3352488ebd3fc2283c2a19eee Mon Sep 17 00:00:00 2001 From: Timothy Allen Date: Sat, 3 Oct 2026 07:55:15 +0200 Subject: [PATCH] Keep the article in {title} and {series} The default template is now {title_sort} - {authors}. It gives the same name as Calibre's default. --- README.md | 22 +++++++++++++--------- bindery/filename_template.py | 14 +++++++------- bindery/orchestrator.py | 5 ++++- bindery/static/js/settings.js | 9 +++++---- tests/test_api.py | 2 +- tests/test_api_flows.py | 8 ++++++-- 6 files changed, 36 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 0898dd1..25e0a64 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/bindery/filename_template.py b/bindery/filename_template.py index 9a8797e..b5aa469 100644 --- a/bindery/filename_template.py +++ b/bindery/filename_template.py @@ -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]}", diff --git a/bindery/orchestrator.py b/bindery/orchestrator.py index 594f7fe..11198bb 100644 --- a/bindery/orchestrator.py +++ b/bindery/orchestrator.py @@ -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}") diff --git a/bindery/static/js/settings.js b/bindery/static/js/settings.js index 82e0556..2cf66cf 100644 --- a/bindery/static/js/settings.js +++ b/bindery/static/js/settings.js @@ -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', diff --git a/tests/test_api.py b/tests/test_api.py index e591f36..f0a9d1d 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -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, diff --git a/tests/test_api_flows.py b/tests/test_api_flows.py index 9a9fed5..12deded 100644 --- a/tests/test_api_flows.py +++ b/tests/test_api_flows.py @@ -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)