diff --git a/tools/boilerplate.py b/tools/boilerplate.py
new file mode 100755
index 00000000..a730000c
--- /dev/null
+++ b/tools/boilerplate.py
@@ -0,0 +1,138 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2013 Laurent Bachelier
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+import argparse
+import subprocess
+import datetime
+import os
+import sys
+from mako.lookup import TemplateLookup
+
+MODULE_PATH = os.getenv(
+ 'MODULE_PATH',
+ os.path.realpath(os.path.join(os.path.dirname(__file__), '../modules')))
+TEMPLATE_PATH = os.getenv(
+ 'TEMPLATE_PATH',
+ os.path.realpath(os.path.join(os.path.dirname(__file__), 'boilerplate_data')))
+VERSION = '0.f'
+
+TEMPLATES = TemplateLookup(directories=[TEMPLATE_PATH])
+
+
+def gitconfig(entry):
+ return subprocess.check_output('git config -z --get %s' % entry, shell=True)[:-1].decode('utf-8')
+
+
+def write(target, contents):
+ if not os.path.isdir(os.path.dirname(target)):
+ os.makedirs(os.path.dirname(target))
+ if os.path.exists(target):
+ print >>sys.stderr, "%s already exists." % target
+ sys.exit(4)
+ with open(target, mode='w') as f:
+ f.write(contents)
+ print 'Created %s' % target
+
+
+class Recipe(object):
+ @classmethod
+ def configure_subparser(cls, subparsers):
+ subparser = subparsers.add_parser(cls.NAME)
+ subparser.add_argument('name', help='Backend name')
+ subparser.set_defaults(recipe=cls)
+ return subparser
+
+ def __init__(self, args):
+ self.name = args.name.lower().replace(' ', '')
+ self.classname = args.name.title().replace(' ', '')
+ self.year = datetime.date.today().year
+ self.author = args.author
+ self.email = args.email
+ self.version = VERSION
+
+ def write(self, filename, contents):
+ return write(os.path.join(MODULE_PATH, self.name, filename), contents)
+
+ def template(self, name, **kwargs):
+ if '.' not in name:
+ name += '.py'
+ return TEMPLATES.get_template(name) \
+ .render(r=self,
+ # workaround, as it's also a mako directive
+ coding='# -*- coding: utf-8 -*-',
+ **kwargs)
+
+ def generate(self):
+ raise NotImplementedError()
+
+
+class BaseRecipe(Recipe):
+ NAME = 'base'
+
+ def generate(self):
+ self.write('__init__.py', self.template('init'))
+
+
+class ComicRecipe(BaseRecipe):
+ NAME = 'comic'
+
+ def generate(self):
+ super(ComicRecipe, self).generate()
+ self.write('backend.py', self.template('comic_backend'))
+
+
+class ComicTestRecipe(Recipe):
+ NAME = 'comic.test'
+
+ @classmethod
+ def configure_subparser(cls, subparsers):
+ subparser = super(ComicTestRecipe, cls).configure_subparser(subparsers)
+ subparser.add_argument('download_id', help='Download ID')
+ return subparser
+
+ def __init__(self, args):
+ super(ComicTestRecipe, self).__init__(args)
+ self.download_id = args.download_id
+
+ def generate(self):
+ self.write('test.py', self.template('comic_test'))
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ '-a', '--author',
+ default=gitconfig('user.name'))
+ parser.add_argument(
+ '-e', '--email',
+ default=gitconfig('user.email'))
+ subparsers = parser.add_subparsers()
+
+ recipes = [ComicRecipe, ComicTestRecipe]
+ for recipe in recipes:
+ recipe.configure_subparser(subparsers)
+
+ args = parser.parse_args()
+
+ recipe = args.recipe(args)
+ recipe.generate()
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/boilerplate_data/comic_backend.py b/tools/boilerplate_data/comic_backend.py
new file mode 100644
index 00000000..cf469c7d
--- /dev/null
+++ b/tools/boilerplate_data/comic_backend.py
@@ -0,0 +1,22 @@
+<%inherit file="layout.py"/>
+from weboob.tools.capabilities.gallery.genericcomicreader import GenericComicReaderBackend, DisplayPage
+
+
+__all__ = ['${r.classname}Backend']
+
+
+class ${r.classname}Backend(GenericComicReaderBackend):
+ NAME = '${r.name}'
+ DESCRIPTION = '${r.name} manga reading site'
+ MAINTAINER = '${r.author}'
+ EMAIL = '${r.email}'
+ VERSION = '${r.version}'
+
+ DOMAIN = 'www.${r.name}.com'
+ BROWSER_PARAMS = dict(
+ img_src_xpath="//img[@id='comic_page']/@src",
+ page_list_xpath="(//select[@id='page_select'])[1]/option/@value")
+ ID_REGEXP = r'[^/]+/[^/]+'
+ URL_REGEXP = r'.+${r.name}.com/(%s).+' % ID_REGEXP
+ ID_TO_URL = 'http://www.${r.name}.com/%s'
+ PAGES = {URL_REGEXP: DisplayPage}
diff --git a/tools/boilerplate_data/comic_test.py b/tools/boilerplate_data/comic_test.py
new file mode 100644
index 00000000..331fad76
--- /dev/null
+++ b/tools/boilerplate_data/comic_test.py
@@ -0,0 +1,9 @@
+<%inherit file="layout.py"/>
+from weboob.tools.capabilities.gallery.genericcomicreadertest import GenericComicReaderTest
+
+
+class ${r.classname}BackendTest(GenericComicReaderTest):
+ BACKEND = '${r.name}'
+
+ def test_download(self):
+ return self._test_download('${r.download_id}')
diff --git a/tools/boilerplate_data/init.py b/tools/boilerplate_data/init.py
new file mode 100644
index 00000000..2dcbdefa
--- /dev/null
+++ b/tools/boilerplate_data/init.py
@@ -0,0 +1,5 @@
+<%inherit file="layout.py"/>
+from .backend import ${r.name}Backend
+
+
+__all__ = ['${r.name}Backend']
diff --git a/tools/boilerplate_data/layout.py b/tools/boilerplate_data/layout.py
new file mode 100644
index 00000000..69796f57
--- /dev/null
+++ b/tools/boilerplate_data/layout.py
@@ -0,0 +1,20 @@
+${coding}
+
+# Copyright(C) ${r.year} ${r.author}
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+${self.body()}\