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