diff --git a/fimfarchive/commands/__init__.py b/fimfarchive/commands/__init__.py
index 47e813d..6fd0e0a 100644
--- a/fimfarchive/commands/__init__.py
+++ b/fimfarchive/commands/__init__.py
@@ -24,13 +24,15 @@ Command module.
from .base import Command
from .build import BuildCommand
+from .fetch import FetchCommand
from .root import RootCommand
from .update import UpdateCommand
__all__ = (
'Command',
- 'RootCommand',
'BuildCommand',
+ 'FetchCommand',
+ 'RootCommand',
'UpdateCommand',
)
diff --git a/fimfarchive/commands/fetch.py b/fimfarchive/commands/fetch.py
new file mode 100644
index 0000000..5043881
--- /dev/null
+++ b/fimfarchive/commands/fetch.py
@@ -0,0 +1,140 @@
+"""
+Fetch command.
+"""
+
+
+#
+# Fimfarchive, preserves stories from Fimfiction.
+# Copyright (C) 2019 Joakim Soderlund
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+
+
+import os
+import re
+from argparse import ArgumentParser, Namespace
+from collections import defaultdict
+from typing import Dict
+
+from colorama import init as colorize, Fore
+
+from fimfarchive.fetchers import Fimfiction2Fetcher
+from fimfarchive.mappers import StorySlugMapper
+from fimfarchive.signals import SignalReceiver
+from fimfarchive.tasks import FetchTask
+from fimfarchive.writers import DirectoryWriter
+
+from .base import Command
+
+
+__all__ = (
+ 'FetchCommand',
+)
+
+
+ACCESS_TOKEN_KEY = 'FIMFICTION_ACCESS_TOKEN'
+
+
+class FetchPrinter(SignalReceiver):
+ """
+ Prints fetcher progress.
+ """
+
+ def __init__(self, task, mapper: StorySlugMapper) -> None:
+ self.results: Dict[str, int] = defaultdict(int)
+ self.slugify = mapper
+ colorize(autoreset=True)
+ super().__init__(task)
+
+ def on_attempt(self, sender, key):
+ print(f"[{key:>8}] ", end='')
+ self.results['attempt'] += 1
+
+ def on_success(self, sender, key, story):
+ slug = self.slugify(story)
+ print(f"{Fore.GREEN}-->", slug)
+ self.results['success'] += 1
+
+ def on_failure(self, sender, key, error):
+ print(f"{Fore.RED} {error}")
+ self.results['failure'] += 1
+
+
+class FetchCommand(Command):
+ """
+ Fetches stories from Fimfiction.
+ """
+
+ def __init__(self):
+ self.pattern = re.compile(r'/story/(?P\d+)/')
+ self.slugify = StorySlugMapper('{story}.{extension}')
+
+ def extract(self, key: str) -> int:
+ """
+ Extrats a story key from an argument.
+ """
+ key = key.strip('\t\n\r /')
+
+ try:
+ return int(key)
+ except ValueError:
+ pass
+
+ match = self.pattern.search(key)
+
+ if match is not None:
+ return int(match.group('key'))
+
+ raise ValueError(f"Invalid argument: {key}")
+
+ @property
+ def parser(self) -> ArgumentParser:
+ """
+ Returns a command line arguments parser.
+ """
+ parser = ArgumentParser(prog='', description=self.__doc__)
+ parser.add_argument('stories', nargs='+')
+
+ return parser
+
+ def configure(self, opts: Namespace) -> FetchTask:
+ token = os.environ.get(ACCESS_TOKEN_KEY)
+ writer = DirectoryWriter(data_path=self.slugify)
+
+ if token:
+ fimfiction = Fimfiction2Fetcher(token)
+ else:
+ exit(f"Environment variable required: {ACCESS_TOKEN_KEY}")
+
+ try:
+ keys = sorted({self.extract(key) for key in opts.stories})
+ except ValueError as error:
+ exit(f"{error}")
+
+ return FetchTask(keys, fimfiction, writer)
+
+ def __call__(self, *args: str) -> int:
+ opts = self.parser.parse_args(args)
+ task = self.configure(opts)
+
+ with FetchPrinter(task, self.slugify) as printer:
+ task.run()
+
+ results = printer.results
+
+ if results['failure'] != 0:
+ return 1
+
+ return 0
diff --git a/fimfarchive/commands/root.py b/fimfarchive/commands/root.py
index cf92524..e08ef06 100644
--- a/fimfarchive/commands/root.py
+++ b/fimfarchive/commands/root.py
@@ -26,6 +26,7 @@ from typing import Dict, Type
from .base import Command
from .build import BuildCommand
+from .fetch import FetchCommand
from .update import UpdateCommand
@@ -40,6 +41,7 @@ class RootCommand(Command):
"""
commands: Dict[str, Type[Command]] = {
'build': BuildCommand,
+ 'fetch': FetchCommand,
'update': UpdateCommand,
}
diff --git a/fimfarchive/tasks/__init__.py b/fimfarchive/tasks/__init__.py
index b2bb66a..2bfd93c 100644
--- a/fimfarchive/tasks/__init__.py
+++ b/fimfarchive/tasks/__init__.py
@@ -23,10 +23,12 @@ Tasks module.
from .build import BuildTask
+from .fetch import FetchTask
from .update import UpdateTask
__all__ = (
'BuildTask',
+ 'FetchTask',
'UpdateTask',
)
diff --git a/fimfarchive/tasks/fetch.py b/fimfarchive/tasks/fetch.py
new file mode 100644
index 0000000..6a282ea
--- /dev/null
+++ b/fimfarchive/tasks/fetch.py
@@ -0,0 +1,81 @@
+"""
+Fetch task.
+"""
+
+
+#
+# Fimfarchive, preserves stories from Fimfiction.
+# Copyright (C) 2019 Joakim Soderlund
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+
+
+from typing import Iterable
+
+from fimfarchive.converters import FpubEpubConverter, JsonFpubConverter
+from fimfarchive.fetchers import Fetcher
+from fimfarchive.writers import Writer
+from fimfarchive.signals import Signal, SignalSender
+
+
+__all__ = (
+ 'FetchTask',
+)
+
+
+class FetchTask(SignalSender):
+ """
+ Fetches a story and writes it to the current directory.
+ """
+ on_attempt = Signal('key')
+ on_success = Signal('key', 'story')
+ on_failure = Signal('key', 'error')
+
+ def __init__(
+ self,
+ keys: Iterable[int],
+ fetcher: Fetcher,
+ writer: Writer,
+ ) -> None:
+ """
+ Constructor.
+
+ Args:
+ keys: Stories to fetch.
+ fetcher: Story source.
+ writer: Story target.
+ """
+ super().__init__()
+ self.fetcher = fetcher
+ self.writer = writer
+ self.to_fpub = JsonFpubConverter()
+ self.to_epub = FpubEpubConverter()
+ self.keys = sorted(set(keys))
+
+ def run(self) -> None:
+ """
+ Runs the task.
+ """
+ for key in sorted(set(self.keys)):
+ self.on_attempt(key)
+
+ try:
+ json = self.fetcher.fetch(key)
+ fpub = self.to_fpub(json)
+ epub = self.to_epub(fpub)
+ self.writer.write(epub)
+ self.on_success(key, epub)
+ except Exception as e:
+ self.on_failure(key, e)
diff --git a/requirements.txt b/requirements.txt
index 4ad9359..aaed813 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,7 @@
arrow
bbcode
blinker
+colorama
flake8
importlib_resources
jinja2
diff --git a/setup.py b/setup.py
index e508244..8fccee2 100755
--- a/setup.py
+++ b/setup.py
@@ -88,6 +88,7 @@ setup(
'arrow',
'bbcode',
'blinker',
+ 'colorama',
'importlib_resources',
'jinja2',
'jmespath',