Use {title} - {authors} as the default download template
This is Calibre's default. An empty template now restores the default.
This commit is contained in:
@@ -447,10 +447,12 @@ downloaded before.
|
||||
|
||||
## File names of downloaded books
|
||||
|
||||
"Download ePub" on a story page sends the book to your browser with a file name. Without a template,
|
||||
the name is the title, for example `The Book.epub`.
|
||||
"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`.
|
||||
|
||||
An admin can set a template for the name under "Book Downloads" in Settings. The template follows
|
||||
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
|
||||
|
||||
@@ -15,6 +15,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}"
|
||||
MAX_TEMPLATE_LENGTH = 1024
|
||||
# The most characters that a format can ask for, as a width or a precision.
|
||||
MAX_WIDTH = 255
|
||||
|
||||
@@ -380,7 +380,7 @@ def get_settings() -> dict[str, Any]:
|
||||
"linking_available": _linking_available(),
|
||||
"hold_new_links": bool(store.get_setting(None, "hold_new_links")),
|
||||
"personal_ini": ini.read_text(encoding="utf-8") if ini.exists() else "",
|
||||
"download_template": store.get_setting(None, "download_template") or "",
|
||||
"download_template": _download_template(),
|
||||
**job_list_settings(),
|
||||
}
|
||||
|
||||
@@ -648,20 +648,22 @@ def download_fields() -> dict[str, str]:
|
||||
return fields
|
||||
|
||||
|
||||
def _download_template() -> str:
|
||||
return store.get_setting(None, "download_template") or filename_template.DEFAULT_TEMPLATE
|
||||
|
||||
|
||||
def _download_name(story: dict[str, Any]) -> str:
|
||||
"""The file name of a downloaded book. It comes from the download template. Without a template, or if the
|
||||
template fails or gives no name for this book, it is the title."""
|
||||
template = store.get_setting(None, "download_template")
|
||||
name = ""
|
||||
if template:
|
||||
"""The file name of a downloaded book, from the download template. If the template fails or gives no name for
|
||||
this book, the name is the title."""
|
||||
meta = _book_metadata(story["site_values"], _epub_overrides(story["pinned"]))
|
||||
meta["title"] = meta.get("title") or story["title"]
|
||||
meta["authors"] = meta.get("authors") or [story["author"]]
|
||||
meta.update(publisher=story["site"], timestamp=story["created_at"], last_modified=story["last_changed"])
|
||||
# Without CWA, the saved CWA book IDs do not apply.
|
||||
book_id = story["calibre_id"] if config.CWA_ENABLED and story["calibre_id"] is not None else story["id"]
|
||||
name = ""
|
||||
try:
|
||||
name = filename_template.file_name(template, filename_template.prepare(meta, book_id))
|
||||
name = filename_template.file_name(_download_template(), filename_template.prepare(meta, book_id))
|
||||
except filename_template.TemplateError as e:
|
||||
logger.warning(f"The download template fails for {story['url']}: {e}")
|
||||
return f"{name or sanitize_filename(story['title'])}.epub"
|
||||
|
||||
@@ -162,7 +162,8 @@ export function settingsPage(main, _params, config) {
|
||||
'This template gives the file name of a book that you download from a story page. It follows the ',
|
||||
externalLink(TEMPLATE_DOCS, 'template rules of Calibre'),
|
||||
' 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. Leave the template empty to name the file after the title. ' +
|
||||
'A field with no value becomes empty text. To go back to the default, {title} - {authors}, clear the field ' +
|
||||
'and save. ' +
|
||||
'For example, ',
|
||||
code('{series:|| - }{series_index:0>2s|| - }{title} - {authors}'),
|
||||
' gives "Saga, The - 02 - Book, The - Ann Writer.epub".',
|
||||
|
||||
+1
-1
@@ -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": "",
|
||||
"download_template": "{title} - {authors}",
|
||||
"story_jobs_hours": 36,
|
||||
"story_jobs_count": 10,
|
||||
"main_jobs_hours": 6,
|
||||
|
||||
@@ -142,7 +142,7 @@ def test_download_builds_from_stored_chapters_without_changing_them(client, stor
|
||||
response = client.get(f"/api/stories/{sid}/epub")
|
||||
assert response.status_code == 200
|
||||
assert response.data.startswith(b"PK")
|
||||
assert "filename=A_B.epub" in response.headers["Content-Disposition"]
|
||||
assert 'filename="A_B - Writer.epub"' in response.headers["Content-Disposition"]
|
||||
assert o.store.revision_html(cid, 1) == original
|
||||
response.close()
|
||||
|
||||
@@ -166,11 +166,12 @@ def test_download_template_names_the_file(client, story):
|
||||
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.
|
||||
client.put("/api/settings", json={"download_template": "{author_sort}/{title} - {authors}"}, headers=H)
|
||||
client.put("/api/settings", json={"download_template": "{author_sort}/{title}"}, headers=H)
|
||||
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 "filename*=UTF-8''Caf%C3%A9%20-%20Ann%20Writer.epub" in download_name(client, sid)
|
||||
# Without a template, the name is the title of the story, with its article in front.
|
||||
assert client.put("/api/settings", json={"download_template": ""}, headers=H).json["download_template"] == ""
|
||||
assert 'filename="The Book.epub"' in download_name(client, sid)
|
||||
|
||||
|
||||
def test_download_template_that_fails_gives_the_title(client, story, caplog):
|
||||
|
||||
Reference in New Issue
Block a user