1#!/usr/local/bin/python3.8
2# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
3
4
5__license__   = 'GPL v3'
6__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
7__docformat__ = 'restructuredtext en'
8
9import os
10
11from calibre.customize.conversion import InputFormatPlugin, OptionRecommendation
12from calibre.constants import numeric_version
13from calibre import walk
14
15
16class RecipeDisabled(Exception):
17    pass
18
19
20class RecipeInput(InputFormatPlugin):
21
22    name        = 'Recipe Input'
23    author      = 'Kovid Goyal'
24    description = _('Download periodical content from the Internet')
25    file_types  = {'recipe', 'downloaded_recipe'}
26    commit_name = 'recipe_input'
27
28    recommendations = {
29        ('chapter', None, OptionRecommendation.HIGH),
30        ('dont_split_on_page_breaks', True, OptionRecommendation.HIGH),
31        ('use_auto_toc', False, OptionRecommendation.HIGH),
32        ('input_encoding', None, OptionRecommendation.HIGH),
33        ('input_profile', 'default', OptionRecommendation.HIGH),
34        ('page_breaks_before', None, OptionRecommendation.HIGH),
35        ('insert_metadata', False, OptionRecommendation.HIGH),
36        }
37
38    options = {
39        OptionRecommendation(name='test', recommended_value=False,
40            help=_(
41            'Useful for recipe development. Forces'
42            ' max_articles_per_feed to 2 and downloads at most 2 feeds.'
43            ' You can change the number of feeds and articles by supplying optional arguments.'
44            ' For example: --test 3 1 will download at most 3 feeds and only 1 article per feed.')),
45        OptionRecommendation(name='username', recommended_value=None,
46            help=_('Username for sites that require a login to access '
47                'content.')),
48        OptionRecommendation(name='password', recommended_value=None,
49            help=_('Password for sites that require a login to access '
50                'content.')),
51        OptionRecommendation(name='dont_download_recipe',
52            recommended_value=False,
53            help=_('Do not download latest version of builtin recipes from the calibre server')),
54        OptionRecommendation(name='lrf', recommended_value=False,
55            help='Optimize fetching for subsequent conversion to LRF.'),
56        }
57
58    def convert(self, recipe_or_file, opts, file_ext, log,
59            accelerators):
60        from calibre.web.feeds.recipes import compile_recipe
61        opts.output_profile.flow_size = 0
62        if file_ext == 'downloaded_recipe':
63            from calibre.utils.zipfile import ZipFile
64            zf = ZipFile(recipe_or_file, 'r')
65            zf.extractall()
66            zf.close()
67            with lopen('download.recipe', 'rb') as f:
68                self.recipe_source = f.read()
69            recipe = compile_recipe(self.recipe_source)
70            recipe.needs_subscription = False
71            self.recipe_object = recipe(opts, log, self.report_progress)
72        else:
73            if os.environ.get('CALIBRE_RECIPE_URN'):
74                from calibre.web.feeds.recipes.collection import get_custom_recipe, get_builtin_recipe_by_id
75                urn = os.environ['CALIBRE_RECIPE_URN']
76                log('Downloading recipe urn: ' + urn)
77                rtype, recipe_id = urn.partition(':')[::2]
78                if not recipe_id:
79                    raise ValueError('Invalid recipe urn: ' + urn)
80                if rtype == 'custom':
81                    self.recipe_source = get_custom_recipe(recipe_id)
82                else:
83                    self.recipe_source = get_builtin_recipe_by_id(urn, log=log, download_recipe=True)
84                if not self.recipe_source:
85                    raise ValueError('Could not find recipe with urn: ' + urn)
86                if not isinstance(self.recipe_source, bytes):
87                    self.recipe_source = self.recipe_source.encode('utf-8')
88                recipe = compile_recipe(self.recipe_source)
89            elif os.access(recipe_or_file, os.R_OK):
90                with lopen(recipe_or_file, 'rb') as f:
91                    self.recipe_source = f.read()
92                recipe = compile_recipe(self.recipe_source)
93                log('Using custom recipe')
94            else:
95                from calibre.web.feeds.recipes.collection import (
96                        get_builtin_recipe_by_title, get_builtin_recipe_titles)
97                title = getattr(opts, 'original_recipe_input_arg', recipe_or_file)
98                title = os.path.basename(title).rpartition('.')[0]
99                titles = frozenset(get_builtin_recipe_titles())
100                if title not in titles:
101                    title = getattr(opts, 'original_recipe_input_arg', recipe_or_file)
102                    title = title.rpartition('.')[0]
103
104                raw = get_builtin_recipe_by_title(title, log=log,
105                        download_recipe=not opts.dont_download_recipe)
106                builtin = False
107                try:
108                    recipe = compile_recipe(raw)
109                    self.recipe_source = raw
110                    if recipe.requires_version > numeric_version:
111                        log.warn(
112                        'Downloaded recipe needs calibre version at least: %s' %
113                        ('.'.join(recipe.requires_version)))
114                        builtin = True
115                except:
116                    log.exception('Failed to compile downloaded recipe. Falling '
117                            'back to builtin one')
118                    builtin = True
119                if builtin:
120                    log('Using bundled builtin recipe')
121                    raw = get_builtin_recipe_by_title(title, log=log,
122                            download_recipe=False)
123                    if raw is None:
124                        raise ValueError('Failed to find builtin recipe: '+title)
125                    recipe = compile_recipe(raw)
126                    self.recipe_source = raw
127                else:
128                    log('Using downloaded builtin recipe')
129
130            if recipe is None:
131                raise ValueError('%r is not a valid recipe file or builtin recipe' %
132                        recipe_or_file)
133
134            disabled = getattr(recipe, 'recipe_disabled', None)
135            if disabled is not None:
136                raise RecipeDisabled(disabled)
137            ro = recipe(opts, log, self.report_progress)
138            ro.download()
139            self.recipe_object = ro
140
141        for key, val in self.recipe_object.conversion_options.items():
142            setattr(opts, key, val)
143
144        for f in os.listdir('.'):
145            if f.endswith('.opf'):
146                return os.path.abspath(f)
147
148        for f in walk('.'):
149            if f.endswith('.opf'):
150                return os.path.abspath(f)
151
152    def postprocess_book(self, oeb, opts, log):
153        if self.recipe_object is not None:
154            self.recipe_object.internal_postprocess_book(oeb, opts, log)
155            self.recipe_object.postprocess_book(oeb, opts, log)
156
157    def specialize(self, oeb, opts, log, output_fmt):
158        if opts.no_inline_navbars:
159            from calibre.ebooks.oeb.base import XPath
160            for item in oeb.spine:
161                for div in XPath('//h:div[contains(@class, "calibre_navbar")]')(item.data):
162                    div.getparent().remove(div)
163
164    def save_download(self, zf):
165        raw = self.recipe_source
166        if isinstance(raw, str):
167            raw = raw.encode('utf-8')
168        zf.writestr('download.recipe', raw)
169