# Path: fimfarchive/exceptions.py
# class InvalidStoryError(FimfarchiveError):
#     """
#     Story does not exist.
#     """
# 
# Path: fimfarchive/flavors.py
# class DataFormat(Flavor):
#     """
#     Indicates the file format of story data.
#     """
#     EPUB = ()
#     FPUB = ()
#     HTML = ()
#     JSON = ()
# 
# class MetaFormat(Flavor):
#     """
#     Indicates the general structure of story meta.
#     """
#     ALPHA = ()
#     BETA = ()
# 
# Path: fimfarchive/stories.py
# class Story:
#     """
#     Represents a story.
#     """
# 
#     def __init__(
#             self,
#             key: int,
#             fetcher: Fetcher = None,
#             meta: Dict[str, Any] = None,
#             data: bytes = None,
#             flavors: Iterable[Flavor] = (),
#             ) -> None:
#         """
#         Constructor.
# 
#         Args:
#             key: Primary key of the story.
#             fetcher: Fetcher to use for lazy fetching.
#             meta: Meta to populate the story with.
#             data: Data to populate the story with.
#             flavors: Content type hints.
#         """
#         if fetcher is None and (data is None or meta is None):
#             raise ValueError("Story must contain fetcher if lazy.")
# 
#         self.key = key
#         self.fetcher = fetcher
#         self.flavors = set(flavors)
#         self._meta = meta
#         self._data = data
# 
#     @property
#     def is_fetched(self) -> bool:
#         """
#         True if no more fetches are necessary.
#         """
#         return self.has_meta and self.has_data
# 
#     @property
#     def has_meta(self) -> bool:
#         """
#         True if story meta has been fetched.
#         """
#         return self._meta is not None
# 
#     @property
#     def meta(self) -> Dict[str, Any]:
#         """
#         Returns the story meta.
# 
#         Meta may be fetched if this story instance is lazy.
# 
#         Raises:
#             InvalidStoryError: If a valid story is not found.
#             StorySourceError: If source does not return any data.
#         """
#         if self._meta is None and self.fetcher:
#             self._meta = self.fetcher.fetch_meta(self.key)
# 
#         if self._meta is None:
#             raise StorySourceError("Meta is missing.")
# 
#         return self._meta
# 
#     @property
#     def has_data(self) -> bool:
#         """
#         True if story data has been fetched.
#         """
#         return self._data is not None
# 
#     @property
#     def data(self) -> bytes:
#         """
#         Returns the story data.
# 
#         Data may be fetched if this story instance is lazy.
# 
#         Raises:
#             InvalidStoryError: If a valid story is not found.
#             StorySourceError: If source does not return any data.
#         """
#         if self._data is None and self.fetcher:
#             self._data = self.fetcher.fetch_data(self.key)
# 
#         if self._data is None:
#             raise StorySourceError("Data is missing.")
# 
#         return self._data
# 
#     def merge(self, **params) -> 'Story':
#         """
#         Returns a shallow copy, optionally replacing attributes.
# 
#         Args:
#             **params: Overrides parameters from the current instance.
# 
#         Raises:
#             TypeError: If passed an unexpected parameter.
#         """
#         kwargs = {k.lstrip('_'): v for k, v in vars(self).items()}
#         kwargs.update(params)
# 
#         return type(self)(**kwargs)
# 
# Path: fimfarchive/utils.py
# def find_flavor(story: Story, flavor: Type[F]) -> Optional[F]:
#     """
#     Searches for a flavor of a specific type.
# 
#     Args:
#         story: The story to search in.
#         flavor: The flavor type to find.
# 
#     Returns:
#         A flavor of the desired type, or None.
#     """
#     for current in story.flavors:
#         if isinstance(current, flavor):
#             return current
# 
#     return None


import string
from abc import abstractmethod
from html import unescape
from io import BytesIO
from pathlib import Path
from typing import Dict, Generic, Optional, Set, TypeVar, Union
from zipfile import ZipFile
from arrow import api as arrow, Arrow
from fimfarchive.exceptions import InvalidStoryError
from fimfarchive.flavors import DataFormat, MetaFormat
from fimfarchive.stories import Story
from fimfarchive.utils import find_flavor
            return f'{key}'

    def group(self, slug: str) -> str:
        """
        Returns the group for part of a path.

        Args:
            slug: The slug to group.

        Return:
            A group for the slug.
        """
        path = slug[:1]

        if path not in self.groupings:
            path = self.spacing

        return path

    def classify(self, story: Story) -> str:
        """
        Returns a file extension for the story.

        Args:
            story: The story to classify.

        Returns:
            A file extension.
        """
        for flavor in story.flavors:

