Add {series_sort} to the download template

It gives the series with a leading article at the end, as Calibre's default does for {series}.
This commit is contained in:
tim
2026-10-03 08:09:41 +02:00
parent dcdd8e4ac7
commit ece59c2558
5 changed files with 19 additions and 4 deletions
+4 -2
View File
@@ -459,8 +459,9 @@ 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 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 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 `save_template_title_series_sorting` set to `strictly_alphabetic`. Use `{title_sort}` and
with the article at the end. `{series_sort}` for the title and the series with the article at the end. `{series_sort}` is a
Bindery field. Calibre does not have it, and gives empty text for it.
| Field | Value | | Field | Value |
| --- | --- | | --- | --- |
@@ -469,6 +470,7 @@ with the article at the end.
| `{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. | | `{series}` | The series. |
| `{series_sort}` | The series with a leading article at the end: `Saga, The`. |
| `{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. |
+6 -1
View File
@@ -5,6 +5,8 @@ the order of titles and series as an argument. Dates take a strftime format, wit
default. Bindery takes {field}, {field[0]}, {field:format|prefix|suffix} and {{ }}. It refuses template 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. functions and the program modes. It never runs template text as code.
{series_sort} is a Bindery field. It is to {series} what {title_sort} is to {title}. Calibre has no such field.
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.
""" """
@@ -34,6 +36,8 @@ FIELDS = {
"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]}",
"series": "The series", "series": "The series",
"series_sort": 'The series with a leading "The", "A" or "An" at the end, such as "Saga, The". '
"Calibre does not have this field",
"series_index": "The series number. To get leading zeros, use {series_index:0>3s}", "series_index": "The series number. To get leading zeros, use {series_index:0>3s}",
"tags": "The tags", "tags": "The tags",
"languages": "The language", "languages": "The language",
@@ -207,7 +211,8 @@ def prepare(
values["authors"] = values["author"] = " & ".join(a.replace("&", "&&") for a in authors) values["authors"] = values["author"] = " & ".join(a.replace("&", "&&") for a in authors)
values["author_sort"] = " & ".join(author_sort(a) for a in authors) values["author_sort"] = " & ".join(author_sort(a) for a in authors)
if series: if series:
values["series"] = title_sort(series) if library else series.strip() values["series_sort"] = title_sort(series)
values["series"] = values["series_sort"] if library else series.strip()
if meta.get("series_index") is not None: if meta.get("series_index") is not None:
values["series_index"] = series_index(meta["series_index"]) values["series_index"] = series_index(meta["series_index"])
values["tags"] = ", ".join(sorted(meta.get("tags") or [], key=_tag_key)).removeprefix("/") values["tags"] = ", ".join(sorted(meta.get("tags") or [], key=_tag_key)).removeprefix("/")
+2 -1
View File
@@ -208,7 +208,8 @@ export function settingsPage(main, _params, config) {
h( h(
'li', 'li',
{}, {},
'{title} and {series} keep a leading "The", "A" or "An". {title_sort} moves it to the end, so ' + '{title} and {series} keep a leading "The", "A" or "An". {title_sort} and {series_sort} move it to the ' +
'end, so ' +
'"The Book" becomes "Book, The". Calibre puts the title sort in {title} as well, unless you set its ' + '"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.', 'tweak save_template_title_series_sorting to strictly_alphabetic.',
), ),
+2
View File
@@ -164,6 +164,8 @@ def test_download_template_names_the_file(client, story):
# The default moves the article to the end, as Calibre's default does. # The default moves the article to the end, as Calibre's default does.
client.put("/api/settings", json={"download_template": ""}, headers=H) client.put("/api/settings", json={"download_template": ""}, headers=H)
assert 'filename="Book, The - Ann Writer.epub"' in download_name(client, sid) assert 'filename="Book, The - Ann Writer.epub"' in download_name(client, sid)
client.put("/api/settings", json={"download_template": "{series_sort} - {title} - {id}"}, headers=H)
assert f'filename="Saga, The - The Book - {sid}.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) client.put("/api/settings", json={"download_template": "{series:|| - }{title} - {id}"}, headers=H)
+5
View File
@@ -55,6 +55,11 @@ def test_reference_cases_for_a_missing_series_and_for_alphabetic_order():
assert expand("{series}", "strictly_alphabetic") == "The Saga" assert expand("{series}", "strictly_alphabetic") == "The Saga"
def test_series_sort_is_a_bindery_field():
assert expand("{series_sort}") == expand("{series_sort}", "strictly_alphabetic") == "Saga, The"
assert ft.expand("x{series_sort}x", ft.prepare(NO_SERIES, 42)) == "xx"
@pytest.mark.parametrize( @pytest.mark.parametrize(
"template,expected", "template,expected",
[ [