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
|
## File names of downloaded books
|
||||||
|
|
||||||
"Download ePub" on a story page sends the book to your browser with a file name. Without a template,
|
"Download ePub" on a story page sends the book to your browser with a file name. A template gives
|
||||||
the name is the title, for example `The Book.epub`.
|
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
|
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, so a template from Calibre gives the same name in Bindery. Bindery adds `.epub`. For example,
|
||||||
`{series:|| - }{series_index:0>2s|| - }{title} - {authors}` gives
|
`{series:|| - }{series_index:0>2s|| - }{title} - {authors}` gives
|
||||||
|
|||||||
@@ -15,6 +15,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.
|
||||||
|
DEFAULT_TEMPLATE = "{title} - {authors}"
|
||||||
MAX_TEMPLATE_LENGTH = 1024
|
MAX_TEMPLATE_LENGTH = 1024
|
||||||
# The most characters that a format can ask for, as a width or a precision.
|
# The most characters that a format can ask for, as a width or a precision.
|
||||||
MAX_WIDTH = 255
|
MAX_WIDTH = 255
|
||||||
|
|||||||
@@ -380,7 +380,7 @@ def get_settings() -> dict[str, Any]:
|
|||||||
"linking_available": _linking_available(),
|
"linking_available": _linking_available(),
|
||||||
"hold_new_links": bool(store.get_setting(None, "hold_new_links")),
|
"hold_new_links": bool(store.get_setting(None, "hold_new_links")),
|
||||||
"personal_ini": ini.read_text(encoding="utf-8") if ini.exists() else "",
|
"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(),
|
**job_list_settings(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -648,20 +648,22 @@ def download_fields() -> dict[str, str]:
|
|||||||
return fields
|
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:
|
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
|
"""The file name of a downloaded book, from the download template. If the template fails or gives no name for
|
||||||
template fails or gives no name for this book, it is the title."""
|
this book, the name is the title."""
|
||||||
template = store.get_setting(None, "download_template")
|
|
||||||
name = ""
|
|
||||||
if template:
|
|
||||||
meta = _book_metadata(story["site_values"], _epub_overrides(story["pinned"]))
|
meta = _book_metadata(story["site_values"], _epub_overrides(story["pinned"]))
|
||||||
meta["title"] = meta.get("title") or story["title"]
|
meta["title"] = meta.get("title") or story["title"]
|
||||||
meta["authors"] = meta.get("authors") or [story["author"]]
|
meta["authors"] = meta.get("authors") or [story["author"]]
|
||||||
meta.update(publisher=story["site"], timestamp=story["created_at"], last_modified=story["last_changed"])
|
meta.update(publisher=story["site"], timestamp=story["created_at"], last_modified=story["last_changed"])
|
||||||
# Without CWA, the saved CWA book IDs do not apply.
|
# 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"]
|
book_id = story["calibre_id"] if config.CWA_ENABLED and story["calibre_id"] is not None else story["id"]
|
||||||
|
name = ""
|
||||||
try:
|
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:
|
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}")
|
||||||
return f"{name or sanitize_filename(story['title'])}.epub"
|
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 ',
|
'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'),
|
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. ' +
|
' 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, ',
|
'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 "Saga, The - 02 - Book, The - Ann Writer.epub".',
|
||||||
|
|||||||
+1
-1
@@ -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": "",
|
"download_template": "{title} - {authors}",
|
||||||
"story_jobs_hours": 36,
|
"story_jobs_hours": 36,
|
||||||
"story_jobs_count": 10,
|
"story_jobs_count": 10,
|
||||||
"main_jobs_hours": 6,
|
"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")
|
response = client.get(f"/api/stories/{sid}/epub")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.data.startswith(b"PK")
|
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
|
assert o.store.revision_html(cid, 1) == original
|
||||||
response.close()
|
response.close()
|
||||||
|
|
||||||
@@ -166,11 +166,12 @@ def test_download_template_names_the_file(client, story):
|
|||||||
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.
|
||||||
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)
|
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):
|
def test_download_template_that_fails_gives_the_title(client, story, caplog):
|
||||||
|
|||||||
Reference in New Issue
Block a user