# Path: fimfarchive/exceptions.py
# class InvalidStoryError(FimfarchiveError):
#     """
#     Story does not exist.
#     """
# 
# Path: fimfarchive/flavors.py
# class DataFormat(Flavor):
#     """
#     Indicates the file format of story data.
#     """
#     EPUB = ()
#     FPUB = ()
#     HTML = ()
#     JSON = ()
# 
# class MetaFormat(Flavor):
#     """
#     Indicates the general structure of story meta.
#     """
#     ALPHA = ()
#     BETA = ()
# 
# Path: fimfarchive/stories.py
# class Story:
#     """
#     Represents a story.
#     """
# 
#     def __init__(
#             self,
#             key: int,
#             fetcher: Fetcher = None,
#             meta: Dict[str, Any] = None,
#             data: bytes = None,
#             flavors: Iterable[Flavor] = (),
#             ) -> None:
#         """
#         Constructor.
# 
#         Args:
#             key: Primary key of the story.
#             fetcher: Fetcher to use for lazy fetching.
#             meta: Meta to populate the story with.
#             data: Data to populate the story with.
#             flavors: Content type hints.
#         """
#         if fetcher is None and (data is None or meta is None):
#             raise ValueError("Story must contain fetcher if lazy.")
# 
#         self.key = key
#         self.fetcher = fetcher
#         self.flavors = set(flavors)
#         self._meta = meta
#         self._data = data
# 
#     @property
#     def is_fetched(self) -> bool:
#         """
#         True if no more fetches are necessary.
#         """
#         return self.has_meta and self.has_data
# 
#     @property
#     def has_meta(self) -> bool:
#         """
#         True if story meta has been fetched.
#         """
#         return self._meta is not None
# 
#     @property
#     def meta(self) -> Dict[str, Any]:
#         """
#         Returns the story meta.
# 
#         Meta may be fetched if this story instance is lazy.
# 
#         Raises:
#             InvalidStoryError: If a valid story is not found.
#             StorySourceError: If source does not return any data.
#         """
#         if self._meta is None and self.fetcher:
#             self._meta = self.fetcher.fetch_meta(self.key)
# 
#         if self._meta is None:
#             raise StorySourceError("Meta is missing.")
# 
#         return self._meta
# 
#     @property
#     def has_data(self) -> bool:
#         """
#         True if story data has been fetched.
#         """
#         return self._data is not None
# 
#     @property
#     def data(self) -> bytes:
#         """
#         Returns the story data.
# 
#         Data may be fetched if this story instance is lazy.
# 
#         Raises:
#             InvalidStoryError: If a valid story is not found.
#             StorySourceError: If source does not return any data.
#         """
#         if self._data is None and self.fetcher:
#             self._data = self.fetcher.fetch_data(self.key)
# 
#         if self._data is None:
#             raise StorySourceError("Data is missing.")
# 
#         return self._data
# 
#     def merge(self, **params) -> 'Story':
#         """
#         Returns a shallow copy, optionally replacing attributes.
# 
#         Args:
#             **params: Overrides parameters from the current instance.
# 
#         Raises:
#             TypeError: If passed an unexpected parameter.
#         """
#         kwargs = {k.lstrip('_'): v for k, v in vars(self).items()}
#         kwargs.update(params)
# 
#         return type(self)(**kwargs)
# 
# Path: fimfarchive/utils.py
# def find_flavor(story: Story, flavor: Type[F]) -> Optional[F]:
#     """
#     Searches for a flavor of a specific type.
# 
#     Args:
#         story: The story to search in.
#         flavor: The flavor type to find.
# 
#     Returns:
#         A flavor of the desired type, or None.
#     """
#     for current in story.flavors:
#         if isinstance(current, flavor):
#             return current
# 
#     return None


# Path: fimfarchive/mappers.py
import string
from abc import abstractmethod
from html import unescape
from io import BytesIO
from pathlib import Path
from typing import Dict, Generic, Optional, Set, TypeVar, Union
from zipfile import ZipFile
from arrow import api as arrow, Arrow
from fimfarchive.exceptions import InvalidStoryError
from fimfarchive.flavors import DataFormat, MetaFormat
from fimfarchive.stories import Story
from fimfarchive.utils import find_flavor

            return f'{key}'

    def group(self, slug: str) -> str:
        """
        Returns the group for part of a path.

        Args:
            slug: The slug to group.

        Return:
            A group for the slug.
        """
        path = slug[:1]

        if path not in self.groupings:
            path = self.spacing

        return path

    def classify(self, story: Story) -> str:
        """
        Returns a file extension for the story.

        Args:
            story: The story to classify.

        Returns:
            A file extension.
        """
        for flavor in story.flavors:

            if isinstance(flavor, DataFormat):