mirror of
https://github.com/JockeTF/fimfarchive.git
synced 2025-02-15 09:44:22 +01:00
Use image only cover for advent calendar
This commit is contained in:
parent
b7eb3e5139
commit
c8722a570a
2 changed files with 116 additions and 97 deletions
|
@ -3,15 +3,20 @@
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from math import ceil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Iterator, override
|
from typing import Iterator
|
||||||
from xml.dom import minidom
|
from xml.dom import minidom
|
||||||
|
from xml.dom.minidom import Document
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
from requests import get
|
||||||
|
|
||||||
from fimfarchive.converters import Converter
|
from fimfarchive.converters import Converter
|
||||||
from fimfarchive.fetchers import FimfarchiveFetcher
|
from fimfarchive.fetchers import FimfarchiveFetcher
|
||||||
from fimfarchive.stories import Story
|
|
||||||
from fimfarchive.mappers import StorySlugMapper
|
from fimfarchive.mappers import StorySlugMapper
|
||||||
|
from fimfarchive.stories import Story
|
||||||
from fimfarchive.writers import DirectoryWriter
|
from fimfarchive.writers import DirectoryWriter
|
||||||
|
|
||||||
from .base import Command
|
from .base import Command
|
||||||
|
@ -19,27 +24,10 @@ from .base import Command
|
||||||
dt = datetime.fromisoformat
|
dt = datetime.fromisoformat
|
||||||
|
|
||||||
|
|
||||||
DATE_START = dt("2023-12-01 00:00:00Z")
|
DATE_START = dt("2023-12-01T00:00:00+00:00")
|
||||||
DATE_STOP = dt("2024-01-01 00:00:00Z")
|
DATE_STOP = dt("2024-01-01T00:00:00+00:00")
|
||||||
TARGET_AUTHOR = 46322
|
TARGET_AUTHOR = 46322
|
||||||
|
|
||||||
COVER_PAGE = """
|
|
||||||
<?xml version='1.0' encoding='utf-8'?>
|
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
|
|
||||||
<head>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
|
||||||
<meta name="calibre:cover" content="true"/>
|
|
||||||
<title>Cover</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<center>
|
|
||||||
<h1>Title</h1>
|
|
||||||
<img src="cover.png"/>
|
|
||||||
</center>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
""".lstrip()
|
|
||||||
|
|
||||||
COVER_IMAGES = [
|
COVER_IMAGES = [
|
||||||
"https://derpicdn.net/img/view/2015/12/1/1034522.png",
|
"https://derpicdn.net/img/view/2015/12/1/1034522.png",
|
||||||
"https://derpicdn.net/img/view/2015/12/2/1035253.png",
|
"https://derpicdn.net/img/view/2015/12/2/1035253.png",
|
||||||
|
@ -71,77 +59,23 @@ COVER_IMAGES = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class CoverPage:
|
def day(story: Story) -> int:
|
||||||
|
"""
|
||||||
def __init__(self, story: Story) -> None:
|
Returns the day of publishing.
|
||||||
published = dt(story.meta["date_published"])
|
"""
|
||||||
self.day = published.day
|
return dt(story.meta["date_published"]).day
|
||||||
|
|
||||||
def get_cover(self) -> bytes:
|
|
||||||
if not (url := COVER_IMAGES[self.day - 1]):
|
|
||||||
raise ValueError("Missing cover")
|
|
||||||
|
|
||||||
_, name = url.rsplit("/", 1)
|
|
||||||
path = Path(f"covers/{name}")
|
|
||||||
|
|
||||||
return path.read_bytes()
|
|
||||||
|
|
||||||
def get_title(self) -> bytes:
|
|
||||||
dom = minidom.parseString(COVER_PAGE)
|
|
||||||
(title,) = dom.getElementsByTagName("h1")
|
|
||||||
(text,) = title.childNodes
|
|
||||||
|
|
||||||
text.replaceWholeText(f"Advent {self.day:02}")
|
|
||||||
|
|
||||||
return dom.toprettyxml().encode()
|
|
||||||
|
|
||||||
|
|
||||||
class AdventConverter(Converter):
|
class AdventConverter(Converter):
|
||||||
"""
|
"""
|
||||||
Replaces titles with advent dates.
|
Base class for modifying story meta and data.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_title(self, story: Story) -> str:
|
def handle_opf(self, story: Story, dom: Document):
|
||||||
published = dt(story.meta["date_published"])
|
pass
|
||||||
|
|
||||||
return f"Advent {published.day:02}"
|
def handle_zip(self, story: Story, arc: ZipFile):
|
||||||
|
pass
|
||||||
def get_opf(self, story: Story, data: bytes) -> bytes:
|
|
||||||
dom = minidom.parseString(data)
|
|
||||||
(package,) = dom.getElementsByTagName("package")
|
|
||||||
(manifest,) = dom.getElementsByTagName("manifest")
|
|
||||||
(spine,) = dom.getElementsByTagName("spine")
|
|
||||||
(title,) = dom.getElementsByTagName("dc:title")
|
|
||||||
|
|
||||||
(text,) = title.childNodes
|
|
||||||
text.replaceWholeText(self.get_title(story))
|
|
||||||
|
|
||||||
index = manifest.firstChild
|
|
||||||
item = dom.createElement("item")
|
|
||||||
item.setAttribute("id", "cover")
|
|
||||||
item.setAttribute("href", "cover.png")
|
|
||||||
item.setAttribute("media-type", "image/png")
|
|
||||||
manifest.insertBefore(item, index)
|
|
||||||
item = dom.createElement("item")
|
|
||||||
item.setAttribute("id", "title")
|
|
||||||
item.setAttribute("href", "title.xhtml")
|
|
||||||
item.setAttribute("media-type", "application/xhtml+xml")
|
|
||||||
manifest.insertBefore(item, index)
|
|
||||||
|
|
||||||
index = spine.firstChild
|
|
||||||
item = dom.createElement("itemref")
|
|
||||||
item.setAttribute("idref", "title")
|
|
||||||
spine.insertBefore(item, index)
|
|
||||||
|
|
||||||
guide = dom.createElement("guide")
|
|
||||||
reference = dom.createElement("reference")
|
|
||||||
reference.setAttribute("type", "cover")
|
|
||||||
reference.setAttribute("href", "title.xhtml")
|
|
||||||
reference.setAttribute("title", "title")
|
|
||||||
guide.appendChild(reference)
|
|
||||||
package.appendChild(guide)
|
|
||||||
|
|
||||||
return dom.toprettyxml().encode()
|
|
||||||
|
|
||||||
def get_data(self, story: Story) -> bytes:
|
def get_data(self, story: Story) -> bytes:
|
||||||
buffer = BytesIO()
|
buffer = BytesIO()
|
||||||
|
@ -153,23 +87,19 @@ class AdventConverter(Converter):
|
||||||
data = source.read(info)
|
data = source.read(info)
|
||||||
|
|
||||||
if info.filename == "content.opf":
|
if info.filename == "content.opf":
|
||||||
data = self.get_opf(story, data)
|
dom = minidom.parseString(data)
|
||||||
|
self.handle_opf(story, dom)
|
||||||
|
data = dom.toprettyxml().encode()
|
||||||
|
|
||||||
target.writestr(info, data)
|
target.writestr(info, data)
|
||||||
|
|
||||||
cover = CoverPage(story)
|
self.handle_zip(story, target)
|
||||||
target.writestr("cover.png", cover.get_cover())
|
|
||||||
target.writestr("title.xhtml", cover.get_title())
|
|
||||||
|
|
||||||
return buffer.getvalue()
|
return buffer.getvalue()
|
||||||
|
|
||||||
def get_meta(self, story: Story) -> dict:
|
def get_meta(self, story: Story) -> dict:
|
||||||
meta = deepcopy(story.meta)
|
return deepcopy(story.meta)
|
||||||
meta["title"] = self.get_title(story)
|
|
||||||
|
|
||||||
return meta
|
|
||||||
|
|
||||||
@override
|
|
||||||
def __call__(self, story: Story) -> Story:
|
def __call__(self, story: Story) -> Story:
|
||||||
return story.merge(
|
return story.merge(
|
||||||
data=self.get_data(story),
|
data=self.get_data(story),
|
||||||
|
@ -177,9 +107,93 @@ class AdventConverter(Converter):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CoverConverter(AdventConverter):
|
||||||
|
"""
|
||||||
|
Adds advent cover by dm29.
|
||||||
|
"""
|
||||||
|
|
||||||
|
file_name = "cover.png"
|
||||||
|
|
||||||
|
def fetch(self, story: Story) -> bytes:
|
||||||
|
if not (url := COVER_IMAGES[day(story) - 1]):
|
||||||
|
raise ValueError("Missing cover")
|
||||||
|
|
||||||
|
_, name = url.rsplit("/", 1)
|
||||||
|
path = Path(f"covers/{name}")
|
||||||
|
|
||||||
|
if not path.is_file():
|
||||||
|
return get(url).content
|
||||||
|
|
||||||
|
return path.read_bytes()
|
||||||
|
|
||||||
|
def draw(self, story: Story) -> bytes:
|
||||||
|
# Load cover art
|
||||||
|
data = self.fetch(story)
|
||||||
|
art = Image.open(BytesIO(data))
|
||||||
|
|
||||||
|
# Create cover image
|
||||||
|
height = ceil(art.width * 1.6)
|
||||||
|
cover = Image.new("RGB", (art.width, height), "lightgray")
|
||||||
|
cover.paste(art, (0, height - art.height))
|
||||||
|
|
||||||
|
# Initialize draw tool
|
||||||
|
draw = ImageDraw.Draw(cover)
|
||||||
|
font = ImageFont.load_default(height // 12)
|
||||||
|
|
||||||
|
# Draw story number
|
||||||
|
ident = f"{story.key}"
|
||||||
|
_, _, tw, th = draw.textbbox((0, 0), ident, font)
|
||||||
|
ts = ((art.width - tw) / 2, (height - art.height - th) / 2 - th / 8)
|
||||||
|
draw.text(ts, ident, "black", font)
|
||||||
|
|
||||||
|
# Draw calendar date
|
||||||
|
title = f"Advent {day(story):02}"
|
||||||
|
_, _, tw, th = draw.textbbox((0, 0), title, font)
|
||||||
|
ts = ((art.width - tw) / 2, (ts[1] + th + th / 4))
|
||||||
|
draw.text(ts, title, "black", font)
|
||||||
|
|
||||||
|
# Render image
|
||||||
|
buffer = BytesIO()
|
||||||
|
cover.save(buffer, "png")
|
||||||
|
|
||||||
|
return buffer.getvalue()
|
||||||
|
|
||||||
|
def handle_opf(self, story: Story, dom: Document):
|
||||||
|
(manifest,) = dom.getElementsByTagName("manifest")
|
||||||
|
|
||||||
|
cover = dom.createElement("item")
|
||||||
|
cover.setAttribute("id", "cover")
|
||||||
|
cover.setAttribute("href", self.file_name)
|
||||||
|
cover.setAttribute("media-type", "image/png")
|
||||||
|
|
||||||
|
manifest.insertBefore(cover, manifest.firstChild)
|
||||||
|
|
||||||
|
def handle_zip(self, story: Story, arc: ZipFile):
|
||||||
|
arc.writestr(self.file_name, self.draw(story))
|
||||||
|
|
||||||
|
|
||||||
|
class TitleConverter(AdventConverter):
|
||||||
|
"""
|
||||||
|
Replaces title with advent date.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def handle_opf(self, story: Story, dom: Document):
|
||||||
|
title = f"Advent {day(story):02} - {story.key}"
|
||||||
|
(node,) = dom.getElementsByTagName("dc:title")
|
||||||
|
(text,) = node.childNodes
|
||||||
|
|
||||||
|
text.replaceWholeText(title)
|
||||||
|
|
||||||
|
def get_meta(self, story: Story) -> dict:
|
||||||
|
meta = super().get_meta(story)
|
||||||
|
meta["title"] = f"Advent {day(story):02}"
|
||||||
|
|
||||||
|
return meta
|
||||||
|
|
||||||
|
|
||||||
class AdventCommand(Command):
|
class AdventCommand(Command):
|
||||||
"""
|
"""
|
||||||
Creates an advent calendar.
|
Creates advent calendar.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def filter(self, fetcher: FimfarchiveFetcher) -> Iterator[Story]:
|
def filter(self, fetcher: FimfarchiveFetcher) -> Iterator[Story]:
|
||||||
|
@ -197,11 +211,15 @@ class AdventCommand(Command):
|
||||||
(archive,) = args
|
(archive,) = args
|
||||||
|
|
||||||
slug = StorySlugMapper()
|
slug = StorySlugMapper()
|
||||||
convert = AdventConverter()
|
|
||||||
fetcher = FimfarchiveFetcher(archive)
|
fetcher = FimfarchiveFetcher(archive)
|
||||||
writer = DirectoryWriter(data_path=slug)
|
writer = DirectoryWriter(data_path=slug)
|
||||||
|
|
||||||
|
convert_cover = CoverConverter()
|
||||||
|
convert_title = TitleConverter()
|
||||||
|
|
||||||
for story in self.filter(fetcher):
|
for story in self.filter(fetcher):
|
||||||
writer.write(convert(story))
|
story = convert_cover(story)
|
||||||
|
story = convert_title(story)
|
||||||
|
writer.write(story)
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
|
@ -11,6 +11,7 @@ dependencies = [
|
||||||
"jinja2~=3.1",
|
"jinja2~=3.1",
|
||||||
"jmespath~=1.0",
|
"jmespath~=1.0",
|
||||||
"jsonapi-client",
|
"jsonapi-client",
|
||||||
|
"pillow~=10.4.0",
|
||||||
"requests~=2.32",
|
"requests~=2.32",
|
||||||
"tqdm~=4.66",
|
"tqdm~=4.66",
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in a new issue