diff --git a/fimfarchive/commands/__init__.py b/fimfarchive/commands/__init__.py
index 47e813d..3b69d28 100644
--- a/fimfarchive/commands/__init__.py
+++ b/fimfarchive/commands/__init__.py
@@ -22,6 +22,7 @@
#
+from .advent import AdventCommand
from .base import Command
from .build import BuildCommand
from .root import RootCommand
@@ -31,6 +32,7 @@
__all__ = (
'Command',
'RootCommand',
+ 'AdventCommand',
'BuildCommand',
'UpdateCommand',
)
diff --git a/fimfarchive/commands/advent.py b/fimfarchive/commands/advent.py
new file mode 100644
index 0000000..5fb79c7
--- /dev/null
+++ b/fimfarchive/commands/advent.py
@@ -0,0 +1,204 @@
+# /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 = """
+
+
+
+
+
+ Cover
+
+
+
+ Title
+
+
+
+
+""".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),
+ )
+
+
+def filter_stories(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
+
+
+class AdventCommand(Command):
+ def __call__(self, *args: str) -> int:
+ (archive,) = args
+
+ slug = StorySlugMapper()
+ convert = AdventConverter()
+ fetcher = FimfarchiveFetcher(archive)
+ writer = DirectoryWriter(data_path=slug)
+
+ for story in filter_stories(fetcher):
+ writer.write(convert(story))
+
+ return 0
diff --git a/fimfarchive/commands/root.py b/fimfarchive/commands/root.py
index cf92524..5e6b12f 100644
--- a/fimfarchive/commands/root.py
+++ b/fimfarchive/commands/root.py
@@ -24,6 +24,7 @@
from typing import Dict, Type
+from .advent import AdventCommand
from .base import Command
from .build import BuildCommand
from .update import UpdateCommand
@@ -39,6 +40,7 @@ class RootCommand(Command):
The main application command.
"""
commands: Dict[str, Type[Command]] = {
+ 'advent': AdventCommand,
'build': BuildCommand,
'update': UpdateCommand,
}