mirror of
https://github.com/JockeTF/fimfarchive.git
synced 2025-03-21 10:47:11 +01:00
Add command for creating advent calendar
This commit is contained in:
parent
e9ca466da8
commit
b7eb3e5139
3 changed files with 211 additions and 0 deletions
|
@ -22,6 +22,7 @@ Command module.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
|
from .advent import AdventCommand
|
||||||
from .base import Command
|
from .base import Command
|
||||||
from .build import BuildCommand
|
from .build import BuildCommand
|
||||||
from .root import RootCommand
|
from .root import RootCommand
|
||||||
|
@ -31,6 +32,7 @@ from .update import UpdateCommand
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Command',
|
'Command',
|
||||||
'RootCommand',
|
'RootCommand',
|
||||||
|
'AdventCommand',
|
||||||
'BuildCommand',
|
'BuildCommand',
|
||||||
'UpdateCommand',
|
'UpdateCommand',
|
||||||
)
|
)
|
||||||
|
|
207
fimfarchive/commands/advent.py
Normal file
207
fimfarchive/commands/advent.py
Normal file
|
@ -0,0 +1,207 @@
|
||||||
|
# /usr/bin/env python3
|
||||||
|
|
||||||
|
from copy import deepcopy
|
||||||
|
from datetime import datetime
|
||||||
|
from io import BytesIO
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Iterator, override
|
||||||
|
from xml.dom import minidom
|
||||||
|
from zipfile import ZipFile
|
||||||
|
|
||||||
|
from fimfarchive.converters import Converter
|
||||||
|
from fimfarchive.fetchers import FimfarchiveFetcher
|
||||||
|
from fimfarchive.stories import Story
|
||||||
|
from fimfarchive.mappers import StorySlugMapper
|
||||||
|
from fimfarchive.writers import DirectoryWriter
|
||||||
|
|
||||||
|
from .base import Command
|
||||||
|
|
||||||
|
dt = datetime.fromisoformat
|
||||||
|
|
||||||
|
|
||||||
|
DATE_START = dt("2023-12-01 00:00:00Z")
|
||||||
|
DATE_STOP = dt("2024-01-01 00:00:00Z")
|
||||||
|
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 = [
|
||||||
|
"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/3/1035985.png",
|
||||||
|
"https://derpicdn.net/img/view/2015/12/4/1036703.png",
|
||||||
|
"https://derpicdn.net/img/view/2015/12/5/1037293.png",
|
||||||
|
"https://derpicdn.net/img/view/2015/12/6/1038118.png",
|
||||||
|
"https://derpicdn.net/img/view/2015/12/7/1039018.png",
|
||||||
|
"https://derpicdn.net/img/view/2015/12/8/1039677.png",
|
||||||
|
"https://derpicdn.net/img/view/2015/12/9/1040332.png",
|
||||||
|
"https://derpicdn.net/img/view/2015/12/10/1041053.png",
|
||||||
|
"https://derpicdn.net/img/view/2015/12/11/1041870.png",
|
||||||
|
"https://derpicdn.net/img/view/2015/12/12/1042606.png",
|
||||||
|
"https://derpicdn.net/img/view/2015/12/13/1043527.png",
|
||||||
|
"https://derpicdn.net/img/view/2015/12/14/1044284.png",
|
||||||
|
"https://derpicdn.net/img/view/2015/12/15/1044966.png",
|
||||||
|
"https://derpicdn.net/img/view/2015/12/16/1045571.png",
|
||||||
|
"https://derpicdn.net/img/view/2015/12/17/1046350.png",
|
||||||
|
"https://derpicdn.net/img/view/2015/12/18/1046935.png",
|
||||||
|
"https://derpicdn.net/img/view/2015/12/19/1047728.png",
|
||||||
|
"https://derpicdn.net/img/view/2015/12/20/1048773.png",
|
||||||
|
"https://derpicdn.net/img/view/2015/12/21/1049626.png",
|
||||||
|
"https://derpicdn.net/img/view/2015/12/22/1050322.png",
|
||||||
|
"https://derpicdn.net/img/view/2015/12/23/1051004.png",
|
||||||
|
"https://derpicdn.net/img/view/2015/12/24/1051596.png",
|
||||||
|
"https://derpicdn.net/img/view/2015/12/25/1052173.png",
|
||||||
|
None,
|
||||||
|
"https://derpicdn.net/img/view/2017/12/22/1613311.png",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CoverPage:
|
||||||
|
|
||||||
|
def __init__(self, story: Story) -> None:
|
||||||
|
published = dt(story.meta["date_published"])
|
||||||
|
self.day = 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):
|
||||||
|
"""
|
||||||
|
Replaces titles with advent dates.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_title(self, story: Story) -> str:
|
||||||
|
published = dt(story.meta["date_published"])
|
||||||
|
|
||||||
|
return f"Advent {published.day:02}"
|
||||||
|
|
||||||
|
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:
|
||||||
|
buffer = BytesIO()
|
||||||
|
target = ZipFile(buffer, "w")
|
||||||
|
source = ZipFile(BytesIO(story.data), "r")
|
||||||
|
|
||||||
|
with source, target:
|
||||||
|
for info in source.infolist():
|
||||||
|
data = source.read(info)
|
||||||
|
|
||||||
|
if info.filename == "content.opf":
|
||||||
|
data = self.get_opf(story, data)
|
||||||
|
|
||||||
|
target.writestr(info, data)
|
||||||
|
|
||||||
|
cover = CoverPage(story)
|
||||||
|
target.writestr("cover.png", cover.get_cover())
|
||||||
|
target.writestr("title.xhtml", cover.get_title())
|
||||||
|
|
||||||
|
return buffer.getvalue()
|
||||||
|
|
||||||
|
def get_meta(self, story: Story) -> dict:
|
||||||
|
meta = deepcopy(story.meta)
|
||||||
|
meta["title"] = self.get_title(story)
|
||||||
|
|
||||||
|
return meta
|
||||||
|
|
||||||
|
@override
|
||||||
|
def __call__(self, story: Story) -> Story:
|
||||||
|
return story.merge(
|
||||||
|
data=self.get_data(story),
|
||||||
|
meta=self.get_meta(story),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AdventCommand(Command):
|
||||||
|
"""
|
||||||
|
Creates an advent calendar.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def filter(self, fetcher: FimfarchiveFetcher) -> Iterator[Story]:
|
||||||
|
for story in fetcher:
|
||||||
|
if story.meta["author"]["id"] != TARGET_AUTHOR:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not (published := story.meta["date_published"]):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if DATE_START < dt(published) < DATE_STOP:
|
||||||
|
yield story
|
||||||
|
|
||||||
|
def __call__(self, *args: str) -> int:
|
||||||
|
(archive,) = args
|
||||||
|
|
||||||
|
slug = StorySlugMapper()
|
||||||
|
convert = AdventConverter()
|
||||||
|
fetcher = FimfarchiveFetcher(archive)
|
||||||
|
writer = DirectoryWriter(data_path=slug)
|
||||||
|
|
||||||
|
for story in self.filter(fetcher):
|
||||||
|
writer.write(convert(story))
|
||||||
|
|
||||||
|
return 0
|
|
@ -24,6 +24,7 @@ Root command.
|
||||||
|
|
||||||
from typing import Dict, Type
|
from typing import Dict, Type
|
||||||
|
|
||||||
|
from .advent import AdventCommand
|
||||||
from .base import Command
|
from .base import Command
|
||||||
from .build import BuildCommand
|
from .build import BuildCommand
|
||||||
from .update import UpdateCommand
|
from .update import UpdateCommand
|
||||||
|
@ -39,6 +40,7 @@ class RootCommand(Command):
|
||||||
The main application command.
|
The main application command.
|
||||||
"""
|
"""
|
||||||
commands: Dict[str, Type[Command]] = {
|
commands: Dict[str, Type[Command]] = {
|
||||||
|
'advent': AdventCommand,
|
||||||
'build': BuildCommand,
|
'build': BuildCommand,
|
||||||
'update': UpdateCommand,
|
'update': UpdateCommand,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue