Add a date format for download names
The new setting gives the strftime format of {pubdate}, {timestamp} and {last_modified}.
The default is Calibre's "%b, %Y".
This commit is contained in:
@@ -469,7 +469,7 @@ names, so a template from Calibre gives the same name in Bindery. Bindery adds `
|
|||||||
| `{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. |
|
||||||
| `{publisher}` | The story site. |
|
| `{publisher}` | The story site. |
|
||||||
| `{pubdate}` | The published date, in the form `May, 2020`. |
|
| `{pubdate}` | The published date. |
|
||||||
| `{timestamp}` | The date when Bindery added the story. |
|
| `{timestamp}` | The date when Bindery added the story. |
|
||||||
| `{last_modified}` | The date when the chapters last changed. |
|
| `{last_modified}` | The date when the chapters last changed. |
|
||||||
| `{id}` | The CWA book ID. For a story with no library book, and without CWA, the Bindery story ID. |
|
| `{id}` | The CWA book ID. For a story with no library book, and without CWA, the Bindery story ID. |
|
||||||
@@ -489,6 +489,12 @@ names, so a template from Calibre gives the same name in Bindery. Bindery adds `
|
|||||||
- Bindery replaces each character that a file name cannot have with `_`. A name has 100 characters
|
- Bindery replaces each character that a file name cannot have with `_`. A name has 100 characters
|
||||||
and 200 bytes at most.
|
and 200 bytes at most.
|
||||||
|
|
||||||
|
"Date format" sets the form of `{pubdate}`, `{timestamp}` and `{last_modified}`. It takes the
|
||||||
|
[codes of Python's strftime](https://docs.python.org/3/library/datetime.html#format-codes). `%d`
|
||||||
|
gives the day, `%b` the month name, `%m` the month number and `%Y` the year. The default is
|
||||||
|
`%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 with its default tweaks, for English. 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.
|
||||||
|
|||||||
@@ -1,8 +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: titles and series in library order with the English
|
||||||
articles, and dates in the form "%b, %Y". It takes {field}, {field[0]}, {field:format|prefix|suffix} and {{ }}.
|
articles. Dates take a strftime format, with Calibre's "%b, %Y" as the default. Bindery takes {field},
|
||||||
It refuses template functions and the program modes. It never runs template text as code.
|
{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.
|
The tags sort close to Calibre's ICU rules, but their order can differ for characters outside ASCII.
|
||||||
"""
|
"""
|
||||||
@@ -17,7 +18,9 @@ from bindery.ingest import sanitize_filename
|
|||||||
|
|
||||||
# Calibre's default template for a file that it sends to a device.
|
# Calibre's default template for a file that it sends to a device.
|
||||||
DEFAULT_TEMPLATE = "{title} - {authors}"
|
DEFAULT_TEMPLATE = "{title} - {authors}"
|
||||||
|
DEFAULT_DATE_FORMAT = "%b, %Y"
|
||||||
MAX_TEMPLATE_LENGTH = 1024
|
MAX_TEMPLATE_LENGTH = 1024
|
||||||
|
MAX_DATE_FORMAT_LENGTH = 100
|
||||||
# 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
|
||||||
MAX_NAME_BYTES = 200
|
MAX_NAME_BYTES = 200
|
||||||
@@ -70,7 +73,6 @@ _QUOTE_PAIRS = {
|
|||||||
}
|
}
|
||||||
# The ASCII punctuation in the order of Calibre's sort. Digits come next, then letters.
|
# The ASCII punctuation in the order of Calibre's sort. Digits come next, then letters.
|
||||||
_PUNCTUATION = " _-,;:!?.'\"()[]{}@*/\\&#%`^+<=>|~$"
|
_PUNCTUATION = " _-,;:!?.'\"()[]{}@*/\\&#%`^+<=>|~$"
|
||||||
_MONTHS = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
|
|
||||||
# Calibre's default tweaks for author names.
|
# Calibre's default tweaks for author names.
|
||||||
_NAME_PREFIXES = {"mr", "mrs", "ms", "dr", "prof"}
|
_NAME_PREFIXES = {"mr", "mrs", "ms", "dr", "prof"}
|
||||||
_NAME_SUFFIXES = {"jr", "sr", "inc", "ph.d", "phd", "md", "m.d", "i", "ii", "iii", "iv", "junior", "senior"}
|
_NAME_SUFFIXES = {"jr", "sr", "inc", "ph.d", "phd", "md", "m.d", "i", "ii", "iii", "iv", "junior", "senior"}
|
||||||
@@ -158,12 +160,21 @@ def series_index(index: Any) -> str:
|
|||||||
return f"{index:.2f}".rstrip("0")
|
return f"{index:.2f}".rstrip("0")
|
||||||
|
|
||||||
|
|
||||||
def _date(value: Any) -> str:
|
def check_date_format(date_format: str) -> None:
|
||||||
|
"""Raise TemplateError if Bindery cannot use the date format."""
|
||||||
|
if len(date_format) > MAX_DATE_FORMAT_LENGTH:
|
||||||
|
raise TemplateError(f"A date format can have {MAX_DATE_FORMAT_LENGTH} characters at most")
|
||||||
try:
|
try:
|
||||||
date = datetime.fromisoformat(value)
|
datetime(2020, 5, 6).strftime(date_format)
|
||||||
|
except ValueError as e:
|
||||||
|
raise TemplateError(f"The date format is not valid: {e}") from e
|
||||||
|
|
||||||
|
|
||||||
|
def _date(value: Any, date_format: str) -> str:
|
||||||
|
try:
|
||||||
|
return datetime.fromisoformat(value).strftime(date_format)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return ""
|
return ""
|
||||||
return f"{_MONTHS[date.month - 1]}, {date.year:04}"
|
|
||||||
|
|
||||||
|
|
||||||
def _tag_key(tag: str) -> tuple[list[tuple[int, str]], str]:
|
def _tag_key(tag: str) -> tuple[list[tuple[int, str]], str]:
|
||||||
@@ -178,11 +189,13 @@ def _tag_key(tag: str) -> tuple[list[tuple[int, str]], str]:
|
|||||||
return letters, text
|
return letters, text
|
||||||
|
|
||||||
|
|
||||||
def prepare(meta: dict[str, Any], book_id: int, order: str = "library_order") -> dict[str, Any]:
|
def prepare(
|
||||||
|
meta: dict[str, Any], book_id: int, order: str = "library_order", date_format: str = DEFAULT_DATE_FORMAT
|
||||||
|
) -> dict[str, Any]:
|
||||||
"""Calibre's get_component_metadata(): the values of a book in the form that a file name takes.
|
"""Calibre's get_component_metadata(): the values of a book in the form that a file name takes.
|
||||||
|
|
||||||
`order` is Calibre's tweak save_template_title_series_sorting. With "strictly_alphabetic", the title and the
|
`order` is Calibre's tweak save_template_title_series_sorting. With "strictly_alphabetic", the title and the
|
||||||
series keep a leading article.
|
series keep a leading article. `date_format` is Calibre's timefmt.
|
||||||
"""
|
"""
|
||||||
library = order == "library_order"
|
library = order == "library_order"
|
||||||
values: dict[str, Any] = {"id": str(book_id), "publisher": meta.get("publisher") or ""}
|
values: dict[str, Any] = {"id": str(book_id), "publisher": meta.get("publisher") or ""}
|
||||||
@@ -200,7 +213,7 @@ def prepare(meta: dict[str, Any], book_id: int, order: str = "library_order") ->
|
|||||||
values["tags"] = ", ".join(sorted(meta.get("tags") or [], key=_tag_key)).removeprefix("/")
|
values["tags"] = ", ".join(sorted(meta.get("tags") or [], key=_tag_key)).removeprefix("/")
|
||||||
values["languages"] = meta.get("languages") or ""
|
values["languages"] = meta.get("languages") or ""
|
||||||
for name in ("pubdate", "timestamp", "last_modified"):
|
for name in ("pubdate", "timestamp", "last_modified"):
|
||||||
values[name] = _date(meta.get(name))
|
values[name] = _date(meta.get(name), date_format)
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+25
-12
@@ -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": _download_template(),
|
**{name: _download_setting(name) for name in DOWNLOAD_DEFAULTS},
|
||||||
**job_list_settings(),
|
**job_list_settings(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,6 +393,7 @@ SETTINGS = (
|
|||||||
"hold_new_links",
|
"hold_new_links",
|
||||||
"pause_uploads",
|
"pause_uploads",
|
||||||
"download_template",
|
"download_template",
|
||||||
|
"download_date_format",
|
||||||
*JOB_LIST_DEFAULTS,
|
*JOB_LIST_DEFAULTS,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -417,12 +418,16 @@ def save_settings(**changes: Any) -> dict[str, Any]:
|
|||||||
if name in changes:
|
if name in changes:
|
||||||
most = config.JOB_LIST_MAX_HOURS if name.endswith("hours") else config.JOB_LIST_MAX_COUNT
|
most = config.JOB_LIST_MAX_HOURS if name.endswith("hours") else config.JOB_LIST_MAX_COUNT
|
||||||
numbers[name] = whole_number(changes[name], name, most)
|
numbers[name] = whole_number(changes[name], name, most)
|
||||||
template = changes.get("download_template")
|
texts: dict[str, str] = {}
|
||||||
if template is not None:
|
for name, label, check in (
|
||||||
if not isinstance(template, str):
|
("download_template", "The download template", filename_template.check),
|
||||||
raise ValueError("The download template must be text")
|
("download_date_format", "The date format", filename_template.check_date_format),
|
||||||
template = template.strip()
|
):
|
||||||
filename_template.check(template)
|
if name in changes:
|
||||||
|
if not isinstance(changes[name], str):
|
||||||
|
raise ValueError(f"{label} must be text")
|
||||||
|
texts[name] = changes[name].strip()
|
||||||
|
check(texts[name])
|
||||||
warnings = fff.check_personal_ini(personal_ini) if personal_ini is not None else []
|
warnings = fff.check_personal_ini(personal_ini) if personal_ini is not None else []
|
||||||
|
|
||||||
released = False
|
released = False
|
||||||
@@ -437,8 +442,9 @@ def save_settings(**changes: Any) -> dict[str, Any]:
|
|||||||
store.set_setting(None, name, True if value else None)
|
store.set_setting(None, name, True if value else None)
|
||||||
for name, number in numbers.items():
|
for name, number in numbers.items():
|
||||||
store.set_setting(None, name, number)
|
store.set_setting(None, name, number)
|
||||||
if template is not None:
|
# An empty value restores the default.
|
||||||
store.set_setting(None, "download_template", template or None)
|
for name, text in texts.items():
|
||||||
|
store.set_setting(None, name, text or None)
|
||||||
if personal_ini is not None:
|
if personal_ini is not None:
|
||||||
# Write a new file, then replace the old one, so a job never reads half a file.
|
# Write a new file, then replace the old one, so a job never reads half a file.
|
||||||
partial = config.PERSONAL_INI.with_name("personal.ini.part")
|
partial = config.PERSONAL_INI.with_name("personal.ini.part")
|
||||||
@@ -648,8 +654,14 @@ def download_fields() -> dict[str, str]:
|
|||||||
return fields
|
return fields
|
||||||
|
|
||||||
|
|
||||||
def _download_template() -> str:
|
DOWNLOAD_DEFAULTS = {
|
||||||
return store.get_setting(None, "download_template") or filename_template.DEFAULT_TEMPLATE
|
"download_template": filename_template.DEFAULT_TEMPLATE,
|
||||||
|
"download_date_format": filename_template.DEFAULT_DATE_FORMAT,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _download_setting(name: str) -> str:
|
||||||
|
return store.get_setting(None, name) or DOWNLOAD_DEFAULTS[name]
|
||||||
|
|
||||||
|
|
||||||
def _download_name(story: dict[str, Any]) -> str:
|
def _download_name(story: dict[str, Any]) -> str:
|
||||||
@@ -663,7 +675,8 @@ 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:
|
||||||
name = filename_template.file_name(_download_template(), filename_template.prepare(meta, book_id))
|
values = filename_template.prepare(meta, book_id, date_format=_download_setting("download_date_format"))
|
||||||
|
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}")
|
||||||
return f"{name or sanitize_filename(story['title'])}.epub"
|
return f"{name or sanitize_filename(story['title'])}.epub"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { globalRulesCard } from './rules.js';
|
|||||||
const INI_DOCS = 'https://github.com/JimmXinu/FanFicFare/wiki/INI-File';
|
const INI_DOCS = 'https://github.com/JimmXinu/FanFicFare/wiki/INI-File';
|
||||||
const DEFAULTS_INI = 'https://github.com/JimmXinu/FanFicFare/blob/main/fanficfare/defaults.ini';
|
const DEFAULTS_INI = 'https://github.com/JimmXinu/FanFicFare/blob/main/fanficfare/defaults.ini';
|
||||||
const TEMPLATE_DOCS = 'https://manual.calibre-ebook.com/template_lang.html';
|
const TEMPLATE_DOCS = 'https://manual.calibre-ebook.com/template_lang.html';
|
||||||
|
const DATE_CODES = 'https://docs.python.org/3/library/datetime.html#format-codes';
|
||||||
const code = (text) => h('code', {}, text);
|
const code = (text) => h('code', {}, text);
|
||||||
|
|
||||||
export function settingsPage(main, _params, config) {
|
export function settingsPage(main, _params, config) {
|
||||||
@@ -46,6 +47,14 @@ export function settingsPage(main, _params, config) {
|
|||||||
spellcheck: 'false',
|
spellcheck: 'false',
|
||||||
autocomplete: 'off',
|
autocomplete: 'off',
|
||||||
});
|
});
|
||||||
|
const dateFormat = h('input', {
|
||||||
|
id: 'download-date-format',
|
||||||
|
class: 'template',
|
||||||
|
type: 'text',
|
||||||
|
maxlength: 100,
|
||||||
|
spellcheck: 'false',
|
||||||
|
autocomplete: 'off',
|
||||||
|
});
|
||||||
const hoursField = (id) => h('input', { id, type: 'number', min: 0, max: config.job_list_max_hours, step: 1 });
|
const hoursField = (id) => h('input', { id, type: 'number', min: 0, max: config.job_list_max_hours, step: 1 });
|
||||||
const countField = (id) => h('input', { id, type: 'number', min: 0, max: config.job_list_max_count, step: 1 });
|
const countField = (id) => h('input', { id, type: 'number', min: 0, max: config.job_list_max_count, step: 1 });
|
||||||
const storyJobsHours = hoursField('story-jobs-hours');
|
const storyJobsHours = hoursField('story-jobs-hours');
|
||||||
@@ -85,6 +94,7 @@ export function settingsPage(main, _params, config) {
|
|||||||
showUploadSettings(settings);
|
showUploadSettings(settings);
|
||||||
ini.value = settings.personal_ini;
|
ini.value = settings.personal_ini;
|
||||||
template.value = settings.download_template;
|
template.value = settings.download_template;
|
||||||
|
dateFormat.value = settings.download_date_format;
|
||||||
storyJobsHours.value = settings.story_jobs_hours;
|
storyJobsHours.value = settings.story_jobs_hours;
|
||||||
storyJobsCount.value = settings.story_jobs_count;
|
storyJobsCount.value = settings.story_jobs_count;
|
||||||
mainJobsHours.value = settings.main_jobs_hours;
|
mainJobsHours.value = settings.main_jobs_hours;
|
||||||
@@ -168,6 +178,15 @@ export function settingsPage(main, _params, config) {
|
|||||||
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".',
|
||||||
),
|
),
|
||||||
|
h('div', { class: 'controls' }, h('label', { for: 'download-date-format' }, 'Date format'), dateFormat),
|
||||||
|
h(
|
||||||
|
'p',
|
||||||
|
{ class: 'hint' },
|
||||||
|
'The form of the dates in {pubdate}, {timestamp} and {last_modified}. %d gives the day, %b the month name, ' +
|
||||||
|
'%m the month number and %Y the year. See the ',
|
||||||
|
externalLink(DATE_CODES, 'list of all codes'),
|
||||||
|
'. To go back to the default, %b, %Y, clear the field and save. The default gives "May, 2020".',
|
||||||
|
),
|
||||||
h('p', { class: 'hint' }, 'You can use these fields:'),
|
h('p', { class: 'hint' }, 'You can use these fields:'),
|
||||||
h(
|
h(
|
||||||
'dl',
|
'dl',
|
||||||
@@ -211,9 +230,10 @@ export function settingsPage(main, _params, config) {
|
|||||||
h('li', {}, 'If the template fails or gives no name for a book, Bindery names the file after the title.'),
|
h('li', {}, 'If the template fails or gives no name for a book, Bindery names the file after the title.'),
|
||||||
),
|
),
|
||||||
saveButton(
|
saveButton(
|
||||||
() => ({ download_template: template.value }),
|
() => ({ download_template: template.value, download_date_format: dateFormat.value }),
|
||||||
(settings) => {
|
(settings) => {
|
||||||
template.value = settings.download_template;
|
template.value = settings.download_template;
|
||||||
|
dateFormat.value = settings.download_date_format;
|
||||||
toast('Saved');
|
toast('Saved');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
+10
-3
@@ -110,18 +110,25 @@ test('the jobs list settings load and save all four fields', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('the download template loads, lists its fields, and saves the stored form', async () => {
|
test('the download template loads, lists its fields, and saves the stored form', async () => {
|
||||||
api.on('GET', 'settings', { recent_chapters: 0, personal_ini: '', download_template: '{title}' });
|
api.on('GET', 'settings', {
|
||||||
|
recent_chapters: 0, personal_ini: '', download_template: '{title}', download_date_format: '%b, %Y',
|
||||||
|
});
|
||||||
settingsPage(main, {}, config);
|
settingsPage(main, {}, config);
|
||||||
await settle();
|
await settle();
|
||||||
const card = section(main, 'Book Downloads');
|
const card = section(main, 'Book Downloads');
|
||||||
assert.equal(field(main, 'download-template').value, '{title}');
|
assert.equal(field(main, 'download-template').value, '{title}');
|
||||||
|
assert.equal(field(main, 'download-date-format').value, '%b, %Y');
|
||||||
assert.ok(find(card, 'code', '{id}'));
|
assert.ok(find(card, 'code', '{id}'));
|
||||||
assert.match(card.textContent, /\{title\}The title\{id\}The story ID/);
|
assert.match(card.textContent, /\{title\}The title\{id\}The story ID/);
|
||||||
field(main, 'download-template').value = ' {title} - {authors} ';
|
field(main, 'download-template').value = ' {title} - {authors} ';
|
||||||
api.on('PUT', 'settings', { download_template: '{title} - {authors}' });
|
field(main, 'download-date-format').value = '';
|
||||||
|
api.on('PUT', 'settings', { download_template: '{title} - {authors}', download_date_format: '%b, %Y' });
|
||||||
await click(card, 'Save');
|
await click(card, 'Save');
|
||||||
assert.deepEqual(api.sent('PUT', 'settings')[0].body, { download_template: ' {title} - {authors} ' });
|
assert.deepEqual(api.sent('PUT', 'settings')[0].body, {
|
||||||
|
download_template: ' {title} - {authors} ', download_date_format: '',
|
||||||
|
});
|
||||||
assert.equal(field(main, 'download-template').value, '{title} - {authors}');
|
assert.equal(field(main, 'download-template').value, '{title} - {authors}');
|
||||||
|
assert.equal(field(main, 'download-date-format').value, '%b, %Y');
|
||||||
api.on('PUT', 'settings', { error: 'Bindery does not support template functions' }, 400);
|
api.on('PUT', 'settings', { error: 'Bindery does not support template functions' }, 400);
|
||||||
field(main, 'download-template').value = '{title:uppercase()}';
|
field(main, 'download-template').value = '{title:uppercase()}';
|
||||||
await click(card, 'Save');
|
await click(card, 'Save');
|
||||||
|
|||||||
@@ -473,6 +473,7 @@ def test_settings(client, monkeypatch, tmp_path):
|
|||||||
"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} - {authors}",
|
||||||
|
"download_date_format": "%b, %Y",
|
||||||
"story_jobs_hours": 36,
|
"story_jobs_hours": 36,
|
||||||
"story_jobs_count": 10,
|
"story_jobs_count": 10,
|
||||||
"main_jobs_hours": 6,
|
"main_jobs_hours": 6,
|
||||||
|
|||||||
@@ -174,6 +174,24 @@ def test_download_template_names_the_file(client, story):
|
|||||||
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)
|
||||||
|
|
||||||
|
|
||||||
|
def test_date_format_of_download_names(client, story):
|
||||||
|
sid, _ = story
|
||||||
|
o.store.update_story(sid, metahtml=metahtml(), site_values={"pubdate": "2020-05-06T00:00:00"})
|
||||||
|
client.put("/api/settings", json={"download_template": "{title} {pubdate}"}, headers=H)
|
||||||
|
assert 'filename="Story May, 2020.epub"' in download_name(client, sid)
|
||||||
|
saved = client.put("/api/settings", json={"download_date_format": " %Y-%m-%d "}, headers=H).json
|
||||||
|
assert saved["download_date_format"] == "%Y-%m-%d"
|
||||||
|
assert "filename=Story 2020-05-06.epub" in download_name(client, sid).replace('"', "")
|
||||||
|
response = client.put("/api/settings", json={"download_date_format": 5}, headers=H)
|
||||||
|
assert (response.status_code, response.json) == (400, {"error": "The date format must be text"})
|
||||||
|
response = client.put("/api/settings", json={"download_date_format": "%Y" * 51}, headers=H)
|
||||||
|
assert (response.status_code, response.json) == (400, {"error": "A date format can have 100 characters at most"})
|
||||||
|
# An empty format restores the default.
|
||||||
|
assert client.put("/api/settings", json={"download_date_format": ""}, headers=H).json["download_date_format"] == (
|
||||||
|
"%b, %Y"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_download_template_that_fails_gives_the_title(client, story, caplog):
|
def test_download_template_that_fails_gives_the_title(client, story, caplog):
|
||||||
sid, _ = story
|
sid, _ = story
|
||||||
o.store.update_story(sid, metahtml=metahtml())
|
o.store.update_story(sid, metahtml=metahtml())
|
||||||
|
|||||||
@@ -91,6 +91,16 @@ def test_values_follow_the_rules_of_calibre():
|
|||||||
"Dec, 1999 Oct, 2026"
|
"Dec, 1999 Oct, 2026"
|
||||||
)
|
)
|
||||||
assert expand("{pubdate}", pubdate="not a date") == ""
|
assert expand("{pubdate}", pubdate="not a date") == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_date_format():
|
||||||
|
# Calibre's helper_date reference case.
|
||||||
|
assert ft.expand("{pubdate}", ft.prepare(BOOK, 42, date_format="%b %Y")) == "May 2020"
|
||||||
|
assert ft.expand("{pubdate}", ft.prepare(BOOK, 42, date_format="%d/%m/%Y")) == "06_05_2020"
|
||||||
|
assert ft.expand("x{pubdate}x", ft.prepare(BOOK, 42, date_format="")) == "xx"
|
||||||
|
ft.check_date_format("%Y-%m-%d " * 10)
|
||||||
|
with pytest.raises(ft.TemplateError, match="A date format can have 100 characters at most"):
|
||||||
|
ft.check_date_format("x" * 101)
|
||||||
# A series without a number has no series_index, and a book without a series has neither.
|
# A series without a number has no series_index, and a book without a series has neither.
|
||||||
assert expand("{series}{series_index:| [|]}", series_index=None) == "Saga, The"
|
assert expand("{series}{series_index:| [|]}", series_index=None) == "Saga, The"
|
||||||
assert ft.expand("{series_index:03d}|{series}", ft.prepare(NO_SERIES, 42)) == "|"
|
assert ft.expand("{series_index:03d}|{series}", ft.prepare(NO_SERIES, 42)) == "|"
|
||||||
|
|||||||
Reference in New Issue
Block a user