"""Build an epub from stored data without network access, then read it back.""" import base64 import datetime import re import zipfile import pytest from defusedxml import ElementTree from bindery import config, fff from bindery.rules import compile_rules, normalize_rules URL = "https://www.royalroad.com/fiction/1234/test-story" PNG = base64.b64decode( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" ) IMG_URL = "https://www.royalroadcdn.com/public/img.png" COVER = {"url": "https://www.royalroadcdn.com/public/covers/1234.png", "ext": "png", "mime": "image/png", "data": PNG} def metahtml(url=URL): adapter = fff.get_adapter(url) story = adapter.story story.setMetadata("title", "Test Story") story.addToList("author", "Writer") story.addToList("authorId", "42") story.setMetadata("description", "

About the story

") story.setMetadata("datePublished", datetime.datetime(2024, 1, 2)) story.setMetadata("dateUpdated", datetime.datetime(2024, 3, 4)) story.addToList("genre", "Fantasy") return story.dump_html_metadata() def chapters(): base = "https://www.royalroad.com/fiction/1234/test-story/chapter/{0}/c{0}" return [ {"url": base.format(10), "title": "One", "html": '

Zorian wakes.

'}, {"url": base.format(11), "title": "Two", "html": f'

Picture

'}, {"url": base.format(15), "title": "Three", "html": "

Zorian again.

"}, ] def test_build_and_read_back(tmp_path): rules = compile_rules(normalize_rules([{"pattern": "Zorian", "replacement": "Z"}])) out = tmp_path / "book.epub" images = [{"url": IMG_URL, "ext": "png", "mime": "image/png", "data": PNG}] meta = fff.build_epub(URL, metahtml(), chapters(), images, None, rules, {"title": "Kept Title"}, out) assert meta["title"] == "Kept Title" assert meta["authors"] == ["Writer"] assert meta["pubdate"].startswith("2024-01-02") with zipfile.ZipFile(out) as z: names = z.namelist() text = "".join(z.read(n).decode("utf-8", "ignore") for n in names if n.endswith(".xhtml")) opf = z.read("content.opf").decode() assert "Z wakes." in text and "Zorian" not in text assert any(n.startswith("OEBPS/images/ffdl-") and n.endswith(".png") for n in names) assert re.search(r"]*>Kept Title", opf) contents = fff.read_epub(out) assert [c["title"] for c in contents.chapters] == ["One", "Two", "Three"] assert [c["url"] for c in contents.chapters] == [c["url"] for c in chapters()] assert IMG_URL in contents.chapters[1]["html"] assert contents.images and contents.images[0]["url"] == IMG_URL def test_a_title_with_entities_builds_and_imports_in_fanficfare_form(tmp_path): # FanFicFare's form, as the site titles in the store have it. title = "Folly & Magic <1>" out = tmp_path / "book.epub" fff.build_epub(URL, metahtml(), [dict(chapters()[0], title=title)], [], None, [], {}, out) with zipfile.ZipFile(out) as z: for name in z.namelist(): if name.endswith((".xhtml", ".ncx", ".opf")): ElementTree.fromstring(z.read(name)) # An import stores the same title as a download, so the next update finds no rename. assert fff.read_epub(out).chapters[0]["title"] == title assert fff.unescape_title(title) == "Folly & Magic <1>" assert fff.escape_title("Folly & Magic <1>") == title @pytest.mark.parametrize( ("personal_ini", "expected"), [ ("", "coveronly"), ("[www.royalroad.com]\ninclude_images:false\n", "coveronly"), ("[defaults]\ninclude_images:true\n", "true"), ("[defaults]\ninclude_images:coveronly\n", "coveronly"), ], ) def test_include_images_is_never_off(personal_ini, expected, tmp_path, monkeypatch): path = tmp_path / "personal.ini" path.write_text(personal_ini) monkeypatch.setattr(config, "PERSONAL_INI", path) assert fff.get_configuration(URL).getConfig("include_images") == expected @pytest.mark.parametrize("personal_ini", ["", "[defaults]\ninclude_images:false\n"]) def test_the_cover_goes_in_the_epub_without_include_images(personal_ini, tmp_path, monkeypatch): path = tmp_path / "personal.ini" path.write_text(personal_ini) monkeypatch.setattr(config, "PERSONAL_INI", path) out = tmp_path / "book.epub" fff.build_epub(URL, metahtml(), chapters(), [], COVER, [], {}, out) with zipfile.ZipFile(out) as z: names = z.namelist() opf = z.read("content.opf").decode() assert "OEBPS/cover.xhtml" in names assert any(n.startswith("OEBPS/images/cover.") for n in names) assert re.search(r'Kept blurb

", "series": ["Kept Saga", 3.0], "pubdate": "2019-05-06T00:00:00+00:00", "languages": "de", "tags": ["mine", "reread"], } meta = fff.build_epub(URL, metahtml(), chapters(), [], None, [], overrides, out) assert meta["authors"] == ["Pen Name", "Co Writer"] assert meta["tags"] == ["mine", "reread"] with zipfile.ZipFile(out) as z: opf = z.read("content.opf").decode() assert re.findall(r"]*>([^<]*)", opf) == ["Pen Name", "Co Writer"] assert re.findall(r"([^<]*)", opf) == ["mine", "reread"] assert 'name="calibre:series" content="Kept Saga"' in opf assert 'name="calibre:series_index" content="3.00"' in opf assert "de" in opf assert "2019-05-06" in opf assert "Kept blurb" in opf def test_unlocked_fields_keep_site_metadata(tmp_path): out = tmp_path / "book.epub" meta = fff.build_epub(URL, metahtml(), chapters(), [], None, [], {}, out) assert meta["title"] == "Test Story" assert meta["authors"] == ["Writer"] assert "Fantasy" in meta["tags"] def test_supported_sites(): assert fff.is_supported_site("www.royalroad.com") assert fff.is_supported_site("WWW.RoyalRoad.com ") assert not fff.is_supported_site("example.com") def test_normalize_url(): assert fff.normalize_url(URL) == ("https://www.royalroad.com/fiction/1234", "www.royalroad.com") assert fff.normalize_url("https://example.com/nothing") is None